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

Revamped Teo's code to simplify it; Add thorough tests

parent 413b80be
Pipeline #5184 failed with stages
in 3 minutes and 18 seconds
#!/usr/bin/env python #!/usr/bin/env python
# vim: set fileencoding=utf-8 : # vim: set fileencoding=utf-8 :
# Tiago de Freitas Pereira <tiago.pereira@idiap.ch> # Thu 03 Nov 2016 12:23:52 CET
# Wed 20 July 14:43:22 CEST 2016
"""Single sample API"""
"""
Verification API for bob.db.voxforge
"""
from bob.bio.base.database.file import BioFile from bob.bio.base.database.file import BioFile
class VeinBioFile(BioFile): class VeinBioFile(BioFile):
def __init__(self, client_id, path, file_id): """A "sample" object that is specific to vein recognition experiments
"""
Initializes this File object with an File equivalent for
VoxForge database. Parameters:
"""
super(VeinBioFile, self).__init__(client_id=client_id, path=path, file_id=file_id) f (object): Low-level file (or sample) object that is kept inside
"""
def __init__(self, f):
super(VeinBioFile, self).__init__(
client_id=f.model_id,
path=f.path,
file_id=f.id,
)
# keep copy of original low-level database file object
self.f = f
...@@ -3,8 +3,36 @@ ...@@ -3,8 +3,36 @@
# Tue 27 Sep 2016 16:48:57 CEST # Tue 27 Sep 2016 16:48:57 CEST
from .database import VeinBioFile from bob.bio.base.database import BioFile, BioDatabase
from bob.bio.base.database import BioDatabase from bob.bio.base.database.file import BioFile
class VerafingerBioFile(BioFile):
"""
Implements extra properties of vein files
Parameters:
f (object): Low-level file (or sample) object that is kept inside
"""
def __init__(self, f):
super(VerafingerBioFile, self).__init__(
client_id=f.model_id,
path=f.path,
file_id=f.id,
)
self.f = f
def roi(self):
"""Returns the binary mask from the ROI annotations available"""
points = self.f.roi()
class VerafingerBioDatabase(BioDatabase): class VerafingerBioDatabase(BioDatabase):
...@@ -45,4 +73,4 @@ class VerafingerBioDatabase(BioDatabase): ...@@ -45,4 +73,4 @@ class VerafingerBioDatabase(BioDatabase):
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, retval = self.__db.objects(groups=groups, protocol=protocol,
purposes=purposes, model_ids=model_ids, **kwargs) purposes=purposes, model_ids=model_ids, **kwargs)
return [VeinBioFile(client_id=f.model_id, path=f.path, file_id=f.id) for f in retval] return [VeinBioFile(f) for f in retval]
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :
"""Utilities for preprocessing vein imagery"""
import numpy
def assert_points(area, points):
"""Checks all points fall within the determined shape region, inclusively
This assertion function, test all points given in ``points`` fall within a
certain area provided in ``area``.
Parameters:
area (tuple): A tuple containing the size of the limiting area where the
points should all be in.
points (numpy.ndarray): A 2D numpy ndarray with any number of rows (points)
and 2 columns (representing ``y`` and ``x`` coordinates respectively), or
any type convertible to this format. This array contains the points that
will be checked for conformity. In case one of the points doesn't fall
into the determined area an assertion is raised.
Raises:
AssertionError: In case one of the input points does not fall within the
area defined.
"""
for k in points:
assert 0 <= k[0] < area[0] and 0 <= k[1] < area[1], \
"Point (%d, %d) is not inside the region determined by area " \
"(%d, %d)" % (k[0], k[1], area[0], area[1])
def fix_points(area, points):
"""Checks/fixes all points so they fall within the determined shape region
Points which are lying outside the determined area will be brought into the
area by moving the offending coordinate to the border of the said area.
Parameters:
area (tuple): A tuple containing the size of the limiting area where the
points should all be in.
points (numpy.ndarray): A 2D :py:class:`numpy.ndarray` with any number of
rows (points) and 2 columns (representing ``y`` and ``x`` coordinates
respectively), or any type convertible to this format. This array
contains the points that will be checked/fixed for conformity. In case
one of the points doesn't fall into the determined area, it is silently
corrected so it does.
Returns:
numpy.ndarray: A **new** array of points with corrected coordinates
"""
retval = numpy.array(points).copy()
retval[retval<0] = 0 #floor at 0 for both axes
y, x = retval[:,0], retval[:,1]
y[y>=area[0]] = area[0] - 1
x[x>=area[1]] = area[1] - 1
return retval
def poly_to_mask(shape, points):
"""Generates a binary mask from a set of 2D points
Parameters:
shape (tuple): A tuple containing the size of the output mask in height and
width, for Bob compatibility ``(y, x)``.
points (list): A list of tuples containing the polygon points that form a
region on the target mask. A line connecting these points will be drawn
and all the points in the mask that fall on or within the polygon line,
will be set to ``True``. All other points will have a value of ``False``.
Returns:
numpy.ndarray: A 2D numpy ndarray with ``dtype=bool`` with the mask
generated with the determined shape, using the points for the polygon.
"""
from PIL import Image, ImageDraw
# 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
fixed = tuple(map(tuple, numpy.roll(fix_points(shape, points), 1, 1)))
# draws polygon
ImageDraw.Draw(mask).polygon(fixed, fill=255)
return numpy.array(mask, dtype=numpy.bool)
def mask_to_image(mask, dtype=numpy.uint8):
"""Converts a binary (boolean) mask into an integer or floating-point image
This function converts a boolean binary mask into an image of the desired
type by setting the points where ``False`` is set to 0 and points where
``True`` is set to the most adequate value taking into consideration the
destination data type ``dtype``. Here are support types and their ranges:
* numpy.uint8: ``[0, (2^8)-1]``
* numpy.uint16: ``[0, (2^16)-1]``
* numpy.uint32: ``[0, (2^32)-1]``
* numpy.uint64: ``[0, (2^64)-1]``
* numpy.float32: ``[0, 1.0]`` (fixed)
* numpy.float64: ``[0, 1.0]`` (fixed)
* numpy.float128: ``[0, 1.0]`` (fixed)
All other types are currently unsupported.
Parameters:
mask (numpy.ndarray): A 2D numpy ndarray with boolean data type, containing
the mask that will be converted into an image.
dtype (numpy.dtype): A valid numpy data-type from the list above for the
resulting image
Returns:
numpy.ndarray: With the designated data type, containing the binary image
formed from the mask.
Raises:
TypeError: If the type is not supported by this function
"""
dtype = numpy.dtype(dtype)
retval = mask.astype(dtype)
if dtype in (numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64):
retval[retval == 1] = numpy.iinfo(dtype).max
elif dtype in (numpy.float32, numpy.float64, numpy.float128):
pass
else:
raise TypeError("Data type %s is unsupported" % dtype)
return retval
from .utils import ManualRoiCut
from .utils import ConstructVeinImage
from .utils import NormalizeImageRotation
# gets sphinx autodoc done right - don't remove it
__all__ = [_ for _ in dir() if not _.startswith('_')]
This diff is collapsed.
11 91
8 322
76 320
114 307
140 300
176 292
225 292
269 288
330 287
405 288
436 290
456 288
468 276
473 242
472 208
470 184
466 146
455 116
440 93
424 77
397 69
358 64
298 60
247 52
201 38
160 25
130 7
106 7
81 16
46 46
22 71
...@@ -93,7 +93,7 @@ def test_finger_crop(): ...@@ -93,7 +93,7 @@ def test_finger_crop():
assert numpy.mean(numpy.abs(preproc - preproc_ref)) < 1.3e2 assert numpy.mean(numpy.abs(preproc - preproc_ref)) < 1.3e2
def test_miuramax(): def test_max_curvature():
#Maximum Curvature method against Matlab reference #Maximum Curvature method against Matlab reference
...@@ -118,7 +118,7 @@ def test_miuramax(): ...@@ -118,7 +118,7 @@ def test_miuramax():
assert numpy.mean(numpy.abs(output_img - output_img_ref)) < 8e-3 assert numpy.mean(numpy.abs(output_img - output_img_ref)) < 8e-3
def test_miurarlt(): def test_repeated_line_tracking():
#Repeated Line Tracking method against Matlab reference #Repeated Line Tracking method against Matlab reference
...@@ -143,7 +143,7 @@ def test_miurarlt(): ...@@ -143,7 +143,7 @@ def test_miurarlt():
assert numpy.mean(numpy.abs(output_img - output_img_ref)) < 0.5 assert numpy.mean(numpy.abs(output_img - output_img_ref)) < 0.5
def test_huangwl(): def test_wide_line_detector():
#Wide Line Detector method against Matlab reference #Wide Line Detector method against Matlab reference
...@@ -187,67 +187,135 @@ def test_miura_match(): ...@@ -187,67 +187,135 @@ def test_miura_match():
score_imp = MM.score(template_vein, probe_imp_vein) score_imp = MM.score(template_vein, probe_imp_vein)
assert numpy.isclose(score_imp, 0.172906739278421) assert numpy.isclose(score_imp, 0.172906739278421)
def test_manualRoiCut():
""" def test_assert_points():
Test ManualRoitCut
""" # Tests that point assertion works as expected
from bob.bio.vein.preprocessor.utils import ManualRoiCut from ..preprocessor import utils
image_path = F(('preprocessors', '0019_3_1_120509-160517.png'))
annotation_path = F(('preprocessors', '0019_3_1_120509-160517.txt')) area = (10, 5)
inside = [(0,0), (3,2), (9, 4)]
c = ManualRoiCut(annotation_path, image_path) utils.assert_points(area, inside) #should not raise
mask_1 = c.roi_mask()
image_1 = c.roi_image() def _check_outside(point):
# create mask using size: # should raise, otherwise it is an error
c = ManualRoiCut(annotation_path, sizes=(672,380)) try:
mask_2 = c.roi_mask() utils.assert_points(area, [point])
except AssertionError as e:
# loading image: assert str(point) in str(e)
image = bob.io.base.load(image_path) else:
c = ManualRoiCut(annotation_path, image) raise AssertionError("Did not assert %s is outside of %s" % (point, area))
mask_3 = c.roi_mask()
image_3 = c.roi_image() outside = [(-1, 0), (10, 0), (0, 5), (10, 5), (15,12)]
# load text file: for k in outside: _check_outside(k)
with open(annotation_path,'r') as f:
retval = numpy.loadtxt(f, ndmin=2)
def test_fix_points():
# carefully -- this is BOB format --- (x,y)
annotation = list([tuple([k[0], k[1]]) for k in retval]) # Tests that point clipping works as expected
c = ManualRoiCut(annotation, image) from ..preprocessor import utils
mask_4 = c.roi_mask()
image_4 = c.roi_image() area = (10, 5)
inside = [(0,0), (3,2), (9, 4)]
assert (mask_1 == mask_2).all() fixed = utils.fix_points(area, inside)
assert (mask_1 == mask_3).all() assert numpy.array_equal(inside, fixed), '%r != %r' % (inside, fixed)
assert (mask_1 == mask_4).all()
assert (image_1 == image_3).all() fixed = utils.fix_points(area, [(-1, 0)])
assert (image_1 == image_4).all() assert numpy.array_equal(fixed, [(0, 0)])
def test_ConstructAnnotations(): fixed = utils.fix_points(area, [(10, 0)])
""" assert numpy.array_equal(fixed, [(9, 0)])
Test ConstructAnnotations preprocessor
""" fixed = utils.fix_points(area, [(0, 5)])
image_filename = F( ( 'preprocessors', 'ConstructAnnotations.png' ) ) assert numpy.array_equal(fixed, [(0, 4)])
roi_annotations_filename = F( ( 'preprocessors', 'ConstructAnnotations.txt' ) )
vein_annotations_filename = F( ( 'preprocessors', 'ConstructAnnotations.npy' ) ) fixed = utils.fix_points(area, [(10, 5)])
assert numpy.array_equal(fixed, [(9, 4)])
image = bob.io.base.load( image_filename )
roi_annotations = np.loadtxt(roi_annotations_filename, dtype='uint16') fixed = utils.fix_points(area, [(15, 12)])
roi_annotations = [tuple([point[0], point[1]]) for point in roi_annotations] assert numpy.array_equal(fixed, [(9, 4)])
fp = open(vein_annotations_filename, 'rb')
vein_annotations = np.load(fp)
vein_annotations = vein_annotations['arr_0'].tolist() def test_poly_to_mask():
fp.close()
vein_annotations = [[tuple([point[0], point[1]]) for point in line] for line in vein_annotations] # Tests we can generate a mask out of a polygon correctly
from ..preprocessor import utils
annotation_dictionary = {"image" : image, "roi_annotations" : roi_annotations, "vein_annotations" : vein_annotations}
from bob.bio.vein.preprocessor.utils import ConstructVeinImage area = (10, 9) #10 rows, 9 columns
from bob.bio.vein.preprocessor.utils import NormalizeImageRotation polygon = [(2, 2), (2, 7), (7, 7), (7, 2)] #square shape, (y, x) format
output = ConstructVeinImage(annotation_dictionary, center = True) mask = utils.poly_to_mask(area, polygon)
output = NormalizeImageRotation(output, dark_lines = False) nose.tools.eq_(mask.dtype, numpy.bool)
assert np.array_equal(output, image)
# This should be the output:
expected = numpy.array([
[False, False, False, False, False, False, False, False, False],
[False, False, False, False, False, False, False, False, False],
[False, False, True, True, True, True, True, True, False],
[False, False, True, True, True, True, True, True, False],
[False, False, True, True, True, True, True, True, False],
[False, False, True, True, True, True, True, True, False],
[False, False, True, True, True, True, True, True, False],
[False, False, True, True, True, True, True, True, False],
[False, False, False, False, False, False, False, False, False],
[False, False, False, False, False, False, False, False, False],
])
assert numpy.array_equal(mask, expected)
polygon = [(3, 2), (5, 7), (8, 7), (7, 3)] #trapezoid, (y, x) format
mask = utils.poly_to_mask(area, polygon)
nose.tools.eq_(mask.dtype, numpy.bool)
# This should be the output:
expected = numpy.array([
[False, False, False, False, False, False, False, False, False],
[False, False, False, False, False, False, False, False, False],
[False, False, False, False, False, False, False, False, False],
[False, False, True, False, False, False, False, False, False],
[False, False, True, True, True, False, False, False, False],
[False, False, False, True, True, True, True, True, False],
[False, False, False, True, True, True, True, True, False],
[False, False, False, True, True, True, True, True, False],
[False, False, False, False, False, False, False, True, False],
[False, False, False, False, False, False, False, False, False],
])
assert numpy.array_equal(mask, expected)
def test_mask_to_image():
# Tests we can correctly convert a boolean array into an image
# that makes sense according to the data types
from ..preprocessor import utils
sample = numpy.array([False, True])
nose.tools.eq_(sample.dtype, numpy.bool)
def _check_uint(n):
conv = utils.mask_to_image(sample, 'uint%d' % n)
nose.tools.eq_(conv.dtype, getattr(numpy, 'uint%d' % n))
target = [0, (2**n)-1]
assert numpy.array_equal(conv, target), '%r != %r' % (conv, target)
_check_uint(8)
_check_uint(16)
_check_uint(32)
_check_uint(64)
def _check_float(n):
conv = utils.mask_to_image(sample, 'float%d' % n)
nose.tools.eq_(conv.dtype, getattr(numpy, 'float%d' % n))
assert numpy.array_equal(conv, [0, 1.0]), '%r != %r' % (conv, target)
_check_float(32)
_check_float(64)
_check_float(128)
# This should be unsupported
try:
conv = utils.mask_to_image(sample, 'int16')
except TypeError as e:
assert 'int16' in str(e)
else:
raise AssertionError('Conversion to int16 did not trigger a TypeError')
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