Coverage for src/bob/pad/face/utils/load_utils.py: 51%

152 statements  

« prev     ^ index     » next       coverage.py v7.6.5, created at 2024-11-14 23:14 +0100

1import random 

2 

3from collections import OrderedDict 

4from functools import partial 

5 

6import numpy 

7 

8from imageio import get_reader 

9from PIL import Image 

10 

11import bob.io.image 

12 

13from bob.bio.face.annotator import ( 

14 bounding_box_from_annotation, 

15 min_face_size_validator, 

16) 

17from bob.bio.face.color import rgb_to_hsv, rgb_to_yuv 

18from bob.bio.video.annotator import normalize_annotations 

19 

20 

21def block(X, block_size, block_overlap, flat=False): 

22 """ 

23 Parameters 

24 ---------- 

25 X : numpy.ndarray 

26 The image to be split into blocks. 

27 block_size : tuple 

28 The size of the block. 

29 block_overlap : tuple 

30 The overlap of the block. 

31 

32 Returns 

33 ------- 

34 numpy.ndarray 

35 The image split into blocks. 

36 """ 

37 

38 assert len(block_size) == 2 

39 assert len(block_overlap) == 2 

40 

41 size_ov_h = int(block_size[0] - block_overlap[0]) 

42 size_ov_w = int(block_size[1] - block_overlap[1]) 

43 n_blocks_h = int((X.shape[0] - block_overlap[0]) / size_ov_h) 

44 n_blocks_w = int((X.shape[1] - block_overlap[1]) / size_ov_w) 

45 

46 blocks = numpy.zeros(shape=(n_blocks_h, n_blocks_w, size_ov_h, size_ov_w)) 

47 for h in range(n_blocks_h): 

48 for w in range(n_blocks_w): 

49 blocks[h, w, :, :] = X[ 

50 h * size_ov_h : h * size_ov_h + block_size[0], 

51 w * size_ov_w : w * size_ov_w + block_size[1], 

52 ] 

53 

54 if flat: 

55 return blocks.reshape( 

56 n_blocks_h * n_blocks_w, blocks.shape[2], blocks.shape[3] 

57 ) 

58 

59 return blocks 

60 

61 

62def scale(image, scaling_factor): 

63 """ 

64 Scales and image using PIL 

65 

66 Parameters 

67 ---------- 

68 

69 image: 

70 Input image to be scaled 

71 

72 new_shape: tuple 

73 Shape of the rescaled image 

74 

75 

76 

77 """ 

78 

79 if isinstance(scaling_factor, float): 

80 new_size = tuple( 

81 (numpy.array(image.shape) * scaling_factor).astype(numpy.int) 

82 ) 

83 return bob.io.image.to_bob( 

84 numpy.array( 

85 Image.fromarray(bob.io.image.to_matplotlib(image)).resize( 

86 size=new_size 

87 ), 

88 dtype="float", 

89 ) 

90 ) 

91 

92 elif isinstance(scaling_factor, tuple): 

93 if len(scaling_factor) > 2: 

94 scaling_factor = scaling_factor[1:] 

95 

96 return bob.io.image.to_bob( 

97 numpy.array( 

98 Image.fromarray(bob.io.image.to_matplotlib(image)).resize( 

99 size=scaling_factor 

100 ), 

101 dtype="float", 

102 ) 

103 ) 

104 else: 

105 raise ValueError(f"Scaling factor not supported: {scaling_factor}") 

106 

107 

108def frames(path): 

109 """Yields the frames of a video file. 

110 

111 Parameters 

112 ---------- 

113 path : str 

114 Path to the video file. 

115 

116 Yields 

117 ------ 

118 numpy.ndarray 

119 A frame of the video. The size is (3, 240, 320). 

120 """ 

121 video = get_reader(path) 

122 for frame in video: 

123 yield bob.io.image.to_bob(frame) 

124 

125 

126def number_of_frames(path): 

127 """returns the number of frames of a video file. 

128 

129 Parameters 

130 ---------- 

131 path : str 

132 Path to the video file. 

133 

134 Returns 

135 ------- 

136 int 

137 The number of frames. Then, it yields the frames. 

138 """ 

139 video = get_reader(path) 

140 return video.count_frames() 

141 

142 

143def bbx_cropper(frame, annotations): 

