diff --git a/bob/pad/face/algorithm/VideoCascadeSvmPadAlgorithm.py b/bob/pad/face/algorithm/VideoCascadeSvmPadAlgorithm.py new file mode 100644 index 0000000000000000000000000000000000000000..c5690a61a2488918f2c806fd4a746f0de6dd6f0a --- /dev/null +++ b/bob/pad/face/algorithm/VideoCascadeSvmPadAlgorithm.py @@ -0,0 +1,1050 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +Created on Wed May 17 09:43:09 2017 + +@author: Olegs Nikisins +""" + +#============================================================================== +# Import what is needed here: + +from bob.pad.base.algorithm import Algorithm + +import numpy as np + +import bob.learn.libsvm + +import bob.learn.linear + +import bob.io.base + +import os + +import fnmatch + +from bob.bio.video.utils import FrameContainer + +#============================================================================== +# Main body : + +class VideoCascadeSvmPadAlgorithm(Algorithm): + """ + This class is designed to train the **cascede** of SVMs given Frame Containers + with features of real and attack classes. The procedure is the following: + + 1. First, the input data is mean-std normalized. + + 2. Second, the PCA is trained on normalized input features. Only the + features of the **real** class are used in PCA training, both + for one-class and two-class SVMs. + + 3. The features are next projected given trained PCA machine. + + 4. Prior to SVM training the features are again mean-std normalized. + + 5. Next SVM machine is trained for each N projected features. First, preojected + features corresponding to highest eigenvalues are selected. N is usually small + N = (2, 3). So, if N = 2, the first SVM is trained for projected features 1 and 2, + second SVM is trained for projected features 3 and 4, and so on. + + 6. These SVMs then form a cascade of classifiers. The input feature vector is then + projected using PCA machine and passed through all classifiers in the cascade. + The decision is then made by majority voting. + + Both one-class SVM and two-class SVM cascades can be trained. + In this implementation the grid search of SVM parameters is not supported. + + **Parameters:** + + ``machine_type`` : :py:class:`str` + A type of the SVM machine. Please check ``bob.learn.libsvm`` for + more details. Default: 'C_SVC'. + + ``kernel_type`` : :py:class:`str` + A type of kerenel for the SVM machine. Please check ``bob.learn.libsvm`` + for more details. Default: 'RBF'. + + ``svm_kwargs`` : :py:class:`dict` + Dictionary containing the hyper-parameters of the SVM. + Default: {'cost': 1, 'gamma': 0}. + + ``N`` : :py:class:`int` + The number of features to be used for training a single SVM machine + in the cascade. Default: 2. + + ``pos_scores_slope`` : :py:class:`float` + The positive scores returned by SVM cascade will be multiplied by this + constant prior to majority voting. Default: 0.01 . + + ``frame_level_scores_flag`` : :py:class:`bool` + Return scores for each frame individually if True. Otherwise, return a + single score per video. Default: False. + """ + + def __init__(self, + machine_type = 'C_SVC', + kernel_type = 'RBF', + svm_kwargs = {'cost': 1, 'gamma': 0}, + N = 2, + pos_scores_slope = 0.01, + frame_level_scores_flag = False): + + + Algorithm.__init__(self, + machine_type = machine_type, + kernel_type = kernel_type, + svm_kwargs = svm_kwargs, + N = N, + pos_scores_slope = pos_scores_slope, + frame_level_scores_flag = frame_level_scores_flag, + performs_projection=True, + requires_projector_training=True) + + self.machine_type = machine_type + self.kernel_type = kernel_type + self.svm_kwargs = svm_kwargs + self.N = N + self.pos_scores_slope = pos_scores_slope + self.frame_level_scores_flag = frame_level_scores_flag + + self.pca_projector_file_name = "pca_projector" # pca machine will be saved to .hdf5 file with this name + self.svm_projector_file_name = "svm_projector" # svm machines will be saved to .hdf5 files with this name augumented by machine number + + self.pca_machine = None + self.svm_machines = None + + + #========================================================================== + def convert_frame_cont_to_array(self, frame_container): + """ + This function converts a single Frame Container into an array of features. + The rows are samples, the columns are features. + + **Parameters:** + + ``frame_container`` : object + A Frame Container conteining the features of an individual, + see ``bob.bio.video.utils.FrameContainer``. + + **Returns:** + + ``features_array`` : 2D :py:class:`numpy.ndarray` + An array containing features for all frames. + The rows are samples, the columns are features. + """ + + feature_vectors = [] + + frame_dictionary = {} + + for frame in frame_container: + + frame_dictionary[frame[0]] = frame[1] + + for idx, _ in enumerate(frame_container): + + # Frames are stored in a mixed order, therefore we get them using incrementing frame index: + feature_vectors.append(frame_dictionary[str(idx)]) + + features_array = np.vstack(feature_vectors) + + return features_array + + + #========================================================================== + def convert_list_of_frame_cont_to_array(self, frame_containers): + """ + This function converts a list of Frame containers into an array of features. + Features from different frame containers (individuals) are concatenated into the + same list. This list is then converted to an array. The rows are samples, + the columns are features. + + **Parameters:** + + ``frame_containers`` : [FrameContainer] + A list of Frame Containers, , see ``bob.bio.video.utils.FrameContainer``. + Each frame Container contains feature vectors for the particular individual/person. + + **Returns:** + + ``features_array`` : 2D :py:class:`numpy.ndarray` + An array containing features for all frames of all individuals. + """ + + feature_vectors = [] + + for frame_container in frame_containers: + + video_features_array = self.convert_frame_cont_to_array(frame_container) + + feature_vectors.append( video_features_array ) + + features_array = np.vstack(feature_vectors) + + return features_array + + + #========================================================================== + def comp_prediction_precision(self, machine, real, attack): + """ + This function computes the precision of the predictions as a ratio + of correctly classified samples to the total number of samples. + + **Parameters:** + + ``machine`` : object + A pre-trained SVM machine. + + ``real`` : 2D :py:class:`numpy.ndarray` + Array of features representing the real class. + + ``attack`` : 2D :py:class:`numpy.ndarray` + Array of features representing the attack class. + + **Returns:** + + ``precision`` : :py:class:`float` + The precision of the predictions. + """ + + labels_real = machine.predict_class(real) + + labels_attack = machine.predict_class(attack) + + samples_num = len(labels_real) + len(labels_attack) + + precision = ( np.sum(labels_real == 1) + np.sum(labels_attack == -1) ).astype( np.float ) / samples_num + + return precision + + + #========================================================================== + def mean_std_normalize(self, features, features_mean= None, features_std = None): + """ + The features in the input 2D array are mean-std normalized. + The rows are samples, the columns are features. If ``features_mean`` + and ``features_std`` are provided, then these vectors will be used for + normalization. Otherwise, the mean and std of the features is + computed on the fly. + + **Parameters:** + + ``features`` : 2D :py:class:`numpy.ndarray` + Array of features to be normalized. + + ``features_mean`` : 1D :py:class:`numpy.ndarray` + Mean of the features. Default: None. + + ``features_std`` : 2D :py:class:`numpy.ndarray` + Standart deviation of the features. Default: None. + + **Returns:** + + ``features_norm`` : 2D :py:class:`numpy.ndarray` + Normalized array of features. + + ``features_mean`` : 1D :py:class:`numpy.ndarray` + Mean of the features. + + ``features_std`` : 1D :py:class:`numpy.ndarray` + Standart deviation of the features. + """ + + features = np.copy(features) + + # Compute mean and std if not given: + if features_mean is None: + + features_mean = np.mean(features, axis=0) + + features_std = np.std(features, axis=0) + + row_norm_list = [] + + for row in features: # row is a sample + + row_norm = (row - features_mean) / features_std + + row_norm_list.append(row_norm) + + features_norm = np.vstack(row_norm_list) + + return features_norm, features_mean, features_std + + + #========================================================================== + def norm_train_data(self, real, attack, one_class_flag): + """ + Mean-std normalization of input data arrays. If ``one_class_flag = True`` + the ``attack`` argument can be anything, it will be skipped. + + **Parameters:** + + ``real`` : 2D :py:class:`numpy.ndarray` + Training features for the real class. + + ``attack`` : 2D :py:class:`numpy.ndarray` + Training features for the attack class. If ``one_class_flag = True`` + this argument can be anything, it will be skipped. + + ``one_class_flag`` : :py:class:`bool` + If ``True``, only real features will be used in the computation of + mean and std normalization vectors. Otherwise both sets are used. + + **Returns:** + + ``real_norm`` : 2D :py:class:`numpy.ndarray` + Mean-std normalized training features for the real class. + + ``attack_norm`` : 2D :py:class:`numpy.ndarray` + Mean-std normalized training features for the attack class. + Or an empty list if ``one_class_flag = True``. + + ``features_mean`` : 1D :py:class:`numpy.ndarray` + Mean of the features. + + ``features_std`` : 1D :py:class:`numpy.ndarray` + Standart deviation of the features. + """ + + 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_norm = features_norm[0:real.shape[0], :] # The array is now normalized + attack_norm = features_norm[real.shape[0]:, :] # The array is now normalized + + else: # one-class SVM case + + real_norm, features_mean, features_std = self.mean_std_normalize(real) # use only real class to compute normalizers + attack_norm = [] +# attack_norm = self.mean_std_normalize(attack, features_mean, features_std) + + return real_norm, attack_norm, features_mean, features_std + + + #========================================================================== + def train_pca(self, data): + """ + Train PCA given input array of feature vectors. The data is mean-std + normalized prior to PCA training. + + **Parameters:** + + ``data`` : 2D :py:class:`numpy.ndarray` + Array of feature vectors of the size (N_samples x N_features). + The features must be already mean-std normalized. + + **Returns:** + + ``machine`` : :py:class:`bob.learn.linear.Machine` + The PCA machine that has been trained. The mean-std normalizers are + also set in the machine. + + ``eig_vals`` : 1D :py:class:`numpy.ndarray` + The eigen-values of the PCA projection. + """ + + # 1. Normalize the training data: + data_norm, features_mean, features_std = self.mean_std_normalize(data) + + trainer = bob.learn.linear.PCATrainer() # Creates a PCA trainer + + [machine, eig_vals] = trainer.train(data_norm) # Trains the machine with the given data + + # Set the normalizers for the PCA machine, needed to normalize the test samples. + machine.input_subtract = features_mean # subtract the mean of train data + machine.input_divide = features_std # divide by std of train data + + return machine, eig_vals + + + #========================================================================== + def train_svm(self, real, attack, machine_type, kernel_type, svm_kwargs): + """ + One-class or two class-SVM is trained in this method given input features. + The value of ``attack`` argument is not important in the case of one-class SVM. + Prior to training the data is mean-std normalized. + + **Parameters:** + + ``real`` : 2D :py:class:`numpy.ndarray` + Training features for the real class. + + ``attack`` : 2D :py:class:`numpy.ndarray` + Training features for the attack class. If machine_type == 'ONE_CLASS' + this argument can be anything, it will be skipped. + + ``machine_type`` : :py:class:`str` + A type of the SVM machine. Please check ``bob.learn.libsvm`` for + more details. + + ``kernel_type`` : :py:class:`str` + A type of kerenel for the SVM machine. Please check ``bob.learn.libsvm`` + for more details. + + ``svm_kwargs`` : :py:class:`dict` + Dictionary containing the hyper-parameters of the SVM. + + **Returns:** + + ``machine`` : object + A trained SVM machine. The mean-std normalizers are also set in the + machine. + """ + + one_class_flag = (machine_type == 'ONE_CLASS') # True if one-class SVM is used + + # Mean-std normalize the data before training + real, attack, features_mean, features_std = self.norm_train_data(real, attack, one_class_flag) + # real and attack - are now mean-std normalized + + trainer = bob.learn.libsvm.Trainer(machine_type = machine_type, + kernel_type = kernel_type, + probability = True) + + for key in svm_kwargs.keys(): + + setattr(trainer, key, svm_kwargs[key]) # set the hyper-parameters of the SVM + + if not( one_class_flag ): # two-class SVM case + + data = [real, attack] # data for final training + + else: # one-class SVM case + + data = [real] # only real class used for training + + machine = trainer.train(data) # train the machine + + # add the normalizers to the trained SVM machine + machine.input_subtract = features_mean # subtract the mean of train data + machine.input_divide = features_std # divide by std of train data + + return machine + + + #========================================================================== + def get_data_start_end_idx(self, data, N): + """ + Get indexes to select the subsets of data related to the cascades. + First (n_machines - 1) SVMs will be trained using N features. + Last SVM will be trained using remaining features, which is less or + equal to N. + + **Parameters:** + + ``data`` : 2D :py:class:`numpy.ndarray` + Data array containing the training features. The dimensionality is + (N_samples x N_features). + + ``N`` : :py:class:`int` + Number of features per single SVM. + + **Returns:** + + ``idx_start`` : [int] + Starting indexes for data subsets. + + ``idx_end`` : [int] + End indexes for data subsets. + + ``n_machines`` : :py:class:`int` + Number of SVMs to be trained. + """ + + n_features = data.shape[1] + + n_machines = np.int(n_features/N) + + if (n_features - n_machines*N) > 1: # if more than one feature remains + + machines_num = range(0, n_machines, 1) + + idx_start = [item*N for item in machines_num] + + idx_end = [(item+1)*N for item in machines_num] + + idx_start.append( n_machines*N ) + + idx_end.append( n_features ) + + n_machines = n_machines + 1 + + else: + + machines_num = range(0, n_machines, 1) + + idx_start = [item*N for item in machines_num] + + idx_end = [(item+1)*N for item in machines_num] + + return idx_start, idx_end, n_machines + + + #========================================================================== + def train_svm_cascade(self, real, attack, machine_type, kernel_type, svm_kwargs, N): + """ + Train a cascade of SVMs, one SVM machine per N features. N is usually small + N = (2, 3). So, if N = 2, the first SVM is trained for features 1 and 2, + second SVM is trained for features 3 and 4, and so on. + + Both one-class and two-class SVM cascades can be trained. The value of + ``attack`` argument is not important in the case of one-class SVM. + + The data is mean-std normalized prior to SVM cascade training. + + **Parameters:** + + ``real`` : 2D :py:class:`numpy.ndarray` + Training features for the real class. + + ``attack`` : 2D :py:class:`numpy.ndarray` + Training features for the attack class. If machine_type == 'ONE_CLASS' + this argument can be anything, it will be skipped. + + ``machine_type`` : :py:class:`str` + A type of the SVM machine. Please check ``bob.learn.libsvm`` for + more details. + + ``kernel_type`` : :py:class:`str` + A type of kerenel for the SVM machine. Please check ``bob.learn.libsvm`` + for more details. + + ``svm_kwargs`` : :py:class:`dict` + Dictionary containing the hyper-parameters of the SVM. + + ``N`` : :py:class:`int` + The number of features to be used for training a single SVM machine + in the cascade. + + **Returns:** + + ``machines`` : :py:class:`dict` + A dictionary containing a cascade of trained SVM machines. + """ + + one_class_flag = (machine_type == 'ONE_CLASS') # True if one-class SVM is used + + idx_start, idx_end, n_machines = self.get_data_start_end_idx(real, N) + + machines = {} + + for machine_num in range(0, n_machines, 1): + + if not(one_class_flag): # two-class SVM + + real_subset = real[:, idx_start[machine_num] : idx_end[machine_num] ] # both real and attack classes are used + attack_subset = attack[:, idx_start[machine_num] : idx_end[machine_num] ] + + else: # one-class SVM case + + real_subset = real[:, idx_start[machine_num] : idx_end[machine_num] ] # only the real class is used + attack_subset = [] + + machine = self.train_svm(real_subset, attack_subset, machine_type, kernel_type, svm_kwargs) + + machines[ str(machine_num) ] = machine + + del machine + + return machines + + + #========================================================================== + def train_pca_svm_cascade(self, real, attack, machine_type, kernel_type, svm_kwargs, N): + """ + This function is designed to train the **cascede** of SVMs given + features of real and attack classes. The procedure is the following: + + 1. First, the PCA machine is trained also incorporating mean-std + feature normalization. Only the features of the **real** class are + used in PCA training, both for one-class and two-class SVMs. + + 2. The features are next projected given trained PCA machine. + + 3. Next, SVM machine is trained for each N projected features. Prior to + SVM training the features are again mean-std normalized. First, preojected + features corresponding to highest eigenvalues are selected. N is usually small + N = (2, 3). So, if N = 2, the first SVM is trained for projected features 1 and 2, + second SVM is trained for projected features 3 and 4, and so on. + + Both one-class SVM and two-class SVM cascades can be trained. + In this implementation the grid search of SVM parameters is not supported. + + **Parameters:** + + ``real`` : 2D :py:class:`numpy.ndarray` + Training features for the real class. + + ``attack`` : 2D :py:class:`numpy.ndarray` + Training features for the attack class. If machine_type == 'ONE_CLASS' + this argument can be anything, it will be skipped. + + ``machine_type`` : :py:class:`str` + A type of the SVM machine. Please check ``bob.learn.libsvm`` for + more details. + + ``kernel_type`` : :py:class:`str` + A type of kerenel for the SVM machine. Please check ``bob.learn.libsvm`` + for more details. + + ``svm_kwargs`` : :py:class:`dict` + Dictionary containing the hyper-parameters of the SVM. + + ``N`` : :py:class:`int` + The number of features to be used for training a single SVM machine + in the cascade. + + **Returns:** + + ``pca_machine`` : object + A trained PCA machine. + + ``svm_machines`` : :py:class:`dict` + A cascade of SVM machines. + """ + + one_class_flag = (machine_type == 'ONE_CLASS') # True if one-class SVM is used + + # 1. Train PCA using normalized features of the real class: + pca_machine, _ = self.train_pca(real) # the mean-std normalizers are already set in this machine + + # 2. Project the features given PCA machine: + if not(one_class_flag): + projected_real = pca_machine(real) # the normalizers are already set for the PCA machine, therefore non-normalized data is passed in + projected_attack = pca_machine(attack) # the normalizers are already set for the PCA machine, therefore non-normalized data is passed in + + else: + projected_real = pca_machine(real) # the normalizers are already set for the PCA machine, therefore non-normalized data is passed in + projected_attack = [] + + # 3. Train a cascade of SVM machines using **projected** data + svm_machines = self.train_svm_cascade(projected_real, projected_attack, machine_type, kernel_type, svm_kwargs, N) + + return pca_machine, svm_machines + + + #========================================================================== + def save_machine(self, projector_file, projector_file_name, machine): + """ + Saves the machine to the hdf5 file. The name of the file is specified in + ``projector_file_name`` string. The location is specified in the + path component of the ``projector_file`` string. + + **Parameters:** + + ``projector_file`` : :py:class:`str` + Absolute name of the file to save the trained projector to, as + returned by ``bob.pad.base`` framework. In this function only the path + component is used. + + ``projector_file_name`` : :py:class:`str` + The relative name of the file to save the machine to. Name without + extension. + + ``machine`` : object + The machine to be saved. + """ + + extension = ".hdf5" + + resulting_file_name = os.path.join( os.path.split(projector_file)[0], projector_file_name + extension ) + + f = bob.io.base.HDF5File(resulting_file_name, 'w') # open hdf5 file to save to + + machine.save(f) # save the machine and normalization parameters + + del f + + + #========================================================================== + def save_cascade_of_machines(self, projector_file, projector_file_name, machines): + """ + Saves a cascade of machines to the hdf5 files. The name of the file is + specified in ``projector_file_name`` string and will be augumented with + a number of the machine. The location is specified in the path component + of the ``projector_file`` string. + + **Parameters:** + + ``projector_file`` : :py:class:`str` + Absolute name of the file to save the trained projector to, as + returned by ``bob.pad.base`` framework. In this function only the path + component is used. + + ``projector_file_name`` : :py:class:`str` + The relative name of the file to save the machine to. This name will + be augumented with a number of the machine. Name without extension. + + ``machines`` : :py:class:`dict` + A cascade of machines. The key in the dictionary is the number of + the machine, value is the machine itself. + """ + + for key in machines: + + augumented_projector_file_name = projector_file_name + key + + machine = machines[key] + + self.save_machine(projector_file, augumented_projector_file_name, machine) + + + #========================================================================== + def train_projector(self, training_features, projector_file): + """ + Train PCA and cascade of SVMs for feature projection and save them + to files. The ``requires_projector_training = True`` flag must be set + to True to enable this function. + + **Parameters:** + + ``training_features`` : [[FrameContainer], [FrameContainer]] + A list containing two elements: [0] - a list of Frame Containers with + feature vectors for the real class; [1] - a list of Frame Containers with + feature vectors for the attack class. + + ``projector_file`` : :py:class:`str` + The file to save the trained projector to, as returned by the + ``bob.pad.base`` framework. In this class the names of the files to + save the projectors to are modified, see ``save_machine`` and + ``save_cascade_of_machines`` methods of this class for more details. + """ + + # training_features[0] - training features for the REAL class. + real = self.convert_list_of_frame_cont_to_array(training_features[0]) # output is array + # training_features[1] - training features for the ATTACK class. + attack = self.convert_list_of_frame_cont_to_array(training_features[1]) # output is array + + # Train the PCA machine and cascade of SVMs + pca_machine, svm_machines = self.train_pca_svm_cascade(real = real, + attack = attack, + machine_type = self.machine_type, + kernel_type = self.kernel_type, + svm_kwargs = self.svm_kwargs, + N = self.N) + + # Save the PCA machine + self.save_machine(projector_file, self.pca_projector_file_name, pca_machine) + + # Save the cascade of SVMs: + self.save_cascade_of_machines(projector_file, self.svm_projector_file_name, svm_machines) + + + #========================================================================== + def load_machine(self, projector_file, projector_file_name): + """ + Loads the machine from the hdf5 file. The name of the file is specified in + ``projector_file_name`` string. The location is specified in the + path component of the ``projector_file`` string. + + **Parameters:** + + ``projector_file`` : :py:class:`str` + Absolute name of the file to load the trained projector from, as + returned by ``bob.pad.base`` framework. In this function only the path + component is used. + + ``projector_file_name`` : :py:class:`str` + The relative name of the file to load the machine from. Name without + extension. + + **Returns:** + + ``machine`` : object + A machine loaded from file. + """ + + extension = ".hdf5" + + resulting_file_name = os.path.join( os.path.split(projector_file)[0], projector_file_name + extension ) # name of the file + + f = bob.io.base.HDF5File(resulting_file_name, 'r') # file to read the machine from + + if "pca_" in projector_file_name: + + machine = bob.learn.linear.Machine(f) + + if "svm_" in projector_file_name: + + machine = bob.learn.libsvm.Machine(f) + + del f + + return machine + + + #========================================================================== + def get_cascade_file_names(self, projector_file, projector_file_name): + """ + Get the list of file-names storing the cascade of machines. The location + of the files is specified in the path component of the ``projector_file`` + argument. + + **Parameters:** + + ``projector_file`` : :py:class:`str` + Absolute name of the file to load the trained projector from, as + returned by ``bob.pad.base`` framework. In this function only the path + component is used. + + ``projector_file_name`` : :py:class:`str` + The **common** string in the names of files storing the + cascade of pretrained machines. Name without extension. + + **Returns:** + + ``cascade_file_names`` : [str] + A list of of **relative** file-names storing the cascade of machines. + """ + + path = os.path.split(projector_file)[0] # directory containing files storing the cascade of machines. + + files = [] + + for f in os.listdir( path ): + + if fnmatch.fnmatch( f, projector_file_name + "*" ): + + files.append(f) + + return files + + + #========================================================================== + def load_cascade_of_machines(self, projector_file, projector_file_name): + """ + Loades a cascade of machines from the hdf5 files. The name of the file is + specified in ``projector_file_name`` string and will be augumented with + a number of the machine. The location is specified in the path component + of the ``projector_file`` string. + + **Parameters:** + + ``projector_file`` : :py:class:`str` + Absolute name of the file to load the trained projector from, as + returned by ``bob.pad.base`` framework. In this function only the path + component is used. + + ``projector_file_name`` : :py:class:`str` + The relative name of the file to load the machine from. This name will + be augumented with a number of the machine. Name without extension. + + **Returns:** + + ``machines`` : :py:class:`dict` + A cascade of machines. The key in the dictionary is the number of + the machine, value is the machine itself. + """ + + files = self.get_cascade_file_names(projector_file, projector_file_name) # files storing the cascade + + machines = {} + + for idx, _ in enumerate(files): + + machine = self.load_machine( projector_file, projector_file_name + str(idx) ) + + machines[ str(idx) ] = machine + + return machines + + + #========================================================================== + def load_projector(self, projector_file): + """ + Load the pretrained PCA machine and a cascade of SVM classifiers from + files to perform feature projection. + This function sets the arguments ``self.pca_machine`` and ``self.svm_machines`` + of this class with loaded machines. + + The function must be capable of reading the data saved with the + :py:meth:`train_projector` method of this class. + + Please register `performs_projection = True` in the constructor to + enable this function. + + **Parameters:** + + ``projector_file`` : :py:class:`str` + The file to read the projector from, as returned by the + ``bob.pad.base`` framework. In this class the names of the files to + read the projectors from are modified, see ``load_machine`` and + ``load_cascade_of_machines`` methods of this class for more details. + """ + + # Load the PCA machine + pca_machine = self.load_machine(projector_file, self.pca_projector_file_name) + + # Load the cascade of SVMs: + svm_machines = self.load_cascade_of_machines(projector_file, self.svm_projector_file_name) + + self.pca_machine = pca_machine + self.svm_machines = svm_machines + + + #========================================================================== + def combine_scores_of_svm_cascade(self, scores_array, pos_scores_slope): + """ + First, multiply positive scores by constant ``pos_scores_slope`` in the + input 2D array. The constant is usually small, making the impact of negative + scores more significant. + Second, the a single score per sample is obtained by avaraging the + **pre-modified** scores of the cascade. + + **Parameters:** + + ``scores_array`` : 2D :py:class:`numpy.ndarray` + 2D score array of the size (N_samples x N_scores). + + ``pos_scores_slope`` : :py:class:`float` + The positive scores returned by SVM cascade will be multiplied by this + constant prior to majority voting. Default: 0.01 . + + **Returns:** + + ``scores`` : 1D :py:class:`numpy.ndarray` + Vector of scores. Scores for the real class are expected to be + higher, than the scores of the negative / attack class. + """ + + cols = [] + + for col in scores_array.T: + + idx_vec = np.where(col>=0) + + col[idx_vec] *= pos_scores_slope # multiply positive scores by the constant + + cols.append(col) + + scores_array_modified = np.stack(cols, axis=1) + + scores = np.mean(scores_array_modified, axis = 1) + + return scores + + + #========================================================================== + def project(self, feature): + """ + This function computes a vector of scores for each sample in the input + array of features. The following steps are apllied: + + 1. Convert input array to numpy array if necessary. + + 2. Project features using pretrained PCA machine. + + 3. Apply the cascade of SVMs to the preojected features. + + 4. Compute a single score per sample by combining the scores produced + by the cascade of SVMs. The combination is done using + ``combine_scores_of_svm_cascade`` method of this class. + + Set ``performs_projection = True`` in the constructor to enable this function. + It is assured that the :py:meth:`load_projector` was **called before** the + ``project`` function is executed. + + **Parameters:** + + ``feature`` : FrameContainer or 2D :py:class:`numpy.ndarray` + Two types of inputs are accepted. + A Frame Container conteining the features of an individual, + see ``bob.bio.video.utils.FrameContainer``. + Or a 2D feature array of the size (N_samples x N_features). + + **Returns:** + + ``scores`` : 1D :py:class:`numpy.ndarray` + Vector of scores. Scores for the real class are expected to be + higher, than the scores of the negative / attack class. + """ + + # 1. Convert input array to numpy array if necessary. + if isinstance(feature, FrameContainer): # if FrameContainer convert to 2D numpy array + + features_array = self.convert_frame_cont_to_array(feature) + + else: + + features_array = feature + + # 2. Project features using pretrained PCA machine. + pca_projected_features = self.pca_machine(features_array) + + # 3. Apply the cascade of SVMs to the preojected features. + all_scores = [] + + idx_start, idx_end, n_machines = self.get_data_start_end_idx(pca_projected_features, self.N) + + for machine_num in range(0, n_machines, 1): # iterate over SVM machines + + svm_machine = self.svm_machines[ str(machine_num) ] # select a machine + + # subset of PCA projected features to be passed to SVM machine + pca_projected_features_subset = pca_projected_features[:, idx_start[machine_num] : idx_end[machine_num] ] + + # for two-class SVM select the scores corresponding to the real class only, done by [:,0]. Index [0] selects the class Index [1] selects the score.. + single_machine_scores = svm_machine.predict_class_and_scores( pca_projected_features_subset )[1][:,0] + + all_scores.append(single_machine_scores) + + all_scores_array = np.stack(all_scores, axis = 1).astype(np.float) + + # 4. Combine the scores: + + one_class_flag = (svm_machine.machine_type == 'ONE_CLASS') # True if one-class SVM is used + + if not(one_class_flag): + + scores = np.mean(all_scores_array, axis = 1) # compute mean for two-class SVM + + else: # one class SVM case + + scores = self.combine_scores_of_svm_cascade(all_scores_array, self.pos_scores_slope) + + return scores + + + #========================================================================== + def score(self, toscore): + """ + Returns a probability of a sample being a real class. + + **Parameters:** + + ``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`] + If ``frame_level_scores_flag = False`` a single score is returned. + One score per video. This score is placed into a list, because + the ``score`` must be an iterable. + Score is a probability of a sample being a real class. + If ``frame_level_scores_flag = True`` a list of scores is returned. + One score per frame/sample. + """ + + if self.frame_level_scores_flag: + + score = list(toscore) + + else: + + score = [np.mean( toscore )] # compute a single score per video + + return score + + + + + diff --git a/bob/pad/face/algorithm/VideoGmmPadAlgorithm.py b/bob/pad/face/algorithm/VideoGmmPadAlgorithm.py new file mode 100644 index 0000000000000000000000000000000000000000..39511ae5ddde544955ef91e8462f10ba597e6bde --- /dev/null +++ b/bob/pad/face/algorithm/VideoGmmPadAlgorithm.py @@ -0,0 +1,476 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +Created on Mon Aug 28 16:47:47 2017 + +@author: Olegs Nikisins +""" + +#============================================================================== +# Import what is needed here: + +from bob.pad.base.algorithm import Algorithm + +from bob.bio.video.utils import FrameContainer + +import numpy as np + +import bob.io.base + +from sklearn import mixture + + +#============================================================================== +# Main body : + +class VideoGmmPadAlgorithm(Algorithm): + """ + This class is designed to train a GMM based PAD system. The GMM is trained + using data of one class (real class) only. The procedure is the following: + + 1. First, the training data is mean-std normalized using mean and std of the + real class only. + + 2. Second, the GMM with ``n_components`` Gaussians is trained using samples + of the real class. + + 3. The input features are next classified using pre-trained GMM machine. + + **Parameters:** + + ``n_components`` : :py:class:`int` + Number of Gaussians in the GMM. Default: 1 . + + ``random_state`` : :py:class:`int` + A seed for the random number generator used in the initialization of + the GMM. Default: 7 . + + ``frame_level_scores_flag`` : :py:class:`bool` + Return scores for each frame individually if True. Otherwise, return a + single score per video. Default: False. + """ + + def __init__(self, + n_components = 1, + random_state = 3, + frame_level_scores_flag = False): + + + Algorithm.__init__(self, + n_components = n_components, + random_state = random_state, + frame_level_scores_flag = frame_level_scores_flag, + performs_projection=True, + requires_projector_training=True) + + self.n_components = n_components + + self.random_state = random_state + + self.frame_level_scores_flag = frame_level_scores_flag + + self.machine = None # this argument will be updated with pretrained GMM machine + + self.features_mean = None # this argument will be updated with features mean + + self.features_std = None # this argument will be updated with features std + + # names of the arguments of the pretrained GMM machine to be saved/loaded to/from HDF5 file: + self.gmm_param_keys = ["covariance_type", "covariances_", "lower_bound_", "means_", "n_components", "weights_", "converged_", "precisions_", "precisions_cholesky_" ] + + + #========================================================================== + def convert_frame_cont_to_array(self, frame_container): + """ + This function converts a single Frame Container into an array of features. + The rows are samples, the columns are features. + + **Parameters:** + + ``frame_container`` : object + A Frame Container conteining the features of an individual, + see ``bob.bio.video.utils.FrameContainer``. + + **Returns:** + + ``features_array`` : 2D :py:class:`numpy.ndarray` + An array containing features for all frames. + The rows are samples, the columns are features. + """ + + feature_vectors = [] + + frame_dictionary = {} + + for frame in frame_container: + + frame_dictionary[frame[0]] = frame[1] + + for idx, _ in enumerate(frame_container): + + # Frames are stored in a mixed order, therefore we get them using incrementing frame index: + feature_vectors.append(frame_dictionary[str(idx)]) + + features_array = np.vstack(feature_vectors) + + return features_array + + + #========================================================================== + def convert_list_of_frame_cont_to_array(self, frame_containers): + """ + This function converts a list of Frame containers into an array of features. + Features from different frame containers (individuals) are concatenated into the + same list. This list is then converted to an array. The rows are samples, + the columns are features. + + **Parameters:** + + ``frame_containers`` : [FrameContainer] + A list of Frame Containers, , see ``bob.bio.video.utils.FrameContainer``. + Each frame Container contains feature vectors for the particular individual/person. + + **Returns:** + + ``features_array`` : 2D :py:class:`numpy.ndarray` + An array containing features for all frames of all individuals. + """ + + feature_vectors = [] + + for frame_container in frame_containers: + + video_features_array = self.convert_frame_cont_to_array(frame_container) + + feature_vectors.append( video_features_array ) + + features_array = np.vstack(feature_vectors) + + return features_array + + + #========================================================================== + def mean_std_normalize(self, features, features_mean= None, features_std = None): + """ + The features in the input 2D array are mean-std normalized. + The rows are samples, the columns are features. If ``features_mean`` + and ``features_std`` are provided, then these vectors will be used for + normalization. Otherwise, the mean and std of the features is + computed on the fly. + + **Parameters:** + + ``features`` : 2D :py:class:`numpy.ndarray` + Array of features to be normalized. + + ``features_mean`` : 1D :py:class:`numpy.ndarray` + Mean of the features. Default: None. + + ``features_std`` : 2D :py:class:`numpy.ndarray` + Standart deviation of the features. Default: None. + + **Returns:** + + ``features_norm`` : 2D :py:class:`numpy.ndarray` + Normalized array of features. + + ``features_mean`` : 1D :py:class:`numpy.ndarray` + Mean of the features. + + ``features_std`` : 1D :py:class:`numpy.ndarray` + Standart deviation of the features. + """ + + features = np.copy(features) + + # Compute mean and std if not given: + if features_mean is None: + + features_mean = np.mean(features, axis=0) + + features_std = np.std(features, axis=0) + + row_norm_list = [] + + for row in features: # row is a sample + + row_norm = (row - features_mean) / features_std + + row_norm_list.append(row_norm) + + features_norm = np.vstack(row_norm_list) + + return features_norm, features_mean, features_std + + + #========================================================================== + def train_gmm(self, real, n_components, random_state): + """ + Train GMM classifier given real class. Prior to the training the data is + mean-std normalized. + + **Parameters:** + + ``real`` : 2D :py:class:`numpy.ndarray` + Training features for the real class. + + ``n_components`` : :py:class:`int` + Number of Gaussians in the GMM. Default: 1 . + + ``random_state`` : :py:class:`int` + A seed for the random number generator used in the initialization of + the GMM. Default: 7 . + + **Returns:** + + ``machine`` : object + A trained GMM machine. + + ``features_mean`` : 1D :py:class:`numpy.ndarray` + Mean of the features. + + ``features_std`` : 1D :py:class:`numpy.ndarray` + Standart deviation of the features. + """ + + features_norm, features_mean, features_std = self.mean_std_normalize(real) + # real is now mean-std normalized + + machine = mixture.GaussianMixture(n_components = n_components, + random_state = random_state, + covariance_type = 'full') + + machine.fit( features_norm ) + + return machine, features_mean, features_std + + + #========================================================================== + def save_gmm_machine_and_mean_std(self, projector_file, machine, features_mean, features_std): + """ + Saves the GMM machine, features mean and std to the hdf5 file. + The absolute name of the file is specified in ``projector_file`` string. + + **Parameters:** + + ``projector_file`` : :py:class:`str` + Absolute name of the file to save the data to, as returned by + ``bob.pad.base`` framework. + + ``machine`` : object + The GMM machine to be saved. As returned by sklearn.linear_model + module. + + ``features_mean`` : 1D :py:class:`numpy.ndarray` + Mean of the features. + + ``features_std`` : 1D :py:class:`numpy.ndarray` + Standart deviation of the features. + """ + + f = bob.io.base.HDF5File(projector_file, 'w') # open hdf5 file to save to + + for key in self.gmm_param_keys: + + data = getattr( machine, key ) + + f.set( key, data ) + + f.set( "features_mean", features_mean ) + + f.set( "features_std", features_std ) + + del f + + + #========================================================================== + def train_projector(self, training_features, projector_file): + """ + Train GMM for feature projection and save it to file. + The ``requires_projector_training = True`` flag must be set to True + to enable this function. + + **Parameters:** + + ``training_features`` : [[FrameContainer], [FrameContainer]] + A list containing two elements: [0] - a list of Frame Containers with + feature vectors for the real class; [1] - a list of Frame Containers with + feature vectors for the attack class. + + ``projector_file`` : :py:class:`str` + The file to save the trained projector to, as returned by the + ``bob.pad.base`` framework. + """ + + # training_features[0] - training features for the REAL class. + real = self.convert_list_of_frame_cont_to_array(training_features[0]) # output is array + + # training_features[1] - training features for the ATTACK class. +# attack = self.convert_list_of_frame_cont_to_array(training_features[1]) # output is array + + # Train the GMM machine and get normalizers: + machine, features_mean, features_std = self.train_gmm(real = real, + n_components = self.n_components, + random_state = self.random_state) + + # Save the GNN machine and normalizers: + self.save_gmm_machine_and_mean_std(projector_file, machine, features_mean, features_std) + + + #========================================================================== + def load_gmm_machine_and_mean_std(self, projector_file): + """ + Loads the machine, features mean and std from the hdf5 file. + The absolute name of the file is specified in ``projector_file`` string. + + **Parameters:** + + ``projector_file`` : :py:class:`str` + Absolute name of the file to load the trained projector from, as + returned by ``bob.pad.base`` framework. + + **Returns:** + + ``machine`` : object + The loaded GMM machine. As returned by sklearn.mixture module. + + ``features_mean`` : 1D :py:class:`numpy.ndarray` + Mean of the features. + + ``features_std`` : 1D :py:class:`numpy.ndarray` + Standart deviation of the features. + """ + + f = bob.io.base.HDF5File(projector_file, 'r') # file to read the machine from + + # initialize the machine: + machine = mixture.GaussianMixture() + + # set the params of the machine: + for key in self.gmm_param_keys: + + data = f.read(key) + + setattr(machine, key, data) + + features_mean = f.read("features_mean") + + features_std = f.read("features_std") + + del f + + return machine, features_mean, features_std + + + #========================================================================== + def load_projector(self, projector_file): + """ + Loads the machine, features mean and std from the hdf5 file. + The absolute name of the file is specified in ``projector_file`` string. + + This function sets the arguments ``self.machine``, ``self.features_mean`` + and ``self.features_std`` of this class with loaded machines. + + The function must be capable of reading the data saved with the + :py:meth:`train_projector` method of this class. + + Please register `performs_projection = True` in the constructor to + enable this function. + + **Parameters:** + + ``projector_file`` : :py:class:`str` + The file to read the projector from, as returned by the + ``bob.pad.base`` framework. In this class the names of the files to + read the projectors from are modified, see ``load_machine`` and + ``load_cascade_of_machines`` methods of this class for more details. + """ + + machine, features_mean, features_std = self.load_gmm_machine_and_mean_std(projector_file) + + self.machine = machine + + self.features_mean = features_mean + + self.features_std = features_std + + + #========================================================================== + def project(self, feature): + """ + This function computes a vector of scores for each sample in the input + array of features. The following steps are applied: + + 1. First, the input data is mean-std normalized using mean and std of the + real class only. + + 2. The input features are next classified using pre-trained GMM machine. + + Set ``performs_projection = True`` in the constructor to enable this function. + It is assured that the :py:meth:`load_projector` was **called before** the + ``project`` function is executed. + + **Parameters:** + + ``feature`` : FrameContainer or 2D :py:class:`numpy.ndarray` + Two types of inputs are accepted. + A Frame Container conteining the features of an individual, + see ``bob.bio.video.utils.FrameContainer``. + Or a 2D feature array of the size (N_samples x N_features). + + **Returns:** + + ``scores`` : 1D :py:class:`numpy.ndarray` + Vector of scores. Scores for the real class are expected to be + higher, than the scores of the negative / attack class. + In this case scores are the weighted log probabilities. + """ + + # 1. Convert input array to numpy array if necessary. + if isinstance(feature, FrameContainer): # if FrameContainer convert to 2D numpy array + + features_array = self.convert_frame_cont_to_array(feature) + + else: + + features_array = feature + + features_array_norm, _, _ = self.mean_std_normalize(features_array, self.features_mean, self.features_std) + + scores = self.machine.score_samples( features_array_norm ) + + return scores + + + #========================================================================== + def score(self, toscore): + """ + Returns a probability of a sample being a real class. + + **Parameters:** + + ``toscore`` : 1D :py:class:`numpy.ndarray` + Vector with scores for each frame/sample defining the probability + of the frame being a sample of the real class. + + **Returns:** + + ``score`` : [:py:class:`float`] + If ``frame_level_scores_flag = False`` a single score is returned. + One score per video. This score is placed into a list, because + the ``score`` must be an iterable. + Score is a probability of a sample being a real class. + If ``frame_level_scores_flag = True`` a list of scores is returned. + One score per frame/sample. + """ + + if self.frame_level_scores_flag: + + score = list(toscore) + + else: + + score = [np.mean( toscore )] # compute a single score per video + + return score + diff --git a/bob/pad/face/algorithm/VideoLRPadAlgorithm.py b/bob/pad/face/algorithm/VideoLRPadAlgorithm.py new file mode 100644 index 0000000000000000000000000000000000000000..ff05a55b20dcce2a16d70f5def6cab98328fa6a4 --- /dev/null +++ b/bob/pad/face/algorithm/VideoLRPadAlgorithm.py @@ -0,0 +1,589 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +""" +Created on Fri Aug 25 09:29:02 2017 + +@author: Olegs Nikisins +""" + +#============================================================================== +# Import what is needed here: + +from bob.pad.base.algorithm import Algorithm + +from bob.bio.video.utils import FrameContainer + +import numpy as np + +from sklearn import linear_model + +import bob.io.base + + +#============================================================================== +# Main body : + +class VideoLRPadAlgorithm(Algorithm): + """ + This class is designed to train Logistic Regression classifier given Frame Containers + with features of real and attack classes. The procedure is the following: + + 1. First, the input data is mean-std normalized using mean and std of the + real class only. + + 2. Second, the Logistic Regression classifier is trained on normalized + input features. + + 3. The input features are next classified using pre-trained LR machine. + + **Parameters:** + + ``C`` : :py:class:`float` + Inverse of regularization strength in LR classifier; must be a positive. + Like in support vector machines, smaller values specify stronger + regularization. Default: 1.0 . + + ``frame_level_scores_flag`` : :py:class:`bool` + Return scores for each frame individually if True. Otherwise, return a + single score per video. Default: ``False``. + + ``subsample_train_data_flag`` : :py:class:`bool` + Uniformly subsample the training data if ``True``. Default: ``False``. + + ``subsampling_step`` : :py:class:`int` + Training data subsampling step, only valid is + ``subsample_train_data_flag = True``. Default: 10 . + + ``subsample_videos_flag`` : :py:class:`bool` + Uniformly subsample the training videos if ``True``. Default: ``False``. + + ``video_subsampling_step`` : :py:class:`int` + Training videos subsampling step, only valid is + ``subsample_videos_flag = True``. Default: 3 . + """ + + def __init__(self, + C = 1, + frame_level_scores_flag = False, + subsample_train_data_flag = False, + subsampling_step = 10, + subsample_videos_flag = False, + video_subsampling_step = 3): + + + Algorithm.__init__(self, + C = C, + frame_level_scores_flag = frame_level_scores_flag, + subsample_train_data_flag = subsample_train_data_flag, + subsampling_step = subsampling_step, + subsample_videos_flag = subsample_videos_flag, + video_subsampling_step = video_subsampling_step, + performs_projection=True, + requires_projector_training=True) + + self.C = C + + self.frame_level_scores_flag = frame_level_scores_flag + + self.subsample_train_data_flag = subsample_train_data_flag + + self.subsampling_step = subsampling_step + + self.subsample_videos_flag = subsample_videos_flag + + self.video_subsampling_step = video_subsampling_step + + self.lr_machine = None # this argument will be updated with pretrained LR machine + + self.features_mean = None # this argument will be updated with features mean + self.features_std = None # this argument will be updated with features std + + # names of the arguments of the pretrained LR machine to be saved/loaded to/from HDF5 file: + self.lr_param_keys = ["C", "classes_", "coef_", "intercept_"] + + + #========================================================================== + def convert_frame_cont_to_array(self, frame_container): + """ + This function converts a single Frame Container into an array of features. + The rows are samples, the columns are features. + + **Parameters:** + + ``frame_container`` : object + A Frame Container conteining the features of an individual, + see ``bob.bio.video.utils.FrameContainer``. + + **Returns:** + + ``features_array`` : 2D :py:class:`numpy.ndarray` + An array containing features for all frames. + The rows are samples, the columns are features. + """ + + feature_vectors = [] + + frame_dictionary = {} + + for frame in frame_container: + + frame_dictionary[frame[0]] = frame[1] + + for idx, _ in enumerate(frame_container): + + # Frames are stored in a mixed order, therefore we get them using incrementing frame index: + feature_vectors.append(frame_dictionary[str(idx)]) + + features_array = np.vstack(feature_vectors) + + return features_array + + + #========================================================================== + def convert_list_of_frame_cont_to_array(self, frame_containers): + """ + This function converts a list of Frame containers into an array of features. + Features from different frame containers (individuals) are concatenated into the + same list. This list is then converted to an array. The rows are samples, + the columns are features. + + **Parameters:** + + ``frame_containers`` : [FrameContainer] + A list of Frame Containers, , see ``bob.bio.video.utils.FrameContainer``. + Each frame Container contains feature vectors for the particular individual/person. + + **Returns:** + + ``features_array`` : 2D :py:class:`numpy.ndarray` + An array containing features for all frames of all individuals. + """ + + feature_vectors = [] + + for frame_container in frame_containers: + + video_features_array = self.convert_frame_cont_to_array(frame_container) + + feature_vectors.append( video_features_array ) + + features_array = np.vstack(feature_vectors) + + return features_array + + + #========================================================================== + def mean_std_normalize(self, features, features_mean= None, features_std = None): + """ + The features in the input 2D array are mean-std normalized. + The rows are samples, the columns are features. If ``features_mean`` + and ``features_std`` are provided, then these vectors will be used for + normalization. Otherwise, the mean and std of the features is + computed on the fly. + + **Parameters:** + + ``features`` : 2D :py:class:`numpy.ndarray` + Array of features to be normalized. + + ``features_mean`` : 1D :py:class:`numpy.ndarray` + Mean of the features. Default: None. + + ``features_std`` : 2D :py:class:`numpy.ndarray` + Standart deviation of the features. Default: None. + + **Returns:** + + ``features_norm`` : 2D :py:class:`numpy.ndarray` + Normalized array of features. + + ``features_mean`` : 1D :py:class:`numpy.ndarray` + Mean of the features. + + ``features_std`` : 1D :py:class:`numpy.ndarray` + Standart deviation of the features. + """ + + features = np.copy(features) + + # Compute mean and std if not given: + if features_mean is None: + + features_mean = np.mean(features, axis=0) + + features_std = np.std(features, axis=0) + + row_norm_list = [] + + for row in features: # row is a sample + + row_norm = (row - features_mean) / features_std + + row_norm_list.append(row_norm) + + features_norm = np.vstack(row_norm_list) + + return features_norm, features_mean, features_std + + + #========================================================================== + def norm_train_data(self, real, attack): + """ + Mean-std normalization of input data arrays. The mean and std normalizers + are computed using real class only. + + **Parameters:** + + ``real`` : 2D :py:class:`numpy.ndarray` + Training features for the real class. + + ``attack`` : 2D :py:class:`numpy.ndarray` + Training features for the attack class. + + **Returns:** + + ``real_norm`` : 2D :py:class:`numpy.ndarray` + Mean-std normalized training features for the real class. + + ``attack_norm`` : 2D :py:class:`numpy.ndarray` + Mean-std normalized training features for the attack class. + Or an empty list if ``one_class_flag = True``. + + ``features_mean`` : 1D :py:class:`numpy.ndarray` + Mean of the features. + + ``features_std`` : 1D :py:class:`numpy.ndarray` + Standart deviation of the features. + """ + + real_norm, features_mean, features_std = self.mean_std_normalize(real) + + attack_norm, _, _ = self.mean_std_normalize(attack, features_mean, features_std) + + return real_norm, attack_norm, features_mean, features_std + + + #========================================================================== + def train_lr(self, real, attack, C): + """ + Train LR classifier given real and attack classes. Prior to training + the data is mean-std normalized. + + **Parameters:** + + ``real`` : 2D :py:class:`numpy.ndarray` + Training features for the real class. + + ``attack`` : 2D :py:class:`numpy.ndarray` + Training features for the attack class. + + ``C`` : :py:class:`float` + Inverse of regularization strength in LR classifier; must be a positive. + Like in support vector machines, smaller values specify stronger + regularization. Default: 1.0 . + + **Returns:** + + ``machine`` : object + A trained LR machine. + + ``features_mean`` : 1D :py:class:`numpy.ndarray` + Mean of the features. + + ``features_std`` : 1D :py:class:`numpy.ndarray` + Standart deviation of the features. + """ + + real, attack, features_mean, features_std = self.norm_train_data(real, attack) + # real and attack - are now mean-std normalized + + X = np.vstack([real, attack]) + + Y = np.hstack( [ np.zeros(len(real) ), np.ones(len(attack) ) ] ) + + machine = linear_model.LogisticRegression( C = C ) + + machine.fit(X, Y) + + return machine, features_mean, features_std + + + #========================================================================== + def save_lr_machine_and_mean_std(self, projector_file, machine, features_mean, features_std): + """ + Saves the LR machine, features mean and std to the hdf5 file. + The absolute name of the file is specified in ``projector_file`` string. + + **Parameters:** + + ``projector_file`` : :py:class:`str` + Absolute name of the file to save the data to, as returned by + ``bob.pad.base`` framework. + + ``machine`` : object + The LR machine to be saved. As returned by sklearn.linear_model + module. + + ``features_mean`` : 1D :py:class:`numpy.ndarray` + Mean of the features. + + ``features_std`` : 1D :py:class:`numpy.ndarray` + Standart deviation of the features. + """ + + f = bob.io.base.HDF5File(projector_file, 'w') # open hdf5 file to save to + + for key in self.lr_param_keys: # ["C", "classes_", "coef_", "intercept_"] + + data = getattr( machine, key ) + + f.set( key, data ) + + f.set( "features_mean", features_mean ) + + f.set( "features_std", features_std ) + + del f + + + #========================================================================== + def subsample_train_videos(self, training_features, step): + """ + Uniformly select subset of frmae containes from the input list + + **Parameters:** + + ``training_features`` : [FrameContainer] + A list of FrameContainers + + ``step`` : :py:class:`int` + Data selection step. + + **Returns:** + + ``training_features_subset`` : [FrameContainer] + A list with selected FrameContainers + """ + + indexes = range(0, len(training_features), step) + + training_features_subset = [training_features[x] for x in indexes] + + return training_features_subset + + + #========================================================================== + def train_projector(self, training_features, projector_file): + """ + Train LR for feature projection and save them to files. + The ``requires_projector_training = True`` flag must be set to True + to enable this function. + + **Parameters:** + + ``training_features`` : [[FrameContainer], [FrameContainer]] + A list containing two elements: [0] - a list of Frame Containers with + feature vectors for the real class; [1] - a list of Frame Containers with + feature vectors for the attack class. + + ``projector_file`` : :py:class:`str` + The file to save the trained projector to, as returned by the + ``bob.pad.base`` framework. + """ + + # training_features[0] - training features for the REAL class. + # training_features[1] - training features for the ATTACK class. + + if self.subsample_videos_flag: # subsample videos of the real class + + real = self.convert_list_of_frame_cont_to_array( self.subsample_train_videos(training_features[0], self.video_subsampling_step) ) # output is array + + else: + + real = self.convert_list_of_frame_cont_to_array(training_features[0]) # output is array + + if self.subsample_train_data_flag: + + real = real[range(0,len(real), self.subsampling_step), :] + + if self.subsample_videos_flag: # subsample videos of the real class + + attack = self.convert_list_of_frame_cont_to_array( self.subsample_train_videos(training_features[1], self.video_subsampling_step) ) # output is array + + else: + + attack = self.convert_list_of_frame_cont_to_array(training_features[1]) # output is array + + if self.subsample_train_data_flag: + + attack = attack[range(0,len(attack), self.subsampling_step), :] + + # Train the LR machine and get normalizers: + machine, features_mean, features_std = self.train_lr(real = real, + attack = attack, + C = self.C) + + # Save the LR machine and normalizers: + self.save_lr_machine_and_mean_std(projector_file, machine, features_mean, features_std) + + + #========================================================================== + def load_lr_machine_and_mean_std(self, projector_file): + """ + Loads the machine, features mean and std from the hdf5 file. + The absolute name of the file is specified in ``projector_file`` string. + + **Parameters:** + + ``projector_file`` : :py:class:`str` + Absolute name of the file to load the trained projector from, as + returned by ``bob.pad.base`` framework. + + **Returns:** + + ``machine`` : object + The loaded LR machine. As returned by sklearn.linear_model module. + + ``features_mean`` : 1D :py:class:`numpy.ndarray` + Mean of the features. + + ``features_std`` : 1D :py:class:`numpy.ndarray` + Standart deviation of the features. + """ + + f = bob.io.base.HDF5File(projector_file, 'r') # file to read the machine from + + # initialize the machine: + machine = linear_model.LogisticRegression() + + # set the params of the machine: + for key in self.lr_param_keys: # ["C", "classes_", "coef_", "intercept_"] + + data = f.read(key) + + setattr(machine, key, data) + + features_mean = f.read("features_mean") + + features_std = f.read("features_std") + + del f + + return machine, features_mean, features_std + + + #========================================================================== + def load_projector(self, projector_file): + """ + Loads the machine, features mean and std from the hdf5 file. + The absolute name of the file is specified in ``projector_file`` string. + + This function sets the arguments ``self.lr_machine``, ``self.features_mean`` + and ``self.features_std`` of this class with loaded machines. + + The function must be capable of reading the data saved with the + :py:meth:`train_projector` method of this class. + + Please register `performs_projection = True` in the constructor to + enable this function. + + **Parameters:** + + ``projector_file`` : :py:class:`str` + The file to read the projector from, as returned by the + ``bob.pad.base`` framework. In this class the names of the files to + read the projectors from are modified, see ``load_machine`` and + ``load_cascade_of_machines`` methods of this class for more details. + """ + + lr_machine, features_mean, features_std = self.load_lr_machine_and_mean_std(projector_file) + + self.lr_machine = lr_machine + + self.features_mean = features_mean + + self.features_std = features_std + + + #========================================================================== + def project(self, feature): + """ + This function computes a vector of scores for each sample in the input + array of features. The following steps are apllied: + + 1. First, the input data is mean-std normalized using mean and std of the + real class only. + + 2. The input features are next classified using pre-trained LR machine. + + Set ``performs_projection = True`` in the constructor to enable this function. + It is assured that the :py:meth:`load_projector` was **called before** the + ``project`` function is executed. + + **Parameters:** + + ``feature`` : FrameContainer or 2D :py:class:`numpy.ndarray` + Two types of inputs are accepted. + A Frame Container conteining the features of an individual, + see ``bob.bio.video.utils.FrameContainer``. + Or a 2D feature array of the size (N_samples x N_features). + + **Returns:** + + ``scores`` : 1D :py:class:`numpy.ndarray` + Vector of scores. Scores for the real class are expected to be + higher, than the scores of the negative / attack class. + In this case scores are probabilities. + """ + + # 1. Convert input array to numpy array if necessary. + if isinstance(feature, FrameContainer): # if FrameContainer convert to 2D numpy array + + features_array = self.convert_frame_cont_to_array(feature) + + else: + + features_array = feature + + features_array_norm, _, _ = self.mean_std_normalize(features_array, self.features_mean, self.features_std) + + scores = self.lr_machine.predict_proba( features_array_norm )[:,0] + + return scores + + + #========================================================================== + def score(self, toscore): + """ + Returns a probability of a sample being a real class. + + **Parameters:** + + ``toscore`` : 1D :py:class:`numpy.ndarray` + Vector with scores for each frame/sample defining the probability + of the frame being a sample of the real class. + + **Returns:** + + ``score`` : [:py:class:`float`] + If ``frame_level_scores_flag = False`` a single score is returned. + One score per video. This score is placed into a list, because + the ``score`` must be an iterable. + Score is a probability of a sample being a real class. + If ``frame_level_scores_flag = True`` a list of scores is returned. + One score per frame/sample. + """ + + if self.frame_level_scores_flag: + + score = list(toscore) + + else: + + score = [np.mean( toscore )] # compute a single score per video + + return score + + + + diff --git a/bob/pad/face/algorithm/VideoSvmPadAlgorithm.py b/bob/pad/face/algorithm/VideoSvmPadAlgorithm.py index d84b07529f7faf4dffbad287b20137794e275172..6fb38d1c9257d4107944b7d1ce115bfb23c871c8 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,26 +816,32 @@ 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: - score = np.mean(toscore, axis=0)[0] # compute a single score per video + score = np.mean( toscore[:,0] ) # compute a single score per video return score @@ -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/algorithm/__init__.py b/bob/pad/face/algorithm/__init__.py index 77265bf492e0f894c2d938013414ba689bb20ab1..eeb27d59b563e895d3bd2f975fc87cb24060a228 100644 --- a/bob/pad/face/algorithm/__init__.py +++ b/bob/pad/face/algorithm/__init__.py @@ -1,5 +1,7 @@ from .VideoSvmPadAlgorithm import VideoSvmPadAlgorithm - +from .VideoCascadeSvmPadAlgorithm import VideoCascadeSvmPadAlgorithm +from .VideoLRPadAlgorithm import VideoLRPadAlgorithm +from .VideoGmmPadAlgorithm import VideoGmmPadAlgorithm def __appropriate__(*args): """Says object was actually declared here, and not in the import module. @@ -9,7 +11,7 @@ def __appropriate__(*args): Parameters ---------- *args - The objects that you want sphinx to beleive that are defined here. + The objects that you want sphinx to believe that are defined here. Resolves `Sphinx referencing issues <https//github.com/sphinx- doc/sphinx/issues/3048>` @@ -21,5 +23,8 @@ def __appropriate__(*args): __appropriate__( VideoSvmPadAlgorithm, + VideoCascadeSvmPadAlgorithm, + VideoLRPadAlgorithm, + VideoGmmPadAlgorithm, ) __all__ = [_ for _ in dir() if not _.startswith('_')] diff --git a/bob/pad/face/config/algorithm/video_cascade_svm_pad_algorithm.py b/bob/pad/face/config/algorithm/video_cascade_svm_pad_algorithm.py new file mode 100644 index 0000000000000000000000000000000000000000..f0ee742ea33ee5226ca752f5caabadb0d4569ed4 --- /dev/null +++ b/bob/pad/face/config/algorithm/video_cascade_svm_pad_algorithm.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python + +from bob.pad.face.algorithm import VideoCascadeSvmPadAlgorithm + + +#======================================================================================= +# Define instances here: + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.2} +N = 2 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_n2_gamma_02 = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.1} +N = 2 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_n2_gamma_01 = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.05} +N = 2 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_n2_gamma_005 = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.01} +N = 2 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_n2_gamma_001 = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + + +#======================================================================================= + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.1} +N = 10 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_n10_gamma_01 = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.05} +N = 10 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_n10_gamma_005 = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.01} +N = 10 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_n10_gamma_001 = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.005} +N = 10 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_n10_gamma_0005 = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + + +#======================================================================================= + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.5} +N = 20 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_n20_gamma_05 = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.2} +N = 20 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_n20_gamma_02 = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.1} +N = 20 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_n20_gamma_01 = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.05} +N = 20 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_n20_gamma_005 = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.01} +N = 20 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_n20_gamma_001 = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.005} +N = 20 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_n20_gamma_0005 = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.001} +N = 20 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_n20_gamma_0001 = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + + +#======================================================================================= + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.1} +N = 2 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = False + +algorithm_n2_gamma_01_video_level = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + + +#======================================================================================= + +# Test the cascade of two-class SVMs. + +MACHINE_TYPE = 'C_SVC' +KERNEL_TYPE = 'RBF' +TRAINER_GRID_SEARCH_PARAMS = {'cost': 1, 'gamma': 0.01} +N = 2 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_n2_two_class_svm_c1_gamma_001 = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = TRAINER_GRID_SEARCH_PARAMS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + + + + + + diff --git a/bob/pad/face/config/algorithm/video_gmm_pad_algorithm.py b/bob/pad/face/config/algorithm/video_gmm_pad_algorithm.py new file mode 100644 index 0000000000000000000000000000000000000000..e46e285d97a7fbbf9561b62a488d90bd4d5eac15 --- /dev/null +++ b/bob/pad/face/config/algorithm/video_gmm_pad_algorithm.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python + +from bob.pad.face.algorithm import VideoGmmPadAlgorithm + + +#======================================================================================= +# Define instances here: + +N_COMPONENTS = 2 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_2 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 3 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_3 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 4 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_4 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 5 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_5 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 6 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_6 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 7 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_7 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 8 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_8 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 9 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_9 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 10 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_10 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + + +#======================================================================================= +# above 10 Gaussians: + +N_COMPONENTS = 12 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_12 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 14 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_14 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 16 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_16 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 18 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_18 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 20 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_20 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + + +#======================================================================================= +# above 20 Gaussians: + +N_COMPONENTS = 25 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_25 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 30 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_30 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 35 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_35 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 40 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_40 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 45 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_45 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 50 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_50 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + + +#======================================================================================= +# above 50 Gaussians: + +N_COMPONENTS = 60 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_60 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 70 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_70 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 80 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_80 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 90 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_90 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 100 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_100 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + + + +#======================================================================================= +# 50 Gaussians, different random seeds: + +N_COMPONENTS = 50 +RANDOM_STATE = 0 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_50_0 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + random_state = RANDOM_STATE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 50 +RANDOM_STATE = 1 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_50_1 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + random_state = RANDOM_STATE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 50 +RANDOM_STATE = 2 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_50_2 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + random_state = RANDOM_STATE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 50 +RANDOM_STATE = 3 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_50_3 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + random_state = RANDOM_STATE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 50 +RANDOM_STATE = 4 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_50_4 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + random_state = RANDOM_STATE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 50 +RANDOM_STATE = 5 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_50_5 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + random_state = RANDOM_STATE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 50 +RANDOM_STATE = 6 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_50_6 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + random_state = RANDOM_STATE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 50 +RANDOM_STATE = 7 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_50_7 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + random_state = RANDOM_STATE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 50 +RANDOM_STATE = 8 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_50_8 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + random_state = RANDOM_STATE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +N_COMPONENTS = 50 +RANDOM_STATE = 9 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm_gmm_50_9 = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + random_state = RANDOM_STATE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + + + diff --git a/bob/pad/face/config/qm_lr.py b/bob/pad/face/config/qm_lr.py new file mode 100644 index 0000000000000000000000000000000000000000..11c4404c7ce2b42d67a5a40259010ec166c3fa75 --- /dev/null +++ b/bob/pad/face/config/qm_lr.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +""" +This file contains configurations to run Image Quality Measures (IQM) and LR based face PAD algorithm. +The settings of the preprocessor and extractor are tuned for the Replay-attack database. +The IQM features used in this algorithm/resource are introduced in the following papers: [WHJ15]_ and [CBVM16]_. +""" + + +#======================================================================================= +sub_directory = 'qm_lr' +""" +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 VideoLRPadAlgorithm + +C = 1. # The regularization parameter for the LR classifier +FRAME_LEVEL_SCORES_FLAG = True # Return one score per frame + +algorithm = VideoLRPadAlgorithm(C = C, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) + +""" +The Logistic Regression 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 sub-sampling of training data is not used here, sub-sampling flags have default ``False`` +values. +""" + + diff --git a/bob/pad/face/config/qm_one_class_gmm.py b/bob/pad/face/config/qm_one_class_gmm.py new file mode 100644 index 0000000000000000000000000000000000000000..d6e877049f39b169fae692cd43c8a6c90b044438 --- /dev/null +++ b/bob/pad/face/config/qm_one_class_gmm.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +""" +This file contains configurations to run Image Quality Measures (IQM) and one-class GMM based face PAD algorithm. +The settings of the preprocessor and extractor are tuned for the Replay-attack database. +The IQM features used in this algorithm/resource are introduced in the following papers: [WHJ15]_ and [CBVM16]_. +""" + + +#======================================================================================= +sub_directory = 'qm_one_class_gmm' +""" +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 VideoGmmPadAlgorithm + +N_COMPONENTS = 50 +RANDOM_STATE = 3 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm = VideoGmmPadAlgorithm(n_components = N_COMPONENTS, + random_state = RANDOM_STATE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) +""" +The GMM with 50 clusters is trained using samples from the real class only. The pre-trained +GMM is next 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``. +""" + + + + + 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 0000000000000000000000000000000000000000..f4cbe08f2d7bd6784e8ef7e039d047c806a7f064 --- /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 one-class SVM based face PAD algorithm. +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_one_class_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/bob/pad/face/config/qm_one_class_svm_cascade_aggregated_db.py b/bob/pad/face/config/qm_one_class_svm_cascade_aggregated_db.py new file mode 100644 index 0000000000000000000000000000000000000000..b6aecc930def955b11cb8cf8849e762732468885 --- /dev/null +++ b/bob/pad/face/config/qm_one_class_svm_cascade_aggregated_db.py @@ -0,0 +1,102 @@ +#!/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 VideoCascadeSvmPadAlgorithm + +MACHINE_TYPE = 'ONE_CLASS' +KERNEL_TYPE = 'RBF' +SVM_KWARGS = {'nu': 0.001, 'gamma': 0.5} +N = 2 +POS_SCORES_SLOPE = 0.01 +FRAME_LEVEL_SCORES_FLAG = True + +algorithm = VideoCascadeSvmPadAlgorithm(machine_type = MACHINE_TYPE, + kernel_type = KERNEL_TYPE, + svm_kwargs = SVM_KWARGS, + N = N, + pos_scores_slope = POS_SCORES_SLOPE, + frame_level_scores_flag = FRAME_LEVEL_SCORES_FLAG) +""" +The cascade of one-class SVMs 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``. +A single SVM in the cascade is trained using two features ``N = 2``. +The positive scores produced by the cascade are reduced by multiplying them with a constant +``pos_scores_slope = 0.01``. +""" + diff --git a/bob/pad/face/config/qm_svm_aggregated_db.py b/bob/pad/face/config/qm_svm_aggregated_db.py index 4bd54236a09e56ef66fb503daef7baa2ee5a7182..a9deb1a554649e8000398d15d13878ba07faa00b 100644 --- a/bob/pad/face/config/qm_svm_aggregated_db.py +++ b/bob/pad/face/config/qm_svm_aggregated_db.py @@ -110,3 +110,7 @@ The data is also mean-std normalized, ``mean_std_norm_flag = True``. """ + + + + diff --git a/bob/pad/face/database/aggregated_db.py b/bob/pad/face/database/aggregated_db.py index 353c2d681d6c0f114dd8d1d2a66af383ae2e33e7..9087f3e747db1c3a21f6a5ede62883e843bd92d9 100644 --- a/bob/pad/face/database/aggregated_db.py +++ b/bob/pad/face/database/aggregated_db.py @@ -200,7 +200,32 @@ class AggregatedDbPadFile(PadFile): class AggregatedDbPadDatabase(PadDatabase): """ A high level implementation of the Database class for the Aggregated Database - uniting 3 databases: REPLAY-ATTACK, REPLAY-MOBILE and MSU MFSD. + uniting 3 databases: REPLAY-ATTACK, REPLAY-MOBILE and MSU MFSD. Currently this + database supports 3 protocols, which are listed in the ``available_protocols`` + argument of this class. + + Available protocols are: + + 1. "grandtest" - this protocol is using all the data available in the + databases Replay-Attack, Replay-Mobile, MSU MFSD. + + 2. "photo-photo-video" - this protocol is used to test the system on + unseen types of attacks. In this case the attacks are splitted + as follows: + 'train' set - only **photo** attacks are used for training, + 'dev' set - only **photo** attacks are used for threshold tuning, + 'eval' set - only **video** attacks are used in final evaluation. + In this case the final performance is estimated on previously + unseen **video** attacks. + + 3. "video-video-photo" - this protocol is used to test the system on + unseen types of attacks. In this case the attacks are splitted + as follows: + 'train' set - only **video** attacks are used for training, + 'dev' set - only **video** attacks are used for threshold tuning, + 'eval' set - only **photo** attacks are used in final evaluation. + In this case the final performance is estimated on previously + unseen **photo** attacks. """ def __init__( @@ -243,6 +268,9 @@ class AggregatedDbPadDatabase(PadDatabase): self.low_level_group_names = ('train', 'devel', 'test') # group names in the low-level database interface self.high_level_group_names = ('train', 'dev', 'eval') # names are expected to be like that in objects() function + # A list of available protocols: + self.available_protocols = ['grandtest', 'photo-photo-video', 'video-video-photo'] + # Always use super to call parent class methods. super(AggregatedDbPadDatabase, self).__init__( name = 'aggregated_db', @@ -252,6 +280,200 @@ class AggregatedDbPadDatabase(PadDatabase): **kwargs) + #========================================================================== + def get_files_given_single_group(self, groups=None, protocol=None, purposes=None, model_ids=None, **kwargs): + """ + This function returns 3 lists of files for Raplay-Attack, Replay-Mobile + and MSU MFSD databases, which fulfill the given restrictions. This + function for the groups parameter accepts a single string ONLY, which + determines the low level name of the group, see ``low_level_group_names`` + argument of this class for available options. + + Keyword parameters: + + ``groups`` : :py:class:`str` + The group of which the clients should be returned. + One element of ('train', 'devel', 'test'). + + ``protocol`` : :py:class:`str` + The protocol for which the clients should be retrieved. + Available options are defined in the ``available_protocols`` argument + of the class. So far the following protocols are available: + + 1. "grandtest" - this protocol is using all the data available in the + databases Replay-Attack, Replay-Mobile, MSU MFSD. + + 2. "photo-photo-video" - this protocol is used to test the system on + unseen types of attacks. In this case the attacks are splitted + as follows: + 'train' set - only **photo** attacks are used for training, + 'dev' set - only **photo** attacks are used for threshold tuning, + 'eval' set - only **video** attacks are used in final evaluation. + In this case the final performance is estimated on previously + unseen **video** attacks. + + 3. "video-video-photo" - this protocol is used to test the system on + unseen types of attacks. In this case the attacks are splitted + as follows: + 'train' set - only **video** attacks are used for training, + 'dev' set - only **video** attacks are used for threshold tuning, + 'eval' set - only **photo** attacks are used in final evaluation. + In this case the final performance is estimated on previously + unseen **photo** attacks. + + ``purposes`` : :py:class:`str` + OR a list of strings. + The purposes for which File objects should be retrieved. + Usually it is either 'real' or 'attack'. + + ``model_ids`` + This parameter is not supported in PAD databases yet + + **Returns:** + + ``replay_files`` : [File] + A list of files corresponding to Replay-Attack database. + + ``replaymobile_files`` : [File] + A list of files corresponding to Replay-Mobile database. + + ``msu_mfsd_files`` : [File] + A list of files corresponding to MSU MFSD database. + """ + + # with grandtest protocol + if protocol == 'grandtest' or protocol is None or groups is None: + + replay_files = self.replay_db.objects(protocol=protocol, groups=groups, cls=purposes, **kwargs) + + replaymobile_files = self.replaymobile_db.objects(protocol=protocol, groups=groups, cls=purposes, **kwargs) + + msu_mfsd_files = self.msu_mfsd_db.objects(group=groups, cls=purposes, **kwargs) + + if protocol == 'photo-photo-video': + + if groups == 'train' or groups == 'devel': # the group names are low-level here: ('train', 'devel', 'test') + + replay_files = self.replay_db.objects(protocol='photo', groups=groups, cls=purposes, **kwargs) + + replaymobile_files = self.replaymobile_db.objects(protocol='grandtest', groups=groups, cls=purposes, sample_type='photo', **kwargs) + + msu_mfsd_files = self.msu_mfsd_db.objects(group=groups, cls=purposes, instrument = ('print', ''), **kwargs) + + if groups == 'test': + + replay_files = self.replay_db.objects(protocol='video', groups=groups, cls=purposes, **kwargs) + + replaymobile_files = self.replaymobile_db.objects(protocol='grandtest', groups=groups, cls=purposes, sample_type='video', **kwargs) + + msu_mfsd_files = self.msu_mfsd_db.objects(group=groups, cls=purposes, instrument = ('video_hd', 'video_mobile', ''), **kwargs) + + if protocol == 'video-video-photo': + + if groups == 'train' or groups == 'devel': # the group names are low-level here: ('train', 'devel', 'test') + + replay_files = self.replay_db.objects(protocol='video', groups=groups, cls=purposes, **kwargs) + + replaymobile_files = self.replaymobile_db.objects(protocol='grandtest', groups=groups, cls=purposes, sample_type='video', **kwargs) + + msu_mfsd_files = self.msu_mfsd_db.objects(group=groups, cls=purposes, instrument = ('video_hd', 'video_mobile', ''), **kwargs) + + if groups == 'test': + + replay_files = self.replay_db.objects(protocol='photo', groups=groups, cls=purposes, **kwargs) + + replaymobile_files = self.replaymobile_db.objects(protocol='grandtest', groups=groups, cls=purposes, sample_type='photo', **kwargs) + + msu_mfsd_files = self.msu_mfsd_db.objects(group=groups, cls=purposes, instrument = ('print', ''), **kwargs) + + return replay_files, replaymobile_files, msu_mfsd_files + + + #========================================================================== + def get_files_given_groups(self, groups=None, protocol=None, purposes=None, model_ids=None, **kwargs): + """ + This function returns 3 lists of files for Raplay-Attack, Replay-Mobile + and MSU MFSD databases, which fulfill the given restrictions. This + function for the groups parameter accepts a single string OR a list + of strings with multiple groups. Group names are low level, see + ``low_level_group_names`` argument of the class for available options. + + Keyword parameters: + + ``groups`` : :py:class:`str` + OR a list of strings. + The groups of which the clients should be returned. + Usually, groups are one or more elements of ('train', 'devel', 'test'). + + ``protocol`` : :py:class:`str` + The protocol for which the clients should be retrieved. + Available options are defined in the ``available_protocols`` argument + of the class. So far the following protocols are available: + + 1. "grandtest" - this protocol is using all the data available in the + databases Replay-Attack, Replay-Mobile, MSU MFSD. + + 2. "photo-photo-video" - this protocol is used to test the system on + unseen types of attacks. In this case the attacks are splitted + as follows: + 'train' set - only **photo** attacks are used for training, + 'dev' set - only **photo** attacks are used for threshold tuning, + 'eval' set - only **video** attacks are used in final evaluation. + In this case the final performance is estimated on previously + unseen **video** attacks. + + 3. "video-video-photo" - this protocol is used to test the system on + unseen types of attacks. In this case the attacks are splitted + as follows: + 'train' set - only **video** attacks are used for training, + 'dev' set - only **video** attacks are used for threshold tuning, + 'eval' set - only **photo** attacks are used in final evaluation. + In this case the final performance is estimated on previously + unseen **photo** attacks. + + ``purposes`` : :py:class:`str` + OR a list of strings. + The purposes for which File objects should be retrieved. + Usually it is either 'real' or 'attack'. + + ``model_ids`` + This parameter is not supported in PAD databases yet + + **Returns:** + + ``replay_files`` : [File] + A list of files corresponding to Replay-Attack database. + + ``replaymobile_files`` : [File] + A list of files corresponding to Replay-Mobile database. + + ``msu_mfsd_files`` : [File] + A list of files corresponding to MSU MFSD database. + """ + + if isinstance(groups, str) or groups is None: # if a single group is given + + groups = [groups] + + replay_files = [] + + replaymobile_files = [] + + msu_mfsd_files = [] + + for group in groups: + + files = self.get_files_given_single_group(groups = group, protocol = protocol, purposes = purposes, model_ids = model_ids, **kwargs) + + replay_files += files[0] + + replaymobile_files += files[1] + + msu_mfsd_files += files[2] + + return replay_files, replaymobile_files, msu_mfsd_files + + #========================================================================== def objects(self, groups=None, protocol=None, purposes=None, model_ids=None, **kwargs): """ @@ -266,8 +488,29 @@ class AggregatedDbPadDatabase(PadDatabase): ``protocol`` : :py:class:`str` The protocol for which the clients should be retrieved. - The protocol is dependent on your database. - If you do not have protocols defined, just ignore this field. + Available options are defined in the ``available_protocols`` argument + of the class. So far the following protocols are available: + + 1. "grandtest" - this protocol is using all the data available in the + databases Replay-Attack, Replay-Mobile, MSU MFSD. + + 2. "photo-photo-video" - this protocol is used to test the system on + unseen types of attacks. In this case the attacks are splitted + as follows: + 'train' set - only **photo** attacks are used for training, + 'dev' set - only **photo** attacks are used for threshold tuning, + 'eval' set - only **video** attacks are used in final evaluation. + In this case the final performance is estimated on previously + unseen **video** attacks. + + 3. "video-video-photo" - this protocol is used to test the system on + unseen types of attacks. In this case the attacks are splitted + as follows: + 'train' set - only **video** attacks are used for training, + 'dev' set - only **video** attacks are used for threshold tuning, + 'eval' set - only **photo** attacks are used in final evaluation. + In this case the final performance is estimated on previously + unseen **photo** attacks. ``purposes`` : :py:class:`str` OR a list of strings. @@ -287,15 +530,23 @@ class AggregatedDbPadDatabase(PadDatabase): groups = self.convert_names_to_lowlevel(groups, self.low_level_group_names, self.high_level_group_names) # Since this database was designed for PAD experiments, nothing special # needs to be done here. - replay_files = self.replay_db.objects(protocol=protocol, groups=groups, cls=purposes, **kwargs) - replaymobile_files = self.replaymobile_db.objects(protocol=protocol, groups=groups, cls=purposes, **kwargs) + replay_files, replaymobile_files, msu_mfsd_files = self.get_files_given_groups(groups = groups, + protocol = protocol, + purposes = purposes, + model_ids = model_ids, + **kwargs) - msu_mfsd_files = self.msu_mfsd_db.objects(group=groups, cls=purposes, **kwargs) +# replay_files = self.replay_db.objects(protocol=protocol, groups=groups, cls=purposes, **kwargs) +# +# replaymobile_files = self.replaymobile_db.objects(protocol=protocol, groups=groups, cls=purposes, **kwargs) +# +# msu_mfsd_files = self.msu_mfsd_db.objects(group=groups, cls=purposes, **kwargs) files = replay_files + replaymobile_files + msu_mfsd_files # append all files to a single list files = [AggregatedDbPadFile(f) for f in files] + return files @@ -343,17 +594,3 @@ class AggregatedDbPadDatabase(PadDatabase): annotations = hldi_db.annotations(f) return annotations - - - - - - - - - - - - - - diff --git a/bob/pad/face/test/test_databases.py b/bob/pad/face/test/test_databases.py index 6c628bd2b28e310221186721254cacc434da5f48..c7b9a76f1a44930e02bb390d4dd53281c95bcd8b 100644 --- a/bob/pad/face/test/test_databases.py +++ b/bob/pad/face/test/test_databases.py @@ -7,9 +7,10 @@ from nose.plugins.skip import SkipTest import bob.bio.base from bob.bio.base.test.utils import db_available -@db_available('replay') + +@db_available('replay') # the name of the package def test_replay(): - replay_database_instance = bob.bio.base.load_resource('replay', 'database', preferred_package='bob.pad.face', package_prefix='bob.pad.') + replay_database_instance = bob.bio.base.load_resource('replay-attack', 'database', preferred_package='bob.pad.face', package_prefix='bob.pad.') # replay-attack is the name of the configuration file try: assert len( replay_database_instance.objects(groups=['train', 'dev', 'eval']) )== 1200 @@ -41,7 +42,7 @@ def test_replaymobile(): "The database could not be queried; probably the db.sql3 file is missing. Here is the error: '%s'" % e) -@db_available('msu_mfsd_mod') # the name of the package defining low-level interface of MSU MFSD +@db_available('msu_mfsd_mod') def test_msu_mfsd(): msu_mfsd = bob.bio.base.load_resource('msu-mfsd', 'database', preferred_package='bob.pad.face', package_prefix='bob.pad.') try: @@ -58,3 +59,38 @@ def test_msu_mfsd(): "The database could not be queried; probably the db.sql3 file is missing. Here is the error: '%s'" % e) +# Test the Aggregated database, which doesn't have a package +def test_aggregated_db(): + aggregated_db = bob.bio.base.load_resource('aggregated-db', 'database', preferred_package='bob.pad.face', package_prefix='bob.pad.') + try: + + assert len( aggregated_db.objects(groups=['train', 'dev', 'eval']) )== 2510 + assert len( aggregated_db.objects(groups=['train', 'dev']) ) == 1608 + assert len( aggregated_db.objects(groups=['train']) ) == 752 + + assert len( aggregated_db.objects(groups='train') ) == 752 + assert len( aggregated_db.objects(groups='dev') ) == 856 + assert len( aggregated_db.objects(groups='eval') ) == 902 + + assert len( aggregated_db.objects(groups=['train', 'dev', 'eval'], protocol = 'grandtest') ) == 2510 + assert len( aggregated_db.objects(groups=['train', 'dev', 'eval'], protocol = 'grandtest', purposes='real') ) == 660 + assert len( aggregated_db.objects(groups=['train', 'dev', 'eval'], protocol = 'grandtest', purposes='attack') ) == 1850 + + assert len( aggregated_db.objects(groups=['train', 'dev', 'eval'], protocol = 'photo-photo-video')) == 1664 + assert len( aggregated_db.objects(groups=['train', 'dev'], protocol = 'photo-photo-video')) == 1176 + assert len( aggregated_db.objects(groups='eval', protocol = 'photo-photo-video')) == 488 + + assert len( aggregated_db.objects(groups=['train', 'dev', 'eval'], protocol = 'video-video-photo')) == 1506 + assert len( aggregated_db.objects(groups=['train', 'dev'], protocol = 'video-video-photo')) == 872 + assert len( aggregated_db.objects(groups='eval', protocol = 'video-video-photo')) == 634 + + 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) + + + + + + + diff --git a/requirements.txt b/requirements.txt index 795e4100ff0332650da93fc9470405eefac9649e..4eff3ad6e2749acdbb9af39bbd8e6ca322a2f2e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,7 @@ bob.bio.video bob.io.image bob.ip.color bob.ip.qualitymeasure -bob.learn.libsvm \ No newline at end of file +bob.learn.libsvm +bob.learn.linear +scikit-learn + diff --git a/setup.py b/setup.py index efe8b2ab31e4fbd15b6e53cf718bd6631b88dc0f..25eaac9a091cf5f93670371c2423cdab5c203285 100644 --- a/setup.py +++ b/setup.py @@ -77,13 +77,25 @@ setup( 'msu-mfsd = bob.pad.face.config.msu_mfsd', 'aggregated-db = bob.pad.face.config.aggregated_db', - # baselines: + # baselines using SVM: 'lbp-svm = bob.pad.face.config.lbp_svm', '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', + 'frame-diff-svm = bob.pad.face.config.frame_diff_svm', 'frame-diff-svm-aggregated-db = bob.pad.face.config.frame_diff_svm_aggregated_db', + + # baselines using one-class SVM + 'qm-one-class-svm-aggregated-db = bob.pad.face.config.qm_one_class_svm_aggregated_db', + 'qm-one-class-svm-cascade-aggregated-db = bob.pad.face.config.qm_one_class_svm_cascade_aggregated_db', + + # baselines using LR: + 'qm-lr = bob.pad.face.config.qm_lr', # this pipe-line can be used both for individual and Aggregated databases. + + # baselines using GMM: + 'qm-one-class-gmm = bob.pad.face.config.qm_one_class_gmm', # this pipe-line can be used both for individual and Aggregated databases. ], # registered preprocessors: @@ -103,6 +115,66 @@ setup( 'video-svm-pad-algorithm-10k-grid-mean-std = bob.pad.face.config.algorithm.video_svm_pad_algorithm:video_svm_pad_algorithm_10k_grid_mean_std', 'video-svm-pad-algorithm-10k-grid-mean-std-frame-level = bob.pad.face.config.algorithm.video_svm_pad_algorithm:video_svm_pad_algorithm_10k_grid_mean_std_frame_level', 'video-svm-pad-algorithm-default-svm-param-mean-std-frame-level = bob.pad.face.config.algorithm.video_svm_pad_algorithm:video_svm_pad_algorithm_default_svm_param_mean_std_frame_level', + + # for grid search experiments with cascade of SVMs N = 2 + 'algorithm-n2-gamma-02 = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n2_gamma_02', + 'algorithm-n2-gamma-01 = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n2_gamma_01', + 'algorithm-n2-gamma-005 = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n2_gamma_005', + 'algorithm-n2-gamma-001 = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n2_gamma_001', + 'algorithm-n2-gamma-01-video-level = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n2_gamma_01_video_level', + 'algorithm-n2-two-class-svm-c1-gamma-001 = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n2_two_class_svm_c1_gamma_001', + + # for grid search experiments with cascade of SVMs N = 10 + 'algorithm-n10-gamma-01 = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n10_gamma_01', + 'algorithm-n10-gamma-005 = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n10_gamma_005', + 'algorithm-n10-gamma-001 = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n10_gamma_001', + 'algorithm-n10-gamma-0005 = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n10_gamma_0005', + + # for grid search experiments with cascade of SVMs N = 20 + 'algorithm-n20-gamma-05 = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n20_gamma_05', + 'algorithm-n20-gamma-02 = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n20_gamma_02', + 'algorithm-n20-gamma-01 = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n20_gamma_01', + 'algorithm-n20-gamma-005 = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n20_gamma_005', + 'algorithm-n20-gamma-001 = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n20_gamma_001', + 'algorithm-n20-gamma-0005 = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n20_gamma_0005', + 'algorithm-n20-gamma-0001 = bob.pad.face.config.algorithm.video_cascade_svm_pad_algorithm:algorithm_n20_gamma_0001', + + # for grid search experiments using GMM + 'algorithm-gmm-2 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_2', + 'algorithm-gmm-3 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_3', + 'algorithm-gmm-4 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_4', + 'algorithm-gmm-5 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_5', + 'algorithm-gmm-6 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_6', + 'algorithm-gmm-7 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_7', + 'algorithm-gmm-8 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_8', + 'algorithm-gmm-9 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_9', + 'algorithm-gmm-10 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_10', + 'algorithm-gmm-12 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_12', + 'algorithm-gmm-14 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_14', + 'algorithm-gmm-16 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_16', + 'algorithm-gmm-18 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_18', + 'algorithm-gmm-20 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_20', + 'algorithm-gmm-25 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_25', + 'algorithm-gmm-30 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_30', + 'algorithm-gmm-35 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_35', + 'algorithm-gmm-40 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_40', + 'algorithm-gmm-45 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_45', + 'algorithm-gmm-50 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_50', + 'algorithm-gmm-60 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_60', + 'algorithm-gmm-70 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_70', + 'algorithm-gmm-80 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_80', + 'algorithm-gmm-90 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_90', + 'algorithm-gmm-100 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_100', + 'algorithm-gmm-50-0 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_50_0', + 'algorithm-gmm-50-1 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_50_1', + 'algorithm-gmm-50-2 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_50_2', + 'algorithm-gmm-50-3 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_50_3', + 'algorithm-gmm-50-4 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_50_4', + 'algorithm-gmm-50-5 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_50_5', + 'algorithm-gmm-50-6 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_50_6', + 'algorithm-gmm-50-7 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_50_7', + 'algorithm-gmm-50-8 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_50_8', + 'algorithm-gmm-50-9 = bob.pad.face.config.algorithm.video_gmm_pad_algorithm:algorithm_gmm_50_9', ], # registered grid configurations: