Commit f4426dd6 authored by Amir MOHAMMADI's avatar Amir MOHAMMADI
Browse files

Improve load_utils.py

parent 07059f88
Pipeline #20606 failed with stage
in 29 minutes and 15 seconds
......@@ -9,8 +9,42 @@ from bob.pad.face.utils import frames, number_of_frames
from bob.db.base.annotations import read_annotation_file
import numpy
from bob.extension import rc
# documentation imports
import bob.bio.video
REPLAYMOBILE_FRAME_SHAPE = (3, 1280, 720)
def replaymobile_annotations(lowlevelfile, original_directory):
# numpy array containing the face bounding box data for each video
# frame, returned data format described in the f.bbx() method of the
# low level interface
annots = lowlevelfile.bbx(directory=original_directory)
annotations = {} # dictionary to return
for fn, frame_annots in enumerate(annots):
topleft = (frame_annots[1], frame_annots[0])
bottomright = (frame_annots[1] + frame_annots[3],
frame_annots[0] + frame_annots[2])
annotations[str(fn)] = {
'topleft': topleft,
'bottomright': bottomright
}
return annotations
def replaymobile_frames(lowlevelfile, original_directory):
vfilename = lowlevelfile.make_path(
directory=original_directory,
extension='.mov')
is_not_tablet = not lowlevelfile.is_tablet()
for frame in frames(vfilename):
frame = numpy.rollaxis(frame, 2, 1)
if is_not_tablet:
frame = frame[:, ::-1, :]
yield frame
class ReplayMobilePadFile(VideoPadFile):
......@@ -80,6 +114,34 @@ class ReplayMobilePadFile(VideoPadFile):
return frame_selector(video_data_array)
@property
def annotations(self):
if self.annotation_directory is not None:
# return the external annotations
annotations = read_annotation_file(
self.make_path(self.annotation_directory,
self.annotation_extension),
self.annotation_type)
return annotations
# return original annotations
return replaymobile_annotations(self.f, self.original_directory)
@property
def frames(self):
return replaymobile_frames(self.f, self.original_directory)
@property
def number_of_frames(self):
vfilename = self.make_path(
directory=self.original_directory,
extension='.mov')
return number_of_frames(vfilename)
@property
def frame_shape(self):
return REPLAYMOBILE_FRAME_SHAPE
class ReplayMobilePadDatabase(PadDatabase):
"""
......@@ -202,6 +264,12 @@ class ReplayMobilePadDatabase(PadDatabase):
files = [ReplayMobilePadFile(f) for f in files]
for f in files:
f.original_directory = self.original_directory
f.annotation_directory = self.annotation_directory
f.annotation_extension = self.annotation_extension
f.annotation_type = self.annotation_type
return files
def annotations(self, f):
......@@ -227,38 +295,11 @@ class ReplayMobilePadDatabase(PadDatabase):
A dictionary containing the annotations for each frame in the
video. Dictionary structure:
``annotations = {'1': frame1_dict, '2': frame1_dict, ...}``. Where
``frameN_dict = {'topleft': (row, col), 'bottomright': (row, col)}``
``frameN_dict = {'topleft': (row, col),'bottomright': (row, col)}``
is the dictionary defining the coordinates of the face bounding box
in frame N.
"""
if self.annotation_directory is not None:
# return the external annotations
annotations = read_annotation_file(
f.make_path(self.annotation_directory,
self.annotation_extension),
self.annotation_type)
return annotations
# numpy array containing the face bounding box data for each video
# frame, returned data format described in the f.bbx() method of the
# low level interface
annots = f.f.bbx(directory=self.original_directory)
annotations = {} # dictionary to return
for fn, frame_annots in enumerate(annots):
topleft = (frame_annots[1], frame_annots[0])
bottomright = (frame_annots[1] + frame_annots[3],
frame_annots[0] + frame_annots[2])
annotations[str(fn)] = {
'topleft': topleft,
'bottomright': bottomright
}
return annotations
return f.annotations
def frames(self, padfile):
"""Yields the frames of the padfile one by one.
......@@ -273,15 +314,7 @@ class ReplayMobilePadDatabase(PadDatabase):
:any:`numpy.array`
A frame of the video. The size is (3, 1280, 720).
"""
vfilename = padfile.make_path(
directory=self.original_directory,
extension=self.original_extension)
is_not_tablet = not padfile.f.is_tablet()
for frame in frames(vfilename):
frame = numpy.rollaxis(frame, 2, 1)
if is_not_tablet:
frame = frame[:, ::-1, :]
yield frame
return padfile.frames
def number_of_frames(self, padfile):
"""Returns the number of frames in a video file.
......@@ -296,10 +329,7 @@ class ReplayMobilePadDatabase(PadDatabase):
int
The number of frames.
"""
vfilename = padfile.make_path(
directory=self.original_directory,
extension=self.original_extension)
return number_of_frames(vfilename)
return padfile.number_of_frames
@property
def frame_shape(self):
......@@ -310,4 +340,4 @@ class ReplayMobilePadDatabase(PadDatabase):
(int, int, int)
The (#Channels, Height, Width) which is (3, 1280, 720).
"""
return (3, 1280, 720)
return REPLAYMOBILE_FRAME_SHAPE
......@@ -4,7 +4,8 @@ import bob.io.base
import os
from bob.pad.face.database import VideoPadFile
from bob.pad.base.database import PadDatabase
from bob.db.base.utils import check_parameters_for_validity, convert_names_to_lowlevel
from bob.db.base.utils import (
check_parameters_for_validity, convert_names_to_lowlevel)
class DummyPadFile(VideoPadFile):
......@@ -14,6 +15,27 @@ class DummyPadFile(VideoPadFile):
fc.add(os.path.basename(file_name), bob.io.base.load(file_name))
return fc
@property
def frames(self):
fc = self.load(self.original_directory)
for _, frame, _ in fc:
yield frame
@property
def number_of_frames(self):
fc = self.load(self.original_directory)
return len(fc)
@property
def frame_shape(self):
return (112, 92)
@property
def annotations(self):
if self.none_annotations:
return None
return {'0': {'topleft': (0, 0), 'bottomright': self.frame_shape}}
class DummyDatabase(PadDatabase):
......@@ -33,9 +55,12 @@ class DummyDatabase(PadDatabase):
self.high_level_names = ('train', 'dev')
def _make_bio(self, files):
return [DummyPadFile(client_id=f.client_id, path=f.path, file_id=f.id,
attack_type=None)
for f in files]
files = [DummyPadFile(client_id=f.client_id, path=f.path, file_id=f.id,
attack_type=None)
for f in files]
for f in files:
f.original_directory = self.original_directory
return files
def objects(self, groups=None, protocol=None, purposes=None,
model_ids=None, **kwargs):
......@@ -59,13 +84,10 @@ class DummyDatabase(PadDatabase):
return None
def frames(self, padfile):
fc = padfile.load(self.original_directory)
for _, frame, _ in fc:
yield frame
return padfile.frames
def number_of_frames(self, padfile):
fc = padfile.load(self.original_directory)
return len(fc)
return padfile.number_of_frames
@property
def frame_shape(self):
......
from bob.pad.face.test.dummy.database import DummyDatabase as Database
from bob.pad.face.utils import yield_frames, yield_faces, scale_face, blocks
from types import MethodType
from bob.pad.face.utils import yield_faces, scale_face, blocks
from nose.tools import raises
import numpy
......@@ -13,33 +12,28 @@ def dummy_cropper(frame, annotations=None):
return frame
def _annotations(self, padfile):
return {'0': {'topleft': (0, 0), 'bottomright': self.frame_shape}}
def test_yield_frames():
database = Database()
assert database.number_of_frames(padfile) == 1
for frame in yield_frames(database, padfile):
nframes = database.number_of_frames(padfile)
assert nframes == 1, nframes
for frame in padfile.frames:
assert frame.ndim == 2
assert frame.shape == database.frame_shape
@raises(ValueError)
def test_yield_faces_1():
database = Database()
for face in yield_faces(database, padfile, dummy_cropper):
padfile.none_annotations = True
for face in yield_faces(padfile, dummy_cropper):
pass
def test_yield_faces_2():
database = Database()
database.annotations = MethodType(
_annotations, database)
assert len(list(yield_faces(database, padfile, dummy_cropper)))
for face in yield_faces(database, padfile, dummy_cropper):
padfile.none_annotations = False
assert len(list(yield_faces(padfile, dummy_cropper)))
for face in yield_faces(padfile, dummy_cropper):
assert face.ndim == 2
assert face.shape == database.frame_shape
assert face.shape == padfile.frame_shape
def test_scale_face():
......
from bob.bio.face.annotator import min_face_size_validator
from bob.bio.video.annotator import normalize_annotations
from bob.io.video import reader
from bob.ip.base import scale, block, block_output_shape
from bob.ip.base import scale, block, block_output_shape, block_generator
from bob.ip.color import rgb_to_yuv, rgb_to_hsv
from bob.ip.facedetect import bounding_box_from_annotation
from collections import OrderedDict
from functools import partial
import numpy
import random
def frames(path):
......@@ -43,25 +44,6 @@ def number_of_frames(path):
return video.number_of_frames
def yield_frames(paddb, padfile):
"""Loads the frames of a video PAD database.
Parameters
----------
paddb : :any:`bob.pad.base.database.PadDatabase`
The video PAD database. The database needs to have implemented the
`.frames()` method.
padfile : :any:`bob.pad.face.database.VideoPadFile`
The PAD file.
Yields
------
:any:`numpy.array`
Frames of the PAD file one by one.
"""
return paddb.frames(padfile)
def bbx_cropper(frame, annotations):
bbx = bounding_box_from_annotation(**annotations)
return frame[..., bbx.top:bbx.bottom, bbx.left:bbx.right]
......@@ -73,15 +55,12 @@ def min_face_size_normalizer(annotations, max_age=15, **kwargs):
max_age=max_age)
def yield_faces(database, padfile, cropper, normalizer=None):
def yield_faces(padfile, cropper, normalizer=None):
"""Yields face images of a padfile. It uses the annotations from the
database. The annotations are further normalized.
Parameters
----------
database : :any:`bob.pad.base.database.PadDatabase`
A face PAD database. This database needs to have implemented the
`frames` method.
padfile : :any:`bob.pad.base.database.PadFile`
The padfile to return the faces.
cropper : callable
......@@ -101,10 +80,10 @@ def yield_faces(database, padfile, cropper, normalizer=None):
ValueError
If the database returns None for annotations.
"""
frames_gen = database.frames(padfile)
frames_gen = padfile.frames
# read annotation
annotations = database.annotations(padfile)
annotations = padfile.annotations
if annotations is None:
raise ValueError("No annotations were returned.")
......@@ -192,6 +171,41 @@ def blocks(data, block_size, block_overlap=(0, 0)):
return output
def blocks_generator(data, block_size, block_overlap=(0, 0)):
"""Yields patches of an image
Parameters
----------
data : :any:`numpy.array`
The image in gray-scale, color, or color video format.
block_size : (int, int)
The size of patches
block_overlap : (:obj:`int`, :obj:`int`), optional
The size of overlap of patches
Yields
------
:any:`numpy.array`
The patches.
Raises
------
ValueError
If data dimension is not between 2 and 4 (inclusive).
"""
data = numpy.asarray(data)
if 1 < data.ndim < 4:
for patch in block_generator(data, block_size, block_overlap):
yield patch
# if a color video:
elif data.ndim == 4:
for frame in data:
for patch in block_generator(frame, block_size, block_overlap):
yield patch
else:
raise ValueError("Unknown data dimension {}".format(data.ndim))
def color_augmentation(image, channels=('rgb',)):
"""Converts an RGB image to different color channels.
......@@ -232,12 +246,55 @@ def the_giant_video_loader(paddb, padfile,
normalizer=None, patches=False,
block_size=(96, 96), block_overlap=(0, 0),
random_patches_per_frame=None, augment=None,
multiple_bonafide_patches=1):
multiple_bonafide_patches=1, keep_pa_samples=None):
"""Loads a video pad file frame by frame and optionally applies
transformations.
Parameters
----------
paddb
Ignored.
padfile
The pad file
region : str
Either `whole` or `crop`. If whole, it will return the whole frame.
Otherwise, you need to provide a cropper and a normalizer.
scaling_factor : float
If given, will scale images to this factor.
cropper
The cropper to use
normalizer
The normalizer to use
patches : bool
If true, will extract patches from images.
block_size : tuple
Size of the patches
block_overlap : tuple
Size of overlap of the patches
random_patches_per_frame : int
If not None, will only take this much patches per frame
augment
If given, frames will be transformed using this function.
multiple_bonafide_patches : int
Will use more random patches for bonafide samples
keep_pa_samples : float
If given, will drop some PA samples.
Returns
-------
object
A generator that yields the samples.
Raises
------
ValueError
If region is not whole or crop.
"""
if region == 'whole':
generator = yield_frames(paddb, padfile)
generator = padfile.frames
elif region == 'crop':
generator = yield_faces(
paddb, padfile, cropper=cropper, normalizer=normalizer)
padfile, cropper=cropper, normalizer=normalizer)
else:
raise ValueError("Invalid region value: `{}'".format(region))
......@@ -248,7 +305,8 @@ def the_giant_video_loader(paddb, padfile,
if random_patches_per_frame is None:
generator = (
patch for frame in generator
for patch in blocks(frame, block_size, block_overlap))
for patch in blocks_generator(
frame, block_size, block_overlap))
else:
if padfile.attack_type is None:
random_patches_per_frame *= multiple_bonafide_patches
......@@ -261,4 +319,8 @@ def the_giant_video_loader(paddb, padfile,
if augment is not None:
generator = (augment(frame) for frame in generator)
if keep_pa_samples is not None and padfile.attack_type is not None:
generator = (frame for frame in generator
if random.random() < keep_pa_samples)
return generator
......@@ -80,6 +80,6 @@ Utilities
bob.pad.face.utils.scale_face
bob.pad.face.utils.the_giant_video_loader
bob.pad.face.utils.yield_faces
bob.pad.face.utils.yield_frames
.. automodule:: bob.pad.face.utils
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment