diff --git a/bob/pad/face/config/casiafasd.py b/bob/pad/face/config/casia_fasd.py similarity index 92% rename from bob/pad/face/config/casiafasd.py rename to bob/pad/face/config/casia_fasd.py index a6e713511d43097e4894f2518c10eae6b950f186..991295545b9bbd5ed4bb34fb05f7d89d55acc1ee 100644 --- a/bob/pad/face/config/casiafasd.py +++ b/bob/pad/face/config/casia_fasd.py @@ -1,5 +1,5 @@ """Config file for the CASIA FASD dataset. -Please run ``bob config set bob.db.casia_fasd.directory /path/to/casia_fasd_files`` +Please run ``bob config set bob.db.casia_fasd.directory /path/to/database/casia_fasd/`` in terminal to point to the original files of the dataset on your computer.""" from bob.pad.face.database import CasiaFasdPadDatabase diff --git a/bob/pad/face/config/casia_surf.py b/bob/pad/face/config/casia_surf.py new file mode 100644 index 0000000000000000000000000000000000000000..28c3e06dbae9c18c3e7fce14f156926ca7373c76 --- /dev/null +++ b/bob/pad/face/config/casia_surf.py @@ -0,0 +1,10 @@ +"""The `CASIA-SURF`_ database for face anti-spoofing + +After downloading, you can tell the bob library where the files are located +using:: + + $ bob config set bob.db.casia_surf.directory /path/to/database/CASIA-SURF/ +""" +from bob.pad.face.database import CasiaSurfPadDatabase + +database = CasiaSurfPadDatabase() diff --git a/bob/pad/face/config/casiasurf.py b/bob/pad/face/config/casiasurf.py deleted file mode 100644 index 05f6dd81e61ae3f59ced2b75c58e547546bf5a1e..0000000000000000000000000000000000000000 --- a/bob/pad/face/config/casiasurf.py +++ /dev/null @@ -1,3 +0,0 @@ -from bob.pad.face.database import CasiaSurfPadDatabase - -database = CasiaSurfPadDatabase() diff --git a/bob/pad/face/config/casiasurf_color.py b/bob/pad/face/config/casiasurf_color.py deleted file mode 100644 index 7f9653eb448f587312f781c40a35a82be41a4cbe..0000000000000000000000000000000000000000 --- a/bob/pad/face/config/casiasurf_color.py +++ /dev/null @@ -1,3 +0,0 @@ -from bob.pad.face.database import CasiaSurfPadDatabase - -database = CasiaSurfPadDatabase(stream_type="color") diff --git a/bob/pad/face/config/deep_pix_bis.py b/bob/pad/face/config/deep_pix_bis.py index 721679d6a56b731e797f0bdab07350b59d01fe64..907db63e4e39562dc9daa90e3cdfdf30433eaf03 100644 --- a/bob/pad/face/config/deep_pix_bis.py +++ b/bob/pad/face/config/deep_pix_bis.py @@ -1,3 +1,16 @@ +""" Deep Pixel-wise Binary Supervision for Face PAD + +This baseline includes the models to replicate the experimental results published in the following publication:: + + @INPROCEEDINGS{GeorgeICB2019, + author = {Anjith George, Sebastien Marcel}, + title = {Deep Pixel-wise Binary Supervision for Face Presentation Attack Detection}, + year = {2019}, + booktitle = {ICB 2019}, + } + +""" + from sklearn.pipeline import Pipeline import bob.pipelines as mario @@ -9,10 +22,12 @@ from bob.pad.face.deep_pix_bis import DeepPixBisClassifier from bob.pad.face.transformer import VideoToFrames database = globals().get("database") +annotation_type, fixed_positions = None, None if database is not None: annotation_type = database.annotation_type fixed_positions = database.fixed_positions -else: + +if annotation_type is None: annotation_type = "eyes-center" fixed_positions = None @@ -39,7 +54,7 @@ preprocessor = mario.wrap( ) # Classifier # -classifier = DeepPixBisClassifier(model_file="oulunpu-p1") +classifier = DeepPixBisClassifier(model_file="oulu-npu-p1") classifier = mario.wrap(["sample"], classifier) # change the decision_function decision_function = "predict_proba" diff --git a/bob/pad/face/config/mask_attack.py b/bob/pad/face/config/mask_attack.py new file mode 100644 index 0000000000000000000000000000000000000000..d6d56e54f57c3d78510146a0bc38059e91191742 --- /dev/null +++ b/bob/pad/face/config/mask_attack.py @@ -0,0 +1,24 @@ +"""The `Mask-Attack`_ database for face anti-spoofing +consists of video clips of mask attacks. This database was produced at the +`Idiap Research Institute <http://www.idiap.ch>`_, in Switzerland. + +If you use this database in your publication, please cite the following paper on +your references: + + + @INPROCEEDINGS{ERDOGMUS_BTAS-2013, + author = {Erdogmus, Nesli and Marcel, Sébastien}, + keywords = {biometric, Counter-Measures, Spoofing Attacks}, + month = september, + title = {Spoofing in 2D Face Recognition with 3D Masks and Anti-spoofing with Kinect}, + journal = {Biometrics: Theory, Applications and Systems}, + year = {2013},} + +After downloading, you can tell the bob library where the files are located +using:: + + $ bob config set bob.db.mask_attack.directory /path/to/database/3dmad/Data/ +""" +from bob.pad.face.database import MaskAttackPadDatabase + +database = MaskAttackPadDatabase() diff --git a/bob/pad/face/config/maskattack.py b/bob/pad/face/config/maskattack.py deleted file mode 100644 index 27dda510e59a2c9fc1efbad74364731ee267a738..0000000000000000000000000000000000000000 --- a/bob/pad/face/config/maskattack.py +++ /dev/null @@ -1,3 +0,0 @@ -from bob.pad.face.database import MaskAttackPadDatabase - -database = MaskAttackPadDatabase() diff --git a/bob/pad/face/config/oulunpu.py b/bob/pad/face/config/oulu_npu.py similarity index 83% rename from bob/pad/face/config/oulunpu.py rename to bob/pad/face/config/oulu_npu.py index d26e331206556dc3e03eafaa4f69a739966de587..3e3713c8f5b9ed0ebfde7343c7d60caa94d33dcb 100644 --- a/bob/pad/face/config/oulunpu.py +++ b/bob/pad/face/config/oulu_npu.py @@ -3,7 +3,7 @@ A mobile face presentation attack database with real-world variations database. To configure the location of the database on your computer, run:: - bob config set bob.db.oulunpu.directory /path/to/oulunpu/database + bob config set bob.db.oulu_npu.directory /path/to/database/oulu-npu If you use this database, please cite the following publication:: @@ -17,6 +17,6 @@ If you use this database, please cite the following publication:: year = {2017}, } """ -from bob.pad.face.database import OulunpuPadDatabase +from bob.pad.face.database import OuluNpuPadDatabase -database = OulunpuPadDatabase() +database = OuluNpuPadDatabase() diff --git a/bob/pad/face/config/replay_attack.py b/bob/pad/face/config/replay_attack.py index 1c22f9a00d941386eb26eb5808de86d4d8b9d1b8..3fc4afbc1448d0a8a0ee8a753a0c682246ba047d 100644 --- a/bob/pad/face/config/replay_attack.py +++ b/bob/pad/face/config/replay_attack.py @@ -9,7 +9,7 @@ You can download the raw data of the `Replay-Attack`_ database by following the link. After downloading, you can tell the bob library where the files are located using:: - $ bob config set bob.db.replayattack.directory /path/to/replayattack/directory + $ bob config set bob.db.replay_attack.directory /path/to/database/replay/protocols/replayattack-database/ """ from bob.pad.face.database import ReplayAttackPadDatabase diff --git a/bob/pad/face/config/replay_mobile.py b/bob/pad/face/config/replay_mobile.py index fde9ed011150ec3a520db22d7795630e3c229776..3aa831160d5de5c906eab9d8830c86656613678f 100644 --- a/bob/pad/face/config/replay_mobile.py +++ b/bob/pad/face/config/replay_mobile.py @@ -8,7 +8,10 @@ of collaboration with Galician Research and Development Center in Advanced Telec The reference citation is [CBVM16]_. You can download the raw data of the `Replay-Mobile`_ database by following -the link. +the link. After downloading, you can tell the bob library where the files are +located using:: + + $ bob config set bob.db.replay_mobile.directory /path/to/database/replay-mobile/database/ """ from bob.pad.face.database import ReplayMobilePadDatabase diff --git a/bob/pad/face/config/svm_frames.py b/bob/pad/face/config/svm_frames.py index 73ece6023b3dc9dcc23882e6fe949c4f92148a1d..ebe9f1063db1185d6ae236e3e32662b6b7446371 100644 --- a/bob/pad/face/config/svm_frames.py +++ b/bob/pad/face/config/svm_frames.py @@ -10,7 +10,6 @@ preprocessor = globals()["preprocessor"] extractor = globals()["extractor"] # Classifier # - param_grid = [ { "C": [2**P for P in range(-3, 14, 2)], @@ -20,6 +19,12 @@ param_grid = [ ] +# TODO: The grid search below does not take into account splitting frames of +# each video into a separate group. You might have frames of the same video in +# both groups of training and validation. + +# TODO: This gridsearch can also be part of dask graph using dask-ml and the +# ``bob_fit_supports_dask_array`` tag from bob.pipelines. classifier = GridSearchCV(SVC(), param_grid=param_grid, cv=3) classifier = mario.wrap( ["sample"], diff --git a/bob/pad/face/config/swan.py b/bob/pad/face/config/swan.py index 0e1c3f17a2d8a7a711e72d694b445c6434f49906..3f8a7513853f1ba0622b06ec6f336425f937a282 100644 --- a/bob/pad/face/config/swan.py +++ b/bob/pad/face/config/swan.py @@ -2,7 +2,7 @@ To configure the location of the database on your computer, run:: - bob config set bob.db.swan.directory /path/to/swan/database + bob config set bob.db.swan.directory /path/to/database/swan The Idiap part of the dataset comprises 150 subjects that are captured in six diff --git a/bob/pad/face/database/__init__.py b/bob/pad/face/database/__init__.py index 38b5ef75ec3d7fc6e4d57154b9a86bbd7e008c2d..ca9b5c211ab784d8d5cd8ab8b790ac95c99f9d54 100644 --- a/bob/pad/face/database/__init__.py +++ b/bob/pad/face/database/__init__.py @@ -1,12 +1,12 @@ # isort: skip_file -from .database import VideoPadSample # noqa: F401 -from .casiafasd import CasiaFasdPadDatabase -from .casiasurf import CasiaSurfPadDatabase -from .maskattack import MaskAttackPadDatabase +from .database import VideoPadSample +from .casia_fasd import CasiaFasdPadDatabase +from .casia_surf import CasiaSurfPadDatabase +from .mask_attack import MaskAttackPadDatabase from .replay_attack import ReplayAttackPadDatabase from .replay_mobile import ReplayMobilePadDatabase from .swan import SwanPadDatabase -from .oulunpu import OulunpuPadDatabase +from .oulu_npu import OuluNpuPadDatabase # gets sphinx autodoc done right - don't remove it @@ -26,13 +26,14 @@ def __appropriate__(*args): __appropriate__( + VideoPadSample, ReplayAttackPadDatabase, ReplayMobilePadDatabase, MaskAttackPadDatabase, CasiaSurfPadDatabase, CasiaFasdPadDatabase, SwanPadDatabase, - OulunpuPadDatabase, + OuluNpuPadDatabase, ) __all__ = [_ for _ in dir() if not _.startswith("_")] diff --git a/bob/pad/face/database/casiafasd.py b/bob/pad/face/database/casia_fasd.py similarity index 100% rename from bob/pad/face/database/casiafasd.py rename to bob/pad/face/database/casia_fasd.py diff --git a/bob/pad/face/database/casia_surf.py b/bob/pad/face/database/casia_surf.py new file mode 100644 index 0000000000000000000000000000000000000000..542be95dbef4de39ff53eece3711ec377a227520 --- /dev/null +++ b/bob/pad/face/database/casia_surf.py @@ -0,0 +1,123 @@ +import logging +import os + +from functools import partial + +from sklearn.preprocessing import FunctionTransformer + +import bob.io.base + +from bob.bio.video import VideoLikeContainer +from bob.extension import rc +from bob.pad.base.database import FileListPadDatabase +from bob.pipelines import CSVToSamples, DelayedSample + +logger = logging.getLogger(__name__) + + +def load_multi_stream(path): + data = bob.io.base.load(path) + video = VideoLikeContainer(data[None, ...], [0]) + return video + + +def casia_surf_multistream_load(samples, original_directory): + mod_to_attr = {} + mod_to_attr["color"] = "filename" + mod_to_attr["infrared"] = "ir_filename" + mod_to_attr["depth"] = "depth_filename" + mods = list(mod_to_attr.keys()) + + def _load(sample): + paths = dict() + for mod in mods: + paths[mod] = os.path.join( + original_directory or "", getattr(sample, mod_to_attr[mod]) + ) + data = partial(load_multi_stream, paths["color"]) + depth = partial(load_multi_stream, paths["depth"]) + infrared = partial(load_multi_stream, paths["infrared"]) + subject = None + key = sample.filename + is_bonafide = sample.is_bonafide == "1" + attack_type = None if is_bonafide else "attack" + + return DelayedSample( + data, + parent=sample, + subject=subject, + key=key, + attack_type=attack_type, + is_bonafide=is_bonafide, + annotations=None, + delayed_attributes={"depth": depth, "infrared": infrared}, + ) + + return [_load(s) for s in samples] + + +def CasiaSurfMultiStreamSample(original_directory): + return FunctionTransformer( + casia_surf_multistream_load, + kw_args=dict(original_directory=original_directory), + ) + + +class CasiaSurfPadDatabase(FileListPadDatabase): + """The CASIA SURF Face PAD database interface. + + Parameters + ---------- + stream_type : str + A str or a list of str of the following choices: ``all``, ``color``, ``depth``, ``infrared``, by default ``all`` + + The returned sample either have their data as a VideoLikeContainer or + a dict of VideoLikeContainers depending on the chosen stream_type. + """ + + def __init__( + self, + **kwargs, + ): + original_directory = rc.get("bob.db.casia_surf.directory") + if original_directory is None or not os.path.isdir(original_directory): + raise FileNotFoundError( + "The original_directory is not set. Please set it in the terminal using `bob config set bob.db.casia_surf.directory /path/to/database/CASIA-SURF/`." + ) + transformer = CasiaSurfMultiStreamSample( + original_directory=original_directory, + ) + super().__init__( + dataset_protocols_path=original_directory, + protocol="all", + reader_cls=partial( + CSVToSamples, + dict_reader_kwargs=dict( + delimiter=" ", + fieldnames=[ + "filename", + "ir_filename", + "depth_filename", + "is_bonafide", + ], + ), + ), + transformer=transformer, + **kwargs, + ) + self.annotation_type = None + self.fixed_positions = None + + def protocols(self): + return ["all"] + + def groups(self): + return ["train", "dev", "eval"] + + def list_file(self, group): + filename = { + "train": "train_list.txt", + "dev": "val_private_list.txt", + "eval": "test_private_list.txt", + }[group] + return os.path.join(self.dataset_protocols_path, filename) diff --git a/bob/pad/face/database/casiasurf.py b/bob/pad/face/database/casiasurf.py deleted file mode 100644 index 652dbd00965f58a9c75b685340c31f77e2d873c1..0000000000000000000000000000000000000000 --- a/bob/pad/face/database/casiasurf.py +++ /dev/null @@ -1,105 +0,0 @@ -import logging -import os - -from functools import partial - -from sklearn.preprocessing import FunctionTransformer - -import bob.io.base - -from bob.bio.video import VideoLikeContainer -from bob.extension import rc -from bob.extension.download import get_file -from bob.pad.base.database import FileListPadDatabase -from bob.pipelines import DelayedSample - -logger = logging.getLogger(__name__) - - -def load_multi_stream(mods, paths): - retval = {} - for mod, path in zip(mods, paths): - data = bob.io.base.load(path) - fc = VideoLikeContainer(data, [0]) - retval[mod] = fc - - if len(retval) == 1: - retval = retval[mods[0]] - - return retval - - -def casia_surf_multistream_load(samples, original_directory, stream_type): - mod_to_attr = {} - mod_to_attr["color"] = "filename" - mod_to_attr["infrared"] = "ir_filename" - mod_to_attr["depth"] = "depth_filename" - - mods = [] - if isinstance(stream_type, str) and stream_type != "all": - mods = [stream_type] - elif isinstance(stream_type, str) and stream_type == "all": - mods = ["color", "infrared", "depth"] - else: - for m in stream_type: - mods.append(m) - - def _load(sample): - paths = [] - for mod in mods: - paths.append( - os.path.join( - original_directory or "", getattr(sample, mod_to_attr[mod]) - ) - ) - data = partial(load_multi_stream, mods, paths) - return DelayedSample(data, parent=sample, annotations=None) - - return [_load(s) for s in samples] - - -def CasiaSurfMultiStreamSample(original_directory, stream_type): - return FunctionTransformer( - casia_surf_multistream_load, - kw_args=dict( - original_directory=original_directory, stream_type=stream_type - ), - ) - - -def CasiaSurfPadDatabase( - stream_type="all", - **kwargs, -): - """The CASIA SURF Face PAD database interface. - - Parameters - ---------- - stream_type : str - A str or a list of str of the following choices: ``all``, ``color``, ``depth``, ``infrared``, by default ``all`` - - The returned sample either have their data as a VideoLikeContainer or - a dict of VideoLikeContainers depending on the chosen stream_type. - """ - name = "pad-face-casia-surf-252f86f2.tar.gz" - dataset_protocols_path = get_file( - name, - [f"http://www.idiap.ch/software/bob/data/bob/bob.pad.face/{name}"], - cache_subdir="protocols", - file_hash="252f86f2", - ) - - transformer = CasiaSurfMultiStreamSample( - original_directory=rc.get("bob.db.casiasurf.directory"), - stream_type=stream_type, - ) - - database = FileListPadDatabase( - dataset_protocols_path, - protocol="all", - transformer=transformer, - **kwargs, - ) - database.annotation_type = None - database.fixed_positions = None - return database diff --git a/bob/pad/face/database/database.py b/bob/pad/face/database/database.py index 32d04a3488e3e17c1736111d7ae0073c60e07a6b..e92a84811ae6dc681da34535db4727a2119259c1 100644 --- a/bob/pad/face/database/database.py +++ b/bob/pad/face/database/database.py @@ -49,6 +49,11 @@ def delayed_video_load( annotation_type="json", ) delayed_attributes = {"annotations": delayed_annotations} + if sample.attack_type == "": + sample.attack_type = None + sample.is_bonafide = sample.attack_type is None + if not hasattr(sample, "key"): + sample.key = sample.filename results.append( DelayedSample( diff --git a/bob/pad/face/database/mask_attack.py b/bob/pad/face/database/mask_attack.py new file mode 100644 index 0000000000000000000000000000000000000000..179f733a6327da309cd2d19cd17ec34acc7f919a --- /dev/null +++ b/bob/pad/face/database/mask_attack.py @@ -0,0 +1,154 @@ +import logging +import os + +from functools import partial + +import h5py +import numpy as np + +from sklearn.preprocessing import FunctionTransformer + +from bob.bio.video import VideoLikeContainer, select_frames +from bob.extension import rc +from bob.extension.download import get_file +from bob.pad.base.database import FileListPadDatabase +from bob.pipelines import DelayedSample + +logger = logging.getLogger(__name__) + + +def load_frames_from_hdf5( + hdf5_file, + key="Color_Data", + selection_style=None, + max_number_of_frames=None, + step_size=None, +): + with h5py.File(hdf5_file) as f: + video = f[key][()] + # reduce the shape of depth from (N, C, H, W) to (N, H, W) since H == 1 + video = np.squeeze(video) + + indices = select_frames( + len(video), + max_number_of_frames=max_number_of_frames, + selection_style=selection_style, + step_size=step_size, + ) + data = VideoLikeContainer(video[indices], indices) + + return data + + +def load_annotations_from_hdf5( + hdf5_file, +): + with h5py.File(hdf5_file) as f: + eye_pos = f["Eye_Pos"][()] + + annotations = { + str(i): { + "reye": [row[1], row[0]], + "leye": [row[3], row[2]], + } + for i, row in enumerate(eye_pos) + } + return annotations + + +def delayed_maskattack_video_load( + samples, + original_directory, + selection_style=None, + max_number_of_frames=None, + step_size=None, +): + + original_directory = original_directory or "" + results = [] + for sample in samples: + hdf5_file = os.path.join(original_directory, sample.filename) + data = partial( + load_frames_from_hdf5, + key="Color_Data", + hdf5_file=hdf5_file, + selection_style=selection_style, + max_number_of_frames=max_number_of_frames, + step_size=step_size, + ) + depth = partial( + load_frames_from_hdf5, + key="Depth_Data", + hdf5_file=hdf5_file, + selection_style=selection_style, + max_number_of_frames=max_number_of_frames, + step_size=step_size, + ) + annotations = partial( + load_annotations_from_hdf5, + hdf5_file=hdf5_file, + ) + delayed_attributes = { + "annotations": annotations, + "depth": depth, + } + + results.append( + DelayedSample( + data, + parent=sample, + delayed_attributes=delayed_attributes, + ) + ) + return results + + +def MaskAttackPadSample( + original_directory, + selection_style=None, + max_number_of_frames=None, + step_size=None, +): + return FunctionTransformer( + delayed_maskattack_video_load, + validate=False, + kw_args=dict( + original_directory=original_directory, + selection_style=selection_style, + max_number_of_frames=max_number_of_frames, + step_size=step_size, + ), + ) + + +def MaskAttackPadDatabase( + protocol="classification", + selection_style=None, + max_number_of_frames=None, + step_size=None, + **kwargs, +): + name = "pad-face-mask-attack-2ab2032c.tar.gz" + dataset_protocols_path = get_file( + name, + [f"http://www.idiap.ch/software/bob/data/bob/bob.pad.face/{name}"], + cache_subdir="protocols", + file_hash="2ab2032c", + ) + + transformer = MaskAttackPadSample( + original_directory=rc.get("bob.db.mask_attack.directory"), + selection_style=selection_style, + max_number_of_frames=max_number_of_frames, + step_size=step_size, + ) + + database = FileListPadDatabase( + dataset_protocols_path, + protocol, + transformer=transformer, + **kwargs, + ) + database.annotation_type = "eyes-center" + database.fixed_positions = None + return database diff --git a/bob/pad/face/database/maskattack.py b/bob/pad/face/database/maskattack.py deleted file mode 100644 index 60c3a6108a6da0d96180425e5edfd2e1cfd8a818..0000000000000000000000000000000000000000 --- a/bob/pad/face/database/maskattack.py +++ /dev/null @@ -1,45 +0,0 @@ -import logging - -from bob.extension import rc -from bob.extension.download import get_file -from bob.pad.base.database import FileListPadDatabase -from bob.pad.face.database import VideoPadSample - -logger = logging.getLogger(__name__) - - -def MaskAttackPadDatabase( - protocol="classification", - selection_style=None, - max_number_of_frames=None, - step_size=None, - annotation_directory=None, - annotation_type=None, - fixed_positions=None, - **kwargs, -): - name = "pad-face-mask-attack-211bd751.tar.gz" - dataset_protocols_path = get_file( - name, - [f"http://www.idiap.ch/software/bob/data/bob/bob.pad.face/{name}"], - cache_subdir="protocols", - file_hash="211bd751", - ) - - transformer = VideoPadSample( - original_directory=rc.get("bob.db.maskattack.directory"), - annotation_directory=annotation_directory, - selection_style=selection_style, - max_number_of_frames=max_number_of_frames, - step_size=step_size, - ) - - database = FileListPadDatabase( - dataset_protocols_path, - protocol, - transformer=transformer, - **kwargs, - ) - database.annotation_type = annotation_type - database.fixed_positions = fixed_positions - return database diff --git a/bob/pad/face/database/oulunpu.py b/bob/pad/face/database/oulu_npu.py similarity index 94% rename from bob/pad/face/database/oulunpu.py rename to bob/pad/face/database/oulu_npu.py index a1b56a023ee882fa8c252860648db0f541597a6f..5db0ec66851d8672a6f0b35c038ab4434488c413 100644 --- a/bob/pad/face/database/oulunpu.py +++ b/bob/pad/face/database/oulu_npu.py @@ -8,7 +8,7 @@ from bob.pad.face.database import VideoPadSample logger = logging.getLogger(__name__) -def OulunpuPadDatabase( +def OuluNpuPadDatabase( protocol="Protocol_1", selection_style=None, max_number_of_frames=None, @@ -37,7 +37,7 @@ def OulunpuPadDatabase( annotation_type = "eyes-center" transformer = VideoPadSample( - original_directory=rc.get("bob.db.oulunpu.directory"), + original_directory=rc.get("bob.db.oulu_npu.directory"), annotation_directory=annotation_directory, selection_style=selection_style, max_number_of_frames=max_number_of_frames, diff --git a/bob/pad/face/database/replay_attack.py b/bob/pad/face/database/replay_attack.py index ef9cf1972a958e434b5bbac84fbc1dc533f327f4..92ae8e419e0cefdd816ffb4f7c2ecce8ebef7d6a 100644 --- a/bob/pad/face/database/replay_attack.py +++ b/bob/pad/face/database/replay_attack.py @@ -37,7 +37,7 @@ def ReplayAttackPadDatabase( annotation_type = "eyes-center" transformer = VideoPadSample( - original_directory=rc.get("bob.db.replayattack.directory"), + original_directory=rc.get("bob.db.replay_attack.directory"), annotation_directory=annotation_directory, selection_style=selection_style, max_number_of_frames=max_number_of_frames, diff --git a/bob/pad/face/database/replay_mobile.py b/bob/pad/face/database/replay_mobile.py index 3ac0550cef4f28a4310a5cd58d28ab92b7ab77e4..9a72add17decdb67b4ede5d95366b623ec6643c0 100644 --- a/bob/pad/face/database/replay_mobile.py +++ b/bob/pad/face/database/replay_mobile.py @@ -54,7 +54,7 @@ def ReplayMobilePadDatabase( transformer = make_pipeline( Str_To_Types(fieldtypes=dict(should_flip=str_to_bool)), VideoPadSample( - original_directory=rc.get("bob.db.replaymobile.directory"), + original_directory=rc.get("bob.db.replay_mobile.directory"), annotation_directory=annotation_directory, selection_style=selection_style, max_number_of_frames=max_number_of_frames, diff --git a/bob/pad/face/deep_pix_bis.py b/bob/pad/face/deep_pix_bis.py index 38ef6029dead068c776c322aa644acf89df38b30..b786595f6db4b2f0044d98bd87dba757cbeda55e 100644 --- a/bob/pad/face/deep_pix_bis.py +++ b/bob/pad/face/deep_pix_bis.py @@ -15,49 +15,49 @@ logger = logging.getLogger(__name__) DEEP_PIX_BIS_PRETRAINED_MODELS = { - "oulunpu-p1": [ + "oulu-npu-p1": [ "http://www.idiap.ch/software/bob/data/bob/bob.pad.face/deep_pix_bis_OULU_Protocol_1_model_0_0-24844429.pth" ], - "oulunpu-p2": [ + "oulu-npu-p2": [ "http://www.idiap.ch/software/bob/data/bob/bob.pad.face/deep_pix_bis_OULU_Protocol_2_model_0_0-4aae2f3a.pth" ], - "oulunpu-p3-1": [ + "oulu-npu-p3-1": [ "http://www.idiap.ch/software/bob/data/bob/bob.pad.face/deep_pix_bis_OULU_Protocol_3_1_model_0_0-f0e70cf3.pth" ], - "oulunpu-p3-2": [ + "oulu-npu-p3-2": [ "http://www.idiap.ch/software/bob/data/bob/bob.pad.face/deep_pix_bis_OULU_Protocol_3_2_model_0_0-92594797.pth" ], - "oulunpu-p3-3": [ + "oulu-npu-p3-3": [ "http://www.idiap.ch/software/bob/data/bob/bob.pad.face/deep_pix_bis_OULU_Protocol_3_3_model_0_0-71e18149.pth" ], - "oulunpu-p3-4": [ + "oulu-npu-p3-4": [ "http://www.idiap.ch/software/bob/data/bob/bob.pad.face/deep_pix_bis_OULU_Protocol_3_4_model_0_0-d7f666e5.pth" ], - "oulunpu-p3-5": [ + "oulu-npu-p3-5": [ "http://www.idiap.ch/software/bob/data/bob/bob.pad.face/deep_pix_bis_OULU_Protocol_3_5_model_0_0-fc40ba69.pth" ], - "oulunpu-p3-6": [ + "oulu-npu-p3-6": [ "http://www.idiap.ch/software/bob/data/bob/bob.pad.face/deep_pix_bis_OULU_Protocol_3_6_model_0_0-123a6c92.pth" ], - "oulunpu-p4-1": [ + "oulu-npu-p4-1": [ "http://www.idiap.ch/software/bob/data/bob/bob.pad.face/deep_pix_bis_OULU_Protocol_4_1_model_0_0-5f8dc7cf.pth" ], - "oulunpu-p4-2": [ + "oulu-npu-p4-2": [ "http://www.idiap.ch/software/bob/data/bob/bob.pad.face/deep_pix_bis_OULU_Protocol_4_2_model_0_0-168f2644.pth" ], - "oulunpu-p4-3": [ + "oulu-npu-p4-3": [ "http://www.idiap.ch/software/bob/data/bob/bob.pad.face/deep_pix_bis_OULU_Protocol_4_3_model_0_0-db57e3b5.pth" ], - "oulunpu-p4-4": [ + "oulu-npu-p4-4": [ "http://www.idiap.ch/software/bob/data/bob/bob.pad.face/deep_pix_bis_OULU_Protocol_4_4_model_0_0-e999b7e8.pth" ], - "oulunpu-p4-5": [ + "oulu-npu-p4-5": [ "http://www.idiap.ch/software/bob/data/bob/bob.pad.face/deep_pix_bis_OULU_Protocol_4_5_model_0_0-dcd13b8b.pth" ], - "oulunpu-p4-6": [ + "oulu-npu-p4-6": [ "http://www.idiap.ch/software/bob/data/bob/bob.pad.face/deep_pix_bis_OULU_Protocol_4_6_model_0_0-96a1ab92.pth" ], - "replaymobile": [ + "replay-mobile": [ "http://www.idiap.ch/software/bob/data/bob/bob.pad.face/deep_pix_bis_RM_grandtest_model_0_0-6761ca7e.pth" ], } diff --git a/bob/pad/face/test/test_databases.py b/bob/pad/face/test/test_databases.py index 511a8c4823aaf0ddc501daaa8a8075f13a95f716..c2b9c446f048a32c55ed11741675f50272a551b3 100644 --- a/bob/pad/face/test/test_databases.py +++ b/bob/pad/face/test/test_databases.py @@ -2,14 +2,14 @@ # vim: set fileencoding=utf-8 : # Thu May 24 10:41:42 CEST 2012 -import numpy as np +from unittest import SkipTest -from nose.plugins.skip import SkipTest +import numpy as np import bob.bio.base -def test_replayattack(): +def test_replay_attack(): database = bob.bio.base.load_resource( "replay-attack", "database", @@ -61,7 +61,7 @@ def test_replayattack(): raise SkipTest(e) -def test_replaymobile(): +def test_replay_mobile(): database = bob.bio.base.load_resource( "replay-mobile", "database", @@ -131,10 +131,10 @@ def test_replaymobile(): raise SkipTest(e) -# Test the maskattack database -def test_maskattack(): - maskattack = bob.bio.base.load_resource( - "maskattack", +# Test the mask_attack database +def test_mask_attack(): + mask_attack = bob.bio.base.load_resource( + "mask-attack", "database", preferred_package="bob.pad.face", package_prefix="bob.pad.", @@ -142,14 +142,16 @@ def test_maskattack(): # all real sequences: 2 sessions, 5 recordings for 17 individuals assert ( len( - maskattack.samples(groups=["train", "dev", "eval"], purposes="real") + mask_attack.samples( + groups=["train", "dev", "eval"], purposes="real" + ) ) == 170 ) # all attacks: 1 session, 5 recordings for 17 individuals assert ( len( - maskattack.samples( + mask_attack.samples( groups=["train", "dev", "eval"], purposes="attack" ) ) @@ -157,70 +159,36 @@ def test_maskattack(): ) # training real: 7 subjects, 2 sessions, 5 recordings - assert len(maskattack.samples(groups=["train"], purposes="real")) == 70 + assert len(mask_attack.samples(groups=["train"], purposes="real")) == 70 # training real: 7 subjects, 1 session, 5 recordings - assert len(maskattack.samples(groups=["train"], purposes="attack")) == 35 + assert len(mask_attack.samples(groups=["train"], purposes="attack")) == 35 # dev and test contains the same number of sequences: # real: 5 subjects, 2 sessions, 5 recordings # attack: 5 subjects, 1 sessions, 5 recordings - assert len(maskattack.samples(groups=["dev"], purposes="real")) == 50 - assert len(maskattack.samples(groups=["eval"], purposes="real")) == 50 - assert len(maskattack.samples(groups=["dev"], purposes="attack")) == 25 - assert len(maskattack.samples(groups=["eval"], purposes="attack")) == 25 - - -# Test the casiasurf database -# def test_casiasurf(): -# casiasurf = bob.bio.base.load_resource( -# "casiasurf", -# "database", -# preferred_package="bob.pad.face", -# package_prefix="bob.pad.", -# ) -# assert len(casiasurf.samples(groups=["train"], purposes="real")) == 8942 -# assert len(casiasurf.samples(groups=["train"], purposes="attack")) == 20324 -# assert len(casiasurf.samples(groups=("dev",), purposes=("real",))) == 2994 -# assert len(casiasurf.samples(groups=("dev",), purposes=("attack",))) == 6614 -# assert ( -# len(casiasurf.samples(groups=("dev",), purposes=("real", "attack"))) == 9608 -# ) -# assert len(casiasurf.samples(groups=("eval",), purposes=("real",))) == 17458 -# assert len(casiasurf.samples(groups=("eval",), purposes=("attack",))) == 40252 -# assert ( -# len(casiasurf.samples(groups=("eval",), purposes=("real", "attack"))) -# == 57710 -# ) - - -def test_casiasurf_color_protocol(): - casiasurf = bob.bio.base.load_resource( - "casiasurf-color", - "database", - preferred_package="bob.pad.face", - package_prefix="bob.pad.", - ) - assert len(casiasurf.samples(groups=["train"], purposes="real")) == 8942 - assert len(casiasurf.samples(groups=["train"], purposes="attack")) == 20324 - assert len(casiasurf.samples(groups=("dev",), purposes=("real",))) == 2994 - assert len(casiasurf.samples(groups=("dev",), purposes=("attack",))) == 6614 - assert ( - len(casiasurf.samples(groups=("dev",), purposes=("real", "attack"))) - == 9608 - ) - assert len(casiasurf.samples(groups=("eval",), purposes=("real",))) == 17458 - assert ( - len(casiasurf.samples(groups=("eval",), purposes=("attack",))) == 40252 - ) - assert ( - len(casiasurf.samples(groups=("eval",), purposes=("real", "attack"))) - == 57710 - ) + assert len(mask_attack.samples(groups=["dev"], purposes="real")) == 50 + assert len(mask_attack.samples(groups=["eval"], purposes="real")) == 50 + assert len(mask_attack.samples(groups=["dev"], purposes="attack")) == 25 + assert len(mask_attack.samples(groups=["eval"], purposes="attack")) == 25 + + sample = mask_attack.samples()[0] + try: + assert sample.data.shape == (20, 3, 480, 640) + np.testing.assert_equal(sample.data[0][:, 0, 0], [185, 166, 167]) + annot = sample.annotations["0"] + assert annot["leye"][1] > annot["reye"][1], annot + assert annot == { + "leye": [212, 287], + "reye": [217, 249], + } + assert sample.depth.shape == (20, 480, 640) + except FileNotFoundError as e: + raise SkipTest(e) def test_casia_fasd(): casia_fasd = bob.bio.base.load_resource( - "casiafasd", + "casia-fasd", "database", preferred_package="bob.pad.face", package_prefix="bob.pad.", @@ -233,6 +201,37 @@ def test_casia_fasd(): assert len(casia_fasd.samples(groups="train")) == 180 assert len(casia_fasd.samples(groups="dev")) == 60 assert len(casia_fasd.samples(groups="eval")) == 360 + sample = casia_fasd.samples()[0] + try: + assert sample.data.shape == (20, 3, 480, 640) + np.testing.assert_equal(sample.data[0][:, 0, 0], [217, 228, 227]) + except FileNotFoundError as e: + raise SkipTest(e) + + +def test_casia_surf(): + try: + casia_surf = bob.bio.base.load_resource( + "casia-surf", + "database", + preferred_package="bob.pad.face", + package_prefix="bob.pad.", + ) + + assert len(casia_surf.samples()) == 96584 + assert len(casia_surf.samples(purposes="real")) == 29394 + assert len(casia_surf.samples(purposes="attack")) == 67190 + assert len(casia_surf.samples(groups=("train", "dev"))) == 38874 + assert len(casia_surf.samples(groups="train")) == 29266 + assert len(casia_surf.samples(groups="dev")) == 9608 + assert len(casia_surf.samples(groups="eval")) == 57710 + sample = casia_surf.samples()[0] + assert sample.data.shape == (1, 3, 279, 279) + np.testing.assert_equal(sample.data[0][:, 0, 0], [0, 0, 0]) + assert sample.depth.shape == (1, 143, 143) + assert sample.infrared.shape == (1, 143, 143) + except FileNotFoundError as e: + raise SkipTest(e) def test_swan(): @@ -284,9 +283,9 @@ def test_swan(): raise SkipTest(e) -def test_oulunpu(): +def test_oulu_npu(): database = bob.bio.base.load_resource( - "oulunpu", + "oulu-npu", "database", preferred_package="bob.pad.face", package_prefix="bob.pad.", diff --git a/bob/pad/face/test/test_utils.py b/bob/pad/face/test/test_utils.py index 1bdc971d3cb452185234e639fa815a2ab8a33b96..e6bf7c0163cf03471acb3f1f32e8e7e05467a52a 100644 --- a/bob/pad/face/test/test_utils.py +++ b/bob/pad/face/test/test_utils.py @@ -1,7 +1,6 @@ import imageio import numpy - -from nose.tools import raises +import pytest from bob.pad.face.test.dummy.database import DummyDatabase as Database from bob.pad.face.utils import ( @@ -57,11 +56,11 @@ def test_yield_frames(): assert frame.shape == (112, 92) -@raises(ValueError) def test_yield_faces_1(): - padfile = get_pad_sample(none_annotations=True) - for face in yield_faces(padfile, dummy_cropper): - pass + with pytest.raises(ValueError): + padfile = get_pad_sample(none_annotations=True) + for face in yield_faces(padfile, dummy_cropper): + pass def test_yield_faces_2(): @@ -105,11 +104,11 @@ def test_blocks(): assert (patches_gray == patches[:, 2, ...]).all() -@raises(ValueError) def test_block_raises1(): - blocks(image[0], (28, 28)) + with pytest.raises(ValueError): + blocks(image[0], (28, 28)) -@raises(ValueError) def test_block_raises2(): - blocks([[[image]]], (28, 28)) + with pytest.raises(ValueError): + blocks([[[image]]], (28, 28)) diff --git a/bob/pad/face/transformer/VideoToFrames.py b/bob/pad/face/transformer/VideoToFrames.py index f04318bb220d832d0e89a095dde8057a1851c230..8a1bdc5eb31131b9c70a25cc1e9ec865e8371ab9 100644 --- a/bob/pad/face/transformer/VideoToFrames.py +++ b/bob/pad/face/transformer/VideoToFrames.py @@ -1,5 +1,7 @@ import logging +from functools import partial + from sklearn.base import BaseEstimator, TransformerMixin import bob.pipelines as mario @@ -9,6 +11,10 @@ from bob.pipelines.wrappers import _frmt logger = logging.getLogger(__name__) +def _get(sth): + return sth + + class VideoToFrames(TransformerMixin, BaseEstimator): """Expands video samples to frame-based samples only when transform is called.""" @@ -20,11 +26,15 @@ class VideoToFrames(TransformerMixin, BaseEstimator): # video is an instance of VideoAsArray or VideoLikeContainer video = sample.data + for frame, frame_id in zip(video, video.indices): if frame is None: continue - new_sample = mario.Sample( - frame, + # create a load method so that we can create DelayedSamples because + # the input samples could be DelayedSamples with delayed attributes + # as well and we don't want to load those delayed attributes. + new_sample = mario.DelayedSample( + partial(_get, frame), frame_id=frame_id, annotations=annotations.get(str(frame_id)), parent=sample, diff --git a/conda/meta.yaml b/conda/meta.yaml index e935d1bf2b22d528efc05c0a271482a437c2e148..79c2ee3c41e34da3cec4bffc3d4a8029fc15a80a 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -48,13 +48,14 @@ test: imports: - {{ name }} commands: - - nosetests --with-coverage --cover-package={{ name }} -sv {{ name }} + - pytest --verbose --cov {{ name }} --cov-report term-missing --cov-report html:{{ project_dir }}/sphinx/coverage --cov-report xml:{{ project_dir }}/coverage.xml --pyargs {{ name }} - sphinx-build -aEW {{ project_dir }}/doc {{ project_dir }}/sphinx - sphinx-build -aEb doctest {{ project_dir }}/doc sphinx - conda inspect linkages -p $PREFIX {{ name }} # [not win] - conda inspect objects -p $PREFIX {{ name }} # [osx] requires: - - nose {{ nose }} + - pytest {{ pytest }} + - pytest-cov {{ pytest_cov }} - coverage {{ coverage }} - sphinx {{ sphinx }} - sphinx_rtd_theme {{ sphinx_rtd_theme }} diff --git a/doc/baselines.rst b/doc/baselines.rst index b2246b0a75f9805c5d6b4c86f622b2eee8258218..b5a6b19ff6c24134f221656c6a40b4312db24bb5 100644 --- a/doc/baselines.rst +++ b/doc/baselines.rst @@ -60,7 +60,7 @@ Documentation for each resource is available on the section .. code-block:: sh - $ bob config set bob.db.replaymobile.directory /path/to/replaymobile-database/ + $ bob config set bob.db.replay_mobile.directory /path/to/replaymobile-database/ Notice it is rather important to correctly configure the database as described above, otherwise ``bob.pad.base`` will not be able to correctly @@ -79,12 +79,13 @@ Baselines on REPLAY-ATTACK database This section summarizes the results of baseline face PAD experiments on the REPLAY-ATTACK (`replay-attack`_) database. The description of the database-related settings, which are used to run face PAD baselines on the -Replay-Attack is given here :ref:`bob.pad.face.resources.databases.replay`. To +Replay-Attack is given here :ref:`bob.pad.face.resources.databases.replay_attack`. To understand the settings in more detail you can check the corresponding configuration file: ``bob/pad/face/config/replay_attack.py``. Deep-Pix-BiS Baseline ~~~~~~~~~~~~~~~~~~~~~ +(see :ref:`bob.pad.face.resources.deep_pix_bis_pad`) .. code-block:: sh @@ -175,7 +176,7 @@ which should give you:: =================== ============== ============== -.. _bob.pad.face.baselines.oulunpu: +.. _bob.pad.face.baselines.oulu_npu: Baselines on OULU-NPU database -------------------------------------- @@ -183,9 +184,9 @@ Baselines on OULU-NPU database This section summarizes the results of baseline face PAD experiments on the `OULU-NPU`_ database. The description of the database-related settings, which are used to run face PAD baselines on the OULU-NPU is given here -:ref:`bob.pad.face.resources.databases.oulunpu`. To understand the +:ref:`bob.pad.face.resources.databases.oulu_npu`. To understand the settings in more detail you can check the corresponding configuration file : -``bob/pad/face/config/oulunpu.py``. +``bob/pad/face/config/oulu_npu.py``. Deep-Pix-BiS Baseline @@ -193,7 +194,7 @@ Deep-Pix-BiS Baseline .. code-block:: sh - $ bob pad run-pipeline -vv oulunpu deep-pix-bis --output <OUTPUT> --dask-client <CLIENT> + $ bob pad run-pipeline -vv oulu-npu deep-pix-bis --output <OUTPUT> --dask-client <CLIENT> This baseline reports scores per frame. To obtain scores per video, you can run:: @@ -229,4 +230,58 @@ which should give you:: AUC-LOG-SCALE 2.9 2.7 ====================== ============= ============ + +.. _bob.pad.face.baselines.swan: + +Baselines on SWAN database +-------------------------- + +This section summarizes the results of baseline face PAD experiments on the +`SWAN`_ database. The description of the database-related settings, +which are used to run face PAD baselines on the SWAN is given here +:ref:`bob.pad.face.resources.databases.swan`. To understand the +settings in more detail you can check the corresponding configuration file : +``bob/pad/face/config/swan.py``. + + +Deep-Pix-BiS Baseline +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: sh + + $ bob pad run-pipeline -vv swan deep-pix-bis --output <OUTPUT> --dask-client <CLIENT> + +This baseline reports scores per frame. To obtain scores per video, you can run:: + + $ bob pad finalize-scores -vv <OUTPUT>/scores-{dev,eval}.csv + +Finally, you can evaluate this baseline using:: + + $ bob pad metrics -vv --eval <OUTPUT>/scores-{dev,eval}.csv + +which should give you:: + + [Min. criterion: EER ] Threshold on Development set `<OUTPUT>/scores-dev.csv`: 4.867174e-01 + ============== ============== ================ + .. Development Evaluation + ============== ============== ================ + APCER (PA.F.1) 60.0% 51.1% + APCER (PA.F.5) 0.8% 2.8% + APCER (PA.F.6) 16.8% 16.3% + APCER_AP 60.0% 51.1% + BPCER 11.7% 21.8% + ACER 35.8% 36.5% + FTA 0.0% 0.0% + FPR 11.8% (59/502) 11.9% (89/749) + FNR 11.7% (35/300) 21.8% (491/2250) + HTER 11.7% 16.9% + FAR 11.8% 11.9% + FRR 11.7% 21.8% + PRECISION 0.8 1.0 + RECALL 0.9 0.8 + F1_SCORE 0.8 0.9 + AUC 1.0 0.9 + AUC-LOG-SCALE 2.0 1.6 + ============== ============== ================ + .. include:: links.rst diff --git a/doc/resources.rst b/doc/resources.rst index a2d558e6199129bf0e00eae64c089276606f6df2..f6c59bb7b21553ad70f5994a74c6a4e4ba4849de 100644 --- a/doc/resources.rst +++ b/doc/resources.rst @@ -26,7 +26,7 @@ The configuration files contain at least the following arguments of the * ``groups`` -.. _bob.pad.face.resources.databases.replay: +.. _bob.pad.face.resources.databases.replay_attack: Replay-Attack Database ================================================================================ @@ -45,12 +45,32 @@ Replay-Mobile Database -.. _bob.pad.face.resources.databases.oulunpu: +.. _bob.pad.face.resources.databases.oulu_npu: OULU-NPU Database ================================================================================ -.. automodule:: bob.pad.face.config.oulunpu +.. automodule:: bob.pad.face.config.oulu_npu + :members: + + + +.. _bob.pad.face.resources.databases.swan: + +SWAN Database +================================================================================ + +.. automodule:: bob.pad.face.config.swan + :members: + + + +.. _bob.pad.face.resources.deep_pix_bis_pad: + +Deep Pixel-wise Binary Supervision for Face PAD +================================================================================ + +.. automodule:: bob.pad.face.config.deep_pix_bis :members: diff --git a/setup.py b/setup.py index 9e8487416a6d4628eec4511825035fd3f9ba5ad2..8a9cc734f7f4c434bb287d46a20f54addf8cafdb 100644 --- a/setup.py +++ b/setup.py @@ -55,14 +55,13 @@ setup( "console_scripts": [], # registered databases: "bob.pad.database": [ + "casia-fasd = bob.pad.face.config.casia_fasd:database", + "casia-surf = bob.pad.face.config.casia_surf:database", + "mask-attack = bob.pad.face.config.mask_attack:database", + "oulu-npu = bob.pad.face.config.oulu_npu:database", "replay-attack = bob.pad.face.config.replay_attack:database", "replay-mobile = bob.pad.face.config.replay_mobile:database", - "casiafasd = bob.pad.face.config.casiafasd:database", - "maskattack = bob.pad.face.config.maskattack:database", - "casiasurf-color = bob.pad.face.config.casiasurf_color:database", - "casiasurf = bob.pad.face.config.casiasurf:database", "swan = bob.pad.face.config.swan:database", - "oulunpu = bob.pad.face.config.oulunpu:database", ], # registered pipelines: "bob.pad.pipeline": [ @@ -72,14 +71,13 @@ setup( # registered configurations: "bob.pad.config": [ # databases + "casia-fasd = bob.pad.face.config.casia_fasd", + "casia-surf = bob.pad.face.config.casia_surf", + "mask-attack = bob.pad.face.config.mask_attack", + "oulu-npu = bob.pad.face.config.oulu_npu", "replay-attack = bob.pad.face.config.replay_attack", "replay-mobile = bob.pad.face.config.replay_mobile", - "casiafasd = bob.pad.face.config.casiafasd", - "maskattack = bob.pad.face.config.maskattack", - "casiasurf-color = bob.pad.face.config.casiasurf_color", - "casiasurf = bob.pad.face.config.casiasurf", "swan = bob.pad.face.config.swan", - "oulunpu = bob.pad.face.config.oulunpu", # pipelines "svm-frames = bob.pad.face.config.svm_frames", "deep-pix-bis = bob.pad.face.config.deep_pix_bis", diff --git a/test-requirements.txt b/test-requirements.txt index f3c7e8e6ffb905f7de8b597eb22213a7dc20bfb3..e079f8a6038dd2dc8512967540f96ee0de172067 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1 @@ -nose +pytest