diff --git a/bob/pad/face/database/__init__.py b/bob/pad/face/database/__init__.py index b842785c16887e19c0bc016b5ff249b53ab98a19..3a831f7fbd1335f23dceca7be79e501d41be12a4 100644 --- a/bob/pad/face/database/__init__.py +++ b/bob/pad/face/database/__init__.py @@ -1,4 +1,3 @@ -from .database import VideoPadFile from .database import VideoPadSample # noqa: F401 from .casiafasd import CasiaFasdPadDatabase from .casiasurf import CasiaSurfPadDatabase @@ -26,7 +25,6 @@ def __appropriate__(*args): __appropriate__( - VideoPadFile, ReplayAttackPadDatabase, ReplayMobilePadDatabase, MaskAttackPadDatabase, diff --git a/bob/pad/face/database/database.py b/bob/pad/face/database/database.py index 475d957bb29e71f67663047419240670798fd31f..c8954f17a61e6203013f0a49aafefec21970f59f 100644 --- a/bob/pad/face/database/database.py +++ b/bob/pad/face/database/database.py @@ -2,12 +2,15 @@ from functools import partial import os from bob.pad.base.database import PadFile import bob.bio.video -import bob.io.video from bob.db.base.annotations import read_annotation_file from sklearn.preprocessing import FunctionTransformer from bob.bio.video import VideoAsArray from bob.pipelines import DelayedSample +from ..utils import frames, number_of_frames + +def get_no_transform(x): + return None def delayed_video_load( samples, @@ -19,9 +22,7 @@ def delayed_video_load( get_transform=None, keep_extension_for_annotation=False, ): - if get_transform is None: - def get_transform(x): - return None + get_transform = get_transform or get_no_transform original_directory = original_directory or "" annotation_directory = annotation_directory or "" @@ -81,119 +82,3 @@ def VideoPadSample( keep_extension_for_annotation=keep_extension_for_annotation, ), ) - - -class VideoPadFile(PadFile): - """A simple base class that defines basic properties of File object for the - use in face PAD experiments. - """ - - def __init__( - self, - attack_type, - client_id, - path, - file_id=None, - original_directory=None, - original_extension=".avi", - annotation_directory=None, - annotation_extension=None, - annotation_type=None, - selection_style=None, - max_number_of_frames=None, - step_size=None, - ): - super().__init__( - attack_type=attack_type, - client_id=client_id, - path=path, - file_id=file_id, - original_directory=original_directory, - original_extension=original_extension, - annotation_directory=annotation_directory, - annotation_extension=annotation_extension, - annotation_type=annotation_type, - ) - self.selection_style = selection_style or "all" - self.max_number_of_frames = max_number_of_frames - self.step_size = step_size - - def load( - self, - ): - """Loads the video file and returns in a `bob.bio.video.FrameContainer`. - - Returns - ------- - :any:`bob.bio.video.VideoAsArray` - The loaded frames inside a frame container. - """ - path = self.make_path(self.original_directory, self.original_extension) - video = bob.bio.video.VideoAsArray( - path, - selection_style=self.selection_style, - max_number_of_frames=self.max_number_of_frames, - step_size=self.step_size, - ) - return video - - @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 - ------- - collections.abc.Iterator - An iterator returning frames of the video. - """ - 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): - 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 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. - """ - 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 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/replay_mobile.py b/bob/pad/face/database/replay_mobile.py index 46d121cb022cf8e16dfd4057b5c815206f9e80bc..08f6f7fd7e2faf7ecb5b66e5f02ad71f9d8e269a 100644 --- a/bob/pad/face/database/replay_mobile.py +++ b/bob/pad/face/database/replay_mobile.py @@ -16,10 +16,9 @@ def get_rm_video_transform(sample): should_flip = sample.should_flip def transform(video): - video = np.asarray(video) - video = np.rollaxis(video, -1, -2) - if should_flip: - video = video[..., ::-1, :] + if not should_flip: + # after changing to imageio-ffmpeg, we need to flip other way around + video = video[..., ::-1] return video return transform diff --git a/bob/pad/face/test/test_databases.py b/bob/pad/face/test/test_databases.py index 3c5fba90acc6ebcace6565f44155d21a0fb55af9..0683529a1034e167b5c0013c23671cfbb9b9d106 100644 --- a/bob/pad/face/test/test_databases.py +++ b/bob/pad/face/test/test_databases.py @@ -5,6 +5,7 @@ from nose.plugins.skip import SkipTest import bob.bio.base +import numpy as np def test_replayattack(): @@ -51,8 +52,8 @@ def test_replayattack(): "nose": [152, 164], } assert sample.data.shape == (20, 3, 240, 320) - assert sample.data[0][0, 0, 0] == 8 - except RuntimeError as e: + np.testing.assert_equal(sample.data[0][:, 0, 0], [8, 9, 11]) + except (RuntimeError, FileNotFoundError) as e: raise SkipTest(e) @@ -76,22 +77,49 @@ def test_replaymobile(): len(database.samples(groups=["train", "dev", "eval"], purposes="attack")) == 640 ) - sample = database.sort(database.samples())[0] + all_samples = database.sort(database.samples()) + sample = all_samples[0] + assert ( + sample.key + == "devel/attack/attack_client005_session01_mattescreen_fixed_mobile_photo_lightoff.mov" + ), sample.key + assert sample.should_flip + annot = dict(sample.annotations["0"]) + assert annot["leye"][1] > annot["reye"][1], annot + assert annot == { + "bottomright": [760, 498], + "topleft": [374, 209], + "leye": [518, 417], + "reye": [522, 291], + "mouthleft": [669, 308], + "mouthright": [666, 407], + "nose": [585, 358], + }, annot + + sample2 = [s for s in all_samples if not s.should_flip][0] + assert ( + sample2.key + == "devel/attack/attack_client005_session01_mattescreen_fixed_tablet_photo_lightoff.mov" + ), sample2.key + assert not sample2.should_flip + annot = dict(sample2.annotations["0"]) + assert annot["leye"][1] > annot["reye"][1], annot + assert annot == { + "reye": [873, 305], + "leye": [879, 423], + "nose": [937, 365], + "mouthleft": [1018, 313], + "mouthright": [1023, 405], + "topleft": [747, 226], + "bottomright": [1111, 495], + }, annot + try: - annot = dict(sample.annotations["0"]) - assert annot["leye"][1] > annot["reye"][1], annot - assert annot == { - "bottomright": [760, 498], - "topleft": [374, 209], - "leye": [518, 417], - "reye": [522, 291], - "mouthleft": [669, 308], - "mouthright": [666, 407], - "nose": [585, 358], - } - assert sample.data.shape == (20, 3, 720, 1280) - assert sample.data[0][0, 0, 0] == 13 - except RuntimeError as e: + assert sample.data.shape == (20, 3, 1280, 720), sample.data.shape + np.testing.assert_equal(sample.data[0][:, 0, 0], [13, 13, 13]) + assert sample2.data.shape == (20, 3, 1280, 720), sample2.data.shape + np.testing.assert_equal(sample2.data[0][:, 0, 0], [19, 33, 30]) + except (RuntimeError, FileNotFoundError) as e: raise SkipTest(e) @@ -226,9 +254,9 @@ def test_swan(): "reye": [510, 265], "topleft": [301, 169], } - assert sample.data.shape == (20, 3, 720, 1280) - assert sample.data[0][0, 0, 0] == 87 - except RuntimeError as e: + assert sample.data.shape == (20, 3, 1280, 720) + np.testing.assert_equal(sample.data[0][:, 0, 0], [255, 255, 253]) + except (RuntimeError, FileNotFoundError) as e: raise SkipTest(e) @@ -285,6 +313,6 @@ def test_oulunpu(): "topleft": [632, 394], } assert sample.data.shape == (20, 3, 1920, 1080) - assert sample.data[0][0, 0, 0] == 195 - except RuntimeError as e: + np.testing.assert_equal(sample.data[0][:, 0, 0], [195, 191, 199]) + except (RuntimeError, FileNotFoundError) as e: raise SkipTest(e) diff --git a/bob/pad/face/test/test_utils.py b/bob/pad/face/test/test_utils.py index b01674bf63d0508a3015483c05e5768e3f455435..609b109b374fa9a566c05528bc21499be79f1195 100644 --- a/bob/pad/face/test/test_utils.py +++ b/bob/pad/face/test/test_utils.py @@ -1,7 +1,8 @@ from bob.pad.face.test.dummy.database import DummyDatabase as Database -from bob.pad.face.utils import yield_faces, scale_face, blocks +from bob.pad.face.utils import yield_faces, scale_face, blocks, frames, number_of_frames from nose.tools import raises import numpy +import imageio def get_pad_sample(none_annotations=False): @@ -12,6 +13,26 @@ def get_pad_sample(none_annotations=False): image = get_pad_sample().data[0] +def test_video_frames(): + # get the path to cockatoo.mp4 from imageio-ffmpeg + path = imageio.core.Request("imageio:cockatoo.mp4", "r").get_local_filename() + # read 2 frames + for i, frame in enumerate(frames(path)): + assert frame.shape == (3, 720, 1280), frame.shape + assert frame.ndim == 3, frame.ndim + if i == 0: + numpy.testing.assert_equal(frame[:, 0, 0], [116, 119, 104]) + elif i == 1: + numpy.testing.assert_equal(frame[:, 0, 0], [116, 119, 104]) + else: + break + + # test number of frames + n_frames = number_of_frames(path) + assert n_frames == 280, n_frames + + + def dummy_cropper(frame, annotations=None): return frame diff --git a/bob/pad/face/utils/load_utils.py b/bob/pad/face/utils/load_utils.py index 2de8ec74ed6e7ab0b33c8a98fbb6c35cafe65f57..e2c3cb6aa465954a2d154cf192713b279d937576 100644 --- a/bob/pad/face/utils/load_utils.py +++ b/bob/pad/face/utils/load_utils.py @@ -1,6 +1,6 @@ from bob.bio.face.annotator import min_face_size_validator from bob.bio.video.annotator import normalize_annotations -from bob.io.video import reader +from imageio import get_reader from bob.ip.base import scale, block, block_output_shape, block_generator from bob.ip.color import rgb_to_yuv, rgb_to_hsv from bob.ip.facedetect import bounding_box_from_annotation @@ -8,6 +8,7 @@ from collections import OrderedDict from functools import partial import numpy import random +import bob.io.image def frames(path): @@ -23,8 +24,9 @@ def frames(path): numpy.ndarray A frame of the video. The size is (3, 240, 320). """ - video = reader(path) - return iter(video) + video = get_reader(path) + for frame in video: + yield bob.io.image.to_bob(frame) def number_of_frames(path): @@ -40,8 +42,8 @@ def number_of_frames(path): int The number of frames. Then, it yields the frames. """ - video = reader(path) - return video.number_of_frames + video = get_reader(path) + return video.count_frames() def bbx_cropper(frame, annotations): diff --git a/conda/meta.yaml b/conda/meta.yaml index 0bd0aac83cde4ad1f7a5b1c07a71d009754f5d3c..d00a6c4b8bf256d9a1cfad13beb6f92f2f296fa4 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -34,12 +34,14 @@ requirements: - numpy {{ numpy }} - scikit-learn {{ scikit_learn }} - scikit-image {{ scikit_image }} + - imageio-ffmpeg {{ imageio_ffmpeg }} run: - python - setuptools - {{ pin_compatible('numpy') }} - {{ pin_compatible('scikit-learn', min_pin='x.x') }} - {{ pin_compatible('scikit-image') }} + - {{ pin_compatible('imageio-ffmpeg') }} test: imports: diff --git a/develop.cfg b/develop.cfg index f6d74885e13cd213f3e7980ff16954986c39f6f3..cc3b6d7dec3253d08561b8f79377998c6d999762 100644 --- a/develop.cfg +++ b/develop.cfg @@ -4,66 +4,14 @@ [buildout] parts = scripts eggs = bob.pad.face - bob.extension - bob.blitz - bob.core - bob.sp - bob.math - bob.io.base - bob.ip.gabor - bob.measure - bob.ip.base - bob.learn.boosting - bob.io.image - bob.ip.draw - bob.ip.color - bob.io.video - bob.io.matlab - bob.ip.flandmark - bob.ip.facedetect - bob.ip.dlib - bob.ip.qualitymeasure - bob.learn.linear - bob.db.base - bob.learn.em - bob.db.atnt - bob.bio.base - bob.bio.face bob.bio.video - bob.pad.base extensions = bob.buildout mr.developer auto-checkout = * -develop = src/bob.extension - src/bob.blitz - src/bob.core - src/bob.sp - src/bob.math - src/bob.io.base - src/bob.ip.gabor - src/bob.measure - src/bob.ip.base - src/bob.learn.boosting - src/bob.io.image - src/bob.ip.draw - src/bob.ip.color - src/bob.io.video - src/bob.io.matlab - src/bob.ip.flandmark - src/bob.ip.facedetect - src/bob.ip.dlib - src/bob.ip.qualitymeasure - src/bob.learn.linear - src/bob.db.base - src/bob.learn.em - src/bob.db.atnt - src/bob.bio.base - src/bob.bio.face - src/bob.bio.video - src/bob.pad.base +develop = src/bob.bio.video . ; options for bob.buildout @@ -72,33 +20,7 @@ verbose = true newest = false [sources] -bob.extension = git git@gitlab.idiap.ch:bob/bob.extension -bob.blitz = git git@gitlab.idiap.ch:bob/bob.blitz -bob.core = git git@gitlab.idiap.ch:bob/bob.core -bob.sp = git git@gitlab.idiap.ch:bob/bob.sp -bob.math = git git@gitlab.idiap.ch:bob/bob.math -bob.io.base = git git@gitlab.idiap.ch:bob/bob.io.base -bob.ip.gabor = git git@gitlab.idiap.ch:bob/bob.ip.gabor -bob.measure = git git@gitlab.idiap.ch:bob/bob.measure -bob.ip.base = git git@gitlab.idiap.ch:bob/bob.ip.base -bob.learn.boosting = git git@gitlab.idiap.ch:bob/bob.learn.boosting -bob.io.image = git git@gitlab.idiap.ch:bob/bob.io.image -bob.ip.draw = git git@gitlab.idiap.ch:bob/bob.ip.draw -bob.ip.color = git git@gitlab.idiap.ch:bob/bob.ip.color -bob.io.video = git git@gitlab.idiap.ch:bob/bob.io.video -bob.io.matlab = git git@gitlab.idiap.ch:bob/bob.io.matlab -bob.ip.flandmark = git git@gitlab.idiap.ch:bob/bob.ip.flandmark -bob.ip.facedetect = git git@gitlab.idiap.ch:bob/bob.ip.facedetect -bob.ip.dlib = git git@gitlab.idiap.ch:bob/bob.ip.dlib -bob.ip.qualitymeasure = git git@gitlab.idiap.ch:bob/bob.ip.qualitymeasure -bob.learn.linear = git git@gitlab.idiap.ch:bob/bob.learn.linear -bob.db.base = git git@gitlab.idiap.ch:bob/bob.db.base -bob.learn.em = git git@gitlab.idiap.ch:bob/bob.learn.em -bob.db.atnt = git git@gitlab.idiap.ch:bob/bob.db.atnt -bob.bio.base = git git@gitlab.idiap.ch:bob/bob.bio.base -bob.bio.face = git git@gitlab.idiap.ch:bob/bob.bio.face bob.bio.video = git git@gitlab.idiap.ch:bob/bob.bio.video -bob.pad.base = git git@gitlab.idiap.ch:bob/bob.pad.base [scripts] diff --git a/doc/api.rst b/doc/api.rst index 8bdbd73c39b09dfc3840c1a10acb946bffba6bd8..4cb67e5c07feaa5afa11765386e426320b7ed403 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -12,12 +12,6 @@ This section lists all the functionality available in this library allowing to r Database Interfaces ------------------------------ -Base classes -============ - -.. autoclass:: bob.pad.face.database.VideoPadFile - - REPLAY-ATTACK Database ======================== diff --git a/requirements.txt b/requirements.txt index 6817dcb93d45874599ff7a903f35a63ac8c59820..c0e85a1e8a73e312c6a7a13bef3f77d51518cef5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ bob.ip.color bob.ip.qualitymeasure scikit-learn scikit-image +imageio-ffmpeg