From 309ecf201364ef181bda77608e83e33fa09b1388 Mon Sep 17 00:00:00 2001
From: Andre Anjos <andre.anjos@idiap.ch>
Date: Fri, 1 Sep 2017 10:38:44 +0200
Subject: [PATCH] Add watershed mask detector

---
 bob/bio/vein/preprocessor/__init__.py |   3 +-
 bob/bio/vein/preprocessor/mask.py     | 171 ++++++++++++++++++++++++++
 bob/bio/vein/preprocessor/utils.py    |   2 +-
 3 files changed, 174 insertions(+), 2 deletions(-)

diff --git a/bob/bio/vein/preprocessor/__init__.py b/bob/bio/vein/preprocessor/__init__.py
index d0afb73..9ad9c7c 100644
--- a/bob/bio/vein/preprocessor/__init__.py
+++ b/bob/bio/vein/preprocessor/__init__.py
@@ -1,6 +1,6 @@
 from .crop import Cropper, FixedCrop, NoCrop
 from .mask import Padder, Masker, FixedMask, NoMask, AnnotatedRoIMask
-from .mask import KonoMask, LeeMask, TomesLeeMask
+from .mask import KonoMask, LeeMask, TomesLeeMask, WatershedMask
 from .normalize import Normalizer, NoNormalization, HuangNormalization
 from .filters import Filter, NoFilter, HistogramEqualization
 from .preprocessor import Preprocessor
@@ -31,6 +31,7 @@ __appropriate__(
     KonoMask,
     LeeMask,
     TomesLeeMask,
+    WatershedMask,
     Normalizer,
     NoNormalization,
     HuangNormalization,
diff --git a/bob/bio/vein/preprocessor/mask.py b/bob/bio/vein/preprocessor/mask.py
index 3c8501b..1b66705 100644
--- a/bob/bio/vein/preprocessor/mask.py
+++ b/bob/bio/vein/preprocessor/mask.py
@@ -3,8 +3,10 @@
 
 '''Base utilities for mask processing'''
 
+import math
 import numpy
 import scipy.ndimage
+import skimage
 
 from .utils import poly_to_mask
 
@@ -474,3 +476,172 @@ class TomesLeeMask(Masker):
     else:
       w = self.padder.padding_width
       return finger_mask[w:-w,w:-w]
+
+
+class WatershedMask(Masker):
+  """Estimates the finger region given an input NIR image using Watershedding
+
+  This method uses the `Watershedding Morphological Algorithm
+  <https://en.wikipedia.org/wiki/Watershed_(image_processing>` for determining
+  the finger mask given an input image.
+
+  The masker works first by determining image edges using a simple 2-D Sobel
+  filter. The next step is to determine markers in the image for both the
+  finger region and background. Markers are set on the image by using a
+  pre-trained feed-forward neural network model (multi-layer perceptron or MLP)
+  learned from existing annotations. The model is trained in a separate
+  program and operates on 3x3 regions around the pixel to be predicted for
+  finger/background. The ``(y,x)`` location also is provided as input to the
+  classifier. The feature vector is then composed of 9 pixel values plus the
+  ``y`` and ``x`` (normalized) coordinates of the pixel. The network then
+  provides a prediction that depends on these input parameters. The closer the
+  output is to ``1.0``, the more likely it is from within the finger region.
+
+  Values output by the network are thresholded in order to remove uncertain
+  markers. The ``threshold`` parameter is configurable.
+
+  A series of morphological opening operations is used to, given the neural net
+  markers, remove noise before watershedding the edges from the Sobel'ed
+  original image.
+
+
+  Parameters:
+
+    model (str): Path to the model file to be used for generating
+      finger/background markers. This model should be pre-trained using a
+      separate program.
+
+    threshold (float): Threshold given a logistic regression output (interval
+      :math:`[0, 1]`) for which we consider finger markers provided by the
+      network.  The higher the value, the more selective the algorithm will be
+      and the less markers will be used from the network selection. This value
+      should be a floating point number in the open-set interval :math:`(0.5,
+      1.0)`.  Values for background selection will be set to :math:`1.0-T`,
+      where ``T`` represents this threshold.
+
+  """
+
+
+  def __init__(self, model, threshold):
+
+    import bob.io.base
+    import bob.learn.mlp
+    import bob.learn.activation
+
+    self.labeller = bob.learn.mlp.Machine((11,10,1))
+    h5f = bob.io.base.HDF5File(model)
+    self.labeller.load(h5f)
+    self.labeller.output_activation = bob.learn.activation.Logistic()
+    del h5f
+    self.threshold = threshold
+
+
+  def _view(self, image, markers, edges, mask):
+    '''displays and overview plot of the mask detection'''
+
+    import matplotlib.pyplot as plt
+
+    plt.subplot(2,2,1)
+    _ = markers.copy()
+    _[_==1] = 128
+    plt.imshow(_, cmap='gray')
+    plt.title('Markers')
+
+    plt.subplot(2,2,2)
+    plt.imshow(edges*255, cmap='gray')
+    plt.title('Edges')
+
+    plt.subplot(2,2,3)
+    plt.imshow(mask.astype('uint8')*255, cmap='gray')
+    plt.title('Mask')
+
+    plt.subplot(2,2,4)
+    plt.imshow(image, cmap='gray')
+    red_mask = numpy.dstack([
+        (~mask).astype('uint8')*255,
+        numpy.zeros_like(image),
+        numpy.zeros_like(image),
+        ])
+    plt.imshow(red_mask, alpha=0.15)
+    plt.title('Image (masked)')
+    plt.show()
+
+
+  class _filterfun(object):
+    '''Callable for filtering the input image with marker predictions'''
+
+
+    def __init__(self, image, labeller):
+      self.labeller = labeller
+      self.features = numpy.zeros(self.labeller.shape[0], dtype='float64')
+      self.output = numpy.zeros(self.labeller.shape[-1], dtype='float64')
+
+      # builds indexes before hand, based on image dimensions
+      idx = numpy.mgrid[:image.shape[0], :image.shape[1]]
+      self.indexes = numpy.array([idx[0].flatten(), idx[1].flatten()],
+          dtype='float64')
+      self.indexes[0,:] /= image.shape[0]
+      self.indexes[1,:] /= image.shape[1]
+      self.current = 0
+
+
+    def __call__(self, arr):
+
+      self.features[:9] = arr.astype('float64')/255
+      self.features[-2:] = self.indexes[:,self.current]
+      self.current += 1
+      return self.labeller(self.features, self.output)
+
+
+  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
+        located
+
+    '''
+
+    # applies the pre-trained neural network model to get predictions about
+    # finger/background regions
+    function = WatershedMask._filterfun(image, self.labeller)
+    predictions = numpy.zeros(image.shape, 'float64')
+    scipy.ndimage.filters.generic_filter(image, function,
+        size=3, mode='nearest', output=predictions)
+
+    selector = skimage.morphology.disk(radius=5)
+
+    # applies a morphological "opening" operation
+    # (https://en.wikipedia.org/wiki/Opening_(morphology)) to remove outliers
+    markers_bg = numpy.where(predictions<(1-self.threshold), 1, 0)
+    markers_bg = skimage.morphology.opening(markers_bg, selem=selector)
+    markers_fg = numpy.where(predictions>self.threshold, 255, 0)
+    markers_fg = skimage.morphology.opening(markers_fg, selem=selector)
+
+    # the final markers are a combination of foreground and background markers
+    markers = markers_fg | markers_bg
+
+    # this will determine the natural boundaries in the image where the
+    # flooding will be limited
+    edges = skimage.filters.sobel(image)
+
+    # applies watersheding to get a final estimate of the finger mask
+    segmentation = skimage.morphology.watershed(edges, markers)
+
+    # removes small perturbations and makes the finger region more uniform
+    segmentation[segmentation==1] = 0
+    mask = skimage.morphology.binary_opening(segmentation.astype('bool'),
+        selem=selector)
+
+    # visualizes processing
+    #self._view(image, markers, edges, mask)
+
+    return mask
diff --git a/bob/bio/vein/preprocessor/utils.py b/bob/bio/vein/preprocessor/utils.py
index 067956f..723b245 100644
--- a/bob/bio/vein/preprocessor/utils.py
+++ b/bob/bio/vein/preprocessor/utils.py
@@ -100,7 +100,7 @@ def poly_to_mask(shape, points):
   # n.b.: PIL images are (x, y), while Bob shapes are represented in (y, x)!
   mask = Image.new('L', (shape[1], shape[0]))
 
-  # coverts whatever comes in into a list of tuples for PIL
+  # converts whatever comes in into a list of tuples for PIL
   fixed = tuple(map(tuple, numpy.roll(fix_points(shape, points), 1, 1)))
 
   # draws polygon
-- 
GitLab