diff --git a/bob/pad/face/database/msu_mfsd.py b/bob/pad/face/database/msu_mfsd.py index 4843ff52ea726d9c118273dab11b547b126ee820..59e055a8dad5d6c00e4f43df64cee32ae6856580 100644 --- a/bob/pad/face/database/msu_mfsd.py +++ b/bob/pad/face/database/msu_mfsd.py @@ -1,20 +1,14 @@ #!/usr/bin/env python2 # -*- coding: utf-8 -*- -#============================================================================== -# Used in ReplayMobilePadFile class from bob.bio.video import FrameSelector, FrameContainer - from bob.pad.face.database import VideoPadFile # Used in MsuMfsdPadFile class - from bob.pad.base.database import PadDatabase - +from bob.extension import rc import os - import numpy as np -#============================================================================== class MsuMfsdPadFile(VideoPadFile): """ A high level implementation of the File class for the MSU MFSD database. @@ -40,18 +34,20 @@ class MsuMfsdPadFile(VideoPadFile): if f.is_real(): attack_type = None else: - attack_type = 'attack' + attack_type = "attack" # attack_type is a string and I decided to make it like this for this # particular database. You can do whatever you want for your own database. super(MsuMfsdPadFile, self).__init__( - client_id=f.client_id, - path=f.path, - attack_type=attack_type, - file_id=f.id) - - #========================================================================== - def load(self, directory=None, extension=None, frame_selector=FrameSelector(selection_style='all')): + client_id=f.client_id, path=f.path, attack_type=attack_type, file_id=f.id + ) + + def load( + self, + directory=None, + extension=None, + frame_selector=FrameSelector(selection_style="all"), + ): """ Overridden version of the load method defined in the ``VideoPadFile``. @@ -76,26 +72,27 @@ class MsuMfsdPadFile(VideoPadFile): for further details. """ - _, extension = os.path.splitext( - self.f.videofile()) # get file extension + _, extension = os.path.splitext(self.f.videofile()) # get file extension - video_data_array = self.f.load( - directory=directory, extension=extension) + video_data_array = self.f.load(directory=directory, extension=extension) return frame_selector(video_data_array) -#============================================================================== class MsuMfsdPadDatabase(PadDatabase): """ A high level implementation of the Database class for the MSU MFSD database. """ def __init__( - self, - protocol='grandtest', # grandtest is the default protocol for this database - original_directory=None, - original_extension=None, - **kwargs): + self, + protocol="grandtest", # grandtest is the default protocol for this database + original_directory=None, + original_extension=None, + annotation_directory=None, + annotation_extension='.json', + annotation_type='json', + **kwargs + ): """ **Parameters:** @@ -119,19 +116,24 @@ class MsuMfsdPadDatabase(PadDatabase): # Since the high level API expects different group names than what the low # level API offers, you need to convert them when necessary self.low_level_group_names = ( - 'train', 'devel', - 'test') # group names in the low-level database interface + "train", + "devel", + "test", + ) # group names in the low-level database interface self.high_level_group_names = ( - 'train', 'dev', - 'eval') # names are expected to be like that in objects() function + "train", + "dev", + "eval", + ) # names are expected to be like that in objects() function # Always use super to call parent class methods. super(MsuMfsdPadDatabase, self).__init__( - name='msu-mfsd', + name="msu-mfsd", protocol=protocol, original_directory=original_directory, original_extension=original_extension, - **kwargs) + **kwargs + ) @property def original_directory(self): @@ -141,13 +143,9 @@ class MsuMfsdPadDatabase(PadDatabase): def original_directory(self, value): self.db.original_directory = value - #========================================================================== - def objects(self, - groups=None, - protocol=None, - purposes=None, - model_ids=None, - **kwargs): + def objects( + self, groups=None, protocol=None, purposes=None, model_ids=None, **kwargs + ): """ This function returns lists of MsuMfsdPadFile objects, which fulfill the given restrictions. @@ -179,16 +177,21 @@ class MsuMfsdPadDatabase(PadDatabase): # Convert group names to low-level group names here. groups = self.convert_names_to_lowlevel( - groups, self.low_level_group_names, self.high_level_group_names) + groups, self.low_level_group_names, self.high_level_group_names + ) # Since this database was designed for PAD experiments, nothing special # needs to be done here. files = self.db.objects(group=groups, cls=purposes, **kwargs) files = [MsuMfsdPadFile(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): """ Return annotations for a given file object ``f``, which is an instance @@ -220,12 +223,14 @@ class MsuMfsdPadDatabase(PadDatabase): for frame_annots in annots: topleft = (np.int(frame_annots[2]), np.int(frame_annots[1])) - bottomright = (np.int(frame_annots[2] + frame_annots[4]), - np.int(frame_annots[1] + frame_annots[3])) + bottomright = ( + np.int(frame_annots[2] + frame_annots[4]), + np.int(frame_annots[1] + frame_annots[3]), + ) annotations[str(np.int(frame_annots[0]))] = { - 'topleft': topleft, - 'bottomright': bottomright + "topleft": topleft, + "bottomright": bottomright, } return annotations diff --git a/bob/pad/face/database/replay.py b/bob/pad/face/database/replay.py index 56004ecffcd6cdc2fe40cdbd823ddb86ba15e558..1d3803e78d3d48ac29c47e215b64be4cfe692835 100644 --- a/bob/pad/face/database/replay.py +++ b/bob/pad/face/database/replay.py @@ -1,11 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Used in ReplayMobilePadFile class -from bob.pad.base.database import PadDatabase +from bob.pad.base.database import PadDatabase # Used in ReplayMobilePadFile class from bob.pad.face.database import VideoPadFile # Used in ReplayPadFile class -from bob.pad.face.utils import frames, number_of_frames from bob.extension import rc +from bob.ip.facedetect import expected_eye_positions, BoundingBox +from bob.db.base.annotations import read_annotation_file + +REPLAY_ATTACK_FRAME_SHAPE = (3, 240, 320) class ReplayPadFile(VideoPadFile): @@ -35,16 +37,73 @@ class ReplayPadFile(VideoPadFile): if f.is_real(): attack_type = None else: - attack_type = 'attack' + attack_type = "attack" # attack_type is a string and I decided to make it like this for this # particular database. You can do whatever you want for your own # database. super(ReplayPadFile, self).__init__( - client_id=f.client_id, - path=f.path, - attack_type=attack_type, - file_id=f.id) + client_id=f.client_id, path=f.path, attack_type=attack_type, file_id=f.id + ) + + @property + def frame_shape(self): + """Returns the size of each frame in this database. + + Returns + ------- + (int, int, int) + The (#Channels, Height, Width) which is (3, 240, 320). + """ + return REPLAY_ATTACK_FRAME_SHAPE + + @property + def annotations(self): + """ + Return annotations as a dictionary of dictionaries. + + If the file object has an attribute of annotation_directory, it will read + annotations from there instead of loading annotations that are shipped with the + database. + + Returns + ------- + annotations : :py:class:`dict` + 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)}`` + is the dictionary defining the coordinates of the face bounding box + in frame N. + """ + if ( + hasattr(self, "annotation_directory") + and self.annotation_directory is not None + ): + path = self.make_path(self.annotation_directory, extension=".json") + return read_annotation_file(path, annotation_type="json") + + # 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 = self.f.bbx(directory=self.original_directory) + + annotations = {} # dictionary to return + + for fn, frame_annots in enumerate(annots): + + topleft = (frame_annots[2], frame_annots[1]) + bottomright = ( + frame_annots[2] + frame_annots[4], + frame_annots[1] + frame_annots[3], + ) + annotations[str(fn)] = {"topleft": topleft, "bottomright": bottomright} + + size = (bottomright[0] - topleft[0], bottomright[1] - topleft[1]) + bounding_box = BoundingBox(topleft, size) + annotations[str(fn)].update(expected_eye_positions(bounding_box)) + + return annotations class ReplayPadDatabase(PadDatabase): @@ -54,12 +113,14 @@ class ReplayPadDatabase(PadDatabase): """ def __init__( - self, - # grandtest is the default protocol for this database - protocol='grandtest', - original_directory=rc['bob.db.replay.directory'], - original_extension='.mov', - **kwargs): + self, + # grandtest is the default protocol for this database + protocol="grandtest", + original_directory=rc["bob.db.replay.directory"], + original_extension=".mov", + annotation_directory=None, + **kwargs + ): """ Parameters ---------- @@ -86,19 +147,25 @@ class ReplayPadDatabase(PadDatabase): # Since the high level API expects different group names than what the # low level API offers, you need to convert them when necessary self.low_level_group_names = ( - 'train', 'devel', - 'test') # group names in the low-level database interface + "train", + "devel", + "test", + ) # group names in the low-level database interface self.high_level_group_names = ( - 'train', 'dev', - 'eval') # names are expected to be like that in objects() function + "train", + "dev", + "eval", + ) # names are expected to be like that in objects() function # Always use super to call parent class methods. super(ReplayPadDatabase, self).__init__( - name='replay', + name="replay", protocol=protocol, original_directory=original_directory, original_extension=original_extension, - **kwargs) + annotation_directory=annotation_directory, + **kwargs + ) @property def original_directory(self): @@ -108,12 +175,9 @@ class ReplayPadDatabase(PadDatabase): def original_directory(self, value): self.db.original_directory = value - def objects(self, - groups=None, - protocol=None, - purposes=None, - model_ids=None, - **kwargs): + def objects( + self, groups=None, protocol=None, purposes=None, model_ids=None, **kwargs + ): """ This function returns lists of ReplayPadFile objects, which fulfill the given restrictions. @@ -146,12 +210,19 @@ class ReplayPadDatabase(PadDatabase): # Convert group names to low-level group names here. groups = self.convert_names_to_lowlevel( - groups, self.low_level_group_names, self.high_level_group_names) + groups, self.low_level_group_names, self.high_level_group_names + ) # Since this database was designed for PAD experiments, nothing special # needs to be done here. files = self.db.objects( - protocol=protocol, groups=groups, cls=purposes, **kwargs) + protocol=protocol, groups=groups, cls=purposes, **kwargs + ) files = [ReplayPadFile(f) for f in files] + # set the attributes + for f in files: + f.original_directory = self.original_directory + f.original_extension = self.original_extension + f.annotation_directory = self.annotation_directory return files def annotations(self, f): @@ -178,26 +249,7 @@ class ReplayPadDatabase(PadDatabase): is the dictionary defining the coordinates of the face bounding box in frame N. """ - - # 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[2], frame_annots[1]) - bottomright = (frame_annots[2] + frame_annots[4], - frame_annots[1] + frame_annots[3]) - - 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. @@ -212,11 +264,7 @@ class ReplayPadDatabase(PadDatabase): :any:`numpy.array` A frame of the video. The size is (3, 240, 320). """ - vfilename = padfile.make_path( - directory=self.original_directory, - extension=self.original_extension) - for retval in frames(vfilename): - yield retval + return padfile.frames def number_of_frames(self, padfile): """Returns the number of frames in a video file. @@ -231,10 +279,7 @@ class ReplayPadDatabase(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): @@ -245,4 +290,4 @@ class ReplayPadDatabase(PadDatabase): (int, int, int) The (#Channels, Height, Width) which is (3, 240, 320). """ - return (3, 240, 320) + return REPLAY_ATTACK_FRAME_SHAPE diff --git a/bob/pad/face/database/replay_mobile.py b/bob/pad/face/database/replay_mobile.py index 8e2547c37256d04d11603ca36ea0ca5b7ab2f942..b7fe209db2c1e3f2feb4a25ec1e03ab01063b1e9 100644 --- a/bob/pad/face/database/replay_mobile.py +++ b/bob/pad/face/database/replay_mobile.py @@ -5,48 +5,13 @@ from bob.bio.video import FrameSelector from bob.pad.base.database import PadDatabase from bob.pad.face.database import VideoPadFile -from bob.pad.face.utils import frames, number_of_frames +from bob.pad.face.utils import number_of_frames from bob.db.base.annotations import read_annotation_file -import numpy from bob.extension import rc 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): """ A high level implementation of the File class for the Replay-Mobile @@ -116,6 +81,7 @@ class ReplayMobilePadFile(VideoPadFile): @property def annotations(self): + from bob.db.replaymobile.models import replaymobile_annotations if self.annotation_directory is not None: # return the external annotations annotations = read_annotation_file( @@ -129,6 +95,7 @@ class ReplayMobilePadFile(VideoPadFile): @property def frames(self): + from bob.db.replaymobile.models import replaymobile_frames return replaymobile_frames(self.f, self.original_directory) @property