Commit 0091d8d9 authored by Amir MOHAMMADI's avatar Amir MOHAMMADI

Merge branch 'deprecate-mlp' into 'master'

Remove WatershedMask since bob.learn.mlp is deprecated

See merge request !50
parents 1be6abee 1d78c1bb
Pipeline #51771 failed with stages
in 11 minutes and 9 seconds
from .crop import Cropper, FixedCrop, NoCrop
from .mask import Padder, Masker, FixedMask, NoMask, AnnotatedRoIMask
from .mask import KonoMask, LeeMask, TomesLeeMask, WatershedMask
from .mask import KonoMask, LeeMask, TomesLeeMask
from .normalize import Normalizer, NoNormalization, HuangNormalization
from .filters import Filter, NoFilter, HistogramEqualization
from .preprocessor import Preprocessor
......@@ -31,7 +31,6 @@ __appropriate__(
KonoMask,
LeeMask,
TomesLeeMask,
WatershedMask,
Normalizer,
NoNormalization,
HuangNormalization,
......
......@@ -477,193 +477,3 @@ 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.
foreground_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 (foreground) markers will be used from the network
selection. This value should be a floating point number in the open-set
interval :math:`(0.0, 1.0)`. If ``background_threshold`` is not set,
values for background selection will be set to :math:`1.0-T`, where ``T``
represents this threshold.
background_threshold (float): Threshold given a logistic regression output
(interval :math:`[0, 1]`) for which we consider finger markers provided
by the network. The smaller the value, the more selective the algorithm
will be and the less (background) markers will be used from the network
selection. This value should be a floating point number in the open-set
interval :math:`(0.0, 1.0)`. If ``foreground_threshold`` is not set,
values for foreground selection will be set to :math:`1.0-T`, where ``T``
represents this threshold.
"""
def __init__(self, model, foreground_threshold, background_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
# adjust threshold from background and foreground
if foreground_threshold is None and background_threshold is not None:
foreground_threshold = 1 - background_threshold
if background_threshold is None and foreground_threshold is not None:
background_threshold = 1 - foreground_threshold
if foreground_threshold is None and background_threshold is None:
foreground_threshold = 0.5
background_threshold = 0.5
self.foreground_threshold = foreground_threshold
self.background_threshold = background_threshold
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 run(self, image):
'''Fully preprocesses the input image and returns intermediate results
Parameters:
image (numpy.ndarray): A 2D numpy array of type ``uint8`` with the
input image
Returns:
numpy.ndarray: A 2D numpy array of type ``uint8`` with the markers for
foreground and background, selected by the neural network model
numpy.ndarray: A 2D numpy array of type ``float64`` with the edges used
to define the borders of the watermasking process
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<self.background_threshold, 1, 0)
markers_bg = skimage.morphology.opening(markers_bg, selem=selector)
markers_fg = numpy.where(predictions>=self.foreground_threshold, 255, 0)
markers_fg = skimage.morphology.opening(markers_fg, selem=selector)
# avoids markers on finger borders
selector = skimage.morphology.disk(radius=2)
markers_fg = skimage.morphology.erosion(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 - dialation is applied on the output of the
# Sobel filter to well mark the finger boundaries
edges = skimage.filters.sobel(image)
edges = skimage.morphology.dilation(edges, selem=selector)
# 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)
return markers, edges, mask
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
'''
markers, edges, mask = self.run(image)
return mask
This diff is collapsed.
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :
# Wed 4 Oct 11:23:52 2017 CEST
"""Preprocesses a fingervein image with a watershed/neural-net seeded mask
Usage: %(prog)s [-v...] [-s <path>] [-f <float>] [-b <float>] [--scan]
<model> <database> [<stem>...]
%(prog)s --help
%(prog)s --version
Arguments:
<model> Path to model to use for find watershed markers
<database> Name of the database to use for creating the model (options are:
"fv3d" or "verafinger")
<stem> Name of the object on the database to display, without the root
or the extension. If none provided, run for all possible stems on
the database
Options:
-h, --help Shows this help message and exits
-V, --version Prints the version and exits
-v, --verbose Increases the output verbosity level
-f, --fg-threshold=<float> Foreground threshold value. Should be set to a
number that is between 0.5 and 1.0. The higher,
the less markers for the foreground watershed
process will be produced. [default: 0.7]
-b, --bg-threshold=<float> Background threshold value. Should be set to a
number that is between 0.0 and 0.5. The smaller,
the less markers for the foreground watershed
process will be produced. [default: 0.3]
-S, --scan If set, ignores settings for the threshold and
scans the whole range of threshold printing the
Jaccard, M1 and M2 merith figures
-s <path>, --save=<path> If set, saves individual image into files instead
of displaying the result of processing. Pass the
name of directory that will be created and
suffixed with the paths of original images.
Examples:
Visualize the preprocessing toolchain over a single image
$ %(prog)s model.hdf5 verafinger sample-stem
Save the results of the preprocessing to several files. In this case, the
program runs non-interactively:
$ %(prog)s -s graphics model.hdf5 verafinger sample-stem
Scans the set of possible thresholds printing Jaccard, M1 and M2 indexes:
$ %(prog)s --scan model.hdf5 verafinger sample-stem
"""
import os
import sys
import time
import numpy
import schema
import docopt
import bob.core
logger = bob.core.log.setup("bob.bio.vein")
import matplotlib.pyplot as plt
import bob.io.base
import bob.io.image
def validate(args):
'''Validates command-line arguments, returns parsed values
This function uses :py:mod:`schema` for validating :py:mod:`docopt`
arguments. Logging level is not checked by this procedure (actually, it is
ignored) and must be previously setup as some of the elements here may use
logging for outputing information.
Parameters:
args (dict): Dictionary of arguments as defined by the help message and
returned by :py:mod:`docopt`
Returns
dict: Validate dictionary with the same keys as the input and with values
possibly transformed by the validation procedure
Raises:
schema.SchemaError: in case one of the checked options does not validate.
'''
valid_databases = ('fv3d', 'verafinger')
sch = schema.Schema({
'<model>': schema.And(os.path.exists,
error='<model> should point to an existing path'),
'<database>': schema.And(lambda n: n in valid_databases,
error='<database> must be one of %s' % ', '.join(valid_databases)),
'--fg-threshold': schema.And(
schema.Use(float), lambda n: 0.5 < n < 1.0,
error='--fg-threshold should be a float between 0.5 and 1.0',
),
'--bg-threshold': schema.And(
schema.Use(float), lambda n: 0.0 < n < 0.5,
error='--bg-threshold should be a float between 0.0 and 0.5',
),
str: object, #ignores strings we don't care about
}, ignore_extra_keys=True)
return sch.validate(args)
def save_figures(title, image, markers, edges, mask):
'''Saves individual images on a directory
'''
dirname = os.path.dirname(title)
if not os.path.exists(dirname): os.makedirs(dirname)
bob.io.base.save(image, os.path.join(title, 'original.png'))
_ = markers.copy().astype('uint8')
_[_==1] = 128
bob.io.base.save(_, os.path.join(title, 'markers.png'))
bob.io.base.save((255*edges).astype('uint8'), os.path.join(title,'edges.png'))
bob.io.base.save(mask.astype('uint8')*255, os.path.join(title, 'mask.png'))
from ..preprocessor.utils import draw_mask_over_image
masked_image = draw_mask_over_image(image, mask)
masked_image.save(os.path.join(title, 'masked.png'))
def make_figure(image, markers, edges, mask):
'''Returns a matplotlib figure with the detailed processing result'''
plt.clf() #completely clears the current figure
figure = plt.gcf()
plt.subplot(2,2,1)
_ = markers.copy().astype('uint8')
_[_==1] = 128
plt.imshow(_, cmap='gray')
plt.title('Markers')
plt.subplot(2,2,2)
_ = numpy.dstack([
(_ | (255*edges).astype('uint8')),
_,
_,
])
plt.imshow(_)
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)')
return figure
def process_one(args, image, path):
'''Processes a single image'''
from bob.bio.vein.preprocessor import WatershedMask, AnnotatedRoIMask
# loads the processor once - avoids re-reading weights from the disk
processor = WatershedMask(
model=args['<model>'],
foreground_threshold=args['--fg-threshold'],
background_threshold=args['--bg-threshold'],
)
annotator = AnnotatedRoIMask()
from bob.bio.vein.preprocessor.utils import \
jaccard_index, intersect_ratio, intersect_ratio_of_complement
start = time.time()
markers, edges, mask = processor.run(image)
total_time = time.time() - start
# error
annotated_mask = annotator(image)
ji = jaccard_index(annotated_mask, mask)
m1 = intersect_ratio(annotated_mask, mask)
m2 = intersect_ratio_of_complement(annotated_mask, mask)
logger.debug('%s, %.2f, %.2f, %.2f, %g, %g, %g', path, total_time,
args['--fg-threshold'], args['--bg-threshold'], ji, m1, m2)
if not args['--scan']:
if args['--save']:
dest = os.path.join(args['--save'], path)
save_figures(dest, image, markers, edges, mask)
else:
fig = make_figure(image, markers, edges, mask)
fig.suptitle('%s @ %s - JI=%.4f, M1=%.4f, M2=%.4f\n' \
'($\\tau_{FG}$ = %.2f - $\\tau_{BG}$ = %.2f)' % \
(path, args['<database>'], ji, m1, m2, args['--fg-threshold'],
args['--bg-threshold']), fontsize=12)
print('Close the figure to continue...')
plt.show()
return (path, total_time, args['--fg-threshold'], args['--bg-threshold'],
ji, m1, m2)
def eval_best_thresholds(results):
'''Evaluates the best thresholds taking into consideration various indexes'''
m1 = numpy.array([k[-2] for k in results])
m2 = numpy.array([k[-1] for k in results])
index = m1/m2
return index.argmax()
def main(user_input=None):
if user_input is not None:
argv = user_input
else:
argv = sys.argv[1:]
import pkg_resources
completions = dict(
prog=os.path.basename(sys.argv[0]),
version=pkg_resources.require('bob.bio.vein')[0].version
)
args = docopt.docopt(
__doc__ % completions,
argv=argv,
version=completions['version'],
)
try:
from .validate import setup_logger
logger = setup_logger('bob.bio.vein', args['--verbose'])
args = validate(args)
except schema.SchemaError as e:
sys.exit(e)
if args['<database>'] == 'fv3d':
from bob.bio.vein.config.fv3d import database as db
elif args['<database>'] == 'verafinger':
from bob.bio.vein.config.verafinger import database as db
database_replacement = "%s/.bob_bio_databases.txt" % os.environ["HOME"]
db.replace_directories(database_replacement)
all_files = db.objects()
# if a specific <stem> was not provided, run for all possible stems
if not args['<stem>']:
args['<stem>'] = [k.path for k in all_files]
# Loads the image, the mask and save it to a PNG file
for stem in args['<stem>']:
f = [k for k in all_files if k.path == stem]
if len(f) == 0:
raise RuntimeError('File with stem "%s" does not exist on "%s"' % \
stem, args['<database>'])
f = f[0]
image = f.load(db.original_directory, db.original_extension)
if args['--scan']:
results = []
logger.debug('stem, time, fg_thres, bg_thres, jaccard, m1, m2')
for fg_threshold in numpy.arange(0.6, 1.0, step=0.1):
for bg_threshold in numpy.arange(0.1, 0.5, step=0.1):
args['--fg-threshold'] = fg_threshold
args['--bg-threshold'] = bg_threshold
results.append(process_one(args, image, f.path))
best_thresholds = eval_best_thresholds(results)
logger.info('%s: FG = %.2f | BG = %.2f | M1/M2 = %.2f', f.path,
results[best_thresholds][2], results[best_thresholds][3],
results[best_thresholds][-2]/results[best_thresholds][-1])
else:
process_one(args, image, f.path)
......@@ -10,8 +10,6 @@ build:
- compare_rois.py = bob.bio.vein.script.compare_rois:main
- view_sample.py = bob.bio.vein.script.view_sample:main
- blame.py = bob.bio.vein.script.blame:main
- markdet.py = bob.bio.vein.script.markdet:main
- watershed_mask.py = bob.bio.vein.script.watershed_mask:main
number: {{ environ.get('BOB_BUILD_NUMBER', 0) }}
run_exports:
- {{ pin_subpackage(name) }}
......@@ -41,8 +39,6 @@ requirements:
- bob.ip.color
- bob.bio.base
- bob.learn.linear
- bob.learn.activation
- bob.learn.mlp
run:
- python
......@@ -62,8 +58,6 @@ test:
- compare_rois.py --help
- view_sample.py --help
- blame.py --help
- markdet.py --help
- watershed_mask.py --help
- nosetests --with-coverage --cover-package={{ name }} -sv {{ name }}
- sphinx-build -aEW {{ project_dir }}/doc {{ project_dir }}/sphinx
- sphinx-build -aEb doctest {{ project_dir }}/doc sphinx
......
......@@ -17,7 +17,7 @@ $ bob bio pipelines vanilla-biometrics [DATABASE_NAME] [BASELINE]
Both, `[DATABASE_NAME]` and `[BASELINE]` can be either python resources or
python files.
Please, refer to :ref:`bob.bio.base <bob.bio.base>` for more information.
Please, refer to :ref:`bob.bio.base <bob.bio.base>` for more information.
Repeated Line-Tracking with Miura Matching
......@@ -37,8 +37,8 @@ protocol, do the following:
.. tip::
If you have more processing cores on your local machine and don't want to
submit your job for SGE execution, you can run it in parallel by adding the options ``-l local-parallel``.
submit your job for SGE execution, you can run it in parallel by adding the options ``-l local-parallel``.
.. code-block:: sh
$ bob bio pipelines vanilla-biometrics verafinger rlt -vv -c -l local-parallel
......@@ -261,49 +261,6 @@ This package contains other resources that can be used to evaluate different
bits of the vein processing toolchain.
Training the Watershed Finger region detector
=============================================
The correct detection of the finger boundaries is an important step of many
algorithms for the recognition of finger veins. It allows to compensate for
eventual rotation and scaling issues one might find when comparing models and
probes. In this package, we propose a novel finger boundary detector based on
the `Watershedding Morphological Algorithm
<https://en.wikipedia.org/wiki/Watershed_(image_processing)>`. Watershedding
works in three steps:
1. Determine markers on the original image indicating the types of areas one
would like to detect (e.g. "finger" or "background")
2. Determine a 2D (gray-scale) surface representing the original image in which
darker spots (representing valleys) are more likely to be filled by
surrounding markers. This is normally achieved by filtering the image with a
high-pass filter like Sobel or using an edge detector such as Canny.
3. Run the watershed algorithm
In order to determine markers for step 1, we train a neural network which
outputs the likelihood of a point being part of a finger, given its coordinates
and values of surrounding pixels.
When used to run an experiment,
:py:class:`bob.bio.vein.preprocessor.WatershedMask` requires you provide a
*pre-trained* neural network model that presets the markers before
watershedding takes place. In order to create one, you can run the program
`bob_bio_vein_markdet.py`:
.. code-block:: sh
$ bob_bio_vein_markdet.py --hidden=20 --samples=500 fv3d central dev
You input, as arguments to this application, the database, protocol and subset
name you wish to use for training the network. The data is loaded observing a
total maximum number of samples from the dataset (passed with ``--samples=N``),
the network is trained and recorded into an HDF5 file (by default, the file is
called ``model.hdf5``, but the name can be changed with the option
``--model=``). Once you have a model, you can use the preprocessor mask by
constructing an object and attaching it to the
:py:class:`bob.bio.vein.preprocessor.Preprocessor` entry on your configuration.
Region of Interest Goodness of Fit
==================================
......
......@@ -21,5 +21,4 @@
.. _mailing list: https://www.idiap.ch/software/bob/discuss
.. _bob.bio.base: https://pypi.python.org/pypi/bob.bio.base
.. _jaccard index: https://en.wikipedia.org/wiki/Jaccard_index
.. _watershed: https://en.wikipedia.org/wiki/Watershed_(image_processing)
.. _gridtk: https://pypi.python.org/pypi/gridtk
......@@ -70,8 +70,6 @@ setup(
"bob_bio_vein_compare_rois.py = bob.bio.vein.script.compare_rois:main",
"bob_bio_vein_view_sample.py = bob.bio.vein.script.view_sample:main",
"bob_bio_vein_blame.py = bob.bio.vein.script.blame:main",
"bob_bio_vein_markdet.py = bob.bio.vein.script.markdet:main",
"bob_bio_vein_watershed_mask.py = bob.bio.vein.script.watershed_mask:main",
],
},
classifiers=[
......
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