diff --git a/bob/pad/face/database/__init__.py b/bob/pad/face/database/__init__.py index 753f5ef010ae763e4886a7213d05a2a327aeeabd..6f1dc8c5d58b7e91f70b58fcd9e4f1c1f970c1dd 100644 --- a/bob/pad/face/database/__init__.py +++ b/bob/pad/face/database/__init__.py @@ -8,6 +8,8 @@ from .batl import BatlPadDatabase from .celeb_a import CELEBAPadDatabase from .maskattack import MaskAttackPadDatabase from .casiasurf import CasiaSurfPadDatabase +from .casiafasd import CasiaFasdPadDatabase + # gets sphinx autodoc done right - don't remove it def __appropriate__(*args): @@ -35,7 +37,8 @@ __appropriate__( BatlPadDatabase, CELEBAPadDatabase, MaskAttackPadDatabase, - CasiaSurfPadDatabase + CasiaSurfPadDatabase, + CasiaFasdPadDatabase, ) __all__ = [_ for _ in dir() if not _.startswith('_')] diff --git a/bob/pad/face/database/casiafasd.py b/bob/pad/face/database/casiafasd.py new file mode 100644 index 0000000000000000000000000000000000000000..7f8e5ca3b999f1c027844911ad0d1e57764fa0df --- /dev/null +++ b/bob/pad/face/database/casiafasd.py @@ -0,0 +1,297 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from bob.bio.video import FrameSelector +from bob.extension import rc +from bob.io.video import reader +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) +import numpy +import os + + +CASIA_FASD_FRAME_SHAPE = (3, 1280, 720) + + +class CasiaFasdPadFile(VideoPadFile): + """ + A high level implementation of the File class for the CASIA_FASD database. + """ + + def __init__(self, f, original_directory=None): + """ + Parameters + ---------- + f : object + An instance of the File class defined in the low level db interface + of the CasiaFasd database, in bob.db.casia_fasd.models + """ + + self.f = f + self.original_directory = original_directory + + if f.is_real(): + attack_type = None + else: + attack_type = 'attack/{}/{}'.format(f.get_type(), f.get_quality()) + + super(CasiaFasdPadFile, self).__init__( + client_id=str(f.get_clientid()), + path=f.filename, + attack_type=attack_type, + file_id=f.filename) + + @property + def frames(self): + """Yields the frames of the biofile one by one. + + Yields + ------ + :any:`numpy.array` + A frame of the video. The size is :any:`CASIA_FASD_FRAME_SHAPE`. + """ + vfilename = self.make_path( + directory=self.original_directory, extension='.avi') + for frame in reader(vfilename): + # pad frames to 1280 x 720 so they all have the same size + h, w = frame.shape[1:] + H, W = CASIA_FASD_FRAME_SHAPE[1:] + assert h <= H + assert w <= W + frame = numpy.pad(frame, ((0, 0), (0, H - h), (0, W - w)), + mode='constant', constant_values=0) + yield frame + + @property + def number_of_frames(self): + """Returns the number of frames in a video file. + + Returns + ------- + int + The number of frames. + """ + vfilename = self.make_path( + directory=self.original_directory, extension='.avi') + return reader(vfilename).number_of_frames + + @property + def frame_shape(self): + """Returns the size of each frame in this database. + + Returns + ------- + (int, int, int) + The (#Channels, Height, Width) which is + :any:`CASIA_FASD_FRAME_SHAPE`. + """ + return CASIA_FASD_FRAME_SHAPE + + @property + def annotations(self): + """Reads the annotations + + 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 = self.f.bbx() + annotations = {} + for i, v in enumerate(annots): + topleft = (v[2], v[1]) + bottomright = (v[2] + v[4], v[1] + v[3]) + annotations[str(i)] = {'topleft': topleft, + 'bottomright': bottomright} + return annotations + + def load(self, directory=None, extension='.avi', + frame_selector=FrameSelector(selection_style='all')): + """Loads the video file and returns in a + :any:`bob.bio.video.FrameContainer`. + + Parameters + ---------- + directory : :obj:`str`, optional + The directory to load the data from. + extension : :obj:`str`, optional + The extension of the file to load. + frame_selector : :any:`bob.bio.video.FrameSelector`, optional + Which frames to select. + + Returns + ------- + :any:`bob.bio.video.FrameContainer` + The loaded frames inside a frame container. + """ + directory = directory or self.original_directory + return frame_selector(self.make_path(directory, extension)) + + +class CasiaFasdPadDatabase(PadDatabase): + """ + A high level implementation of the Database class for the CASIA_FASD + database. Please run ``bob config set bob.db.casia_fasd.directory + /path/to/casia_fasd_files`` in a terminal to point to the original files on + your computer. This interface is different from the one implemented in + ``bob.db.casia_fasd.Database``. + """ + + def __init__( + self, + # grandtest is the new modified protocol for this database + protocol='grandtest', + original_directory=rc['bob.db.casia_fasd.directory'], + **kwargs): + """ + Parameters + ---------- + protocol : str or None + The name of the protocol that defines the default experimental + setup for this database. Only grandtest is supported for now. + + original_directory : str + The directory where the original data of the database are stored. + + kwargs + The arguments of the :py:class:`bob.pad.base.database.PadDatabase` + base class constructor. + """ + return super(CasiaFasdPadDatabase, self).__init__( + name='casiafasd', + protocol=protocol, + original_directory=original_directory, + original_extension='.avi', + training_depends_on_protocol=True, + **kwargs) + + def objects(self, + groups=None, + protocol=None, + purposes=None, + model_ids=None, + **kwargs): + """ + This function returns lists of CasiaFasdPadFile objects, which fulfill + the given restrictions. + + Parameters + ---------- + groups : :obj:`str` or [:obj:`str`] + The groups of which the clients should be returned. + Usually, groups are one or more elements of + ('train', 'dev', 'eval') + + protocol : str + The protocol for which the clients should be retrieved. + Only 'grandtest' is supported for now. This protocol modifies the + 'Overall Test' protocol and adds some ids to dev set. + + purposes : :obj:`str` or [:obj:`str`] + The purposes for which File objects should be retrieved either + 'real' or 'attack' or both. + + model_ids + Ignored. + + **kwargs + Ignored. + + Returns + ------- + files : [CasiaFasdPadFile] + A list of CasiaFasdPadFile objects. + """ + groups = check_parameters_for_validity( + groups, 'groups', ('train', 'dev', 'eval'), + ('train', 'dev', 'eval')) + protocol = check_parameter_for_validity( + protocol, 'protocol', ('grandtest'), 'grandtest') + purposes = check_parameters_for_validity( + purposes, 'purposes', ('real', 'attack'), ('real', 'attack')) + + qualities = ('low', 'normal', 'high') + types = ('warped', 'cut', 'video') + from bob.db.casia_fasd.models import File + + files = [] + + db_mappings = { + 'real_normal': '1', + 'real_low': '2', + 'real_high': 'HR_1', + 'warped_normal': '3', + 'warped_low': '4', + 'warped_high': 'HR_2', + 'cut_normal': '5', + 'cut_low': '6', + 'cut_high': 'HR_3', + 'video_normal': '7', + 'video_low': '8', + 'video_high': 'HR_4' + } + + # identitites 1-15 are for train, 16-20 are dev, and 21-50 for eval + grp_id_map = { + 'train': list(range(1, 16)), + 'dev': list(range(16, 21)), + 'eval': list(range(21, 51)), + } + grp_map = { + 'train': 'train', + 'dev': 'train', + 'eval': 'test', + } + + for g in groups: + ids = grp_id_map[g] + for i in ids: + cur_id = i + if g == 'eval': + cur_id = i - 20 + # the id within the group subset + + # this group name (grp) is only train and test + grp = grp_map[g] + + folder_name = grp + '_release' + + for q in qualities: + for c in purposes: + # the class real doesn't have any different types, only + # the attacks can be of different type + if c == 'real': + filename = os.path.join(folder_name, "%d" % cur_id, + db_mappings['real_' + q]) + files.append(CasiaFasdPadFile( + File(filename, c, grp), + self.original_directory)) + else: + for t in types: + filename = os.path.join( + folder_name, "%d" % cur_id, + db_mappings[t + '_' + q]) + files.append(CasiaFasdPadFile( + File(filename, c, grp), + self.original_directory)) + return files + + def annotations(self, padfile): + return padfile.annotations + + def frames(self, padfile): + return padfile.frames + + def number_of_frames(self, padfile): + return padfile.number_of_frames + + @property + def frame_shape(self): + return CASIA_FASD_FRAME_SHAPE diff --git a/bob/pad/face/database/replay.py b/bob/pad/face/database/replay.py index e144511e42f0586f568ee3b3ea192929f27012d5..56004ecffcd6cdc2fe40cdbd823ddb86ba15e558 100644 --- a/bob/pad/face/database/replay.py +++ b/bob/pad/face/database/replay.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python # -*- coding: utf-8 -*- # Used in ReplayMobilePadFile class diff --git a/bob/pad/face/test/test_databases.py b/bob/pad/face/test/test_databases.py index fb01fb324b8dcc21a505ff5f456513c4604ac3ab..7bb3cc8ea4f1a30d7d6fd55989cf8a470d1172fa 100644 --- a/bob/pad/face/test/test_databases.py +++ b/bob/pad/face/test/test_databases.py @@ -119,14 +119,17 @@ def test_maskattack(): package_prefix='bob.pad.') try: # all real sequences: 2 sessions, 5 recordings for 17 individuals - assert len(maskattack.objects(groups=['train', 'dev', 'eval'], purposes='real')) == 170 + assert len(maskattack.objects( + groups=['train', 'dev', 'eval'], purposes='real')) == 170 # all attacks: 1 session, 5 recordings for 17 individuals - assert len(maskattack.objects(groups=['train', 'dev', 'eval'], purposes='attack')) == 85 - + assert len(maskattack.objects( + groups=['train', 'dev', 'eval'], purposes='attack')) == 85 + # training real: 7 subjects, 2 sessions, 5 recordings assert len(maskattack.objects(groups=['train'], purposes='real')) == 70 # training real: 7 subjects, 1 session, 5 recordings - assert len(maskattack.objects(groups=['train'], purposes='attack')) == 35 + assert len(maskattack.objects( + groups=['train'], purposes='attack')) == 35 # dev and test contains the same number of sequences: # real: 5 subjects, 2 sessions, 5 recordings @@ -134,7 +137,8 @@ def test_maskattack(): assert len(maskattack.objects(groups=['dev'], purposes='real')) == 50 assert len(maskattack.objects(groups=['eval'], purposes='real')) == 50 assert len(maskattack.objects(groups=['dev'], purposes='attack')) == 25 - assert len(maskattack.objects(groups=['eval'], purposes='attack')) == 25 + assert len(maskattack.objects( + groups=['eval'], purposes='attack')) == 25 except IOError as e: raise SkipTest( @@ -142,6 +146,8 @@ def test_maskattack(): % e) # Test the Aggregated database, which doesn't have a package + + def test_aggregated_db(): aggregated_db = bob.bio.base.load_resource( 'aggregated-db', @@ -210,147 +216,61 @@ def test_casiasurf(): preferred_package='bob.pad.face', package_prefix='bob.pad.') try: - assert len(casiasurf.objects(groups=['train'], purposes='real')) == 8942 + assert len(casiasurf.objects(groups=['train'], purposes='real')) == 8942 assert len(casiasurf.objects(groups=['train'], purposes='attack')) == 20324 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=('attack',))) == 57710 - + 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) -# # Test the BATL database -# @db_available('batl-db') -# def test_aggregated_db(): -# batl_db = bob.bio.base.load_resource( -# 'batl-db', -# 'database', -# preferred_package='bob.pad.face', -# package_prefix='bob.pad.') -# try: - -# assert len( -# batl_db.objects(groups=['train', 'dev', 'eval'])) == 1679 -# assert len(batl_db.objects(groups=['train', 'dev'])) == 1122 -# assert len(batl_db.objects(groups=['train'])) == 565 - -# assert len(batl_db.objects(groups='train')) == 565 -# assert len(batl_db.objects(groups='dev')) == 557 -# assert len(batl_db.objects(groups='eval')) == 557 - -# assert len( -# batl_db.objects( -# groups=['train', 'dev', 'eval'], protocol='grandtest')) == 1679 -# assert len( -# batl_db.objects( -# groups=['train', 'dev', 'eval'], -# protocol='grandtest', -# purposes='real')) == 347 -# assert len( -# batl_db.objects( -# groups=['train', 'dev', 'eval'], -# protocol='grandtest', -# purposes='attack')) == 1332 -# #tests for join_train_dev protocols -# assert len( -# batl_db.objects( -# groups=['train', 'dev', 'eval'], -# protocol='grandtest-color-50-join_train_dev')) == 1679 -# assert len( -# batl_db.objects( -# groups=['train', 'dev'], protocol='grandtest-color-50-join_train_dev')) == 1679 -# assert len( -# batl_db.objects(groups='eval', -# protocol='grandtest-color-50-join_train_dev')) == 557 -# # test for LOO_fakehead -# assert len( -# batl_db.objects( -# groups=['train', 'dev', 'eval'], -# protocol='grandtest-color-50-LOO_fakehead')) == 1149 -# assert len( -# batl_db.objects( -# groups=['train', 'dev'], protocol='grandtest-color-50-LOO_fakehead')) == 1017 -# assert len( -# batl_db.objects(groups='eval', -# protocol='grandtest-color-50-LOO_fakehead')) == 132 - -# # test for LOO_flexiblemask -# assert len( -# batl_db.objects( -# groups=['train', 'dev', 'eval'], -# protocol='grandtest-color-50-LOO_flexiblemask')) == 1132 -# assert len( -# batl_db.objects( -# groups=['train', 'dev'], protocol='grandtest-color-50-LOO_flexiblemask')) == 880 -# assert len( -# batl_db.objects(groups='eval', -# protocol='grandtest-color-50-LOO_flexiblemask')) == 252 - -# # test for LOO_glasses -# assert len( -# batl_db.objects( -# groups=['train', 'dev', 'eval'], -# protocol='grandtest-color-50-LOO_glasses')) == 1206 -# assert len( -# batl_db.objects( -# groups=['train', 'dev'], protocol='grandtest-color-50-LOO_glasses')) == 1069 -# assert len( -# batl_db.objects(groups='eval', -# protocol='grandtest-color-50-LOO_glasses')) == 137 - -# # test for LOO_papermask -# assert len( -# batl_db.objects( -# groups=['train', 'dev', 'eval'], -# protocol='grandtest-color-50-LOO_papermask')) == 1308 -# assert len( -# batl_db.objects( -# groups=['train', 'dev'], protocol='grandtest-color-50-LOO_papermask')) == 1122 -# assert len( -# batl_db.objects(groups='eval', -# protocol='grandtest-color-50-LOO_papermask')) == 186 - -# # test for LOO_prints -# assert len( -# batl_db.objects( -# groups=['train', 'dev', 'eval'], -# protocol='grandtest-color-50-LOO_prints')) == 1169 -# assert len( -# batl_db.objects( -# groups=['train', 'dev'], protocol='grandtest-color-50-LOO_prints')) == 988 -# assert len( -# batl_db.objects(groups='eval', -# protocol='grandtest-color-50-LOO_prints')) == 181 - -# # test for LOO_replay -# assert len( -# batl_db.objects( -# groups=['train', 'dev', 'eval'], -# protocol='grandtest-color-50-LOO_replay')) == 1049 -# assert len( -# batl_db.objects( -# groups=['train', 'dev'], protocol='grandtest-color-50-LOO_replay')) == 854 -# assert len( -# batl_db.objects(groups='eval', -# protocol='grandtest-color-50-LOO_replay')) == 195 - -# # test for LOO_rigidmask -# assert len( -# batl_db.objects( -# groups=['train', 'dev', 'eval'], -# protocol='grandtest-color-50-LOO_rigidmask')) == 1198 -# assert len( -# batl_db.objects( -# groups=['train', 'dev'], protocol='grandtest-color-50-LOO_rigidmask')) == 1034 -# assert len( -# batl_db.objects(groups='eval', -# protocol='grandtest-color-50-LOO_rigidmask')) == 164 - - -# 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) + +@db_available('casia_fasd') +def test_casia_fasd(): + casia_fasd = bob.bio.base.load_resource( + 'casiafasd', + 'database', + preferred_package='bob.pad.face', + package_prefix='bob.pad.') + + assert len(casia_fasd.objects()) == 600 + assert len(casia_fasd.objects(purposes='real')) == 150 + assert len(casia_fasd.objects(purposes='attack')) == 450 + assert len(casia_fasd.objects(groups=('train', 'dev'))) == 240 + assert len(casia_fasd.objects(groups='train')) == 180 + assert len(casia_fasd.objects(groups='dev')) == 60 + assert len(casia_fasd.objects(groups='eval')) == 360 + + # 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)} + + +@db_available('casia_fasd') +def test_casia_fasd_frames(): + casia_fasd = bob.bio.base.load_resource( + 'casiafasd', + 'database', + preferred_package='bob.pad.face', + package_prefix='bob.pad.') + + # test frame loading if the db original files are available + try: + files = casia_fasd.objects()[:12] + for f in files: + for frame in f.frames: + assert frame.shape == (3, 1280, 720) + break + except (IOError, RuntimeError)as e: + raise SkipTest( + "The database original files are missing. To run this test run " + "``bob config set bob.db.casia_fasd.directory " + "/path/to/casia_fasd_files`` in a terminal to point to the " + "original files on your computer. . Here is the error: '%s'" + % e) diff --git a/conda/meta.yaml b/conda/meta.yaml index 5b5670f0b04ffdadd2442b22cbde302e174664a2..8e7159051cf6321952be6e7c087d96586e6d9bd6 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -47,6 +47,7 @@ requirements: - {{ pin_compatible('numpy') }} - {{ pin_compatible('scikit-learn') }} - {{ pin_compatible('scikit-image') }} + - {{ pin_compatible('opencv') }} test: imports: @@ -67,6 +68,7 @@ test: - bob.db.replay - bob.db.replaymobile - bob.db.msu_mfsd_mod + - bob.db.casia_fasd - bob.db.mobio - bob.db.maskattack - bob.db.casiasurf diff --git a/setup.py b/setup.py index a1824b9f3e5dbc42f4e06a3b8e1b356b331a6e49..6b62d22ea726838d2b6741208894258203402179 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,7 @@ setup( 'replay-attack = bob.pad.face.config.replay_attack:database', 'replay-mobile = bob.pad.face.config.replay_mobile:database', 'msu-mfsd = bob.pad.face.config.msu_mfsd:database', + 'casiafasd = bob.pad.face.config.casiafasd:database', 'aggregated-db = bob.pad.face.config.aggregated_db:database', 'mifs = bob.pad.face.config.mifs:database', 'batl-db = bob.pad.face.config.database.batl.batl_db:database', @@ -85,6 +86,7 @@ setup( 'replay-attack = bob.pad.face.config.replay_attack', 'replay-mobile = bob.pad.face.config.replay_mobile', 'msu-mfsd = bob.pad.face.config.msu_mfsd', + 'casiafasd = bob.pad.face.config.casiafasd', 'aggregated-db = bob.pad.face.config.aggregated_db', 'mifs = bob.pad.face.config.mifs', 'batl-db = bob.pad.face.config.database.batl.batl_db', diff --git a/test-requirements.txt b/test-requirements.txt index a2ec5b9057eaa8e301cd821942792508b416f054..fae37ea3fb6ed9f6464be836218e5eb9547cc4d6 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,6 +2,7 @@ nose bob.db.replay bob.db.replaymobile bob.db.msu_mfsd_mod +bob.db.casia_fasd bob.db.mobio bob.db.maskattack bob.db.casiasurf