Coverage for src/bob/bio/vein/preprocessor/mask.py: 50%

123 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"""Base utilities for mask processing""" 

5 

6import math 

7 

8import numpy 

9import scipy.ndimage 

10 

11from .utils import poly_to_mask 

12 

13 

14class Padder(object): 

15 """A class that pads the input image returning a new object 

16 

17 

18 Parameters: 

19 

20 padding_width (:py:obj:`int`, optional): How much padding (in pixels) to 

21 add around the borders of the input image. We normally always keep this 

22 value on its default (5 pixels). This parameter is always used before 

23 normalizing the finger orientation. 

24 

25 padding_constant (:py:obj:`int`, optional): What is the value of the pixels 

26 added to the padding. This number should be a value between 0 and 255. 

27 (From Pedro Tome: for UTFVP (high-quality samples), use 0. For the VERA 

28 Fingervein database (low-quality samples), use 51 (that corresponds to 

29 0.2 in a float image with values between 0 and 1). This parameter is 

30 always used before normalizing the finger orientation. 

31 

32 """ 

33 

34 def __init__(self, padding_width=5, padding_constant=51): 

35 self.padding_width = padding_width 

36 self.padding_constant = padding_constant 

37 

38 def __call__(self, image): 

39 """Inputs an image, returns a padded (larger) image 

40 

41 Parameters: 

42 

43 image (numpy.ndarray): A 2D numpy array of type ``uint8`` with the 

44 input image 

45 

46 

47 Returns: 

48 

49 numpy.ndarray: A 2D numpy array of the same type as the input, but with 

50 the extra padding 

51 

52 """ 

53 

54 return numpy.pad( 

55 image, 

56 self.padding_width, 

57 "constant", 

58 constant_values=self.padding_constant, 

59 ) 

60 

61 

62class Masker(object): 

63 """This is the base class for all maskers 

64 

65 It defines the minimum requirements for all derived masker classes. 

66 

67 

68 """ 

69 

70 def __init__(self): 

71 pass 

72 

73 def __call__(self, image): 

74 """Overwrite this method to implement your masking method 

75 

76 

77 Parameters: 

78 

79 image (numpy.ndarray): A 2D numpy array of type ``uint8`` with the 

80 input image 

81 

82 

83 Returns: 

84 

85 numpy.ndarray: A 2D numpy array of type boolean with the caculated 

86 mask. ``True`` values correspond to regions where the finger is 

87 situated 

88 

89 """ 

90 

91 raise NotImplementedError("You must implement the __call__ slot") 

92 

93 

94class FixedMask(Masker): 

95 """Implements masking using a fixed suppression of border pixels 

96 

97 The defaults mask no lines from the image and returns a mask of the same size 

98 of the original image where all values are ``True``. 

99 

100 

101 .. note:: 

102 

103 Before choosing values, note you're responsible for knowing what is the 

104 orientation of images fed into this masker. 

105 

106 

107 Parameters: 

108 

109 top (:py:class:`int`, optional): Number of lines to suppress from the top 

110 of the image. The top of the image corresponds to ``y = 0``. 

111 

112 bottom (:py:class:`int`, optional): Number of lines to suppress from the 

113 bottom of the image. The bottom of the image corresponds to ``y = 

114 height``. 

115 

116 left (:py:class:`int`, optional): Number of lines to suppress from the left 

117 of the image. The left of the image corresponds to ``x = 0``. 

118 

119 right (:py:class:`int`, optional): Number of lines to suppress from the 

120 right of the image. The right of the image corresponds to ``x = width``. 

121 

122 """ 

123 

124 def __init__(self, top=0, bottom=0, left=0, right=0): 

125 self.top = top 

126 self.bottom = bottom 

127 self.left = left 

128 self.right = right 

129 

130 def __call__(self, image): 

131 """Returns a big mask 

132 

133 

134 Parameters: 

135 

136 image (numpy.ndarray): A 2D numpy array of type ``uint8`` with the 

137 input image 

138 

139 

140 Returns: 

141 

142 numpy.ndarray: A 2D numpy array of type boolean with the caculated 

143 mask. ``True`` values correspond to regions where the finger is 

144 situated 

145 

146 

147 """ 

148 

149 retval = numpy.zeros(image.shape, dtype="bool") 

150 h, w = image.shape 

151 retval[self.top : h - self.bottom, self.left : w - self.right] = True 

152 return retval 

153 

154 

155class NoMask(FixedMask): 

156 """Convenience: same as FixedMask()""" 

157 

158 def __init__(self): 

159 super(NoMask, self).__init__(0, 0, 0, 0) 

160 

161 

162class AnnotatedRoIMask(Masker): 

163 """Devises the mask from the annotated RoI""" 

164 

165 def __init__(self): 

166 pass 

167 

168 def __call__(self, image): 

169 """Returns a mask extrapolated from RoI annotations 

170 

171 

172 Parameters: 

173 

174 image (bob.bio.vein.database.AnnotatedArray): A 2D numpy array of type 

175 ``uint8`` with the input image containing an attribute called 

176 ``metadata`` (a python dictionary). The ``metadata`` object just 

177 contain a key called ``roi`` containing the annotated points 

178 

179 

180 Returns: 

181 

182 numpy.ndarray: A 2D numpy array of type boolean with the caculated 

183 mask. ``True`` values correspond to regions where the finger is 

184 situated 

185 

186 

187 """ 

188 

189 return poly_to_mask(image.shape, image.metadata["roi"]) 

190 

191 

192class KonoMask(Masker): 

193 """Estimates the finger region given an input NIR image using Kono et al. 

194 

195 This method is based on the work of M. Kono, H. Ueki and S. Umemura. 

196 Near-infrared finger vein patterns for personal identification, Applied 

197 Optics, Vol. 41, Issue 35, pp. 7429-7436 (2002). 

198 

199 

200 Parameters: 

201 

202 sigma (:py:obj:`float`, optional): The standard deviation of the gaussian 

203 blur filter to apply for low-passing the input image (background 

204 extraction). Defaults to ``5``. 

205 

206 padder (:py:class:`Padder`, optional): If passed, will pad the image before 

207 evaluating the mask. The returned value will have the padding removed and 

208 is, therefore, of the exact size of the input image. 

209 

210 """ 

211 

212 def __init__(self, sigma=5, padder=Padder()): 

213 self.sigma = sigma 

214 self.padder = padder 

215 

216 def __call__(self, image): 

217 """Inputs an image, returns a mask (numpy boolean array) 

218 

219 Parameters: 

220 

221 image (numpy.ndarray): A 2D numpy array of type ``uint8`` with the 

222 input image 

223 

224 

225 Returns: 

226 

227 numpy.ndarray: A 2D numpy array of type boolean with the caculated 

228 mask. ``True`` values correspond to regions where the finger is 

229 situated 

230 

231 """ 

232 

233 image = image if self.padder is None else self.padder(image) 

234 if image.dtype == numpy.uint8: 

235 image = image.astype("float64") / 255.0 

236 

237 img_h, img_w = image.shape 

238 

239 # Determine lower half starting point 

240 if numpy.mod(img_h, 2) == 0: 

241 half_img_h = img_h / 2 + 1 

242 else: 

243 half_img_h = numpy.ceil(img_h / 2) 

244 

245 # Construct filter kernel 

246 winsize = numpy.ceil(4 * self.sigma) 

247 

248 x = numpy.arange(-winsize, winsize + 1) 

249 y = numpy.arange(-winsize, winsize + 1) 

250 X, Y = numpy.meshgrid(x, y) 

251 

252 hy = (-Y / (2 * math.pi * self.sigma**4)) * numpy.exp( 

253 -(X**2 + Y**2) / (2 * self.sigma**2) 

254 ) 

255 

256 # Filter the image with the directional kernel 

257 fy = scipy.ndimage.convolve(image, hy, mode="nearest") 

258 

259 # Upper part of filtred image 

260 img_filt_up = fy[0:half_img_h, :] 

261 y_up = img_filt_up.argmax(axis=0) 

262 

263 # Lower part of filtred image 

264 img_filt_lo = fy[half_img_h - 1 :, :] 

265 y_lo = img_filt_lo.argmin(axis=0) 

266 

267 # Fill region between upper and lower edges 

268 finger_mask = numpy.ndarray(image.shape, bool) 

269 finger_mask[:, :] = False 

270 

271 for i in range(0, img_w): 

272 finger_mask[ 

273 y_up[i] : y_lo[i] + image.shape[0] - half_img_h + 2, i 

274 ] = True 

275 

276 if not self.padder: 

277 return finger_mask 

278 else: 

279 w = self.padder.padding_width 

280 return finger_mask[w:-w, w:-w] 

281 

282 

283class LeeMask(Masker): 

284 """Estimates the finger region given an input NIR image using Lee et al. 

285 

286 This method is based on the work of Finger vein recognition using 

287 minutia-based alignment and local binary pattern-based feature extraction, 

288 E.C. Lee, H.C. Lee and K.R. Park, International Journal of Imaging Systems 

289 and Technology, Volume 19, Issue 3, September 2009, Pages 175--178, doi: 

290 10.1002/ima.20193 

291 

292 This code is based on the Matlab implementation by Bram Ton, available at: 

293 

294 https://nl.mathworks.com/matlabcentral/fileexchange/35752-finger-region-localisation/content/lee_region.m 

295 

296 In this method, we calculate the mask of the finger independently for each 

297 column of the input image. Firstly, the image is convolved with a [1,-1] 

298 filter of size ``(self.filter_height, self.filter_width)``. Then, the upper and 

299 lower parts of the resulting filtered image are separated. The location of 

300 the maxima in the upper part is located. The same goes for the location of 

301 the minima in the lower part. The mask is then calculated, per column, by 

302 considering it starts in the point where the maxima is in the upper part and 

303 goes up to the point where the minima is detected on the lower part. 

304 

305 

306 Parameters: 

307 

308 filter_height (:py:obj:`int`, optional): Height of contour mask in pixels, 

309 must be an even number 

310 

311 filter_width (:py:obj:`int`, optional): Width of the contour mask in pixels 

312 

313 """ 

314 

315 def __init__(self, filter_height=4, filter_width=40, padder=Padder()): 

316 self.filter_height = filter_height 

317 self.filter_width = filter_width 

318 self.padder = padder 

319 

320 def __call__(self, image): 

321 """Inputs an image, returns a mask (numpy boolean array) 

322 

323 Parameters: 

324 

325 image (numpy.ndarray): A 2D numpy array of type ``uint8`` with the 

326 input image 

327 

328 

329 Returns: 

330 

331 numpy.ndarray: A 2D numpy array of type boolean with the caculated 

332 mask. ``True`` values correspond to regions where the finger is 

333 situated 

334 

335 """ 

336 

337 image = image if self.padder is None else self.padder(image) 

338 if image.dtype == numpy.uint8: 

339 image = image.astype("float64") / 255.0 

340 

341 img_h, img_w = image.shape 

342 

343 # Determine lower half starting point 

344 half_img_h = int(img_h / 2) 

345 

346 # Construct mask for filtering 

347 mask = numpy.ones( 

348 (self.filter_height, self.filter_width), dtype="float64" 

349 ) 

350 mask[int(self.filter_height / 2.0) :, :] = -1.0 

351 

352 img_filt = scipy.ndimage.convolve(image, mask, mode="nearest") 

353 

354 # Upper part of filtered image 

355 img_filt_up = img_filt[:half_img_h, :] 

356 y_up = img_filt_up.argmax(axis=0) 

357 

358 # Lower part of filtered image 

359 img_filt_lo = img_filt[half_img_h:, :] 

360 y_lo = img_filt_lo.argmin(axis=0) 

361 

362 # Translation: for all columns of the input image, set to True all pixels 

363 # of the mask from index where the maxima occurred in the upper part until 

364 # the index where the minima occurred in the lower part. 

365 finger_mask = numpy.zeros(image.shape, dtype="bool") 

366 for i in range(img_filt.shape[1]): 

367 finger_mask[ 

368 y_up[i] : (y_lo[i] + img_filt_lo.shape[0] + 1), i 

369 ] = True 

370 

371 if not self.padder: 

372 return finger_mask 

373 else: 

374 w = self.padder.padding_width 

375 return finger_mask[w:-w, w:-w] 

