diff --git a/MANIFEST.in b/MANIFEST.in index ee00970910914bfc8313f22271ff670bf746e7e5..7196e9f60d3039ff505005708ed53b46adf1819e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ include README.rst bootstrap-buildout.py buildout.cfg COPYING version.txt requirements.txt recursive-include doc *.py *.rst *.ico *.png -recursive-include bob/pad/face/test/data *.hdf5 +recursive-include bob/pad/face/test/data *.hdf5 *.png diff --git a/bob/pad/face/__init__.py b/bob/pad/face/__init__.py index f42ae5389b813f1a2a9d7d9be7c1f78c5ad5200b..37b53c4ffc7dc1156ef21b4cb3352478828aefc6 100644 --- a/bob/pad/face/__init__.py +++ b/bob/pad/face/__init__.py @@ -1,4 +1,4 @@ -from . import algorithm, extractor, preprocessor, database +from . import algorithm, extractor, preprocessor, database, test def get_config(): diff --git a/bob/pad/face/algorithm/VideoSvmPadAlgorithm.py b/bob/pad/face/algorithm/VideoSvmPadAlgorithm.py index 110c07866f644525d86cf6385d13416a602069ad..9cbaeb7d79365420c4cd7646afe5d50f0cd03237 100644 --- a/bob/pad/face/algorithm/VideoSvmPadAlgorithm.py +++ b/bob/pad/face/algorithm/VideoSvmPadAlgorithm.py @@ -210,9 +210,9 @@ class VideoSvmPadAlgorithm(Algorithm): else: - uniform_step = features.shape[0]/n_samples + uniform_step = np.int(features.shape[0]/n_samples) - features_subset = features[0 : uniform_step*n_samples : uniform_step, :] + features_subset = features[0 : np.int(uniform_step*n_samples) : uniform_step, :] return features_subset @@ -240,7 +240,7 @@ class VideoSvmPadAlgorithm(Algorithm): Selected subset of cross-validation features. """ - half_samples_num = features.shape[0]/2 + half_samples_num = np.int(features.shape[0]/2) features_train = features[ 0 : half_samples_num, : ] features_cv = features[ half_samples_num : 2 * half_samples_num + 1, : ] @@ -446,7 +446,8 @@ class VideoSvmPadAlgorithm(Algorithm): machine_type = 'C_SVC', kernel_type = 'RBF', trainer_grid_search_params = { 'cost': [2**p for p in range(-5, 16, 2)], 'gamma': [2**p for p in range(-15, 4, 2)]}, mean_std_norm_flag = False, - projector_file = ""): + projector_file = "", + save_debug_data_flag = True): """ First, this function tunes the hyper-parameters of the SVM classifier using grid search on the sub-sets of training data. Train and cross-validation @@ -487,6 +488,10 @@ class VideoSvmPadAlgorithm(Algorithm): be save in this path. This file contains information, which might be usefull for debugging. + ``save_debug_data_flag`` : :py:class:`bool` + Save the data, which might be usefull for debugging if ``True``. + Default: ``True``. + **Returns:** ``machine`` : object @@ -544,16 +549,18 @@ class VideoSvmPadAlgorithm(Algorithm): setattr(trainer, key, selected_params[key]) # set the params of trainer # Save the data, which is usefull for debugging. - debug_file = os.path.join( os.path.split(projector_file)[0], "debug_data.hdf5" ) - debug_dict = {} - debug_dict['precisions_train'] = precisions_train - debug_dict['precisions_cv'] = precisions_cv - debug_dict['cost'] = selected_params['cost'] - debug_dict['gamma'] = selected_params['gamma'] - f = bob.io.base.HDF5File(debug_file, 'w') # open hdf5 file to save the debug data - for key in debug_dict.keys(): - f.set(key, debug_dict[key]) - del f + if save_debug_data_flag: + + debug_file = os.path.join( os.path.split(projector_file)[0], "debug_data.hdf5" ) + debug_dict = {} + debug_dict['precisions_train'] = precisions_train + debug_dict['precisions_cv'] = precisions_cv + debug_dict['cost'] = selected_params['cost'] + debug_dict['gamma'] = selected_params['gamma'] + f = bob.io.base.HDF5File(debug_file, 'w') # open hdf5 file to save the debug data + for key in debug_dict.keys(): + f.set(key, debug_dict[key]) + del f # training_features[0] - training features for the REAL class. real = self.convert_list_of_frame_cont_to_array(training_features[0]) # output is array diff --git a/bob/pad/face/config/frame_diff_svm.py b/bob/pad/face/config/frame_diff_svm.py index d55a46237352fd68f936f9d4f43a8aec7281a0a3..35c6d4f8f997e06cc2ff4a02a036ae0e9c7eec29 100644 --- a/bob/pad/face/config/frame_diff_svm.py +++ b/bob/pad/face/config/frame_diff_svm.py @@ -2,8 +2,6 @@ # -*- coding: utf-8 -*- """ -@author: Olegs Nikisins - This file contains configurations to run Frame Differences and SVM based face PAD baseline. The settings are tuned for the Replay-attack database. The idea of the algorithms is inherited from the following paper: [AM11]_. diff --git a/bob/pad/face/config/lbp_svm.py b/bob/pad/face/config/lbp_svm.py index 3c95b9850cf7bb4745c6174fe5fdd2a92d25d371..ff16f3b47f99f67b0ac566f437f12d3146548779 100644 --- a/bob/pad/face/config/lbp_svm.py +++ b/bob/pad/face/config/lbp_svm.py @@ -2,8 +2,6 @@ # -*- coding: utf-8 -*- """ -@author: Olegs Nikisins - This file contains configurations to run LBP and SVM based face PAD baseline. The settings are tuned for the Replay-attack database. The idea of the algorithm is introduced in the following paper: [CAM12]_. diff --git a/bob/pad/face/config/qm_svm.py b/bob/pad/face/config/qm_svm.py index dcf1473c76f85b3ea806324e55d4f60152c6e95c..b6ee74c4fdda3d0e57fe42d5d90bce1050650dc2 100644 --- a/bob/pad/face/config/qm_svm.py +++ b/bob/pad/face/config/qm_svm.py @@ -2,8 +2,6 @@ # -*- coding: utf-8 -*- """ -@author: Olegs Nikisins - This file contains configurations to run Image Quality Measures (IQM) and SVM based face PAD baseline. The settings are tuned for the Replay-attack database. The IQM features used in this algorithm/resource are introduced in the following papers: [WHJ15]_ and [CBVM16]_. diff --git a/bob/pad/face/test/data/test_image.png b/bob/pad/face/test/data/test_image.png new file mode 100644 index 0000000000000000000000000000000000000000..beabf86e61305f8bf66a578f2b9b4b7a4ca23ac9 Binary files /dev/null and b/bob/pad/face/test/data/test_image.png differ diff --git a/bob/pad/face/test/test.py b/bob/pad/face/test/test.py index 153e404b6ca1ec529f431c45a050d48ce50fdf21..160fbdc9600d8ebf60269ea99e33093e90a51b1c 100644 --- a/bob/pad/face/test/test.py +++ b/bob/pad/face/test/test.py @@ -3,14 +3,37 @@ """Test Units """ +#============================================================================== +# Import what is needed here: import numpy as np + from bob.io.base.test_utils import datafile + from bob.io.base import load + import bob.io.image # for image loading functionality + +import bob.bio.video + from bob.ip.color import rgb_to_gray + from ..extractor import LBPHistogram +from ..preprocessor import ImageFaceCrop + +from ..preprocessor import VideoFaceCrop +from ..preprocessor import FrameDifference + +from ..extractor import FrameDiffFeatures + +from ..extractor import VideoLBPHistogram + +from ..algorithm import VideoSvmPadAlgorithm + +import random + +#============================================================================== def test_lbp_histogram(): lbp = LBPHistogram() img = load(datafile('testimage.jpg', 'bob.bio.face.test')) @@ -18,3 +41,316 @@ def test_lbp_histogram(): features = lbp(img) reference = load(datafile('lbp.hdf5', 'bob.pad.face.test')) assert np.allclose(features, reference) + + +#============================================================================== +def test_image_face_crop(): + """ + Test ImageFaceCrop preprocessor, which is designed to crop faces in the images. + """ + + image = load(datafile('test_image.png', 'bob.pad.face.test')) + annotations = {'topleft': (95, 155), 'bottomright': (215, 265)} + + preprocessor = ImageFaceCrop(face_size = 64, rgb_output_flag = False) + face = preprocessor(image, annotations) + + assert face.shape == (64, 64) + assert np.sum(face) == 429158 + + preprocessor = ImageFaceCrop(face_size = 64, rgb_output_flag = True) + face = preprocessor(image, annotations) + + assert face.shape == (3, 64, 64) + assert np.sum(face) == 1215525 + + +#============================================================================== +def convert_image_to_video_data(image, annotations, n_frames): + """ + Convert input image to video and image annotations to frame annotations. + + **Parameters:** + + ``image`` : 2D or 3D :py:class:`numpy.ndarray` + Input image (RGB or gray-scale). + + ``annotations`` : :py:class:`dict` + A dictionary containing annotations of the face bounding box. + Dictionary must be as follows ``{'topleft': (row, col), 'bottomright': (row, col)}`` + + ``n_frames`` : :py:class:`int` + Number of frames in the output video + + **Returns:** + + ``frame_container`` : FrameContainer + Video data stored in the FrameContainer, see ``bob.bio.video.utils.FrameContainer`` + for further details. + + ``video_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. + """ + + frame_container = bob.bio.video.FrameContainer() # initialize the FrameContainer + + video_annotations = {} + + for idx, fn in enumerate( range(0, n_frames) ): + + frame_container.add(idx, image) # add current frame to FrameContainer + + video_annotations[str(idx)] = annotations + + return frame_container, video_annotations + + +#============================================================================== +def test_video_face_crop(): + """ + Test VideoFaceCrop preprocessor, which is designed to crop faces in the video. + """ + + image = load(datafile('test_image.png', 'bob.pad.face.test')) + annotations = {'topleft': (95, 155), 'bottomright': (215, 265)} + + CROPPED_IMAGE_SIZE = (64, 64) # The size of the resulting face + CROPPED_POSITIONS = {'topleft' : (0,0) , 'bottomright' : CROPPED_IMAGE_SIZE} + FIXED_POSITIONS = None + MASK_SIGMA = None # The sigma for random values areas outside image + MASK_NEIGHBORS = 5 # The number of neighbors to consider while extrapolating + MASK_SEED = None # The seed for generating random values during extrapolation + CHECK_FACE_SIZE_FLAG = True # Check the size of the face + MIN_FACE_SIZE = 50 # Minimal possible size of the face + USE_LOCAL_CROPPER_FLAG = True # Use the local face cropping class (identical to Ivana's paper) + COLOR_CHANNEL = 'gray' # Convert image to gray-scale format + + preprocessor = VideoFaceCrop(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, + check_face_size_flag = CHECK_FACE_SIZE_FLAG, + min_face_size = MIN_FACE_SIZE, + use_local_cropper_flag = USE_LOCAL_CROPPER_FLAG, + color_channel = COLOR_CHANNEL) + + video, annotations = convert_image_to_video_data(image, annotations, 20) + + faces = preprocessor(frames = video, annotations = annotations) + + assert len(faces) == 20 + assert faces[0][1].shape == (64, 64) + assert faces[-1][1].shape == (64, 64) + assert np.sum(faces[0][1]) == 429158 + assert np.sum(faces[-1][1]) == 429158 + + +#============================================================================== +def test_frame_difference(): + """ + Test FrameDifference preprocessor computing frame differences for both + facial and non-facial/background regions. + """ + + image = load(datafile('test_image.png', 'bob.pad.face.test')) + annotations = {'topleft': (95, 155), 'bottomright': (215, 265)} + + n_frames = 20 + + video, annotations = convert_image_to_video_data(image, annotations, n_frames) + + NUMBER_OF_FRAMES = None # process all frames + CHECK_FACE_SIZE_FLAG = True # Check size of the face + MIN_FACE_SIZE = 50 # Minimal size of the face to consider + + preprocessor = FrameDifference(number_of_frames = NUMBER_OF_FRAMES, + check_face_size_flag = CHECK_FACE_SIZE_FLAG, + min_face_size = MIN_FACE_SIZE) + + diff = preprocessor(frames = video, annotations = annotations) + + assert diff.shape == (n_frames-1, 2) + assert (diff==0).all() + + +#============================================================================== +def test_frame_diff_features(): + """ + Test FrameDiffFeatures extractor computing 10 features given frame differences. + """ + + WINDOW_SIZE=20 + OVERLAP=0 + + extractor = FrameDiffFeatures(window_size=WINDOW_SIZE, + overlap=OVERLAP) + + data = np.transpose( np.vstack( [range(0,100), range(0,100)] ) ) + + features = extractor(data) + + assert len(features) == 5 + assert len(features[0][1]) == 10 + assert len(features[-1][1]) == 10 + assert (features[0][1][0:5]==features[0][1][5:]).all() + assert (np.sum(features[0][1]) - 73.015116873109207) < 0.000001 + + +#============================================================================== +def test_video_lbp_histogram(): + """ + Test VideoLBPHistogram extractor. + """ + + image = load(datafile('test_image.png', 'bob.pad.face.test')) + annotations = {'topleft': (95, 155), 'bottomright': (215, 265)} + + CROPPED_IMAGE_SIZE = (64, 64) # The size of the resulting face + CROPPED_POSITIONS = {'topleft' : (0,0) , 'bottomright' : CROPPED_IMAGE_SIZE} + FIXED_POSITIONS = None + MASK_SIGMA = None # The sigma for random values areas outside image + MASK_NEIGHBORS = 5 # The number of neighbors to consider while extrapolating + MASK_SEED = None # The seed for generating random values during extrapolation + CHECK_FACE_SIZE_FLAG = True # Check the size of the face + MIN_FACE_SIZE = 50 # Minimal possible size of the face + USE_LOCAL_CROPPER_FLAG = True # Use the local face cropping class (identical to Ivana's paper) + RGB_OUTPUT_FLAG = False # The output is gray-scale + COLOR_CHANNEL = 'gray' # Convert image to gray-scale format + + preprocessor = VideoFaceCrop(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, + check_face_size_flag = CHECK_FACE_SIZE_FLAG, + min_face_size = MIN_FACE_SIZE, + use_local_cropper_flag = USE_LOCAL_CROPPER_FLAG, + rgb_output_flag = RGB_OUTPUT_FLAG, + color_channel = COLOR_CHANNEL) + + video, annotations = convert_image_to_video_data(image, annotations, 20) + + faces = preprocessor(frames = video, annotations = annotations) + + LBPTYPE='uniform' + ELBPTYPE='regular' + RAD=1 + NEIGHBORS=8 + CIRC=False + DTYPE=None + + extractor = VideoLBPHistogram(lbptype=LBPTYPE, + elbptype=ELBPTYPE, + rad=RAD, + neighbors=NEIGHBORS, + circ=CIRC, + dtype=DTYPE) + + lbp_histograms = extractor(faces) + + assert len(lbp_histograms) == 20 + assert len(lbp_histograms[0][1]) == 59 + assert (lbp_histograms[0][1]==lbp_histograms[-1][1]).all() + assert (lbp_histograms[0][1][0] - 0.12695109261186263) < 0.000001 + assert (lbp_histograms[0][1][-1] - 0.031737773152965658) < 0.000001 + + +#============================================================================== +def convert_array_to_list_of_frame_cont(data): + """ + Convert an input 2D array to a list of FrameContainers. + + **Parameters:** + + ``data`` : 2D :py:class:`numpy.ndarray` + Input data array of the dimensionality (N_samples X N_features ). + + **Returns:** + + ``frame_container_list`` : [FrameContainer] + A list of FrameContainers, see ``bob.bio.video.utils.FrameContainer`` + for further details. Each frame container contains one feature vector. + """ + + frame_container_list = [] + + for idx, vec in enumerate(data): + + frame_container = bob.bio.video.FrameContainer() # initialize the FrameContainer + + frame_container.add(0, vec) + + frame_container_list.append( frame_container ) # add current frame to FrameContainer + + return frame_container_list + + +#============================================================================== +def test_video_svm_pad_algorithm(): + """ + Test the VideoSvmPadAlgorithm algorithm. + """ + + random.seed(7) + + N = 20000 + mu = 1 + sigma = 1 + real_array = np.transpose( np.vstack([[random.gauss(mu, sigma) for _ in range(N)], [random.gauss(mu, sigma) for _ in range(N)]]) ) + + mu = 5 + sigma = 1 + attack_array = np.transpose( np.vstack([[random.gauss(mu, sigma) for _ in range(N)], [random.gauss(mu, sigma) for _ in range(N)]]) ) + + real = convert_array_to_list_of_frame_cont(real_array) + attack = convert_array_to_list_of_frame_cont(attack_array) + + training_features = [real, attack] + + MACHINE_TYPE = 'C_SVC' + KERNEL_TYPE = 'RBF' + N_SAMPLES = 1000 + TRAINER_GRID_SEARCH_PARAMS = {'cost': [1], 'gamma': [0.5, 1]} + MEAN_STD_NORM_FLAG = True # enable mean-std normalization + FRAME_LEVEL_SCORES_FLAG = True # one score per frame(!) in this case + + algorithm = VideoSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + n_samples = N_SAMPLES, + trainer_grid_search_params = TRAINER_GRID_SEARCH_PARAMS, + mean_std_norm_flag = MEAN_STD_NORM_FLAG, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + + machine = algorithm.train_svm(training_features = training_features, + n_samples = algorithm.n_samples, + machine_type = algorithm.machine_type, + kernel_type = algorithm.kernel_type, + trainer_grid_search_params = algorithm.trainer_grid_search_params, + mean_std_norm_flag = algorithm.mean_std_norm_flag, + projector_file = "", + save_debug_data_flag = False) + + assert machine.n_support_vectors == [148, 150] + assert machine.gamma == 0.5 + + real_sample = algorithm.convert_frame_cont_to_array(real[0]) + + prob = machine.predict_class_and_probabilities( real_sample )[1] + + assert prob[0,0] > prob[0,1] + + precision = algorithm.comp_prediction_precision(machine, real_array, attack_array) + + assert precision > 0.99 + + + + + + diff --git a/setup.py b/setup.py index db586b2bc847b47e41069bfba8c598b3403f7fb2..eb04789aa236b9abd403cc422ef6a532df15c8d8 100644 --- a/setup.py +++ b/setup.py @@ -1,37 +1,5 @@ #!/usr/bin/env python # vim: set fileencoding=utf-8 : -# Andre Anjos <andre.anjos@idiap.ch> -# Mon 16 Apr 08:18:08 2012 CEST -# -# Copyright (C) Idiap Research Institute, Martigny, Switzerland -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, version 3 of the License. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -# This file contains the python (distutils/setuptools) instructions so your -# package can be installed on **any** host system. It defines some basic -# information like the package name for instance, or its homepage. -# -# It also defines which other packages this python package depends on and that -# are required for this package's operation. The python subsystem will make -# sure all dependent packages are installed or will install them for you upon -# the installation of this package. -# -# The 'buildout' system we use here will go further and wrap this package in -# such a way to create an isolated python working environment. Buildout will -# make sure that dependencies which are not yet installed do get installed, but -# **without** requiring administrative privileges on the host system. This -# allows you to test your package with new python dependencies w/o requiring -# administrative interventions. from setuptools import setup, dist dist.Distribution(dict(setup_requires = ['bob.extension'])) @@ -52,8 +20,8 @@ setup( url = 'https://gitlab.idiap.ch/bob/bob.pad.face', license = 'GPLv3', - author = 'Amir Mohammadi', - author_email = 'amir.mohammadi@idiap.ch', + author = 'Olegs Nikisins', + author_email = 'olegs.nikisins@idiap.ch', keywords = 'bob', # If you have a better, long description of your package, place it on the