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