Commit 788367d6 authored by André Anjos's avatar André Anjos 💬

Better docs, simpler implementation

parent 192eb5af
Pipeline #5195 failed with stages
in 3 minutes and 9 seconds
......@@ -18,57 +18,70 @@ from .. import utils
class FingerCrop (Preprocessor):
"""
Extracts the mask and pre-processes fingervein images.
Extracts the mask heuristically and pre-processes fingervein images.
Based on the implementation: E.C. Lee, H.C. Lee and K.R. Park. Finger vein
recognition using minutia-based alignment and local binary pattern-based
feature extraction. International Journal of Imaging Systems and
Technology. Vol. 19, No. 3, pp. 175-178, September 2009.
Finger orientation is 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.
The ``konomask`` option 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).
In this implementation, the finger image is (in this order):
1. Padded
2. The mask is extracted
3. The finger is normalized (made horizontal)
4. (optionally) Post processed
1. The mask is extracted (if ``annotation`` is not chosen as a parameter to
``fingercontour``). Other mask extraction options correspond to
heuristics developed by Lee et al. (2009) or Kono et al. (2002)
2. The finger is normalized (made horizontal), via a least-squares
normalization procedure concerning the center of the annotated area,
width-wise. Before normalization, the image is padded to avoid loosing
pixels corresponding to veins during the rotation
3. (optionally) Post processed with histogram-equalization to enhance vein
information. Notice that only the area inside the mask is used for
normalization. Areas outside of the mask (where the mask is ``False``
are set to black)
**Parameters:**
Parameters:
mask_h : :py:class:`int`
Optional, Height of contour mask in pixels, must be an even
number
mask_h (:py:obj:`int`, optional): Height of contour mask in pixels, must
be an even number (used by the methods ``leemaskMod`` or
``leemaskMatlab``)
mask_w : :py:class:`int`
Optional, Width of the contour mask in pixels
mask_w (:py:obj:`int`, optional): Width of the contour mask in pixels
(used by the methods ``leemaskMod`` or ``leemaskMatlab``)
padding_width : :py:class:`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).
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:class:`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
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).
fingercontour : :py:class:`str`
Optional, Select between three finger contour
implementations: ``"leemaskMod"``, ``"leemaskMatlab"`` or ``"konomask"``.
(From Pedro Tome: the option ``leemaskMatlab`` was just implemented for
testing purposes so we could compare with MAT files generated from Matlab
code of other authors. He only used it with the UTFVP database, using
``leemaskMod`` with that database yields slight worse results.)
postprocessing : :py:class:`str`
Optional, Select between ``HE`` (histogram
equalization, as with :py:func:`bob.ip.base.histogram_equalization`),
``HFE`` (high-frequency emphasis filter, with hard-coded parameters - see
implementation) or ``CircGabor`` (circular Gabor filter with band-width
1.12 octaves and standard deviation of 5 pixels (this is hard-coded)). By
default, no postprocessing is applied on the image.
0.2 in a float image with values between 0 and 1). This parameter is
always used before normalizing the finger orientation.
fingercontour (:py:obj:`str`, optional): Select between three finger
contour implementations: ``"leemaskMod"``, ``"leemaskMatlab"``,
``"konomask"`` or ``annotation``. (From Pedro Tome: the option
``leemaskMatlab`` was just implemented for testing purposes so we could
compare with MAT files generated from Matlab code of other authors. He
only used it with the UTFVP database, using ``leemaskMod`` with that
database yields slight worse results.)
postprocessing (:py:obj:`str`, optional): Select between ``HE`` (histogram
equalization, as with :py:func:`skimage.exposure.equalize_hist`) or
``None`` (the default).
"""
......@@ -142,12 +155,7 @@ class FingerCrop (Preprocessor):
for i in range(0,img_w):
finger_mask[y_up[i]:y_lo[i]+image.shape[0]-half_img_h+2,i] = True
# Extract y-position of finger edges
edges = numpy.zeros((2,img_w))
edges[0,:] = y_up
edges[1,:] = y_lo + image.shape[0] - half_img_h + 1
return (finger_mask, edges)
return finger_mask
def __leemaskMod__(self, image):
......@@ -168,7 +176,7 @@ class FingerCrop (Preprocessor):
a horizontal filter is also applied on the vertical axis.
**Parameters:**
Parameters:
image (numpy.ndarray): raw image to use for finding the mask, as 2D array
of unsigned 8-bit integers
......@@ -230,15 +238,7 @@ class FingerCrop (Preprocessor):
# meadian
finger_mask[:,int(numpy.median(y_rg)+img_filt_rg.shape[1]):] = False
# Extract y-position of finger edges
edges = numpy.zeros((2,img_w))
edges[0,:] = y_up
edges[0,0:int(round(numpy.mean(y_lf))+1)] = edges[0,int(round(numpy.mean(y_lf))+1)]
edges[1,:] = numpy.round(y_lo + img_filt_lo.shape[0])
edges[1,0:int(round(numpy.mean(y_lf))+1)] = edges[1,int(round(numpy.mean(y_lf))+1)]
return finger_mask, edges
return finger_mask
def __leemaskMatlab__(self, image):
......@@ -308,15 +308,10 @@ class FingerCrop (Preprocessor):
for i in range(img_filt.shape[1]):
finger_mask[y_up[i]:(y_lo[i]+img_filt_lo.shape[0]+1), i] = True
# Extract y-position of finger edges
edges = numpy.zeros((2,img_w), dtype='float64')
edges[0,:] = y_up
edges[1,:] = numpy.round(y_lo + img_filt_lo.shape[0])
return finger_mask, edges
return finger_mask
def __huangnormalization__(self, image, mask, edges):
def __huangnormalization__(self, image, mask):
"""
Simple finger normalization.
......@@ -342,10 +337,6 @@ class FingerCrop (Preprocessor):
mask (numpy.ndarray): mask to normalize as 2D array of booleans
edges (numpy.ndarray): edges of the mask, 2D array with 2 rows and as
many columns as the input image, containing the start of the mask and
the end of the mask.
**Returns:**
......@@ -358,6 +349,11 @@ class FingerCrop (Preprocessor):
img_h, img_w = image.shape
# Calculates the mask edges along the columns
edges = numpy.zeros(2, img_w)
edges[0,:] = mask.argmax(axis=0) # get upper edges
edges[1,:] = len(mask) - numpy.flipup(mask).argmax(axis=0) - 1
bl = edges.mean(axis=0) #baseline
x = numpy.arange(0,img_w)
A = numpy.vstack([x, numpy.ones(len(x))]).T
......@@ -396,14 +392,16 @@ class FingerCrop (Preprocessor):
Tinvtuple = (Tinv[0,0],Tinv[0,1], Tinv[0,2], Tinv[1,0],Tinv[1,1],Tinv[1,2])
img=Image.fromarray(image)
image_norm = img.transform(img.size, Image.AFFINE, Tinvtuple, resample=Image.BICUBIC)
image_norm = img.transform(img.size, Image.AFFINE, Tinvtuple,
resample=Image.BICUBIC)
image_norm = numpy.array(image_norm)
finger_mask = numpy.zeros(mask.shape)
finger_mask[mask] = 1
img_mask=Image.fromarray(finger_mask)
mask_norm = img_mask.transform(img_mask.size, Image.AFFINE, Tinvtuple, resample=Image.BICUBIC)
mask_norm = img_mask.transform(img_mask.size, Image.AFFINE, Tinvtuple,
resample=Image.BICUBIC)
mask_norm = numpy.array(mask_norm).astype('bool')
return (image_norm, mask_norm)
......@@ -416,7 +414,7 @@ class FingerCrop (Preprocessor):
In this implementation, only the pixels that lie inside the mask will be
used to calculate the histogram equalization parameters. Because of this
particularity, we don't use Bob's implementation for histogram equalization
and have one based exclusively on NumPy.
and have one based exclusively on scikit-image.
**Parameters:**
......@@ -434,119 +432,42 @@ class FingerCrop (Preprocessor):
numpy.ndarray: normalized image as a 2D array of unsigned 8-bit integers
"""
from skimage.exposure import equalize_hist
image_histogram, bins = numpy.histogram(image[mask], 256, normed=True)
cdf = image_histogram.cumsum() # cumulative distribution function
cdf = 255 * cdf / cdf[-1] # normalize
# use linear interpolation of cdf to find new pixel values
image_equalized = numpy.interp(image.flatten(), bins[:-1], cdf)
image_equalized = image_equalized.reshape(image.shape)
retval = equalize_hist(image, mask=mask)
# normalized image to be returned is a composition of the original image
# (background) and the equalized image (finger area)
retval = image.copy()
retval[mask] = image_equalized[mask]
# make the parts outside the mask totally black
retval[~mask] = 0
return retval
def __circularGabor__(self, image, bw, sigma):
"""
Applies a circular gabor filter on the input image, with parameters.
**Parameters:**
image (numpy.ndarray): raw image to be filtered, as 2D array of
unsigned 8-bit integers
bw (float): bandwidth (1.12 octave)
sigma (int): standard deviation (5 pixels)
**Returns:**
numpy.ndarray: normalized image as a 2D array of unsigned 8-bit integers
"""
# Converts image to doubles
image_new = bob.core.convert(image,numpy.float64,(0,1),(0,255))
img_h, img_w = image_new.shape
def __call__(self, data):
"""Reads the input image or (image, mask) and prepares for fex.
fc = (1/math.pi * math.sqrt(math.log(2)/2) * (2**bw+1)/(2**bw-1))/sigma
Parameters:
sz = numpy.fix(8*numpy.max([sigma,sigma]))
data (numpy.ndarray, tuple): Either a :py:class:`numpy.ndarray`
containing a gray-scaled image with dtype ``uint8`` or a 2-tuple
containing both the gray-scaled image and a mask, with the same size of
the image, with dtype ``bool`` containing the points which should be
considered part of the finger
if numpy.mod(sz,2) == 0: sz = sz+1
#Constructs filter kernel
winsize = numpy.fix(sz/2)
x = numpy.arange(-winsize, winsize+1)
y = numpy.arange(winsize, numpy.fix(-sz/2)-1, -1)
X, Y = numpy.meshgrid(x, y)
# X (right +)
# Y (up +)
Returns:
gaborfilter = numpy.exp(-0.5*(X**2/sigma**2+Y**2/sigma**2))*numpy.cos(2*math.pi*fc*numpy.sqrt(X**2+Y**2))*(1/(2*math.pi*sigma))
numpy.ndarray: The image, preprocessed and normalized
# Without normalisation
#gaborfilter = numpy.exp(-0.5*(X**2/sigma**2+Y**2/sigma**2))*numpy.cos(2*math.pi*fc*numpy.sqrt(X**2+Y**2))
numpy.ndarray: A mask, of the same size of the image, indicating where
the valid data for the object is.
imageEnhance = utils.imfilter(image, gaborfilter)
imageEnhance = numpy.abs(imageEnhance)
return bob.core.convert(imageEnhance,numpy.uint8, (0,255),
(imageEnhance.min(),imageEnhance.max()))
def __HFE__(self,image):
"""
High Frequency Emphasis Filtering (HFE).
"""
### Hard-coded parameters for the HFE filtering
D0 = 0.025
a = 0.6
b = 1.2
n = 2.0
# converts image to doubles
image_new = bob.core.convert(image,numpy.float64, (0,1), (0,255))
img_h, img_w = image_new.shape
# DFT
Ffreq = bob.sp.fftshift(bob.sp.fft(image_new.astype(numpy.complex128))/math.sqrt(img_h*img_w))
row = numpy.arange(1,img_w+1)
x = (numpy.tile(row,(img_h,1)) - (numpy.fix(img_w/2)+1)) /img_w
col = numpy.arange(1,img_h+1)
y = (numpy.tile(col,(img_w,1)).T - (numpy.fix(img_h/2)+1))/img_h
# D is the distance from point (u,v) to the centre of the
# frequency rectangle.
radius = numpy.sqrt(x**2 + y**2)
f = a + b / (1.0 + (D0 / radius)**(2*n))
Ffreq = Ffreq * f
# implements the inverse DFT
imageEnhance = bob.sp.ifft(bob.sp.ifftshift(Ffreq))
# skips complex part
imageEnhance = numpy.abs(imageEnhance)
# renormalizes and returns
return bob.core.convert(imageEnhance, numpy.uint8, (0, 255),
(imageEnhance.min(), imageEnhance.max()))
def __call__(self, image, annotations=None):
"""
Reads the input image, extract the mask of the fingervein, postprocesses.
"""
if isinstance(data, numpy.ndarray):
image = data
mask = None
else:
image, mask = data
# 1. Pads the input image if any padding should be added
image = numpy.pad(image, self.padding_width, 'constant',
......@@ -554,22 +475,26 @@ class FingerCrop (Preprocessor):
## Finger edges and contour extraction:
if self.fingercontour == 'leemaskMatlab':
mask, edges = self.__leemaskMatlab__(image) #for UTFVP
mask = self.__leemaskMatlab__(image) #for UTFVP
elif self.fingercontour == 'leemaskMod':
mask, edges = self.__leemaskMod__(image) #for VERA
mask = self.__leemaskMod__(image) #for VERA
elif self.fingercontour == 'konomask':
mask, edges = self.__konomask__(image, sigma=5)
mask = self.__konomask__(image, sigma=5)
elif self.fingercontour == 'annotation':
if mask is None:
raise RuntimeError("Cannot use fingercontour=annotation - the " \
"current sample being processed does not provide a mask")
else:
raise RuntimeError("Please choose between leemaskMod, leemaskMatlab, " \
"konomask or annotation for parameter 'fingercontour'. %s is not " \
"valid" % self.fingercontour)
## Finger region normalization:
image_norm, mask_norm = self.__huangnormalization__(image, mask, edges)
image_norm, mask_norm = self.__huangnormalization__(image, mask)
## veins enhancement:
if self.postprocessing == 'HE':
image_norm = self.__HE__(image_norm, mask_norm)
elif self.postprocessing == 'HFE':
image_norm = self.__HFE__(image_norm)
elif self.postprocessing == 'CircGabor':
image_norm = self.__circularGabor__(image_norm, 1.12, 5)
## returns the normalized image and the finger mask
return image_norm, mask_norm
......@@ -580,13 +505,11 @@ class FingerCrop (Preprocessor):
f = bob.io.base.HDF5File(filename, 'w')
f.set('image', data[0])
f.set('finger_mask', data[1])
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')
image = f.read('image')
finger_mask = f.read('finger_mask')
return (image, finger_mask)
return f.read('image'), f.read('mask')
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment