From f58d5fde1ff3a95752210810cceb37398b185431 Mon Sep 17 00:00:00 2001
From: Guillaume HEUSCH <guillaume.heusch@idiap.ch>
Date: Thu, 28 Jun 2018 14:41:19 +0200
Subject: [PATCH] [extractor] fixed extractors for pulse-based PAD

---
 bob/pad/face/extractor/FFTFeatures.py       |  88 ----------------
 bob/pad/face/extractor/FrequencySpectrum.py |  85 ---------------
 bob/pad/face/extractor/LTSS.py              | 110 +++++++++++++++-----
 bob/pad/face/extractor/LiFeatures.py        |   1 +
 bob/pad/face/extractor/PPGSecure.py         |  51 +++++----
 bob/pad/face/extractor/__init__.py          |   5 +-
 6 files changed, 117 insertions(+), 223 deletions(-)
 delete mode 100644 bob/pad/face/extractor/FFTFeatures.py
 delete mode 100644 bob/pad/face/extractor/FrequencySpectrum.py

diff --git a/bob/pad/face/extractor/FFTFeatures.py b/bob/pad/face/extractor/FFTFeatures.py
deleted file mode 100644
index 48148832..00000000
--- a/bob/pad/face/extractor/FFTFeatures.py
+++ /dev/null
@@ -1,88 +0,0 @@
-#!/usr/bin/env python
-# encoding: utf-8
-
-import numpy
-
-from bob.bio.base.extractor import Extractor
-
-import logging
-logger = logging.getLogger("bob.pad.face")
-
-
-class FFTFeatures(Extractor, object):
-  """
-  Compute the Frequency Spectrum of the given signal.
-
-  The computation is made using numpy's rfft routine 
-
-  **Parameters:**
-
-  framerate: int
-    The sampling frequency of the signal (i.e the framerate ...) 
-
-  nfft: int
-    Number of points to compute the FFT
-
-  debug: boolean
-    Plot stuff
-  """
-  def __init__(self, framerate=25, nfft=256, concat=False, debug=False, **kwargs):
-
-    super(FFTFeatures, self).__init__(**kwargs)
-    
-    self.framerate = framerate
-    self.nfft = nfft
-    self.concat = concat
-    self.debug = debug
-
-
-  def __call__(self, signal):
-    """
-    Compute the frequency spectrum for the given signal.
-
-    **Parameters:**
-
-    signal: numpy.array 
-      The signal
-
-    **Returns:**
-
-      freq: numpy.array 
-       the frequency spectrum 
-    """
-    # sanity check
-    if signal.ndim == 1:
-      if numpy.isnan(numpy.sum(signal)):
-        return
-    if signal.ndim == 2 and (signal.shape[1] == 3):
-      if numpy.isnan(numpy.sum(signal[:, 1])):
-        return
-
-    output_dim = int((self.nfft / 2) + 1)
-    
-    # get the frequencies
-    f = numpy.fft.fftfreq(self.nfft) * self.framerate
-   
-    # we have a single pulse signal
-    if signal.ndim == 1:
-      fft = abs(numpy.fft.rfft(signal, n=self.nfft))
-
-    # we have 3 pulse signal (Li's preprocessing)
-    # in this case, return the signal corresponding to the green channel
-    if signal.ndim == 2 and (signal.shape[1] == 3):
-      ffts = numpy.zeros((3, output_dim))
-      for i in range(3):
-        ffts[i] = abs(numpy.fft.rfft(signal[:, i], n=self.nfft))
-
-      if self.concat:
-        fft = numpy.concatenate([ffts[0], ffts[1], ffts[2]])
-      else:
-        fft = ffts[1]
-      
-    if self.debug: 
-      from matplotlib import pyplot
-      pyplot.plot(f, fft, 'k')
-      pyplot.title('Power spectrum of the signal')
-      pyplot.show()
-
-    return fft
diff --git a/bob/pad/face/extractor/FrequencySpectrum.py b/bob/pad/face/extractor/FrequencySpectrum.py
deleted file mode 100644
index 059dd91a..00000000
--- a/bob/pad/face/extractor/FrequencySpectrum.py
+++ /dev/null
@@ -1,85 +0,0 @@
-#!/usr/bin/env python
-# encoding: utf-8
-
-import numpy
-
-from bob.bio.base.extractor import Extractor
-
-import logging
-logger = logging.getLogger("bob.pad.face")
-
-from scipy.signal import welch
-
-
-class FrequencySpectrum(Extractor, object):
-  """
-  Compute the Frequency Spectrum of the given signal.
-
-  The computation is made using Welch's algorithm.
-
-  **Parameters:**
-
-  framerate: int
-    The sampling frequency of the signal (i.e the framerate ...) 
-
-  nsegments: int
-    Number of overlapping segments in Welch procedure
-
-  nfft: int
-    Number of points to compute the FFT
-
-  debug: boolean
-    Plot stuff
-  """
-  def __init__(self, framerate=25, nsegments=12, nfft=256, debug=False, **kwargs):
-
-    super(FrequencySpectrum, self).__init__()
-    
-    self.framerate = framerate
-    self.nsegments = nsegments
-    self.nfft = nfft
-    self.debug = debug
-
-  def __call__(self, signal):
-    """
-    Compute the frequency spectrum for the given signal.
-
-    **Parameters:**
-
-    signal: numpy.array 
-      The signal
-
-    **Returns:**
-
-      freq: numpy.array 
-       the frequency spectrum 
-    """
-    # sanity check
-    if signal.ndim == 1:
-      if numpy.isnan(numpy.sum(signal)):
-        return
-    if signal.ndim == 2 and (signal.shape[1] == 3):
-      if numpy.isnan(numpy.sum(signal[:, 1])):
-        return
-
-    output_dim = int((self.nfft / 2) + 1)
-   
-    # we have a single pulse signal
-    if signal.ndim == 1:
-      f, psd = welch(signal, self.framerate, nperseg=self.nsegments, nfft=self.nfft)
-
-    # we have 3 pulse signal (Li's preprocessing)
-    # in this case, return the signal corresponding to the green channel
-    if signal.ndim == 2 and (signal.shape[1] == 3):
-      psds = numpy.zeros((3, output_dim))
-      for i in range(3):
-        f, psds[i] = welch(signal[:, i], self.framerate, nperseg=self.nsegments, nfft=self.nfft)
-      psd = psds[1]
-      
-    if self.debug: 
-      from matplotlib import pyplot
-      pyplot.semilogy(f, psd, 'k')
-      pyplot.title('Power spectrum of the signal')
-      pyplot.show()
-
-    return psd
diff --git a/bob/pad/face/extractor/LTSS.py b/bob/pad/face/extractor/LTSS.py
index 5637b5d9..3b3555f1 100644
--- a/bob/pad/face/extractor/LTSS.py
+++ b/bob/pad/face/extractor/LTSS.py
@@ -5,15 +5,13 @@ import numpy
 
 from bob.bio.base.extractor import Extractor
 
-import logging
-logger = logging.getLogger("bob.pad.face")
+from bob.core.log import setup
+logger = setup("bob.pad.face")
 
 from scipy.fftpack import rfft
 
-
 class LTSS(Extractor, object):
-  """
-  Compute Long-term spectral statistics of a pulse signal.
+  """Compute Long-term spectral statistics of a pulse signal.
   
   The features are described in the following article:
   
@@ -29,38 +27,78 @@ class LTSS(Extractor, object):
     year           = 2017
     }
 
-  **Parameters:**
-
+  Attributes
+  ----------
   framerate: int
     The sampling frequency of the signal (i.e the framerate ...) 
-
   nfft: int
     Number of points to compute the FFT
-
-  debug: boolean
+  debug: bool
     Plot stuff
+  concat: bool
+    Flag if you would like to concatenate features from the 3 color channels
+  time: int
+    The length of the signal to consider (in seconds)
+  
   """
-  def __init__(self, window_size=25, framerate=25, nfft=64, concat=False, debug=False, **kwargs):
+  def __init__(self, window_size=25, framerate=25, nfft=64, concat=False, debug=False, time=0, **kwargs):
+    """Init function
+
+    Parameters
+    ----------
+    window_size: int
+      The size of the window where FFT is computed
+    framerate: int
+      The sampling frequency of the signal (i.e the framerate ...) 
+    nfft: int
+      Number of points to compute the FFT
+    concat: bool
+      Flag if you would like to concatenate features from the 3 color channels
+    debug: bool
+      Plot stuff
+    time: int
+      The length of the signal to consider (in seconds)
 
+    """
     super(LTSS, self).__init__()
-    
     self.framerate = framerate
     self.nfft = nfft
     self.debug = debug
     self.window_size = window_size
     self.concat = concat
+    self.time = time
 
   def _get_ltss(self, signal):
-    
+    """Compute long term spectral statistics for  a signal
+
+    Parameters
+    ----------
+    signal: numpy.ndarray
+      The signal
+
+    Returns
+    -------
+    ltss: numpy.ndarray
+      The spectral statistics of the signal.
+
+    """
+    window_stride = int(self.window_size / 2)
+
     # log-magnitude of DFT coefficients
     log_mags = []
-    window_stride = int(self.window_size / 2)
    
     # go through windows
     for w in range(0, (signal.shape[0] - self.window_size), window_stride):
       fft = rfft(signal[w:w+self.window_size], n=self.nfft)
       mags = numpy.zeros(int(self.nfft/2), dtype=numpy.float64)
-      mags[0] = abs(fft[0])
+      
+      # XXX : bug was here (no clipping)
+      if abs(fft[0]) < 1:
+        mags[0] = 1
+      else:
+        mags[0] = abs(fft[0])
+      # XXX 
+
       index = 1
       for i in range(1, (fft.shape[0]-1), 2):
         mags[index] = numpy.sqrt(fft[i]**2 + fft[i+1]**2)
@@ -69,7 +107,6 @@ class LTSS(Extractor, object):
         index += 1
       log_mags.append(numpy.log(mags))
 
-    # get the long term statistics
     log_mags = numpy.array(log_mags)
     mean = numpy.mean(log_mags, axis=0)
     std = numpy.std(log_mags, axis=0)
@@ -78,18 +115,18 @@ class LTSS(Extractor, object):
 
 
   def __call__(self, signal):
-    """
-    Computes the long-term spectral statistics for a given signal.
-
-    **Parameters**
+    """Computes the long-term spectral statistics for given pulse signals.
 
-    signal: numpy.array 
+    Parameters
+    ----------
+    signal: numpy.ndarray 
       The signal
 
-    **Returns:**
+    Returns
+    -------
+    feature: numpy.ndarray 
+     the computed LTSS features 
 
-      feature: numpy.array 
-       the long-term spectral statistics feature vector 
     """
     # sanity check
     if signal.ndim == 1:
@@ -100,9 +137,34 @@ class LTSS(Extractor, object):
         if numpy.isnan(numpy.sum(signal[:, i])):
           return
 
+    # truncate the signal according to time
+    if self.time > 0:
+      number_of_frames = self.time * self.framerate
+      
+      # check that the truncated signal is not longer 
+      # than the original one
+      if number_of_frames < signal.shape[0]:
+
+        if signal.ndim == 1:
+         signal = signal[:number_of_frames]
+        if signal.ndim == 2 and (signal.shape[1] == 3):
+          new_signal = numpy.zeros((number_of_frames, 3))
+          for i in range(signal.shape[1]):
+            new_signal[:, i] = signal[:number_of_frames, i]
+          signal = new_signal
+      else:
+        logger.warning("Sequence should be truncated to {}, but only contains {} => keeping original one".format(number_of_frames, signal.shape[0]))
+
+      # also, be sure that the window_size is not bigger that the signal
+      if self.window_size > int(signal.shape[0] / 2):
+        self.window_size = int(signal.shape[0] / 2)
+        logger.warning("Window size reduced to {}".format(self.window_size))
+
+    # we have a single pulse
     if signal.ndim == 1:
       feature = self._get_ltss(signal)
 
+    # pulse for the 3 color channels
     if signal.ndim == 2 and (signal.shape[1] == 3):
       
       if not self.concat:
diff --git a/bob/pad/face/extractor/LiFeatures.py b/bob/pad/face/extractor/LiFeatures.py
index 7e089b5c..9fbd4c48 100644
--- a/bob/pad/face/extractor/LiFeatures.py
+++ b/bob/pad/face/extractor/LiFeatures.py
@@ -68,6 +68,7 @@ class LiFeatures(Extractor, object):
     -------
     feature: numpy.ndarray 
      the computed features 
+
     """
     # sanity check
     assert signal.ndim == 2 and signal.shape[1] == 3, "You should provide 3 pulse signals"
diff --git a/bob/pad/face/extractor/PPGSecure.py b/bob/pad/face/extractor/PPGSecure.py
index 09db66e7..bf4a8ddd 100644
--- a/bob/pad/face/extractor/PPGSecure.py
+++ b/bob/pad/face/extractor/PPGSecure.py
@@ -5,13 +5,12 @@ import numpy
 
 from bob.bio.base.extractor import Extractor
 
-import logging
-logger = logging.getLogger("bob.pad.face")
+from bob.core.log import setup
+logger = setup("bob.pad.face")
 
 
 class PPGSecure(Extractor, object):
-  """
-  This class extract the frequency features from pulse signals.
+  """Extract frequency spectra from pulse signals.
   
   The feature are extracted according to what is described in 
   the following article:
@@ -30,39 +29,48 @@ class PPGSecure(Extractor, object):
       year           = 2017
     }
 
-  **Parameters:**
-
+  Attributes
+  ----------
   framerate: int
     The sampling frequency of the signal (i.e the framerate ...) 
-
   nfft: int
     Number of points to compute the FFT
-
-  debug: boolean
+  debug: bool
     Plot stuff
+  
   """
   def __init__(self, framerate=25, nfft=32, debug=False, **kwargs):
-
-    super(PPGSecure, self).__init__(**kwargs)
+    """Init function
+
+    Parameters
+    ----------
+    framerate: int
+      The sampling frequency of the signal (i.e the framerate ...) 
+    nfft: int
+      Number of points to compute the FFT
+    debug: bool
+      Plot stuff
     
+    """
+    super(PPGSecure, self).__init__(**kwargs)
     self.framerate = framerate
     self.nfft = nfft
     self.debug = debug
 
 
   def __call__(self, signal):
-    """
-    Compute the frequency spectrum for the given signal.
-
-    **Parameters:**
+    """Compute and concatenate frequency spectra for the given signals.
 
-    signal: numpy.array 
+    Parameters
+    ----------
+    signal: numpy.ndarray 
       The signal
 
-    **Returns:**
-
-      freq: numpy.array 
-       the frequency spectrum 
+    Returns
+    -------
+    fft: numpy.ndarray 
+     the computed FFT features 
+    
     """
     # sanity check
     assert signal.shape[1] == 5, "You should provide 5 pulses"
@@ -74,7 +82,7 @@ class PPGSecure(Extractor, object):
     # get the frequencies
     f = numpy.fft.fftfreq(self.nfft) * self.framerate
    
-    # we have 5 pulse signal (Li's preprocessing)
+    # we have 5x3 pulse signals, in different regions across 3 channels
     ffts = numpy.zeros((5, output_dim))
     for i in range(5):
       ffts[i] = abs(numpy.fft.rfft(signal[:, i], n=self.nfft))
@@ -94,5 +102,4 @@ class PPGSecure(Extractor, object):
       logger.warn("Feature not extracted")
       return
 
-
     return fft
diff --git a/bob/pad/face/extractor/__init__.py b/bob/pad/face/extractor/__init__.py
index fabde901..c63151f5 100644
--- a/bob/pad/face/extractor/__init__.py
+++ b/bob/pad/face/extractor/__init__.py
@@ -5,10 +5,7 @@ from .VideoDataLoader import VideoDataLoader
 from .VideoQualityMeasure import VideoQualityMeasure
 from .FrameDiffFeatures import FrameDiffFeatures
 
-from .FrequencySpectrum import FrequencySpectrum
-from .FreqFeatures import FreqFeatures
-from .NormalizeLength import NormalizeLength
-from .FFTFeatures import FFTFeatures 
+from .LiFeatures import LiFeatures 
 from .LTSS import LTSS 
 from .PPGSecure import PPGSecure 
 
-- 
GitLab