376 

377 

378class TomesLeeMask(Masker): 

379 """Estimates the finger region given an input NIR image using Lee et al. 

380 

381 This method is based on the work of Finger vein recognition using 

382 minutia-based alignment and local binary pattern-based feature extraction, 

383 E.C. Lee, H.C. Lee and K.R. Park, International Journal of Imaging Systems 

384 and Technology, Volume 19, Issue 3, September 2009, Pages 175--178, doi: 

385 10.1002/ima.20193 

386 

387 This code is a variant of the Matlab implementation by Bram Ton, available 

388 at: 

389 

390 https://nl.mathworks.com/matlabcentral/fileexchange/35752-finger-region-localisation/content/lee_region.m 

391 

392 In this variant from Pedro Tome, the technique of filtering the image with 

393 a horizontal filter is also applied on the vertical axis. The objective is to 

394 find better limits on the horizontal axis in case finger images show the 

395 finger tip. If that is not your case, you may use the original variant 

396 :py:class:`LeeMask` above. 

397 

398 

399 Parameters: 

400 

401 filter_height (:py:obj:`int`, optional): Height of contour mask in pixels, 

402 must be an even number 

403 

404 filter_width (:py:obj:`int`, optional): Width of the contour mask in pixels 

405 

406 """ 

407 

408 def __init__(self, filter_height=4, filter_width=40, padder=Padder()): 

409 self.filter_height = filter_height 

410 self.filter_width = filter_width 

411 self.padder = padder 

412 

413 def __call__(self, image): 

414 """Inputs an image, returns a mask (numpy boolean array) 

415 

416 Parameters: 

417 

418 image (numpy.ndarray): A 2D numpy array of type ``uint8`` with the 

419 input image 

420 

421 

422 Returns: 

423 

424 numpy.ndarray: A 2D numpy array of type boolean with the caculated 

425 mask. ``True`` values correspond to regions where the finger is 

426 situated 

427 

428 """ 

429 

430 image = image if self.padder is None else self.padder(image) 

431 if image.dtype == numpy.uint8: 

432 image = image.astype("float64") / 255.0 

433 

434 img_h, img_w = image.shape 

435 

436 # Determine lower half starting point 

437 half_img_h = img_h / 2 

438 half_img_w = img_w / 2 

439 

440 # Construct mask for filtering (up-bottom direction) 

441 mask = numpy.ones( 

442 (self.filter_height, self.filter_width), dtype="float64" 

443 ) 

444 mask[int(self.filter_height / 2.0) :, :] = -1.0 

445 

446 img_filt = scipy.ndimage.convolve(image, mask, mode="nearest") 

447 

448 # Upper part of filtred image 

449 img_filt_up = img_filt[: int(half_img_h), :] 

450 y_up = img_filt_up.argmax(axis=0) 

451 

452 # Lower part of filtred image 

453 img_filt_lo = img_filt[int(half_img_h) :, :] 

454 y_lo = img_filt_lo.argmin(axis=0) 

455 

456 img_filt = scipy.ndimage.convolve(image, mask.T, mode="nearest") 

457 

458 # Left part of filtered image 

459 img_filt_lf = img_filt[:, : int(half_img_w)] 

460 y_lf = img_filt_lf.argmax(axis=1) 

461 

462 # Right part of filtred image 

463 img_filt_rg = img_filt[:, int(half_img_w) :] 

464 y_rg = img_filt_rg.argmin(axis=1) 

465 

466 finger_mask = numpy.zeros(image.shape, dtype="bool") 

467 

468 for i in range(0, y_up.size): 

469 finger_mask[y_up[i] : y_lo[i] + img_filt_lo.shape[0] + 1, i] = True 

470 

471 # Left region 

472 for i in range(0, y_lf.size): 

473 finger_mask[i, 0 : y_lf[i] + 1] = False 

474 

475 # Right region has always the finger ending, crop the padding with the 

476 # meadian 

477 finger_mask[:, int(numpy.median(y_rg) + img_filt_rg.shape[1]) :] = False 

478 

479 if not self.padder: 

480 return finger_mask 

481 else: 

482 w = self.padder.padding_width 

483 return finger_mask[w:-w, w:-w]