Coverage for src/bob/bio/vein/preprocessor/utils.py: 78%

45 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2024-07-12 23:27 +0200

1#!/usr/bin/env python 

2# vim: set fileencoding=utf-8 : 

3 

4"""Utilities for preprocessing vein imagery""" 

5 

6import numpy 

7 

8 

9def assert_points(area, points): 

10 """Checks all points fall within the determined shape region, inclusively 

11 

12 This assertion function, test all points given in ``points`` fall within a 

13 certain area provided in ``area``. 

14 

15 

16 Parameters: 

17 

18 area (tuple): A tuple containing the size of the limiting area where the 

19 points should all be in. 

20 

21 points (numpy.ndarray): A 2D numpy ndarray with any number of rows (points) 

22 and 2 columns (representing ``y`` and ``x`` coordinates respectively), or 

23 any type convertible to this format. This array contains the points that 

24 will be checked for conformity. In case one of the points doesn't fall 

25 into the determined area an assertion is raised. 

26 

27 

28 Raises: 

29 

30 AssertionError: In case one of the input points does not fall within the 

31 area defined. 

32 

33 """ 

34 

35 for k in points: 

36 assert 0 <= k[0] < area[0] and 0 <= k[1] < area[1], ( 

37 "Point (%d, %d) is not inside the region determined by area " 

38 "(%d, %d)" % (k[0], k[1], area[0], area[1]) 

39 ) 

40 

41 

42def fix_points(area, points): 

43 """Checks/fixes all points so they fall within the determined shape region 

44 

45 Points which are lying outside the determined area will be brought into the 

46 area by moving the offending coordinate to the border of the said area. 

47 

48 

49 Parameters: 

50 

51 area (tuple): A tuple containing the size of the limiting area where the 

52 points should all be in. 

53 

54 points (numpy.ndarray): A 2D :py:class:`numpy.ndarray` with any number of 

55 rows (points) and 2 columns (representing ``y`` and ``x`` coordinates 

56 respectively), or any type convertible to this format. This array 

57 contains the points that will be checked/fixed for conformity. In case 

58 one of the points doesn't fall into the determined area, it is silently 

59 corrected so it does. 

60 

61 

62 Returns: 

63 

64 numpy.ndarray: A **new** array of points with corrected coordinates 

65 

66 """ 

67 

68 retval = numpy.array(points).copy() 

69 

70 retval[retval < 0] = 0 # floor at 0 for both axes 

71 y, x = retval[:, 0], retval[:, 1] 

72 y[y >= area[0]] = area[0] - 1 

73 x[x >= area[1]] = area[1] - 1 

74 

75 return retval 

76 

77 

78def poly_to_mask(shape, points): 

79 """Generates a binary mask from a set of 2D points 

80 

81 

82 Parameters: 

83 

84 shape (tuple): A tuple containing the size of the output mask in height and 

85 width, for Bob compatibility ``(y, x)``. 

86 

87 points (list): A list of tuples containing the polygon points that form a 

88 region on the target mask. A line connecting these points will be drawn 

89 and all the points in the mask that fall on or within the polygon line, 

90 will be set to ``True``. All other points will have a value of ``False``. 

91 

92 

93 Returns: 

94 

95 numpy.ndarray: A 2D numpy ndarray with ``dtype=bool`` with the mask 

96 generated with the determined shape, using the points for the polygon. 

97 

98 """ 

99 from PIL import Image, ImageDraw 

100 

101 # n.b.: PIL images are (x, y), while Bob shapes are represented in (y, x)! 

102 mask = Image.new("L", (shape[1], shape[0])) 

103 

104 # converts whatever comes in into a list of tuples for PIL 

105 fixed = tuple(map(tuple, numpy.roll(fix_points(shape, points), 1, 1))) 

106 

107 # draws polygon 

108 ImageDraw.Draw(mask).polygon(fixed, fill=255) 

109 

110 return numpy.array(mask, dtype=bool) 

111 

112 

113def mask_to_image(mask, dtype=numpy.uint8): 

114 """Converts a binary (boolean) mask into an integer or floating-point image 

115 

116 This function converts a boolean binary mask into an image of the desired 

117 type by setting the points where ``False`` is set to 0 and points where 

118 ``True`` is set to the most adequate value taking into consideration the 

119 destination data type ``dtype``. Here are support types and their ranges: 

120 

121 * numpy.uint8: ``[0, (2^8)-1]`` 

122 * numpy.uint16: ``[0, (2^16)-1]`` 

123 * numpy.uint32: ``[0, (2^32)-1]`` 

124 * numpy.uint64: ``[0, (2^64)-1]`` 

125 * numpy.float32: ``[0, 1.0]`` (fixed) 

126 * numpy.float64: ``[0, 1.0]`` (fixed) 

127 

128 All other types are currently unsupported. 

129 

130 

131 Parameters: 

132 

133 mask (numpy.ndarray): A 2D numpy ndarray with boolean data type, containing 

134 the mask that will be converted into an image. 

135 

136 dtype (numpy.dtype): A valid numpy data-type from the list above for the 

137 resulting image 

138 

139 

140 Returns: 

141 

142 numpy.ndarray: With the designated data type, containing the binary image 

143 formed from the mask. 

144 

145 

146 Raises: 

147 

148 TypeError: If the type is not supported by this function 

149 

150 """ 

151 

152 dtype = numpy.dtype(dtype) 

153 retval = mask.astype(dtype) 

154 

155 if dtype in (numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64): 

156 retval[retval == 1] = numpy.iinfo(dtype).max 

157 

158 elif dtype in (numpy.float32, numpy.float64): 

159 pass 

160 

161 else: 

162 raise TypeError("Data type %s is unsupported" % dtype) 

163 

164 return retval 

165 

166 

167def show_image(image): 

168 """Shows a single image using :py:meth:`PIL.Image.Image.show` 

169 

170 .. warning:: 

171 

172 This function opens a new window. You must be operating interactively in a 

173 windowing system for it to work properly. 

174 

175 Parameters: 

176 

177 image (numpy.ndarray): A 2D numpy.ndarray compose of 8-bit unsigned 

178 integers containing the original image 

179 

180 """ 

181 

182 from PIL import Image 

183 

184 img = Image.fromarray(image) 

185 img.show() 

186 

187 

188def draw_mask_over_image(image, mask, color="red"): 

189 """Plots the mask over the image of a finger, for debugging purposes 

190 

191 Parameters: 

192 

193 image (numpy.ndarray): A 2D numpy.ndarray compose of 8-bit unsigned 

194 integers containing the original image 

195 

196 mask (numpy.ndarray): A 2D numpy.ndarray compose of boolean values 

197 containing the calculated mask 

198 

199 

200 Returns: 

201 

202 PIL.Image: An image in PIL format 

203 

204 """ 

205 

206 from PIL import Image 

207 

208 img = Image.fromarray(image).convert(mode="RGBA") 

209 msk = Image.fromarray((~mask).astype("uint8") * 80) 

210 red = Image.new("RGBA", img.size, color=color) 

211 img.paste(red, mask=msk) 

212 

213 return img 

214 

215 

216def show_mask_over_image(image, mask, color="red"): 

217 """Plots the mask over the image of a finger using :py:meth:`PIL.Image.Image.show` 

218 

219 .. warning:: 

220 

221 This function opens a new window. You must be operating interactively in a 

222 windowing system for it to work properly. 

223 

224 Parameters: 

225 

226 image (numpy.ndarray): A 2D numpy.ndarray compose of 8-bit unsigned 

227 integers containing the original image 

228 

229 mask (numpy.ndarray): A 2D numpy.ndarray compose of boolean values 

230 containing the calculated mask 

231 

232 """ 

233 

234 draw_mask_over_image(image, mask, color).show() 

235 

236 

237def jaccard_index(a, b): 

238 r"""Calculates the intersection over union for two masks 

239 

240 This function calculates the Jaccard index: 

241 

242 .. math:: 

243 

244 J(A,B) &= \\frac{|A \cap B|}{|A \\cup B|} \\\\ 

245 &= \\frac{|A \cap B|}{|A|+|B|-|A \\cup B|} 

246 

247 

248 Parameters: 

249 

250 a (numpy.ndarray): A 2D numpy array with dtype :py:obj:`bool` 

251 

252 b (numpy.ndarray): A 2D numpy array with dtype :py:obj:`bool` 

253 

254 

255 Returns: 

256 

257 float: The floating point number that corresponds to the Jaccard index. The 

258 float value lies inside the interval :math:`[0, 1]`. If ``a`` and ``b`` are 

259 equal, then the similarity is maximum and the value output is ``1.0``. If 

260 the areas are exclusive, then the value output by this function is ``0.0``. 

261 

262 """ 

263 

264 return (a & b).sum().astype(float) / (a | b).sum().astype(float) 

265 

266 

267def intersect_ratio(a, b): 

268 """Calculates the intersection ratio between the ground-truth and a probe 

269 

270 This function calculates the intersection ratio between a ground-truth mask 

271 (:math:`A`; probably generated from an annotation) and a probe mask 

272 (:math:`B`), returning the ratio of overlap when the probe is compared to the 

273 ground-truth data: 

274 

275 .. math:: 

276 

277 R(A,B) = \\frac{|A \\cap B|}{|A|} 

278 

279 So, if the probe occupies the entirety of the ground-truth data, then the 

280 output of this function is ``1.0``, otherwise, if areas are exclusive, then 

281 this function returns ``0.0``. The output of this function should be analyzed 

282 against the output of :py:func:`intersect_ratio_of_complement`, which 

283 provides the complementary information about the intersection of the areas 

284 being analyzed. 

285 

286 

287 Parameters: 

288 

289 a (numpy.ndarray): A 2D numpy array with dtype :py:obj:`bool`, that 

290 corresponds to the **ground-truth object** 

291 

292 b (numpy.ndarray): A 2D numpy array with dtype :py:obj:`bool`, that 

293 corresponds to the probe object that will be compared to the ground-truth 

294 

295 

296 Returns: 

297 

298 float: The floating point number that corresponds to the overlap ratio. The 

299 float value lies inside the interval :math:`[0, 1]`. 

300 

301 """ 

302 

303 return (a & b).sum().astype(float) / a.sum().astype(float) 

304 

305 

306def intersect_ratio_of_complement(a, b): 

307 """Calculates the intersection ratio between the complement of ground-truth and a probe 

308 

309 This function calculates the intersection ratio between *the complement* of a 

310 ground-truth mask (:math:`A`; probably generated from an annotation) and a 

311 probe mask (:math:`B`), returning the ratio of overlap when the probe is 

312 compared to the ground-truth data: 

313 

314 .. math:: 

315 

316 R(A,B) = \\frac{|A^c \\cap B|}{|A|} = B \\setminus A 

317 

318 

319 So, if the probe is totally inside the ground-truth data, then the output of 

320 this function is ``0.0``, otherwise, if areas are exclusive for example, then 

321 this function outputs greater than zero. The output of this function should 

322 be analyzed against the output of :py:func:`intersect_ratio`, which provides 

323 the complementary information about the intersection of the areas being 

324 analyzed. 

325 

326 Parameters: 

327 

328 a (numpy.ndarray): A 2D numpy array with dtype :py:obj:`bool`, that 

329 corresponds to the **ground-truth object** 

330 

331 b (numpy.ndarray): A 2D numpy array with dtype :py:obj:`bool`, that 

332 corresponds to the probe object that will be compared to the ground-truth 

333 

334 

335 Returns: 

336 

337 float: The floating point number that corresponds to the overlap ratio 

338 between the probe area and the *complement* of the ground-truth area. 

339 There are no bounds for the float value on the right side: 

340 :math:`[0, +\\infty)`. 

341 

342 """ 

343 

344 return ((~a) & b).sum().astype(float) / a.sum().astype(float)