diff --git a/bob/pad/face/database/batl.py b/bob/pad/face/database/batl.py index 8452baf74d4ee26f3845dcd7225ee4bb2333bf3f..0ff725d4ea8b963c354455f7b1085641bca264ba 100644 --- a/bob/pad/face/database/batl.py +++ b/bob/pad/face/database/batl.py @@ -17,6 +17,14 @@ import bob.io.base import pkg_resources +map_file = pkg_resources.resource_filename('bob.pad.face', 'lists/batl/idiap_converter_v2.json') + +with open(map_file, 'r') as fp: + map_dict=json.load(fp) + +two_d_attacks=['prints','replay'] +three_d_attacks=['fakehead','glasses','flexiblemask','papermask','rigidmask','makeup'] +BATL_FRAME_SHAPE = (3, 1920, 1080) # ============================================================================= class BatlPadFile(PadFile): @@ -77,7 +85,16 @@ class BatlPadFile(PadFile): self.f = f if f.is_attack(): - attack_type = 'attack' + pai_desc=map_dict["_"+"_".join(os.path.split(f.path)[-1].split("_")[-2:])].split("_")[-1] + + if pai_desc in two_d_attacks: + category='2D' + elif pai_desc in three_d_attacks: + category='3D' + else: + category='Unknown' + + attack_type = 'attack/' + category +'/'+pai_desc else: attack_type = None @@ -160,6 +177,99 @@ class BatlPadFile(PadFile): return data_all_streams + @property + def annotations(self): + file_path = os.path.join(self.annotations_temp_dir, self.f.path + ".json") + + if not os.path.isfile(file_path): # no file with annotations + + try: + # original values of the arguments of f: + stream_type_original = self.stream_type + reference_stream_type_original = self.reference_stream_type + warp_to_reference_original = self.warp_to_reference + convert_to_rgb_original = self.convert_to_rgb + crop_original = self.crop + video_data_only_original = self.video_data_only + self.stream_type = "color" + self.reference_stream_type = "color" + self.warp_to_reference = False + self.convert_to_rgb = False + self.crop = None + self.video_data_only = True + + video = self.load(directory=self.original_directory, + extension=self.original_extension) + + finally: + # set arguments of f to the original values: + self.stream_type = stream_type_original + self.reference_stream_type = reference_stream_type_original + self.warp_to_reference = warp_to_reference_original + self.convert_to_rgb = convert_to_rgb_original + self.crop = crop_original + self.video_data_only = video_data_only_original + + annotations = {} + + for idx, image in enumerate(video.as_array()): + + frame_annotations = detect_face_landmarks_in_image(image, method=self.landmark_detect_method) + + if frame_annotations: + + annotations[str(idx)] = frame_annotations + + if self.annotations_temp_dir: # if directory is not an empty string + + bob.io.base.create_directories_safe(directory=os.path.split(file_path)[0], dryrun=False) + + with open(file_path, 'w+') as json_file: + + json_file.write(json.dumps(annotations)) + + else: # if file with annotations exists load them from file + + with open(file_path, 'r') as json_file: + + annotations = json.load(json_file) + + if not annotations: # if dictionary is empty + + return None + + # If specified append annotations for the roi in the facial region: + if self.append_color_face_roi_annot: + + file_path = pkg_resources.resource_filename('bob.pad.face', os.path.join('lists/batl/color_skin_non_skin_annotations/', "annotations_train_dev_set" + ".json")) + + with open(file_path, 'r') as json_file: # open the file containing all annotations: + + roi_annotations = json.load(json_file) # load the annotations + + if not self.f.path in roi_annotations: # no annotations for this file + + return None + + else: # annotations are available + + annotations['face_roi'] = roi_annotations[self.f.path] + + return annotations + + @property + def frames(self): + frame_container = self.load(directory=self.original_directory, extension=self.original_extension) + return frame_container.as_array() + + @property + def number_of_frames(self): + return len(self.frames) + + @property + def frame_shape(self): + return BATL_FRAME_SHAPE + # ============================================================================= class BatlPadDatabase(PadDatabase): @@ -173,7 +283,8 @@ class BatlPadDatabase(PadDatabase): protocol='grandtest', original_directory=rc['bob.db.batl.directory'], original_extension='.h5', - annotations_temp_dir=rc['bob.pad.face.database.batl.annotations_temp_dir'], + # annotations_temp_dir=rc['bob.pad.face.database.batl.annotations_temp_dir'], + annotations_temp_dir=None, landmark_detect_method="mtcnn", exclude_attacks_list=['makeup'], exclude_pai_all_sets=True, @@ -196,7 +307,7 @@ class BatlPadDatabase(PadDatabase): "grandtest-depth-5" - baseline protocol, depth data only, use 5 first frames. - "grandtest-color" - baseline protocol, depth data only, use all frames. + "grandtest-color" - baseline protocol, color data only, use all frames. "grandtest-infrared-50-join_train_dev" - baseline protocol, infrared data only, use 50 frames, join train and dev sets forming @@ -205,9 +316,9 @@ class BatlPadDatabase(PadDatabase): "grandtest-infrared-50-LOO_<unseen_attack>", for example "grandtest-infrared-50-LOO_fakehead" - Leave One Out (LOO) protocol with fakehead attacks present only in the `test` set. The original partitioning in the `grandtest` protocol is queried first and subselects the file list such - that the specified `unknown_attack` is removed from both `train` and `dev` sets. + that the specified `unknown_attack` is removed from both `train` and `dev` sets. The `test` set will consist of only the selected `unknown_attack` and `bonafide` files. - This protocol is used to evaluate the robustness against attacks unseen in training. + This protocol is used to evaluate the robustness against attacks unseen in training. . "grandtest-color*infrared-50" - baseline protocol, @@ -314,9 +425,9 @@ class BatlPadDatabase(PadDatabase): def unseen_attack_list_maker(self,files,unknown_attack,train=True): """ - Selects and returns a list of files for Leave One Out (LOO) protocols. - This utilizes the original partitioning in the `grandtest` protocol and subselects - the file list such that the specified `unknown_attack` is removed from both `train` and `dev` sets. + Selects and returns a list of files for Leave One Out (LOO) protocols. + This utilizes the original partitioning in the `grandtest` protocol and subselects + the file list such that the specified `unknown_attack` is removed from both `train` and `dev` sets. The `test` set will consist of only the selected `unknown_attack` and `bonafide` files. **Parameters:** @@ -344,14 +455,14 @@ class BatlPadDatabase(PadDatabase): attack_category=self.map_dict["_"+"_".join(os.path.split(file.path)[-1].split("_")[-2:])].split("_")[-1] if train: - if attack_category==unknown_attack: + if attack_category in unknown_attack: pass else: - mod_files.append(file) # everything except the attack specified is there + mod_files.append(file) # everything except the attack specified is there if not train: - if attack_category==unknown_attack or attack_category=='bonafide': + if attack_category in unknown_attack or attack_category=='bonafide': mod_files.append(file) # only the attack mentioned and bonafides in testing else: pass @@ -365,7 +476,7 @@ class BatlPadDatabase(PadDatabase): An example of protocols it can parse: "grandtest-color-5" - grandtest protocol, color data only, use 5 first frames. "grandtest-depth-5" - grandtest protocol, depth data only, use 5 first frames. - "grandtest-color" - grandtest protocol, depth data only, use all frames. + "grandtest-color" - grandtest protocol, color data only, use all frames. **Parameters:** @@ -392,7 +503,7 @@ class BatlPadDatabase(PadDatabase): forming a single training set. """ - possible_extras = ['join_train_dev','LOO_fakehead','LOO_flexiblemask','LOO_glasses','LOO_papermask','LOO_prints','LOO_replay','LOO_rigidmask','LOO_makeup'] + possible_extras = ['join_train_dev','LOO_fakehead','LOO_flexiblemask','LOO_glasses','LOO_papermask','LOO_prints','LOO_replay','LOO_rigidmask','LOO_makeup','PrintReplay'] components = protocol.split("-") @@ -537,7 +648,6 @@ class BatlPadDatabase(PadDatabase): ``files`` : [BatlPadFile] A list of BATLPadFile objects. """ - if protocol is None: protocol = self.protocol @@ -555,7 +665,6 @@ class BatlPadDatabase(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) - if not isinstance(groups, list) and groups is not None and not isinstance(groups,str): # if a single group is given make it a list groups = list(groups) @@ -597,7 +706,7 @@ class BatlPadDatabase(PadDatabase): batl_files=batl_files+tbatl_files - if 'validation' in groups: + if 'validation' in groups: tbatl_files = self.db.objects(protocol=protocol, groups=['validation'], purposes=purposes, **kwargs) @@ -621,6 +730,49 @@ class BatlPadDatabase(PadDatabase): + # for the PrintReplay protocol + elif extra == "PrintReplay": + + batl_files=[] + + # remove all attacks except replay and print + unknown_attack=["fakehead","flexiblemask","glasses","papermask","rigidmask","makeup"] + + if 'train' in groups: + tbatl_files = self.db.objects(protocol=protocol, + groups=['train'], + purposes=purposes, **kwargs) + + tbatl_files=self.unseen_attack_list_maker(tbatl_files,unknown_attack,train=True) + + batl_files=batl_files+tbatl_files + + + if 'validation' in groups: + tbatl_files = self.db.objects(protocol=protocol, + groups=['validation'], + purposes=purposes, **kwargs) + + tbatl_files=self.unseen_attack_list_maker(tbatl_files,unknown_attack,train=True) + + + batl_files=batl_files+tbatl_files + + if 'test' in groups: + tbatl_files = self.db.objects(protocol=protocol, + groups=['test'], + purposes=purposes, **kwargs) + + # train=True since we want to remove all attacks except replay and print + tbatl_files=self.unseen_attack_list_maker(tbatl_files,unknown_attack,train=True) + + batl_files=batl_files+tbatl_files + + + files=batl_files + + + else: # files = self._fix_funny_eyes_in_objects(protocol=protocol, # groups=groups, @@ -644,10 +796,15 @@ class BatlPadDatabase(PadDatabase): files = [f for f in files if os.path.split(f.path)[-1].split("_")[-2:-1][0] != "5"] files = [BatlPadFile(f, stream_type, max_frames) for f in files] + for f in files: + f.original_directory = self.original_directory + f.original_extension = self.original_extension + f.annotations_temp_dir = self.annotations_temp_dir + f.append_color_face_roi_annot = self.append_color_face_roi_annot + f.landmark_detect_method = self.landmark_detect_method return files - def annotations(self, f): """ Computes annotations for a given file object ``f``, which @@ -674,82 +831,4 @@ class BatlPadDatabase(PadDatabase): ``frameN_dict`` contains coordinates of the face bounding box and landmarks in frame N. """ - - file_path = os.path.join(self.annotations_temp_dir, f.f.path + ".json") - - if not os.path.isfile(file_path): # no file with annotations - - # original values of the arguments of f: - stream_type_original = f.stream_type - reference_stream_type_original = f.reference_stream_type - warp_to_reference_original = f.warp_to_reference - convert_to_rgb_original = f.convert_to_rgb - crop_original = f.crop - video_data_only_original = f.video_data_only - - f.stream_type = "color" - f.reference_stream_type = "color" - f.warp_to_reference = False - f.convert_to_rgb = False - f.crop = None - f.video_data_only = True - - video = f.load(directory=self.original_directory, - extension=self.original_extension) - - # set arguments of f to the original values: - f.stream_type = stream_type_original - f.reference_stream_type = reference_stream_type_original - f.warp_to_reference = warp_to_reference_original - f.convert_to_rgb = convert_to_rgb_original - f.crop = crop_original - f.video_data_only = video_data_only_original - - annotations = {} - - for idx, image in enumerate(video.as_array()): - - frame_annotations = detect_face_landmarks_in_image(image, method=self.landmark_detect_method) - - if frame_annotations: - - annotations[str(idx)] = frame_annotations - - if self.annotations_temp_dir: # if directory is not an empty string - - bob.io.base.create_directories_safe(directory=os.path.split(file_path)[0], dryrun=False) - - with open(file_path, 'w+') as json_file: - - json_file.write(json.dumps(annotations)) - - else: # if file with annotations exists load them from file - - with open(file_path, 'r') as json_file: - - annotations = json.load(json_file) - - if not annotations: # if dictionary is empty - - return None - - # If specified append annotations for the roi in the facial region: - if self.append_color_face_roi_annot: - - file_path = pkg_resources.resource_filename( 'bob.pad.face', os.path.join('lists/batl/color_skin_non_skin_annotations/', "annotations_train_dev_set" + ".json") ) - - with open(file_path, 'r') as json_file: # open the file containing all annotations: - - roi_annotations = json.load(json_file) # load the annotations - - if not f.f.path in roi_annotations: # no annotations for this file - - return None - - else: # annotations are available - - annotations['face_roi'] = roi_annotations[f.f.path] - - return annotations - - + return f.annotations diff --git a/bob/pad/face/database/casiafasd.py b/bob/pad/face/database/casiafasd.py index 7f8e5ca3b999f1c027844911ad0d1e57764fa0df..f3aea4406060bfca1a287408702c134e7c955ec9 100644 --- a/bob/pad/face/database/casiafasd.py +++ b/bob/pad/face/database/casiafasd.py @@ -8,6 +8,8 @@ from bob.pad.base.database import PadDatabase from bob.pad.face.database import VideoPadFile from bob.db.base.utils import ( check_parameter_for_validity, check_parameters_for_validity) +from bob.db.base.annotations import read_annotation_file +from bob.ip.facedetect import expected_eye_positions, BoundingBox import numpy import os @@ -20,7 +22,7 @@ class CasiaFasdPadFile(VideoPadFile): A high level implementation of the File class for the CASIA_FASD database. """ - def __init__(self, f, original_directory=None): + def __init__(self, f, original_directory=None, annotation_directory=None): """ Parameters ---------- @@ -31,6 +33,7 @@ class CasiaFasdPadFile(VideoPadFile): self.f = f self.original_directory = original_directory + self.annotation_directory = annotation_directory if f.is_real(): attack_type = None @@ -93,6 +96,10 @@ class CasiaFasdPadFile(VideoPadFile): def annotations(self): """Reads the annotations + 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` @@ -103,6 +110,10 @@ class CasiaFasdPadFile(VideoPadFile): is the dictionary defining the coordinates of the face bounding box in frame N. """ + if self.annotation_directory is not None: + path = self.make_path(self.annotation_directory, extension=".json") + return read_annotation_file(path, annotation_type="json") + annots = self.f.bbx() annotations = {} for i, v in enumerate(annots): @@ -110,6 +121,9 @@ class CasiaFasdPadFile(VideoPadFile): bottomright = (v[2] + v[4], v[1] + v[3]) annotations[str(i)] = {'topleft': topleft, 'bottomright': bottomright} + size = (bottomright[0] - topleft[0], bottomright[1] - topleft[1]) + bounding_box = BoundingBox(topleft, size) + annotations[str(i)].update(expected_eye_positions(bounding_box)) return annotations def load(self, directory=None, extension='.avi', @@ -149,6 +163,7 @@ class CasiaFasdPadDatabase(PadDatabase): # grandtest is the new modified protocol for this database protocol='grandtest', original_directory=rc['bob.db.casia_fasd.directory'], + annotation_directory=None, **kwargs): """ Parameters @@ -169,6 +184,7 @@ class CasiaFasdPadDatabase(PadDatabase): protocol=protocol, original_directory=original_directory, original_extension='.avi', + annotation_directory=annotation_directory, training_depends_on_protocol=True, **kwargs) @@ -280,7 +296,8 @@ class CasiaFasdPadDatabase(PadDatabase): db_mappings[t + '_' + q]) files.append(CasiaFasdPadFile( File(filename, c, grp), - self.original_directory)) + original_directory=self.original_directory, + annotation_directory=self.annotation_directory)) return files def annotations(self, padfile): diff --git a/bob/pad/face/database/database.py b/bob/pad/face/database/database.py index 8f88445f8d0ff4614ddd6d8d24fffde186bfa1b3..40d59d2298ce2d67c961d5221e6200572648c661 100644 --- a/bob/pad/face/database/database.py +++ b/bob/pad/face/database/database.py @@ -1,5 +1,7 @@ from bob.pad.base.database import PadFile import bob.bio.video +import bob.io.video +from bob.db.base.annotations import read_annotation_file class VideoPadFile(PadFile): @@ -9,14 +11,15 @@ class VideoPadFile(PadFile): def __init__(self, attack_type, client_id, path, file_id=None): super(VideoPadFile, self).__init__( - attack_type=attack_type, - client_id=client_id, - path=path, - file_id=file_id, + attack_type=attack_type, client_id=client_id, path=path, file_id=file_id ) - def load(self, directory=None, extension='.avi', - frame_selector=bob.bio.video.FrameSelector(selection_style='all')): + def load( + self, + directory=None, + extension=".avi", + frame_selector=bob.bio.video.FrameSelector(selection_style="all"), + ): """Loads the video file and returns in a :any:`bob.bio.video.FrameContainer`. Parameters @@ -34,3 +37,97 @@ class VideoPadFile(PadFile): The loaded frames inside a frame container. """ return frame_selector(self.make_path(directory, extension)) + + def check_original_directory_and_extension(self): + if not hasattr(self, "original_directory"): + raise RuntimeError( + "Please set the original_directory attribute of files in your " + "database's object method." + ) + if not hasattr(self, "original_extension"): + raise RuntimeError( + "Please set the original_extension attribute of files in your " + "database's object method." + ) + + @property + def frames(self): + """Returns an iterator of frames in the video. + If your database video files need to be loaded in a special way, you need to + override this property. + + Returns + ------- + collection.Iterator + An iterator returning frames of the video. + + Raises + ------ + RuntimeError + In your database implementation, the original_directory and + original_extension attributes of the files need to be set when database's + object method is called. + """ + self.check_original_directory_and_extension() + path = self.make_path( + directory=self.original_directory, extension=self.original_extension + ) + return iter(bob.io.video.reader(path)) + + @property + def number_of_frames(self): + self.check_original_directory_and_extension() + path = self.make_path( + directory=self.original_directory, extension=self.original_extension + ) + return bob.io.video.reader(path).number_of_frames + + @property + def frame_shape(self): + """Returns the size of each frame in this database. + This implementation assumes all videos and frames have the same shape. + It's best to override this method in your database implementation and return + a constant. + + Returns + ------- + (int, int, int) + The (Channels, Height, Width) sizes. + """ + self.check_original_directory_and_extension() + path = self.make_path( + directory=self.original_directory, extension=self.original_extension + ) + frame = next(bob.io.video.reader(path)) + return frame.shape + + @property + def annotations(self): + """Reads the annotations + For this property to work, you need to set ``annotation_directory``, + ``annotation_extension``, and ``annotation_type`` attributes of the files when + database's object method is called. + + Returns + ------- + dict + The annotations as a dictionary. + """ + if not ( + hasattr(self, "annotation_directory") + and hasattr(self, "annotation_extension") + and hasattr(self, "annotation_type") + ): + raise RuntimeError( + "For this property to work, you need to set ``annotation_directory``, " + "``annotation_extension``, and ``annotation_type`` attributes of the " + "files when database's object method is called." + ) + + if self.annotation_directory is None: + return None + + annotation_file = self.make_path( + directory=self.annotation_directory, extension=self.annotation_extension + ) + return read_annotation_file(annotation_file, self.annotation_type) diff --git a/bob/pad/face/database/maskattack.py b/bob/pad/face/database/maskattack.py index 64bc5bcc98c7f4f2c656f2650079fc67112d41f4..f636c57f5cd5f5fcee0ecd7cef79b37720c47547 100644 --- a/bob/pad/face/database/maskattack.py +++ b/bob/pad/face/database/maskattack.py @@ -5,7 +5,7 @@ import os import numpy as np import bob.io.video from bob.bio.video import FrameSelector, FrameContainer -from bob.pad.face.database import VideoPadFile +from bob.pad.face.database import VideoPadFile from bob.pad.base.database import PadDatabase class MaskAttackPadFile(VideoPadFile): @@ -17,18 +17,18 @@ class MaskAttackPadFile(VideoPadFile): f : :py:class:`object` An instance of the File class defined in the low level db interface of the 3DMAD database, in the bob.db.maskattack.models.py file. - + """ def __init__(self, f): """Init function - + Parameters ---------- f : :py:class:`object` An instance of the File class defined in the low level db interface of the 3DMAD database, in the bob.db.maskattack.models.py file. - + """ self.f = f if f.is_real(): @@ -48,10 +48,10 @@ class MaskAttackPadFile(VideoPadFile): Parameters ---------- directory : :py:class:`str` - String containing the path to the 3DMAD database + String containing the path to the 3DMAD database (generated sequences from original data). extension : :py:class:`str` - Extension of the video files + Extension of the video files frame_selector : :py:class:`bob.bio.video.FrameSelector` The frame selector to use. @@ -59,7 +59,7 @@ class MaskAttackPadFile(VideoPadFile): ------- video_data : :py:class:`bob.bio.video.utils.FrameContainer` video data stored in a FrameContainer - + """ vfilename = self.make_path(directory, extension) video = bob.io.video.reader(vfilename) @@ -69,7 +69,7 @@ class MaskAttackPadFile(VideoPadFile): class MaskAttackPadDatabase(PadDatabase): """High level implementation of the Database class for the 3DMAD database. - + Attributes ---------- db : :py:class:`bob.db.maskattack.Database` @@ -92,12 +92,12 @@ class MaskAttackPadDatabase(PadDatabase): The directory where the original data of the database are stored. original_extension : :py:class:`str` The file name extension of the original data. - + """ from bob.db.maskattack import Database as LowLevelDatabase self.db = LowLevelDatabase() - self.low_level_group_names = ('world', 'dev', 'test') + self.low_level_group_names = ('world', 'dev', 'test') self.high_level_group_names = ('train', 'dev', 'eval') super(MaskAttackPadDatabase, self).__init__( @@ -147,13 +147,13 @@ class MaskAttackPadDatabase(PadDatabase): groups = self.convert_names_to_lowlevel(groups, self.low_level_group_names, self.high_level_group_names) if groups is not None: - + # for training lowlevel_purposes = [] if 'world' in groups and purposes == 'real': - lowlevel_purposes.append('trainReal') + lowlevel_purposes.append('trainReal') if 'world' in groups and purposes == 'attack': - lowlevel_purposes.append('trainMask') + lowlevel_purposes.append('trainMask') # for dev and eval if ('dev' in groups or 'test' in groups) and purposes == 'real': @@ -163,10 +163,14 @@ class MaskAttackPadDatabase(PadDatabase): files = self.db.objects(sets=groups, purposes=lowlevel_purposes, **kwargs) files = [MaskAttackPadFile(f) for f in files] + # set the attributes + for f in files: + f.original_directory = self.original_directory + f.original_extension = self.original_extension return files - + def annotations(self, file): """Return annotations for a given file object. 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 diff --git a/bob/pad/face/test/test_databases.py b/bob/pad/face/test/test_databases.py index 594899a638264d15d881d95877a1735eeabe316e..0dd94cdc2cca4f5b77c80a44d1d3702430c16c51 100644 --- a/bob/pad/face/test/test_databases.py +++ b/bob/pad/face/test/test_databases.py @@ -224,7 +224,7 @@ def test_casiasurf(): assert len(casiasurf.objects(groups=('dev',), purposes=('real',))) == 2994 assert len(casiasurf.objects(groups=('dev',), purposes=('attack',))) == 6614 assert len(casiasurf.objects(groups=('dev',), purposes=('real','attack'))) == 9608 - assert len(casiasurf.objects(groups=('eval',), purposes=('real',))) == 17458 + assert len(casiasurf.objects(groups=('eval',), purposes=('real',))) == 17458 assert len(casiasurf.objects(groups=('eval',), purposes=('attack',))) == 40252 assert len(casiasurf.objects(groups=('eval',), purposes=('real','attack'))) == 57710 @@ -270,8 +270,10 @@ def test_casia_fasd(): # test annotations since they are shipped with bob.db.casia_fasd f = [f for f in casia_fasd.objects() if f.path == 'train_release/1/2'][0] assert len(f.annotations) == 132 - assert f.annotations['0'] == \ - {'topleft': (102, 214), 'bottomright': (242, 354)} + a = f.annotations['0'] + oracle = {'topleft': (102, 214), 'bottomright': (242, 354), + 'reye': (151.0, 249.0), 'leye': (151.0, 319.0)} + assert a == oracle, a @db_available('casia_fasd')