From b9af7a82745faedb37ce2e607d57f515f17289ee Mon Sep 17 00:00:00 2001
From: Andre Anjos <andre.anjos@idiap.ch>
Date: Fri, 30 Jun 2017 16:02:23 +0200
Subject: [PATCH] Re-estructured Preprocessor to simplify experimentation and
 configuration

---
 .../vein/configurations/maximum_curvature.py  |  19 +-
 .../configurations/repeated_line_tracking.py  |  19 +-
 .../vein/configurations/wide_line_detector.py |  19 +-
 bob/bio/vein/database/__init__.py             |  23 +
 bob/bio/vein/database/fv3d.py                 |  97 ++--
 bob/bio/vein/database/utfvp.py                |   2 +-
 bob/bio/vein/database/verafinger.py           |  28 +-
 bob/bio/vein/preprocessor/__init__.py         |  35 +-
 bob/bio/vein/preprocessor/mask.py             | 430 ++++++++++++++++++
 bob/bio/vein/preprocessor/normalize.py        | 185 ++++++++
 bob/bio/vein/preprocessor/preprocessor.py     |  87 ++++
 11 files changed, 880 insertions(+), 64 deletions(-)
 create mode 100644 bob/bio/vein/preprocessor/mask.py
 create mode 100644 bob/bio/vein/preprocessor/normalize.py
 create mode 100644 bob/bio/vein/preprocessor/preprocessor.py

diff --git a/bob/bio/vein/configurations/maximum_curvature.py b/bob/bio/vein/configurations/maximum_curvature.py
index f92b50f..ba69e19 100644
--- a/bob/bio/vein/configurations/maximum_curvature.py
+++ b/bob/bio/vein/configurations/maximum_curvature.py
@@ -20,8 +20,23 @@ or the attribute ``sub_directory`` in a configuration file loaded **after**
 this resource.
 """
 
-from ..preprocessor import FingerCrop
-preprocessor = FingerCrop()
+from ..preprocessor import Padder, TomesLeeMask, HuangNormalization, NoFilter
+from ..preprocessor import Preprocessor
+
+# Filter sizes for the vertical "high-pass" filter
+FILTER_HEIGHT = 4
+FILTER_WIDTH = 40
+
+# Padding (to create a buffer during normalization)
+PAD_WIDTH = 5
+PAD_CONST = 51
+
+preprocessor = Preprocessor(
+    mask=TomesLeeMask(filter_height=FILTER_HEIGHT, filter_width=FILTER_WIDTH),
+    normalize=HuangNormalization(padding_width=PAD_WIDTH,
+      padding_constant=PAD_CONST),
+    filter=NoFilter(),
+    )
 """Preprocessing using gray-level based finger cropping and no post-processing
 """
 
diff --git a/bob/bio/vein/configurations/repeated_line_tracking.py b/bob/bio/vein/configurations/repeated_line_tracking.py
index 46d91d7..2050174 100644
--- a/bob/bio/vein/configurations/repeated_line_tracking.py
+++ b/bob/bio/vein/configurations/repeated_line_tracking.py
@@ -20,8 +20,23 @@ or the attribute ``sub_directory`` in a configuration file loaded **after**
 this resource.
 """
 
-from ..preprocessor import FingerCrop
-preprocessor = FingerCrop()
+from ..preprocessor import Padder, TomesLeeMask, HuangNormalization, NoFilter
+from ..preprocessor import Preprocessor
+
+# Filter sizes for the vertical "high-pass" filter
+FILTER_HEIGHT = 4
+FILTER_WIDTH = 40
+
+# Padding (to create a buffer during normalization)
+PAD_WIDTH = 5
+PAD_CONST = 51
+
+preprocessor = Preprocessor(
+    mask=TomesLeeMask(filter_height=FILTER_HEIGHT, filter_width=FILTER_WIDTH),
+    normalize=HuangNormalization(padding_width=PAD_WIDTH,
+      padding_constant=PAD_CONST),
+    filter=NoFilter(),
+    )
 """Preprocessing using gray-level based finger cropping and no post-processing
 """
 