144 for source in ("direct", "eyes", None): 

145 try: 

146 bbx = bounding_box_from_annotation(source=source, **annotations) 

147 break 

148 except Exception: 

149 if source is None: 

150 raise 

151 return frame[..., bbx.top : bbx.bottom, bbx.left : bbx.right] 

152 

153 

154def min_face_size_normalizer(annotations, max_age=15, **kwargs): 

155 return normalize_annotations( 

156 annotations, partial(min_face_size_validator, **kwargs), max_age=max_age 

157 ) 

158 

159 

160def yield_faces(pad_sample, cropper, normalizer=None): 

161 """Yields face images of a padfile. It uses the annotations from the 

162 database. The annotations are further normalized. 

163 

164 Parameters 

165 ---------- 

166 pad_sample 

167 The pad sample to return the faces. 

168 cropper : collections.abc.Callable 

169 A face image cropper that works with database's annotations. 

170 normalizer : collections.abc.Callable 

171 If not None, it should be a function that takes all the annotations of 

172 the whole video and yields normalized annotations frame by frame. It 

173 should yield same as ``annotations.items()``. 

174 

175 Yields 

176 ------ 

177 numpy.ndarray 

178 Face images 

179 

180 Raises 

181 ------ 

182 ValueError 

183 If the database returns None for annotations. 

184 """ 

185 

186 # read annotation 

187 annotations = pad_sample.annotations 

188 if annotations is None: 

189 raise ValueError("No annotations were returned.") 

190 

191 if normalizer is not None: 

192 annotations = OrderedDict(normalizer(annotations)) 

193 

194 # normalize annotations and crop faces 

195 for frame_id, frame in enumerate(pad_sample.data): 

196 annot = annotations.get(str(frame_id), None) 

197 if annot is None: 

198 continue 

199 face = cropper(frame, annotations=annot) 

200 if face is not None: 

201 yield face 

202 

203 

204def scale_face(face, face_height, face_width=None): 

205 """Scales a face image to the given size. 

206 

207 Parameters 

208 ---------- 

209 face : numpy.ndarray 

210 The face image. It can be 2D or 3D in bob image format. 

211 face_height : int 

212 The height of the scaled face. 

213 face_width : :obj:`None`, optional 

214 The width of the scaled face. If None, face_height is used. 

215 

216 Returns 

217 ------- 

218 numpy.ndarray 

219 The scaled face. 

220 """ 

221 face_width = face_height if face_width is None else face_width 

222 shape = list(face.shape) 

223 shape[-2:] = (face_height, face_width) 

224 # scaled_face = numpy.empty(shape, dtype="float64") 

225 scaled_face = scale(face, tuple(shape)) 

226 return scaled_face 

227 

228 

229def blocks(data, block_size, block_overlap=(0, 0)): 

230 """Extracts patches of an image 

231 

232 Parameters 

233 ---------- 

234 data : numpy.ndarray 

235 The image in gray-scale, color, or color video format. 

236 block_size : (int, int) 

237 The size of patches 

238 block_overlap : (:obj:`int`, :obj:`int`), optional 

239 The size of overlap of patches 

240 

241 Returns 

242 ------- 

243 numpy.ndarray 

244 The patches. 

245 

246 Raises 

247 ------ 

248 ValueError 

249 If data dimension is not between 2 and 4 (inclusive). 

250 """ 

251 

252 data = numpy.asarray(data) 

253 # if a gray scale image: 

254 if data.ndim == 2: 

255 output = block(data, block_size, block_overlap, flat=True) 

256 # if a color image: 

257 elif data.ndim == 3: 

258 # out_shape = list(data.shape[0:1]) + list( 

259 # block_output_shape(data[0], block_size, block_overlap, flat=True) 

260 # ) 

261 

262 # output = numpy.empty(out_shape, dtype=data.dtype) 

263 output = [] 

264 for i, img2d in enumerate(data): 

265 output.append(block(img2d, block_size, block_overlap, flat=True)) 

266 output = numpy.moveaxis(output, 0, 1) 

267 # if a color video: 

268 elif data.ndim == 4: 

269 output = [blocks(img3d, block_size, block_overlap) for img3d in data] 

270 output = numpy.concatenate(output, axis=0) 

271 else: 

