From 16693a2d83817958b616ab8a8b43971688dd905c Mon Sep 17 00:00:00 2001 From: Olegs NIKISINS <onikisins@italix03.idiap.ch> Date: Fri, 4 Aug 2017 18:01:34 +0200 Subject: [PATCH] Added one-class SVM option in VideoSvmPadAlgorith, created a couple config files for tests --- .../face/algorithm/VideoSvmPadAlgorithm.py | 122 +++++++++++++----- .../face/config/frame_diff_one_class_svm.py | 94 ++++++++++++++ .../config/qm_one_class_svm_aggregated_db.py | 111 ++++++++++++++++ setup.py | 2 + 4 files changed, 300 insertions(+), 29 deletions(-) create mode 100644 bob/pad/face/config/frame_diff_one_class_svm.py create mode 100644 bob/pad/face/config/qm_one_class_svm_aggregated_db.py diff --git a/bob/pad/face/algorithm/VideoSvmPadAlgorithm.py b/bob/pad/face/algorithm/VideoSvmPadAlgorithm.py index d84b0752..63908d3a 100644 --- a/bob/pad/face/algorithm/VideoSvmPadAlgorithm.py +++ b/bob/pad/face/algorithm/VideoSvmPadAlgorithm.py @@ -453,7 +453,7 @@ class VideoSvmPadAlgorithm(Algorithm): #========================================================================== - def norm_train_cv_data(self, real_train, real_cv, attack_train, attack_cv): + def norm_train_cv_data(self, real_train, real_cv, attack_train, attack_cv, one_class_flag = False): """ Mean-std normalization of train and cross-validation data arrays. @@ -471,6 +471,11 @@ class VideoSvmPadAlgorithm(Algorithm): ``attack_cv`` : 2D :py:class:`numpy.ndarray` Subset of cross-validation features for the attack class. + ``one_class_flag`` : :py:class:`bool` + If set to ``True``, only positive/real samples will be used to + compute the mean and std normalization vectors. Set to ``True`` if + using one-class SVM. Default: False. + **Returns:** ``real_train_norm`` : 2D :py:class:`numpy.ndarray` @@ -485,18 +490,30 @@ class VideoSvmPadAlgorithm(Algorithm): ``attack_cv_norm`` : 2D :py:class:`numpy.ndarray` Normalized subset of cross-validation features for the attack class. """ + if not(one_class_flag): + + features_train = np.vstack([real_train, attack_train]) + + features_train_norm, features_mean, features_std = self.mean_std_normalize(features_train) + + real_train_norm = features_train_norm[0:real_train.shape[0], :] - features_train = np.vstack([real_train, attack_train]) + attack_train_norm = features_train_norm[real_train.shape[0]:, :] - features_train_norm, features_mean, features_std = self.mean_std_normalize(features_train) + real_cv_norm, _, _ = self.mean_std_normalize(real_cv, features_mean, features_std) - real_train_norm = features_train_norm[0:real_train.shape[0], :] + attack_cv_norm, _, _ = self.mean_std_normalize(attack_cv, features_mean, features_std) - attack_train_norm = features_train_norm[real_train.shape[0]:, :] + else: # one-class SVM case - real_cv_norm, _, _ = self.mean_std_normalize(real_cv, features_mean, features_std) + #only real class used for training in one class SVM: + real_train_norm, features_mean, features_std = self.mean_std_normalize(real_train) - attack_cv_norm, _, _ = self.mean_std_normalize(attack_cv, features_mean, features_std) + attack_train_norm, _, _ = self.mean_std_normalize(attack_train, features_mean, features_std) + + real_cv_norm, _, _ = self.mean_std_normalize(real_cv, features_mean, features_std) + + attack_cv_norm, _, _ = self.mean_std_normalize(attack_cv, features_mean, features_std) return real_train_norm, real_cv_norm, attack_train_norm, attack_cv_norm @@ -569,12 +586,15 @@ class VideoSvmPadAlgorithm(Algorithm): A trained SVM machine. """ + one_class_flag = (machine_type == 'ONE_CLASS') # True if one-class SVM is used + # get the data for the hyper-parameter grid-search: real_train, real_cv, attack_train, attack_cv = self.prepare_data_for_hyper_param_grid_search(training_features, n_samples) if mean_std_norm_flag: # normalize the data: - real_train, real_cv, attack_train, attack_cv = self.norm_train_cv_data(real_train, real_cv, attack_train, attack_cv) + real_train, real_cv, attack_train, attack_cv = self.norm_train_cv_data(real_train, real_cv, attack_train, attack_cv, + one_class_flag) precisions_cv = [] # for saving the precision on the cross-validation set @@ -593,7 +613,13 @@ class VideoSvmPadAlgorithm(Algorithm): setattr(trainer, key, trainer_grid_search_param[key]) # set the params of trainer - data = [np.copy(real_train), np.copy(attack_train)] # data used for training the machine in the grid-search + if not( one_class_flag ): # two-class SVM case + + data = [np.copy(real_train), np.copy(attack_train)] # data used for training the machine in the grid-search + + else: # one class SVM case + + data = [np.copy(real_train)] # only real class is used for training machine = trainer.train(data) # train the machine @@ -626,8 +652,10 @@ class VideoSvmPadAlgorithm(Algorithm): 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'] + + for key in selected_params.keys(): + debug_dict[key] = selected_params[key] + 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]) @@ -640,10 +668,18 @@ class VideoSvmPadAlgorithm(Algorithm): if mean_std_norm_flag: # Normalize the data: - features = np.vstack([real, attack]) - features_norm, features_mean, features_std = self.mean_std_normalize(features) - real = features_norm[0:real.shape[0], :] # The array is now normalized - attack = features_norm[real.shape[0]:, :] # The array is now normalized + if not( one_class_flag ): # two-class SVM case + + features = np.vstack([real, attack]) + features_norm, features_mean, features_std = self.mean_std_normalize(features) + real = features_norm[0:real.shape[0], :] # The array is now normalized + attack = features_norm[real.shape[0]:, :] # The array is now normalized + + else: # one-class SVM case + + real, features_mean, features_std = self.mean_std_normalize(real) # use only real class to compute normalizers + attack = self.mean_std_normalize(attack, features_mean, features_std) + # ``real`` and ``attack`` arrays are now normalizaed if reduced_train_data_flag: @@ -651,7 +687,13 @@ class VideoSvmPadAlgorithm(Algorithm): real = self.select_quasi_uniform_data_subset(real, n_train_samples) attack = self.select_quasi_uniform_data_subset(attack, n_train_samples) - data = [np.copy(real), np.copy(attack)] # data for final training + if not( one_class_flag ): # two-class SVM case + + data = [np.copy(real), np.copy(attack)] # data for final training + + else: # one-class SVM case + + data = [np.copy(real)] # only real class used for training machine = trainer.train(data) # train the machine @@ -743,17 +785,26 @@ class VideoSvmPadAlgorithm(Algorithm): **Returns:** - ``probabilities`` : 2D :py:class:`numpy.ndarray` + ``probabilities`` : 1D or 2D :py:class:`numpy.ndarray` + 2D in the case of two-class SVM. An array containing class probabilities for each frame. First column contains probabilities for each frame being a real class. Second column contains probabilities for each frame being an attack class. + 1D in the case of one-class SVM. + Vector with scores for each frame defining belonging to the real class. Must be writable with the ``write_feature`` function and readable with the ``read_feature`` function. """ features_array = self.convert_frame_cont_to_array(feature) - probabilities = self.machine.predict_class_and_probabilities(features_array)[1] + if not( self.machine_type == 'ONE_CLASS' ): # two-class SVM case + + probabilities = self.machine.predict_class_and_probabilities(features_array)[1] + + else: + + probabilities = self.machine.predict_class_and_scores(features_array)[1] return probabilities @@ -765,22 +816,28 @@ class VideoSvmPadAlgorithm(Algorithm): **Parameters:** - ``toscore`` : 2D :py:class:`numpy.ndarray` + ``toscore`` : 1D or 2D :py:class:`numpy.ndarray` + 2D in the case of two-class SVM. An array containing class probabilities for each frame. First column contains probabilities for each frame being a real class. Second column contains probabilities for each frame being an attack class. + 1D in the case of one-class SVM. + Vector with scores for each frame defining belonging to the real class. **Returns:** - ``score`` : :py:class:`float` - or a list of scores containing individual score for each frame. - A score value for the object ``toscore``. - A probability of a sample being a real class. + ``score`` : :py:class:`float` or a 1D :py:class:`numpy.ndarray` + If ``frame_level_scores_flag = False`` a single score is returned. + One score per video. + Score is a probability of a sample being a real class. + If ``frame_level_scores_flag = True`` a 1D array of scores is returned. + One score per frame. + Score is a probability of a sample being a real class. """ if self.frame_level_scores_flag: - score = toscore[:,0] # here score is a list containing scores for each frame + score = toscore[:,0] # here score is a 1D array containing scores for each frame else: @@ -796,8 +853,13 @@ class VideoSvmPadAlgorithm(Algorithm): **Parameters:** - ``toscore`` : 2D :py:class:`numpy.ndarray` - An array containing scores computed by score() method of this class. + ``toscore`` : 1D or 2D :py:class:`numpy.ndarray` + 2D in the case of two-class SVM. + An array containing class probabilities for each frame. + First column contains probabilities for each frame being a real class. + Second column contains probabilities for each frame being an attack class. + 1D in the case of one-class SVM. + Vector with scores for each frame defining belonging to the real class. **Returns:** @@ -805,13 +867,15 @@ class VideoSvmPadAlgorithm(Algorithm): A list containing the scores. """ - if self.frame_level_scores_flag: + scores = self.score(toscore) # returns float score or 1D array of scores + + if isinstance(scores, np.float): # if a single score - list_of_scores = self.score(toscore) + list_of_scores = [scores] else: - list_of_scores = [self.score(toscore)] + list_of_scores = list(scores) return list_of_scores diff --git a/bob/pad/face/config/frame_diff_one_class_svm.py b/bob/pad/face/config/frame_diff_one_class_svm.py new file mode 100644 index 00000000..320522a1 --- /dev/null +++ b/bob/pad/face/config/frame_diff_one_class_svm.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +""" +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]_. +""" + + +#======================================================================================= +sub_directory = 'frame_diff_svm' +""" +Sub-directory where results will be placed. + +You may change this setting using the ``--sub-directory`` command-line option +or the attribute ``sub_directory`` in a configuration file loaded **after** +this resource. +""" + + +#======================================================================================= +# define preprocessor: + +from ..preprocessor import FrameDifference + +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) +""" +In the preprocessing stage the frame differences are computed for both facial and non-facial/background +regions. In this case all frames of the input video are considered, which is defined by +``number_of_frames = None``. The frames containing faces of the size below ``min_face_size = 50`` threshold +are discarded. Both RGB and gray-scale videos are acceptable by the preprocessor. +The preprocessing idea is introduced in [AM11]_. +""" + + +#======================================================================================= +# define extractor: + +from ..extractor import FrameDiffFeatures + +WINDOW_SIZE=20 +OVERLAP=0 + +extractor = FrameDiffFeatures(window_size=WINDOW_SIZE, + overlap=OVERLAP) +""" +In the feature extraction stage 5 features are extracted for all non-overlapping windows in +the Frame Difference input signals. Five features are computed for each of windows in the +facial face regions, the same is done for non-facial regions. The non-overlapping option +is controlled by ``overlap = 0``. The length of the window is defined by ``window_size`` +argument. +The features are introduced in the following paper: [AM11]_. +""" + + +#======================================================================================= +# define algorithm: + +from ..algorithm import VideoSvmPadAlgorithm + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +N_SAMPLES = 10000 +TRAINER_GRID_SEARCH_PARAMS = {'nu': [0.001, 0.01, 0.05, 0.1], 'gamma': [2**P for P in range(-15, 0, 2)]} +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) +""" +The one-class SVM algorithm with RBF kernel is used to classify the data into *real* and *attack* classes. +One score is produced for each frame of the input video, ``frame_level_scores_flag = True``. +The grid search of SVM parameters is used to select the successful settings. +The grid search is done on the subset of training data. +The size of this subset is defined by ``n_samples`` parameter. + +The data is also mean-std normalized, ``mean_std_norm_flag = True``. +""" + + + + + diff --git a/bob/pad/face/config/qm_one_class_svm_aggregated_db.py b/bob/pad/face/config/qm_one_class_svm_aggregated_db.py new file mode 100644 index 00000000..a1b3501e --- /dev/null +++ b/bob/pad/face/config/qm_one_class_svm_aggregated_db.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +""" +This file contains configurations to run Image Quality Measures (IQM) and SVM based face PAD baseline. +The settings of the preprocessor and extractor are tuned for the Replay-attack database. +In the SVM algorithm the amount of training data is reduced speeding-up the training for +large data sets, such as Aggregated PAD database. +The IQM features used in this algorithm/resource are introduced in the following papers: [WHJ15]_ and [CBVM16]_. +""" + + +#======================================================================================= +sub_directory = 'qm_svm_aggregated_db' +""" +Sub-directory where results will be placed. + +You may change this setting using the ``--sub-directory`` command-line option +or the attribute ``sub_directory`` in a configuration file loaded **after** +this resource. +""" + + +#======================================================================================= +# define preprocessor: + +from ..preprocessor import VideoFaceCrop + +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 +USE_LOCAL_CROPPER_FLAG = True # Use the local face cropping class (identical to Ivana's paper) +RGB_OUTPUT_FLAG = True # Return RGB cropped face using local cropper + +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 = None, + 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) +""" +In the preprocessing stage the face is cropped in each frame of the input video given facial annotations. +The size of the face is normalized to ``cropped_image_size`` dimensions. The faces of the size +below ``min_face_size`` threshold are discarded. The preprocessor is similar to the one introduced in +[CAM12]_, which is defined by ``use_local_cropper_flag = True``. The preprocessed frame is the RGB +facial image, which is defined by ``RGB_OUTPUT_FLAG = True``. +""" + + +#======================================================================================= +# define extractor: + +from ..extractor import VideoQualityMeasure + +GALBALLY=True +MSU=True +DTYPE=None + +extractor = VideoQualityMeasure(galbally=GALBALLY, + msu=MSU, + dtype=DTYPE) +""" +In the feature extraction stage the Image Quality Measures are extracted from each frame of the preprocessed RGB video. +The features to be computed are introduced in the following papers: [WHJ15]_ and [CBVM16]_. +""" + + +#======================================================================================= +# define algorithm: + +from ..algorithm import VideoSvmPadAlgorithm + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +N_SAMPLES = 50000 +TRAINER_GRID_SEARCH_PARAMS = {'nu': [0.001, 0.01, 0.05, 0.1], 'gamma': [0.01, 0.1, 1, 10]} +MEAN_STD_NORM_FLAG = True # enable mean-std normalization +FRAME_LEVEL_SCORES_FLAG = True # one score per frame(!) in this case +SAVE_DEBUG_DATA_FLAG = True # save the data, which might be useful for debugging +REDUCED_TRAIN_DATA_FLAG = False # DO NOT reduce the amount of training data in the final training stage +N_TRAIN_SAMPLES = 50000 # number of training samples per class in the final SVM training stage (NOT considered, because REDUCED_TRAIN_DATA_FLAG = False) + +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, + save_debug_data_flag = SAVE_DEBUG_DATA_FLAG, + reduced_train_data_flag = REDUCED_TRAIN_DATA_FLAG, + n_train_samples = N_TRAIN_SAMPLES) +""" +The one-class SVM algorithm with RBF kernel is used to classify the data into *real* and *attack* classes. +One score is produced for each frame of the input video, ``frame_level_scores_flag = True``. +The grid search of SVM parameters is used to select the successful settings. +The grid search is done on the subset of training data. +The size of this subset is defined by ``n_samples`` parameter. +The final training of the SVM is done on all training data ``reduced_train_data_flag = False``. +The data is also mean-std normalized, ``mean_std_norm_flag = True``. +""" + + diff --git a/setup.py b/setup.py index efe8b2ab..43d4b2ba 100644 --- a/setup.py +++ b/setup.py @@ -82,8 +82,10 @@ setup( 'lbp-svm-aggregated-db = bob.pad.face.config.lbp_svm_aggregated_db', 'qm-svm = bob.pad.face.config.qm_svm', 'qm-svm-aggregated-db = bob.pad.face.config.qm_svm_aggregated_db', + 'qm-one-class-svm-aggregated-db = bob.pad.face.config.qm_one_class_svm_aggregated_db', 'frame-diff-svm = bob.pad.face.config.frame_diff_svm', 'frame-diff-svm-aggregated-db = bob.pad.face.config.frame_diff_svm_aggregated_db', + 'frame-diff-one-class-svm = bob.pad.face.config.frame_diff_one_class_svm', ], # registered preprocessors: -- GitLab