diff --git a/bob/bio/vein/configurations/wide_line_detector.py b/bob/bio/vein/configurations/wide_line_detector.py
index e025093..aef74c0 100644
--- a/bob/bio/vein/configurations/wide_line_detector.py
+++ b/bob/bio/vein/configurations/wide_line_detector.py
@@ -20,8 +20,23 @@ or the attribute ``sub_directory`` in a configuration file loaded **after**
 this resource.
 """
 
-from ..preprocessor import FingerCrop
-preprocessor = FingerCrop()
+from ..preprocessor import Padder, TomesLeeMask, HuangNormalization, NoFilter
+from ..preprocessor import Preprocessor
+
+# Filter sizes for the vertical "high-pass" filter
+FILTER_HEIGHT = 4
+FILTER_WIDTH = 40
+
+# Padding (to create a buffer during normalization)
+PAD_WIDTH = 5
+PAD_CONST = 51
+
+preprocessor = Preprocessor(
+    mask=TomesLeeMask(filter_height=FILTER_HEIGHT, filter_width=FILTER_WIDTH),
+    normalize=HuangNormalization(padding_width=PAD_WIDTH,
+      padding_constant=PAD_CONST),
+    filter=NoFilter(),
+    )
 """Preprocessing using gray-level based finger cropping and no post-processing
 """
 
diff --git a/bob/bio/vein/database/__init__.py b/bob/bio/vein/database/__init__.py
index e69de29..b930169 100644
--- a/bob/bio/vein/database/__init__.py
+++ b/bob/bio/vein/database/__init__.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+
+'''Database definitions for Vein Recognition'''
+
+
+import numpy
+
+
+class AnnotatedArray(numpy.ndarray):
+  """Defines a numpy array subclass that can carry its own metadata
+
+  Copied from: https://docs.scipy.org/doc/numpy-1.12.0/user/basics.subclassing.html#slightly-more-realistic-example-attribute-added-to-existing-array
+  """
+
+  def __new__(cls, input_array, metadata=None):
+      obj = numpy.asarray(input_array).view(cls)
+      obj.metadata = metadata if metadata is not None else dict()
+      return obj
+
+  def __array_finalize__(self, obj):
+      if obj is None: return
+      self.metadata = getattr(obj, 'metadata', dict())
diff --git a/bob/bio/vein/database/fv3d.py b/bob/bio/vein/database/fv3d.py
index db3c02a..8d8251d 100644
--- a/bob/bio/vein/database/fv3d.py
+++ b/bob/bio/vein/database/fv3d.py
@@ -4,80 +4,93 @@
 
 
 import numpy
+
 from bob.bio.base.database import BioFile, BioDatabase
 
+from . import AnnotatedArray
+
 
 class File(BioFile):
-  """
-  Implements extra properties of vein files for the Vera Fingervein database
+    """
+    Implements extra properties of vein files for the Vera Fingervein database
+
+
+    Parameters:
+
+      f (object): Low-level file (or sample) object that is kept inside
+
+    """
 
+    def __init__(self, f):
 
-  Parameters:
+        super(File, self).__init__(client_id=f.finger.unique_name, path=f.path,
+            file_id=f.id)
+        self.__f = f
 
-    f (object): Low-level file (or sample) object that is kept inside
 
-  """
+    def mask(self, shape):
+        """Returns the binary mask from the ROI annotations available"""
 
-  def __init__(self, f):
+        return poly_to_mask(shape, self.__f.roi())
 
-    super(File, self).__init__(client_id=f.finger.unique_name, path=f.path,
-        file_id=f.id)
-    self.__f = f
 
+    def load(self, *args, **kwargs):
+        """(Overrides base method) Loads both image and mask"""
 
-  def load(self, *args, **kwargs):
-    """(Overrides base method) Loads both image and mask"""
+        image = super(File, self).load(*args, **kwargs)
+        roi = self.__f.roi()
 
-    image = super(File, self).load(*args, **kwargs)
+        # calculates the 90 degrees anti-clockwise rotated RoI points
+        h, w = image.shape
+        roi = [(x,-y+h) for k in roi]
 
-    # image is upside, whereas this package requires fingers to be horizontal
-    return numpy.rot90(image)
+        return ImageWithAnnotation(numpy.rot90(image), metadata=dict(mask=roi))
 
 
 class Database(BioDatabase):
-  """
-  Implements verification API for querying Vera Fingervein database.
-  """
+    """
+    Implements verification API for querying Vera Fingervein database.
+    """
 
-  def __init__(self, **kwargs):
+    def __init__(self, **kwargs):
 
-    super(Database, self).__init__(name='fv3d', **kwargs)
-    from bob.db.fv3d.query import Database as LowLevelDatabase
-    self.__db = LowLevelDatabase()
+        super(Database, self).__init__(name='fv3d', **kwargs)
+        from bob.db.fv3d.query import Database as LowLevelDatabase
+        self.__db = LowLevelDatabase()
 
-    self.low_level_group_names = ('train', 'dev', 'eval')
-    self.high_level_group_names = ('world', 'dev', 'eval')
+        self.low_level_group_names = ('train', 'dev', 'eval')
+        self.high_level_group_names = ('world', 'dev', 'eval')
 
 
-  def groups(self):
+    def groups(self):
 
-    return self.convert_names_to_highlevel(self.__db.groups(),
-        self.low_level_group_names, self.high_level_group_names)
+        return self.convert_names_to_highlevel(self.__db.groups(),
+            self.low_level_group_names, self.high_level_group_names)
 
 
-  def client_id_from_model_id(self, model_id, group='dev'):
-    """Required as ``model_id != client_id`` on this database"""
+    def client_id_from_model_id(self, model_id, group='dev'):
+        """Required as ``model_id != client_id`` on this database"""
 
-    return self.__db.finger_name_from_model_id(model_id)
+        return self.__db.finger_name_from_model_id(model_id)
 
 
-  def model_ids_with_protocol(self, groups=None, protocol=None, **kwargs):
+    def model_ids_with_protocol(self, groups=None, protocol=None, **kwargs):
 
-    groups = self.convert_names_to_lowlevel(groups,
-        self.low_level_group_names, self.high_level_group_names)
-    return self.__db.model_ids(groups=groups, protocol=protocol)
+        groups = self.convert_names_to_lowlevel(groups,
+            self.low_level_group_names, self.high_level_group_names)
+        return self.__db.model_ids(groups=groups, protocol=protocol)
 
 
-  def objects(self, groups=None, protocol=None, purposes=None,
-      model_ids=None, **kwargs):
+    def objects(self, groups=None, protocol=None, purposes=None,
+        model_ids=None, **kwargs):
 
-    groups = self.convert_names_to_lowlevel(groups,
-        self.low_level_group_names, self.high_level_group_names)
-    retval = self.__db.objects(groups=groups, protocol=protocol,
-        purposes=purposes, model_ids=model_ids, **kwargs)
+        groups = self.convert_names_to_lowlevel(groups,
+            self.low_level_group_names, self.high_level_group_names)
+        retval = self.__db.objects(groups=groups, protocol=protocol,
+            purposes=purposes, model_ids=model_ids, **kwargs)
 
-    return [File(f) for f in retval]
+        return [File(f) for f in retval]
 
 
-  def annotations(self, file):
-    return None
+    def annotations(self, file):
+        return None
diff --git a/bob/bio/vein/database/utfvp.py b/bob/bio/vein/database/utfvp.py
index e9099b8..91b09ea 100644
--- a/bob/bio/vein/database/utfvp.py
+++ b/bob/bio/vein/database/utfvp.py
@@ -42,7 +42,7 @@ class Database(BioDatabase):
                 model_ids=None, **kwargs):
 
         retval = self._db.objects(groups=groups, protocol=protocol,
-                                  purposes=purposes, model_ids=model_ids, **kwargs)
+            purposes=purposes, model_ids=model_ids, **kwargs)
 
         return [File(f) for f in retval]
 
diff --git a/bob/bio/vein/database/verafinger.py b/bob/bio/vein/database/verafinger.py
index d8cea83..46d3538 100644
--- a/bob/bio/vein/database/verafinger.py
+++ b/bob/bio/vein/database/verafinger.py
@@ -5,6 +5,8 @@
 
 from bob.bio.base.database import BioFile, BioDatabase
 
+from . import AnnotatedArray
+
 
 class File(BioFile):
     """
@@ -20,23 +22,17 @@ class File(BioFile):
     def __init__(self, f):
 
         super(File, self).__init__(client_id=f.unique_finger_name, path=f.path,
-                                   file_id=f.id)
+            file_id=f.id)
         self.__f = f
 
-    def mask(self):
-        """Returns the binary mask from the ROI annotations available"""
-
-        from ..preprocessor.utils import poly_to_mask
-
-        # The size of images in this database is (250, 665) pixels (h, w)
-        return poly_to_mask((250, 665), self.__f.roi())
 
     def load(self, *args, **kwargs):
         """(Overrides base method) Loads both image and mask"""
 
         image = super(File, self).load(*args, **kwargs)
-
-        return image, self.mask()
+        roi = self.__f.roi()
+        mask = poly_to_mask(image.shape, roi)
+        return AnnotatedArray(image, metadata=dict(mask=mask, roi=roi))
 
 
 class Database(BioDatabase):
@@ -56,28 +52,32 @@ class Database(BioDatabase):
     def groups(self):
 
         return self.convert_names_to_highlevel(self._db.groups(),
-                                               self.low_level_group_names, self.high_level_group_names)
+            self.low_level_group_names, self.high_level_group_names)
 
     def client_id_from_model_id(self, model_id, group='dev'):
         """Required as ``model_id != client_id`` on this database"""
 
         return self._db.finger_name_from_model_id(model_id)
 
+
     def model_ids_with_protocol(self, groups=None, protocol=None, **kwargs):
 
         groups = self.convert_names_to_lowlevel(groups,
-                                                self.low_level_group_names, self.high_level_group_names)
+            self.low_level_group_names, self.high_level_group_names)
         return self._db.model_ids(groups=groups, protocol=protocol)
 
