diff --git a/bob/pad/face/__init__.py b/bob/pad/face/__init__.py index 1d16bcbc879c7c6054b3c555e01c7e710eae801e..f42ae5389b813f1a2a9d7d9be7c1f78c5ad5200b 100644 --- a/bob/pad/face/__init__.py +++ b/bob/pad/face/__init__.py @@ -1,4 +1,4 @@ -from . import algorithm, extractor, preprocessor +from . import algorithm, extractor, preprocessor, database def get_config(): @@ -11,3 +11,4 @@ def get_config(): # gets sphinx autodoc done right - don't remove it __all__ = [_ for _ in dir() if not _.startswith('_')] + diff --git a/bob/pad/face/config/__init__.py b/bob/pad/face/config/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/bob/pad/face/config/database/__init__.py b/bob/pad/face/config/database/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/bob/pad/face/config/database/replay.py b/bob/pad/face/config/database/replay.py new file mode 100644 index 0000000000000000000000000000000000000000..f1cbd70cb6725aa1ff17a172456eecd24d6c8f70 --- /dev/null +++ b/bob/pad/face/config/database/replay.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python + +from bob.pad.face.database import ReplayPadDatabase + + +# Directory where the data files are stored. +# This directory is given in the .bob_bio_databases.txt file located in your home directory +original_directory = "[YOUR_REPLAY_ATTACK_DIRECTORY]" +original_extension = ".mov" # extension of the data files + + +database = ReplayPadDatabase( + protocol='grandtest', + original_directory=original_directory, + original_extension=original_extension, + training_depends_on_protocol=True, +) diff --git a/bob/pad/face/database/__init__.py b/bob/pad/face/database/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5d92e76c278fc31d6f36fa814b6ebe62643787cd --- /dev/null +++ b/bob/pad/face/database/__init__.py @@ -0,0 +1,21 @@ +from .replay import ReplayPadDatabase + +# gets sphinx autodoc done right - don't remove it +def __appropriate__(*args): + """Says object was actually declared here, and not in the import module. + Fixing sphinx warnings of not being able to find classes, when path is shortened. + Parameters: + + *args: An iterable of objects to modify + + Resolves `Sphinx referencing issues + <https://github.com/sphinx-doc/sphinx/issues/3048>` + """ + + for obj in args: obj.__module__ = __name__ + +__appropriate__( + ReplayPadDatabase, + ) + +__all__ = [_ for _ in dir() if not _.startswith('_')] diff --git a/bob/pad/face/database/replay.py b/bob/pad/face/database/replay.py new file mode 100644 index 0000000000000000000000000000000000000000..a272479b4f53967a3a1b159766d55fa4b7edca20 --- /dev/null +++ b/bob/pad/face/database/replay.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +Created on Thu May 4 12:03:36 2017 + +High level implementation for the REPLAY-ATTACK database + +@author: Olegs Nikisins <olegs.nikisins@idiap.ch> +""" + +#============================================================================== + +import bob.bio.video # Used in ReplayPadFile class + +from bob.pad.base.database import PadFile # Used in ReplayPadFile class + +from bob.pad.base.database import PadDatabase + +#============================================================================== + +class ReplayPadFile(PadFile): + """ + A high level implementation of the File class for the REPLAY-ATTACK database. + """ + + def __init__(self, f): + """ + **Parameters:** + + ``f`` : :py:class:`object` + An instance of the File class defined in the low level db interface + of the Replay database, in the bob.db.replay.models.py file. + """ + + self.f = f + # this f is actually an instance of the File class that is defined in + # bob.db.replay.models and the PadFile class here needs + # client_id, path, attack_type, file_id for initialization. We have to + # convert information here and provide them to PadFile. attack_type is a + # little tricky to get here. Based on the documentation of PadFile: + # In cased of a spoofed data, this parameter should indicate what kind of spoofed attack it is. + # The default None value is interpreted that the PadFile is a genuine or real sample. + if f.is_real(): + attack_type = None + else: + 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, path=f.path, + attack_type=attack_type, file_id=f.id) + + #========================================================================== + + def load(self, directory=None, extension='.mov'): + """ + Overridden version of the load method defined in the ``bob.db.base.File``. + + **Parameters:** + + ``directory`` : :py:class:`str` + String containing the path to the Replay database. + + ``extension`` : :py:class:`str` + Extension of the video files in the Replay database. + + **Returns:** + + ``filtered_image`` : :py:class:`dict` + A dictionary containing the key-value pairs: "video" key containing the frames data, + and "bbx" containing the coordinates of the face bounding boxes for each frame. + """ + + path = self.f.make_path(directory=directory, extension=extension) # path to the video file + + frame_selector = bob.bio.video.FrameSelector(selection_style = 'all') # this frame_selector will select all frames from the video file + + video_data = frame_selector(path) # video data + + return video_data # video data + +#============================================================================== + +class ReplayPadDatabase(PadDatabase): + """ + A high level implementation of the Database class for the REPLAY-ATTACK database. + """ + + def __init__( + self, + protocol='grandtest', # grandtest is the default protocol for this database + original_directory=None, + original_extension=None, + **kwargs): + """ + **Parameters:** + + ``protocol`` : :py:class:`str` or ``None`` + The name of the protocol that defines the default experimental setup for this database. + + ``original_directory`` : :py:class:`str` + The directory where the original data of the database are stored. + + ``original_extension`` : :py:class:`str` + The file name extension of the original data. + + ``kwargs`` + The arguments of the :py:class:`bob.bio.base.database.BioDatabase` base class constructor. + """ + + from bob.db.replay import Database as LowLevelDatabase + + self.db = LowLevelDatabase() + + # 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 + self.high_level_group_names = ('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', + protocol = protocol, + original_directory = original_directory, + original_extension = original_extension, + **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. + + Keyword parameters: + + ``groups`` : :py:class:`str` + OR a list of strings. + The groups of which the clients should be returned. + Usually, groups are one or more elements of ('train', 'dev', 'eval') + + ``protocol`` : :py:class:`str` + The protocol for which the clients should be retrieved. + The protocol is dependent on your database. + If you do not have protocols defined, just ignore this field. + + ``purposes`` : :py:class:`str` + OR a list of strings. + The purposes for which File objects should be retrieved. + Usually it is either 'real' or 'attack'. + + ``model_ids`` + This parameter is not supported in PAD databases yet + """ + # 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) + # 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) + files = [ReplayPadFile(f) for f in files] + return files + + #========================================================================== + + def annotations(self, f): + """ + Return annotations for a given file object ``f``, which is an instance + of ``ReplayPadFile`` defined in the HLDI of the Replay-Attack DB. + The ``load()`` method of ``ReplayPadFile`` class (see above) + returns a video, therefore this method returns bounding-box annotations + for each video frame. The annotations are returned as dictionary of dictionaries. + + **Parameters:** + + ``f`` : :py:class:`object` + An instance of ``ReplayPadFile`` defined above. + + **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. + """ + + annots = f.f.bbx(directory=self.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 + + 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 + diff --git a/bob/pad/face/preprocessor/VideoFaceCrop.py b/bob/pad/face/preprocessor/VideoFaceCrop.py new file mode 100644 index 0000000000000000000000000000000000000000..0c7aa5efe6329c866b967713c18a3e8e4d2bb004 --- /dev/null +++ b/bob/pad/face/preprocessor/VideoFaceCrop.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +Created on Fri May 12 14:14:23 2017 + +@author: onikisins +""" +#============================================================================== +# Import what is needed here: + +from bob.bio.base.preprocessor import Preprocessor + +from bob.bio.face.preprocessor import FaceCrop + +import bob.bio.video + +#============================================================================== +# Main body: + +class VideoFaceCrop(Preprocessor, object): + """ + This class is designed to crop faces in each frame of the input video given + annotations defining the position of the face. + + **Parameters:** + + ``cropped_image_size`` : (int, int) + The size of the resulting cropped images. + + ``cropped_positions`` : :py:class:`dict` + The coordinates in the cropped image, where the annotated points should be put to. + This parameter is a dictionary with usually two elements, e.g., ``{'reye':(RIGHT_EYE_Y, RIGHT_EYE_X) , 'leye':(LEFT_EYE_Y, LEFT_EYE_X)}``. + However, also other parameters, such as ``{'topleft' : ..., 'bottomright' : ...}`` are supported, as long as the ``annotations`` in the `__call__` function are present. + + ``fixed_positions`` : :py:class:`dict` + Or None. + If specified, ignore the annotations from the database and use these fixed positions throughout. + + ``mask_sigma`` : :py:class:`float` + Or None + Fill the area outside of image boundaries with random pixels from the border, by adding noise to the pixel values. + To disable extrapolation, set this value to ``None``. + To disable adding random noise, set it to a negative value or 0. + + ``mask_neighbors`` : :py:class:`int` + The number of neighbors used during mask extrapolation. + See :py:func:`bob.ip.base.extrapolate_mask` for details. + + ``mask_seed`` : :py:class:`int` + Or None. + The random seed to apply for mask extrapolation. + + .. warning:: + When run in parallel, the same random seed will be applied to all parallel processes. + Hence, results of parallel execution will differ from the results in serial execution. + + ``kwargs`` + Remaining keyword parameters passed to the :py:class:`Base` constructor, such as ``color_channel`` or ``dtype``. + + """ + + #========================================================================== + def __init__(self, + cropped_image_size, + cropped_positions, + fixed_positions = None, + mask_sigma = None, + mask_neighbors = 5, + mask_seed = None, + **kwargs): + + super(VideoFaceCrop, self).__init__(cropped_image_size = cropped_image_size, + cropped_positions = cropped_positions, + fixed_positions = fixed_positions, + mask_sigma = mask_sigma, + mask_neighbors = mask_neighbors, + mask_seed = mask_seed, + **kwargs) + + self.cropped_image_size = cropped_image_size + self.cropped_positions = cropped_positions + self.fixed_positions = fixed_positions + self.mask_sigma = mask_sigma + self.mask_neighbors = mask_neighbors + self.mask_seed = mask_seed + + # Save also the data stored in the kwargs: + for (k, v) in kwargs.items(): + setattr(self, k, v) + + self.preprocessor = FaceCrop(cropped_image_size = cropped_image_size, + cropped_positions = cropped_positions, + fixed_positions = fixed_positions, + mask_sigma = mask_sigma, + mask_neighbors = mask_neighbors, + mask_seed = mask_seed, + **kwargs) + + #========================================================================== + def __call__(self, frames, annotations): + """ + Crop the face in the input video frames given annotations for each frame. + + **Parameters:** + + ``image`` : FrameContainer + Video data stored in the FrameContainer, see ``bob.bio.video.utils.FrameContainer`` + for further details. + + ``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. + + **Returns:** + + ``preprocessed_video`` : FrameContainer + Cropped faces stored in the FrameContainer. + """ + + video_preprocessor = bob.bio.video.preprocessor.Wrapper(self.preprocessor) + + preprocessed_video = video_preprocessor(frames = frames, annotations = annotations) + + return preprocessed_video + + #========================================================================== + def write_data( self, frames, file_name ): + """ + Writes the given data (that has been generated using the __call__ function of this class) to file. + This method overwrites the write_data() method of the Preprocessor class. + + **Parameters:** + + ``frames`` : + data returned by the __call__ method of the class. + + ``file_name`` : :py:class:`str` + name of the file. + """ + + bob.bio.video.preprocessor.Wrapper.write_data(frames, file_name) + + #========================================================================== + def read_data( self, file_name ): + """ + Reads the preprocessed data from file. + his method overwrites the read_data() method of the Preprocessor class. + + **Parameters:** + + ``file_name`` : :py:class:`str` + name of the file. + + **Returns:** + + ``frames`` : :py:class:`bob.bio.video.FrameContainer` + Frames stored in the frame container. + """ + + frames = bob.bio.video.preprocessor.Wrapper.read_data(file_name) + + return frames + + diff --git a/bob/pad/face/preprocessor/__init__.py b/bob/pad/face/preprocessor/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..60586054834555c02d0e06f3964dc5272657e023 100644 --- a/bob/pad/face/preprocessor/__init__.py +++ b/bob/pad/face/preprocessor/__init__.py @@ -0,0 +1,25 @@ +from .VideoFaceCrop import VideoFaceCrop + + +def __appropriate__(*args): + """Says object was actually declared here, and not in the import module. + Fixing sphinx warnings of not being able to find classes, when path is + shortened. + + Parameters + ---------- + *args + The objects that you want sphinx to beleive that are defined here. + + Resolves `Sphinx referencing issues <https//github.com/sphinx- + doc/sphinx/issues/3048>` + """ + + for obj in args: + obj.__module__ = __name__ + + +__appropriate__( + VideoFaceCrop, +) +__all__ = [_ for _ in dir() if not _.startswith('_')] diff --git a/bob/pad/face/test/test_databases.py b/bob/pad/face/test/test_databases.py new file mode 100644 index 0000000000000000000000000000000000000000..19f956926bcbcda45ebd4127909967252404201a --- /dev/null +++ b/bob/pad/face/test/test_databases.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : +# Thu May 24 10:41:42 CEST 2012 + +from nose.plugins.skip import SkipTest + +import bob.bio.base +from bob.bio.base.test.utils import db_available + +@db_available('replay') +def test_replay(): + replay_database_instance = bob.bio.base.load_resource('replay', 'database', preferred_package='bob.pad.face', package_prefix='bob.pad.') + try: + + assert len( replay_database_instance.objects(groups=['train', 'dev', 'eval']) )== 1200 + assert len( replay_database_instance.objects(groups=['train', 'dev']) ) == 720 + assert len( replay_database_instance.objects(groups=['train']) ) == 360 + assert len( replay_database_instance.objects(groups=['train', 'dev', 'eval'], protocol = 'grandtest') )== 1200 + assert len( replay_database_instance.objects(groups=['train', 'dev', 'eval'], protocol = 'grandtest', purposes='real') ) == 200 + assert len( replay_database_instance.objects(groups=['train', 'dev', 'eval'], protocol = 'grandtest', purposes='attack') ) == 1000 + + except IOError as e: + raise SkipTest( + "The database could not be queried; probably the db.sql3 file is missing. Here is the error: '%s'" % e) diff --git a/setup.py b/setup.py index 7dc09b29749731b7e17836b1c766f522ff6e3662..3ccff59989c4aa43e4f921a72c89f461513a1b32 100644 --- a/setup.py +++ b/setup.py @@ -88,10 +88,15 @@ setup( # the version of bob. entry_points = { - # scripts should be declared using this entry: - 'console_scripts' : [ - 'version.py = bob.pad.face.script.version:main', - ], + # scripts should be declared using this entry: + 'console_scripts' : [ + 'version.py = bob.pad.face.script.version:main', + ], + + 'bob.pad.database': [ + 'replay = bob.pad.face.config.database.replay:database', + ], + }, # Classifiers are important if you plan to distribute this package through diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..0b585f15407adc7db098f6c64c870b2bc7ae47c4 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +bob.db.replay