272 raise ValueError("Unknown data dimension {}".format(data.ndim)) 

273 return output 

274 

275 

276def block_generator(input, block_size, block_overlap=(0, 0)): 

277 """Performs a block decomposition of a 2D or 3D array/image 

278 

279 It works exactly as :any:`bob.ip.base.block` except that it yields the blocks 

280 one by one instead of concatenating them. It also works with color images. 

281 

282 Parameters 

283 ---------- 

284 input : :any:`numpy.ndarray` 

285 A 2D array (Height, Width) or a color image (Bob format: Channels, 

286 Height, Width). 

287 block_size : (:obj:`int`, :obj:`int`) 

288 The size of the blocks in which the image is decomposed. 

289 block_overlap : (:obj:`int`, :obj:`int`), optional 

290 The overlap of the blocks. 

291 

292 Yields 

293 ------ 

294 array_like 

295 A block view of the image. Modifying the blocks will change the original 

296 image as well. This is different from :any:`bob.ip.base.block`. 

297 

298 Raises 

299 ------ 

300 ValueError 

301 If the block_overlap is not smaller than block_size. 

302 If the block_size is bigger than the image size. 

303 """ 

304 block_h, block_w = block_size 

305 overlap_h, overlap_w = block_overlap 

306 img_h, img_w = input.shape[-2:] 

307 

308 if overlap_h >= block_h or overlap_w >= block_w: 

309 raise ValueError( 

310 "block_overlap: {} must be smaller than block_size: {}.".format( 

311 block_overlap, block_size 

312 ) 

313 ) 

314 if img_h < block_h or img_w < block_w: 

315 raise ValueError( 

316 "block_size: {} must be smaller than the image size: {}.".format( 

317 block_size, input.shape[-2:] 

318 ) 

319 ) 

320 

321 # Determine the number of block per row and column 

322 size_ov_h = block_h - overlap_h 

323 size_ov_w = block_w - overlap_w 

324 

325 # Perform the block decomposition 

326 for h in range(0, img_h - block_h + 1, size_ov_h): 

327 for w in range(0, img_w - block_w + 1, size_ov_w): 

328 yield input[..., h : h + block_h, w : w + block_w] 

329 

330 

331def blocks_generator(data, block_size, block_overlap=(0, 0)): 

332 """Yields patches of an image 

333 

334 Parameters 

335 ---------- 

336 data : numpy.ndarray 

337 The image in gray-scale, color, or color video format. 

338 block_size : (int, int) 

339 The size of patches 

340 block_overlap : (:obj:`int`, :obj:`int`), optional 

341 The size of overlap of patches 

342 

343 Yields 

344 ------ 

345 numpy.ndarray 

346 The patches. 

347 

348 Raises 

349 ------ 

350 ValueError 

351 If data dimension is not between 2 and 4 (inclusive). 

352 """ 

353 data = numpy.asarray(data) 

354 if 1 < data.ndim < 4: 

355 for patch in block_generator(data, block_size, block_overlap): 

356 yield patch 

357 # if a color video: 

358 elif data.ndim == 4: 

359 for frame in data: 

360 for patch in block_generator(frame, block_size, block_overlap): 

361 yield patch 

362 else: 

363 raise ValueError("Unknown data dimension {}".format(data.ndim)) 

364 

365 

366def color_augmentation(image, channels=("rgb",)): 

367 """Converts an RGB image to different color channels. 

368 

369 Parameters 

370 ---------- 

371 image : numpy.ndarray 

372 The image in RGB Bob format. 

373 channels : :obj:`tuple`, optional 

374 List of channels to convert the image to. It can be any of ``rgb``, 

375 ``yuv``, ``hsv``. 

376 

377 Returns 

378 ------- 

379 numpy.ndarray 

380 The image that contains several channels: 

381 ``(3*len(channels), height, width)``. 

382 """ 

383 final_image = [] 

384 

385 if "rgb" in channels: 

386 final_image.append(image) 

387 

388 if "yuv" in channels: 

389 final_image.append(rgb_to_yuv(image)) 

390 

391 if "hsv" in channels: 

392 final_image.append(rgb_to_hsv(image)) 

393 

394 return numpy.concatenate(final_image, axis=0) 

395 

396 

397def random_sample(A, size): 

398 """Randomly selects ``size`` samples from the array ``A``""" 

399 return A[numpy.random.choice(A.shape[0], size, replace=False), ...] 

400 

401 

402def random_patches(image, block_size, n_random_patches=1): 

403 """Extracts N random patches of block_size from an image""" 

404 h, w = image.shape[-2:] 

405 bh, bw = block_size 

406 if h < block_size[0] or w < block_size[1]: 

407 raise ValueError("block_size must be smaller than image shape") 

408 hl = numpy.random.randint(0, h - bh, size=n_random_patches) 

409 wl = numpy.random.randint(0, w - bw, size=n_random_patches) 

410 for ch, cw in zip(hl, wl): 

411 yield image[..., ch : ch + bh, cw : cw + bw] 

412 

413 

414def extract_patches( 

415 image, block_size, block_overlap=(0, 0), n_random_patches=None 

416): 

417 """Yields either all patches from an image or N random patches.""" 

418 if n_random_patches is None: 

419 return blocks_generator(image, block_size, block_overlap) 

420 else: 

421 return random_patches( 

422 image, block_size, n_random_patches=n_random_patches 

423 ) 

424 

425 

426def the_giant_video_loader( 

427 pad_sample, 

428 region="whole", 

429 scaling_factor=None, 

430 cropper=None, 

431 normalizer=None, 

432 patches=False, 

433 block_size=(96, 96), 

434 block_overlap=(0, 0), 

435 random_patches_per_frame=None, 

436 augment=None, 

437 multiple_bonafide_patches=1, 

438 keep_pa_samples=None, 

439 keep_bf_samples=None, 

440): 

441 """Loads a video pad file frame by frame and optionally applies 

442 transformations. 

443 

444 Parameters 

445 ---------- 

446 pad_sample 

447 The pad sample 

448 region : str 

449 Either `whole` or `crop`. If whole, it will return the whole frame. 

450 Otherwise, you need to provide a cropper and a normalizer. 

451 scaling_factor : float 

452 If given, will scale images to this factor. 

453 cropper 

454 The cropper to use 

455 normalizer 

456 The normalizer to use 

457 patches : bool 

458 If true, will extract patches from images. 

459 block_size : tuple 

460 Size of the patches 

461 block_overlap : tuple 

462 Size of overlap of the patches 

463 random_patches_per_frame : int 

464 If not None, will only take this much patches per frame 

465 augment 

466 If given, frames will be transformed using this function. 

467 multiple_bonafide_patches : int 

468 Will use more random patches for bonafide samples 

469 keep_pa_samples : float 

470 If given, will drop some PA samples. 

471 keep_bf_samples : float 

472 If given, will drop some BF samples. 

473 

474 Returns 

475 ------- 

476 object 

477 A generator that yields the samples. 

478 

479 Raises 

480 ------ 

481 ValueError 

482 If region is not whole or crop. 

483 """ 

484 if region == "whole": 

485 generator = iter(pad_sample.data) 

486 elif region == "crop": 

487 generator = yield_faces( 

488 pad_sample, cropper=cropper, normalizer=normalizer 

489 ) 

490 else: 

491 raise ValueError("Invalid region value: `{}'".format(region)) 

492 

493 if scaling_factor is not None: 

494 generator = (scale(frame, scaling_factor) for frame in generator) 

495 if patches: 

496 if random_patches_per_frame is None: 

497 generator = ( 

498 patch 

499 for frame in generator 

500 for patch in blocks_generator(frame, block_size, block_overlap) 

501 ) 

502 else: 

503 if pad_sample.is_bonafide: 

504 random_patches_per_frame *= multiple_bonafide_patches 

505 generator = ( 

506 patch 

507 for frame in generator 

508 for patch in random_sample( 

509 blocks(frame, block_size, block_overlap), 

510 random_patches_per_frame, 

511 ) 

512 ) 

513 

514 if augment is not None: 

515 generator = (augment(frame) for frame in generator) 

516 

517 if keep_pa_samples is not None and not pad_sample.is_bonafide: 

518 generator = ( 

519 frame for frame in generator if random.random() < keep_pa_samples 

520 ) 

521 

522 if keep_bf_samples is not None and pad_sample.is_bonafide: 

523 generator = ( 

524 frame for frame in generator if random.random() < keep_bf_samples 

525 ) 

526 

527 return generator