Commit b9af7a82 authored by André Anjos's avatar André Anjos 💬

Re-estructured Preprocessor to simplify experimentation and configuration

parent a0aed3f5
......@@ -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
"""
......
......@@ -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
"""
......
......@@ -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
"""
......
#!/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())
......@@ -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
......@@ -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]
......
......@@ -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
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('_')]
This diff is collapsed.
#!/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)
#!/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')
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