From 16693a2d83817958b616ab8a8b43971688dd905c Mon Sep 17 00:00:00 2001
From: Olegs NIKISINS <onikisins@italix03.idiap.ch>
Date: Fri, 4 Aug 2017 18:01:34 +0200
Subject: [PATCH] Added one-class SVM option in VideoSvmPadAlgorith, created a
 couple config files for tests

---
 .../face/algorithm/VideoSvmPadAlgorithm.py    | 122 +++++++++++++-----
 .../face/config/frame_diff_one_class_svm.py   |  94 ++++++++++++++
 .../config/qm_one_class_svm_aggregated_db.py  | 111 ++++++++++++++++
 setup.py                                      |   2 +
 4 files changed, 300 insertions(+), 29 deletions(-)
 create mode 100644 bob/pad/face/config/frame_diff_one_class_svm.py
 create mode 100644 bob/pad/face/config/qm_one_class_svm_aggregated_db.py

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