+
     def objects(self, groups=None, protocol=None, purposes=None,
                 model_ids=None, **kwargs):
 
         groups = self.convert_names_to_lowlevel(groups,
-                                                self.low_level_group_names, self.high_level_group_names)
+            self.low_level_group_names, self.high_level_group_names)
         retval = self._db.objects(groups=groups, protocol=protocol,
-                                  purposes=purposes, model_ids=model_ids, **kwargs)
+                                  purposes=purposes, model_ids=model_ids,
+                                  **kwargs)
 
         return [File(f) for f in retval]
 
+
     def annotations(self, file):
             return None
diff --git a/bob/bio/vein/preprocessor/__init__.py b/bob/bio/vein/preprocessor/__init__.py
index 41d1e04..7d24970 100644
--- a/bob/bio/vein/preprocessor/__init__.py
+++ b/bob/bio/vein/preprocessor/__init__.py
@@ -1,4 +1,37 @@
-from .FingerCrop import FingerCrop
+from .mask import Padder, Masker, NoMask, AnnotatedRoIMask
+from .mask import KonoMask, LeeMask, TomesLeeMask
+from .normalize import Normalizer, NoNormalization, HuangNormalization
+from .filters import Filter, NoFilter, HistogramEqualization
+from .preprocessor import Preprocessor
 
 # gets sphinx autodoc done right - don't remove it
+def __appropriate__(*args):
+  """Says object was actually declared here, an not on the import module.
+
+  Parameters:
+
+    *args: An iterable of objects to modify
+
+  Resolves `Sphinx referencing issues
+  <https://github.com/sphinx-doc/sphinx/issues/3048>`
+  """
+
+  for obj in args: obj.__module__ = __name__
+
+__appropriate__(
+    Padder,
+    Masker,
+    NoMask,
+    AnnotatedRoIMask,
+    KonoMask,
+    LeeMask,
+    TomesLeeMask,
+    Normalizer,
+    NoNormalization,
+    HuangNormalization,
+    Filter,
+    NoFilter,
+    HistogramEqualization,
+    Preprocessor,
+    )
 __all__ = [_ for _ in dir() if not _.startswith('_')]
diff --git a/bob/bio/vein/preprocessor/mask.py b/bob/bio/vein/preprocessor/mask.py
new file mode 100644
index 0000000..aa09d17
--- /dev/null
+++ b/bob/bio/vein/preprocessor/mask.py
@@ -0,0 +1,430 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+
+'''Base utilities for mask processing'''
+
+import numpy
+import scipy.ndimage
+
+from .utils import poly_to_mask
+
+
+class Padder(object):
+  """A class that pads the input image returning a new object
+
+
+  Parameters:
+
+    padding_width (:py:obj:`int`, optional): How much padding (in pixels) to
+      add around the borders of the input image. We normally always keep this
+      value on its default (5 pixels). This parameter is always used before
+      normalizing the finger orientation.
+
+    padding_constant (:py:obj:`int`, optional): What is the value of the pixels
+      added to the padding. This number should be a value between 0 and 255.
+      (From Pedro Tome: for UTFVP (high-quality samples), use 0. For the VERA
+      Fingervein database (low-quality samples), use 51 (that corresponds to
+      0.2 in a float image with values between 0 and 1). This parameter is
+      always used before normalizing the finger orientation.
+
+  """
+
+  def __init__(self, padding_width = 5, padding_constant = 51):
+
+    self.padding_width = padding_width
+    self.padding_constant = padding_constant
+
+
+  def __call__(self, image):
+    '''Inputs an image, returns a padded (larger) image
+
+      Parameters:
+
+        image (numpy.ndarray): A 2D numpy array of type ``uint8`` with the
+          input image
+
+
+      Returns:
+
+        numpy.ndarray: A 2D numpy array of the same type as the input, but with
+        the extra padding
+
+    '''
+
+    return numpy.pad(image, self.padding_width, 'constant',
+        constant_values = self.padding_constant)
+
+
+
+class Masker(object):
+    """This is the base class for all maskers
+
+    It defines the minimum requirements for all derived masker classes.
+
+
+    """
+
+    def __init__(self):
+      pass
+
+
+    def __call__(self, image):
+      """Overwrite this method to implement your masking method
+
+
+      Parameters:
+
+        image (numpy.ndarray): A 2D numpy array of type ``uint8`` with the
+          input image
+
+
+      Returns:
+
+        numpy.ndarray: A 2D numpy array of type boolean with the caculated
+        mask. ``True`` values correspond to regions where the finger is
+        situated
+
+      """
+
+      raise NotImplemented('You must implement the __call__ slot')
+
+
+class NoMask(object):
+  """Implements no masking - i.e. returns a mask the same size as input
+  """
+
+  def __init__(self):
+    pass
+
+
+  def __call__(self, image):
+    """Returns a big mask
+
+
+    Parameters:
+
+      image (numpy.ndarray): A 2D numpy array of type ``uint8`` with the
+        input image
+
+
+    Returns:
+
+      numpy.ndarray: A 2D numpy array of type boolean with the caculated
+      mask. ``True`` values correspond to regions where the finger is
+      situated
+
+
+    """
+    return numpy.ones(image.shape, dtype='bool')
+
+
+class AnnotatedRoIMask(object):
+  """Devises the mask from the annotated RoI"""
+
+
+  def __init__(self):
+    pass
+
+
+  def __call__(self, image):
+    """Returns a mask extrapolated from RoI annotations
+
+
+    Parameters:
+
+      image (bob.bio.vein.database.AnnotatedArray): A 2D numpy array of type
+        ``uint8`` with the input image containing an attribute called
+        ``metadata`` (a python dictionary). The ``metadata`` object just
+        contain a key called ``roi`` containing the annotated points
+
+
+    Returns:
+
+      numpy.ndarray: A 2D numpy array of type boolean with the caculated
+      mask. ``True`` values correspond to regions where the finger is
+      situated
+
+
+    """
+    return poly_to_mask(image.shape, image.metadata['roi'])
+
+
+class KonoMask(Masker):
+  """Estimates the finger region given an input NIR image using Kono et al.
+
+  This method is based on the work of M. Kono, H. Ueki and S.  Umemura.
+  Near-infrared finger vein patterns for personal identification, Applied
+  Optics, Vol. 41, Issue 35, pp. 7429-7436 (2002).
+
+
+  Parameters:
+
+    sigma (:py:obj:`float`, optional): The standard deviation of the gaussian
+      blur filter to apply for low-passing the input image (background
+      extraction). Defaults to ``5``.
+
+    padder (:py:class:`Padder`, optional): If passed, will pad the image before
+      evaluating the mask. The returned value will have the padding removed and
+      is, therefore, of the exact size of the input image.
+
+  """
+
+  def __init__(self, sigma=5, padder=None):
+
+    self.sigma = sigma
+    self.padder = padder
+
+
+  def __call__(self, image):
+    '''Inputs an image, returns a mask (numpy boolean array)
+
+      Parameters:
+
+        image (numpy.ndarray): A 2D numpy array of type ``uint8`` with the
+          input image
+
+
+      Returns:
+
+        numpy.ndarray: A 2D numpy array of type boolean with the caculated
+        mask. ``True`` values correspond to regions where the finger is
+        situated
+
+    '''
+
+    image = image if self.padder is None else self.padder(image)
+
+    img_h,img_w = image.shape
+
+    # Determine lower half starting point
+    if numpy.mod(img_h,2) == 0:
+        half_img_h = img_h/2 + 1
+    else:
+        half_img_h = numpy.ceil(img_h/2)
+
+    #Construct filter kernel
+    winsize = numpy.ceil(4*self.sigma)
+
+    x = numpy.arange(-winsize, winsize+1)
+    y = numpy.arange(-winsize, winsize+1)
+    X, Y = numpy.meshgrid(x, y)
+
+    hy = (-Y/(2*math.pi*self.sigma**4)) * \
+        numpy.exp(-(X**2 + Y**2)/(2*self.sigma**2))
+
+    # Filter the image with the directional kernel
+    fy = scipy.ndimage.convolve(image, hy, mode='nearest')
+
+    # Upper part of filtred image
+    img_filt_up = fy[0:half_img_h,:]
+    y_up = img_filt_up.argmax(axis=0)
+
+    # Lower part of filtred image
+    img_filt_lo = fy[half_img_h-1:,:]
+    y_lo = img_filt_lo.argmin(axis=0)
+
+    # Fill region between upper and lower edges
+    finger_mask = numpy.ndarray(image.shape, numpy.bool)
+    finger_mask[:,:] = False
+
+    for i in range(0,img_w):
+      finger_mask[y_up[i]:y_lo[i]+image.shape[0]-half_img_h+2,i] = True
+
+    if not self.padder:
+      return finger_mask
+    else:
+      w = self.padder.padding_width
+      return finger_mask[w:-w,w:-w]
+
+
+class LeeMask(Masker):
+  """Estimates the finger region given an input NIR image using Lee et al.
+
+  This method is based on the work of Finger vein recognition using
+  minutia-based alignment and local binary pattern-based feature extraction,
+  E.C. Lee, H.C. Lee and K.R. Park, International Journal of Imaging Systems
+  and Technology, Volume 19, Issue 3, September 2009, Pages 175--178, doi:
+  10.1002/ima.20193
+
+  This code is based on the Matlab implementation by Bram Ton, available at:
+
+  https://nl.mathworks.com/matlabcentral/fileexchange/35752-finger-region-localisation/content/lee_region.m
+
+  In this method, we calculate the mask of the finger independently for each
+  column of the input image. Firstly, the image is convolved with a [1,-1]
+  filter of size ``(self.filter_height, self.filter_width)``. Then, the upper and
+  lower parts of the resulting filtered image are separated. The location of
+  the maxima in the upper part is located. The same goes for the location of
+  the minima in the lower part. The mask is then calculated, per column, by
+  considering it starts in the point where the maxima is in the upper part and
+  goes up to the point where the minima is detected on the lower part.
+
+
+  Parameters:
+
+    filter_height (:py:obj:`int`, optional): Height of contour mask in pixels,
+      must be an even number
+
+    filter_width (:py:obj:`int`, optional): Width of the contour mask in pixels
+
+  """
+
+  def __init__(self, filter_height = 4, filter_width = 40, padder=None):
+    self.filter_height = filter_height
+    self.filter_width = filter_width
+    self.padder = padder
+
+
+  def __call__(self, image):
+    '''Inputs an image, returns a mask (numpy boolean array)
+
+      Parameters:
+
+        image (numpy.ndarray): A 2D numpy array of type ``uint8`` with the
+          input image
+
+
+      Returns:
+
+        numpy.ndarray: A 2D numpy array of type boolean with the caculated
+        mask. ``True`` values correspond to regions where the finger is
+        situated
+
+    '''
+
+    image = image if self.padder is None else self.padder(image)
+
+    img_h,img_w = image.shape
+
+    # Determine lower half starting point
+    half_img_h = int(img_h/2)
+
+    # Construct mask for filtering
+    mask = numpy.ones((self.filter_height,self.filter_width), dtype='float64')
+    mask[int(self.filter_height/2):,:] = -1.0
+
+    img_filt = scipy.ndimage.convolve(image.astype(numpy.float64), mask,
+        mode='nearest')
+
+    # Upper part of filtered image
+    img_filt_up = img_filt[:half_img_h,:]
+    y_up = img_filt_up.argmax(axis=0)
+
+    # Lower part of filtered image
+    img_filt_lo = img_filt[half_img_h:,:]
+    y_lo = img_filt_lo.argmin(axis=0)
+
+    # Translation: for all columns of the input image, set to True all pixels
+    # of the mask from index where the maxima occurred in the upper part until
+    # the index where the minima occurred in the lower part.
+    finger_mask = numpy.zeros(image.shape, dtype='bool')
+    for i in range(img_filt.shape[1]):
+      finger_mask[y_up[i]:(y_lo[i]+img_filt_lo.shape[0]+1), i] = True
+
+    if not self.padder:
+      return finger_mask
+    else:
+      w = self.padder.padding_width
+      return finger_mask[w:-w,w:-w]
+
+
+class TomesLeeMask(Masker):
+  """Estimates the finger region given an input NIR image using Lee et al.
+
+  This method is based on the work of Finger vein recognition using
+  minutia-based alignment and local binary pattern-based feature extraction,
+  E.C. Lee, H.C. Lee and K.R. Park, International Journal of Imaging Systems
+  and Technology, Volume 19, Issue 3, September 2009, Pages 175--178, doi:
+  10.1002/ima.20193
+
+  This code is a variant of the Matlab implementation by Bram Ton, available
+  at:
+
+  https://nl.mathworks.com/matlabcentral/fileexchange/35752-finger-region-localisation/content/lee_region.m
+
+  In this variant from Pedro Tome, the technique of filtering the image with
+  a horizontal filter is also applied on the vertical axis.
+
+
+  Parameters:
+
+    filter_height (:py:obj:`int`, optional): Height of contour mask in pixels,
+      must be an even number
+
+    filter_width (:py:obj:`int`, optional): Width of the contour mask in pixels
+
+  """
+
+  def __init__(self, filter_height = 4, filter_width = 40, padder=None):
+    self.filter_height = filter_height
+    self.filter_width = filter_width
+    self.padder = padder
+
+
+  def __call__(self, image):
+    '''Inputs an image, returns a mask (numpy boolean array)
+
+      Parameters:
+
+        image (numpy.ndarray): A 2D numpy array of type ``uint8`` with the
+          input image
+
+
+      Returns:
+
+        numpy.ndarray: A 2D numpy array of type boolean with the caculated
+        mask. ``True`` values correspond to regions where the finger is
+        situated
+
+    '''
+
+    image = image if self.padder is None else self.padder(image)
+
+    img_h,img_w = image.shape
+
+    # Determine lower half starting point
+    half_img_h = img_h/2
+    half_img_w = img_w/2
+
+    # Construct mask for filtering (up-bottom direction)
+    mask = numpy.ones((self.filter_height, self.filter_width), dtype='float64')
+    mask[int(self.filter_height/2):,:] = -1.0
+
+    img_filt = scipy.ndimage.convolve(image, mask, mode='nearest')
+
+    # Upper part of filtred image
+    img_filt_up = img_filt[:int(half_img_h),:]
+    y_up = img_filt_up.argmax(axis=0)
+
+    # Lower part of filtred image
+    img_filt_lo = img_filt[int(half_img_h):,:]
+    y_lo = img_filt_lo.argmin(axis=0)
+
+    img_filt = scipy.ndimage.convolve(image, mask, mode='nearest')
+
+    # Left part of filtered image
+    img_filt_lf = img_filt[:,:int(half_img_w)]
+    y_lf = img_filt_lf.argmax(axis=1)
+
+    # Right part of filtred image
+    img_filt_rg = img_filt[:,int(half_img_w):]
+    y_rg = img_filt_rg.argmin(axis=1)
+
+    finger_mask = numpy.zeros(image.shape, dtype='bool')
+
+    for i in range(0,y_up.size):
+      finger_mask[y_up[i]:y_lo[i]+img_filt_lo.shape[0]+1,i] = True
+
+    # Left region
+    for i in range(0,y_lf.size):
+      finger_mask[i,0:y_lf[i]+1] = False
+
+    # Right region has always the finger ending, crop the padding with the
+    # meadian
+    finger_mask[:,int(numpy.median(y_rg)+img_filt_rg.shape[1]):] = False
+
+    if not self.padder:
+      return finger_mask
+    else:
+      w = self.padder.padding_width
+      return finger_mask[w:-w,w:-w]
diff --git a/bob/bio/vein/preprocessor/normalize.py b/bob/bio/vein/preprocessor/normalize.py
new file mode 100644
index 0000000..0fce4cb
--- /dev/null
+++ b/bob/bio/vein/preprocessor/normalize.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+
+'''Base utilities for normalization'''
+
+import math
+import numpy
+from PIL import Image
+
+
+class Normalizer(object):
+  '''Objects of this class normalize the input image orientation and scale'''
+
+
+  def __init__(self):
+    pass
+
+
+  def __call__(self, image, mask):
+    '''Inputs image and mask and outputs a normalized version of those
+
+
+    Parameters:
+
+      image (numpy.ndarray): raw image to normalize as 2D array of unsigned
+          8-bit integers
+
+      mask (numpy.ndarray): mask to normalize as 2D array of booleans
+
+
+    Returns:
+
+      numpy.ndarray: A 2D boolean array with the same shape and data type of
+      the input image representing the newly aligned image.
+
+      numpy.ndarray: A 2D boolean array with the same shape and data type of
+      the input mask representing the newly aligned mask.
+
+    '''
+
+    raise NotImplemented('You must implement the __call__ slot')
+
+
+
+class NoNormalization(Normalizer):
+  '''Trivial implementation with no normalization'''
+
+
+  def __init__(self):
+    pass
+
+
+  def __call__(self, image, mask):
+    '''Returns the input parameters, without changing them
+
+
+    Parameters:
+
+      image (numpy.ndarray): raw image to normalize as 2D array of unsigned
+          8-bit integers
+
+      mask (numpy.ndarray): mask to normalize as 2D array of booleans
+
+
+    Returns:
+
+      numpy.ndarray: A 2D boolean array with the same shape and data type of
+      the input image representing the newly aligned image.
+
+      numpy.ndarray: A 2D boolean array with the same shape and data type of
+      the input mask representing the newly aligned mask.
+
+    '''
+
+    return image, mask
+
+
+
+class HuangNormalization(Normalizer):
+  '''Simple finger normalization from Huang et. al
+
+  Based on B. Huang, Y. Dai, R. Li, D. Tang and W. Li, Finger-vein
+  authentication based on wide line detector and pattern normalization,
+  Proceedings on 20th International Conference on Pattern Recognition (ICPR),
+  2010.
+
+  This implementation aligns the finger to the centre of the image using an
+  affine transformation. Elliptic projection which is described in the
+  referenced paper is **not** included.
+
+  In order to defined the affine transformation to be performed, the
+  algorithm first calculates the center for each edge (column wise) and
+  calculates the best linear fit parameters for a straight line passing
+  through those points.
+  '''
+
+  def __init__(self, padding_width=5, padding_constant=51):
+    self.padding_width = padding_width
+    self.padding_constant = padding_constant
+
+
+  def __call__(self, image, mask):
+    '''Inputs image and mask and outputs a normalized version of those
+
+
+    Parameters:
+
+      image (numpy.ndarray): raw image to normalize as 2D array of unsigned
+          8-bit integers
+
+      mask (numpy.ndarray): mask to normalize as 2D array of booleans
+
+
+    Returns:
+
+      numpy.ndarray: A 2D boolean array with the same shape and data type of
+      the input image representing the newly aligned image.
+
+      numpy.ndarray: A 2D boolean array with the same shape and data type of
+      the input mask representing the newly aligned mask.
+
+    '''
+
+    img_h, img_w = image.shape
+
+    # Calculates the mask edges along the columns
+    edges = numpy.zeros((2, mask.shape[1]), dtype=int)
+
+    edges[0,:] = mask.argmax(axis=0) # get upper edges
+    edges[1,:] = len(mask) - numpy.flipud(mask).argmax(axis=0) - 1
+
+    bl = edges.mean(axis=0) #baseline
+    x = numpy.arange(0, edges.shape[1])
+    A = numpy.vstack([x, numpy.ones(len(x))]).T
+
+    # Fit a straight line through the base line points
+    w = numpy.linalg.lstsq(A,bl)[0] # obtaining the parameters
+
+    angle = -1*math.atan(w[0])  # Rotation
+    tr = img_h/2 - w[1]         # Translation
+    scale = 1.0                 # Scale
+
+    #Affine transformation parameters
+    sx=sy=scale
+    cosine = math.cos(angle)
+    sine = math.sin(angle)
+
+    a = cosine/sx
+    b = -sine/sy
+    #b = sine/sx
+    c = 0 #Translation in x
+
+    d = sine/sx
+    e = cosine/sy
+    f = tr #Translation in y
+    #d = -sine/sy
+    #e = cosine/sy
+    #f = 0
+
+    g = 0
+    h = 0
+    #h=tr
+    i = 1
+
+    T = numpy.matrix([[a,b,c],[d,e,f],[g,h,i]])
+    Tinv = numpy.linalg.inv(T)
+    Tinvtuple = (Tinv[0,0],Tinv[0,1], Tinv[0,2], Tinv[1,0],Tinv[1,1],Tinv[1,2])
+
+    def _afftrans(img):
+      '''Applies the affine transform on the resulting image'''
+
+      t = Image.fromarray(img.astype('uint8'))
+      w, h = t.size #pillow image is encoded w, h
+      w += 2*self.padding_width
+      h += 2*self.padding_width
+      t = t.transform(
+          (w,h),
+          Image.AFFINE,
+          Tinvtuple,
+          resample=Image.BICUBIC,
+          fill=self.padding_constant)
+
+      return numpy.array(t).astype(img.dtype)
+
+    return _afftrans(image), _afftrans(mask)
diff --git a/bob/bio/vein/preprocessor/preprocessor.py b/bob/bio/vein/preprocessor/preprocessor.py
new file mode 100644
index 0000000..f925ef0
--- /dev/null
+++ b/bob/bio/vein/preprocessor/preprocessor.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+
+import bob.io.base
+from bob.bio.base.preprocessor import Preprocessor as BasePreprocessor
+
+
+class Preprocessor (BasePreprocessor):
+  """
+  Extracts the mask and pre-processes fingervein images.
+
+  In this implementation, the finger image is (in this order):
+
+    #. The mask is expolated from the image using one of our
+       :py:class:`Masker`'s concrete implementations
+    #. The image is normalized with one of our :py:class:`Normalizer`'s
+    #. The image is filtered with one of our :py:class:`Filter`'s
+
+
+  Parameters:
+
+    mask (:py:class:`Masker`): An object representing a Masker instance which
+      will extrapolate the mask from the input image.
+
+    normalize (:py:class:`Normalizer`): An object representing a Normalizer
+      instance which will normalize the input image and its mask returning a
+      new image mask pair.
+
+    filter (:py:class:`Filter`): An object representing a Filter instance will
+      will filter the input image and return a new filtered image. The filter
+      instance also receives the extrapolated mask so it can, if desired, only
+      apply the filtering operation where the mask has a value of ``True``
+
+  """
+
+
+  def __init__(self, mask, normalize, filter, **kwargs):
+
+    BasePreprocessor.__init__(self,
+        mask = mask,
+        normalize = normalize,
+        filter = filter,
+        **kwargs
+        )
+
+    self.mask = mask
+    self.normalize = normalize
+    self.filter = filter
+
+
+  def __call__(self, data, annotations=None):
+    """Reads the input image or (image, mask) and prepares for fex.
+
+    Parameters:
+
+      data (numpy.ndarray): An 2D numpy array containing a gray-scaled image
+        with dtype ``uint8``. The image maybe annotated with an RoI.
+
+
+    Returns:
+
+      numpy.ndarray: The image, preprocessed and normalized
+
+      numpy.ndarray: A mask, of the same size of the image, indicating where
+      the valid data for the object is.
+
+    """
+
+    mask = self.mask(data)
+    data, mask = self.normalize(data, mask)
+    data = self.filter(data, mask)
+    return data, mask
+
+
+  def write_data(self, data, filename):
+    '''Overrides the default method implementation to handle our tuple'''
+
+    f = bob.io.base.HDF5File(filename, 'w')
+    f.set('image', data[0])
+    f.set('mask', data[1])
+
+
+  def read_data(self, filename):
+    '''Overrides the default method implementation to handle our tuple'''
+
+    f = bob.io.base.HDF5File(filename, 'r')
+    return f.read('image'), f.read('mask')
-- 
GitLab