Coverage for src/bob/bio/vein/preprocessor/utils.py: 78%
45 statements
« prev ^ index » next coverage.py v7.6.0, created at 2024-07-12 23:27 +0200
« prev ^ index » next coverage.py v7.6.0, created at 2024-07-12 23:27 +0200
1#!/usr/bin/env python
2# vim: set fileencoding=utf-8 :
4"""Utilities for preprocessing vein imagery"""
6import numpy
9def assert_points(area, points):
10 """Checks all points fall within the determined shape region, inclusively
12 This assertion function, test all points given in ``points`` fall within a
13 certain area provided in ``area``.
16 Parameters:
18 area (tuple): A tuple containing the size of the limiting area where the
19 points should all be in.
21 points (numpy.ndarray): A 2D numpy ndarray with any number of rows (points)
22 and 2 columns (representing ``y`` and ``x`` coordinates respectively), or
23 any type convertible to this format. This array contains the points that
24 will be checked for conformity. In case one of the points doesn't fall
25 into the determined area an assertion is raised.
28 Raises:
30 AssertionError: In case one of the input points does not fall within the
31 area defined.
33 """
35 for k in points:
36 assert 0 <= k[0] < area[0] and 0 <= k[1] < area[1], (
37 "Point (%d, %d) is not inside the region determined by area "
38 "(%d, %d)" % (k[0], k[1], area[0], area[1])
39 )
42def fix_points(area, points):
43 """Checks/fixes all points so they fall within the determined shape region
45 Points which are lying outside the determined area will be brought into the
46 area by moving the offending coordinate to the border of the said area.
49 Parameters:
51 area (tuple): A tuple containing the size of the limiting area where the
52 points should all be in.
54 points (numpy.ndarray): A 2D :py:class:`numpy.ndarray` with any number of
55 rows (points) and 2 columns (representing ``y`` and ``x`` coordinates
56 respectively), or any type convertible to this format. This array
57 contains the points that will be checked/fixed for conformity. In case
58 one of the points doesn't fall into the determined area, it is silently
59 corrected so it does.
62 Returns:
64 numpy.ndarray: A **new** array of points with corrected coordinates
66 """
68 retval = numpy.array(points).copy()
70 retval[retval < 0] = 0 # floor at 0 for both axes
71 y, x = retval[:, 0], retval[:, 1]
72 y[y >= area[0]] = area[0] - 1
73 x[x >= area[1]] = area[1] - 1
75 return retval
78def poly_to_mask(shape, points):
79 """Generates a binary mask from a set of 2D points
82 Parameters:
84 shape (tuple): A tuple containing the size of the output mask in height and
85 width, for Bob compatibility ``(y, x)``.
87 points (list): A list of tuples containing the polygon points that form a
88 region on the target mask. A line connecting these points will be drawn
89 and all the points in the mask that fall on or within the polygon line,
90 will be set to ``True``. All other points will have a value of ``False``.
93 Returns:
95 numpy.ndarray: A 2D numpy ndarray with ``dtype=bool`` with the mask
96 generated with the determined shape, using the points for the polygon.
98 """
99 from PIL import Image, ImageDraw
101 # n.b.: PIL images are (x, y), while Bob shapes are represented in (y, x)!
102 mask = Image.new("L", (shape[1], shape[0]))
104 # converts whatever comes in into a list of tuples for PIL
105 fixed = tuple(map(tuple, numpy.roll(fix_points(shape, points), 1, 1)))
107 # draws polygon
108 ImageDraw.Draw(mask).polygon(fixed, fill=255)
110 return numpy.array(mask, dtype=bool)
113def mask_to_image(mask, dtype=numpy.uint8):
114 """Converts a binary (boolean) mask into an integer or floating-point image
116 This function converts a boolean binary mask into an image of the desired
117 type by setting the points where ``False`` is set to 0 and points where
118 ``True`` is set to the most adequate value taking into consideration the
119 destination data type ``dtype``. Here are support types and their ranges:
121 * numpy.uint8: ``[0, (2^8)-1]``
122 * numpy.uint16: ``[0, (2^16)-1]``
123 * numpy.uint32: ``[0, (2^32)-1]``
124 * numpy.uint64: ``[0, (2^64)-1]``
125 * numpy.float32: ``[0, 1.0]`` (fixed)
126 * numpy.float64: ``[0, 1.0]`` (fixed)
128 All other types are currently unsupported.
131 Parameters:
133 mask (numpy.ndarray): A 2D numpy ndarray with boolean data type, containing
134 the mask that will be converted into an image.
136 dtype (numpy.dtype): A valid numpy data-type from the list above for the
137 resulting image
140 Returns:
142 numpy.ndarray: With the designated data type, containing the binary image
143 formed from the mask.
146 Raises:
148 TypeError: If the type is not supported by this function
150 """
152 dtype = numpy.dtype(dtype)
153 retval = mask.astype(dtype)
155 if dtype in (numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64):
156 retval[retval == 1] = numpy.iinfo(dtype).max
158 elif dtype in (numpy.float32, numpy.float64):
159 pass
161 else:
162 raise TypeError("Data type %s is unsupported" % dtype)
164 return retval
167def show_image(image):
168 """Shows a single image using :py:meth:`PIL.Image.Image.show`
170 .. warning::
172 This function opens a new window. You must be operating interactively in a
173 windowing system for it to work properly.
175 Parameters:
177 image (numpy.ndarray): A 2D numpy.ndarray compose of 8-bit unsigned
178 integers containing the original image
180 """
182 from PIL import Image
184 img = Image.fromarray(image)
185 img.show()
188def draw_mask_over_image(image, mask, color="red"):
189 """Plots the mask over the image of a finger, for debugging purposes
191 Parameters:
193 image (numpy.ndarray): A 2D numpy.ndarray compose of 8-bit unsigned
194 integers containing the original image
196 mask (numpy.ndarray): A 2D numpy.ndarray compose of boolean values
197 containing the calculated mask
200 Returns:
202 PIL.Image: An image in PIL format
204 """
206 from PIL import Image
208 img = Image.fromarray(image).convert(mode="RGBA")
209 msk = Image.fromarray((~mask).astype("uint8") * 80)
210 red = Image.new("RGBA", img.size, color=color)
211 img.paste(red, mask=msk)
213 return img
216def show_mask_over_image(image, mask, color="red"):
217 """Plots the mask over the image of a finger using :py:meth:`PIL.Image.Image.show`
219 .. warning::
221 This function opens a new window. You must be operating interactively in a
222 windowing system for it to work properly.
224 Parameters:
226 image (numpy.ndarray): A 2D numpy.ndarray compose of 8-bit unsigned
227 integers containing the original image
229 mask (numpy.ndarray): A 2D numpy.ndarray compose of boolean values
230 containing the calculated mask
232 """
234 draw_mask_over_image(image, mask, color).show()
237def jaccard_index(a, b):
238 r"""Calculates the intersection over union for two masks
240 This function calculates the Jaccard index:
242 .. math::
244 J(A,B) &= \\frac{|A \cap B|}{|A \\cup B|} \\\\
245 &= \\frac{|A \cap B|}{|A|+|B|-|A \\cup B|}
248 Parameters:
250 a (numpy.ndarray): A 2D numpy array with dtype :py:obj:`bool`
252 b (numpy.ndarray): A 2D numpy array with dtype :py:obj:`bool`
255 Returns:
257 float: The floating point number that corresponds to the Jaccard index. The
258 float value lies inside the interval :math:`[0, 1]`. If ``a`` and ``b`` are
259 equal, then the similarity is maximum and the value output is ``1.0``. If
260 the areas are exclusive, then the value output by this function is ``0.0``.
262 """
264 return (a & b).sum().astype(float) / (a | b).sum().astype(float)
267def intersect_ratio(a, b):
268 """Calculates the intersection ratio between the ground-truth and a probe
270 This function calculates the intersection ratio between a ground-truth mask
271 (:math:`A`; probably generated from an annotation) and a probe mask
272 (:math:`B`), returning the ratio of overlap when the probe is compared to the
273 ground-truth data:
275 .. math::
277 R(A,B) = \\frac{|A \\cap B|}{|A|}
279 So, if the probe occupies the entirety of the ground-truth data, then the
280 output of this function is ``1.0``, otherwise, if areas are exclusive, then
281 this function returns ``0.0``. The output of this function should be analyzed
282 against the output of :py:func:`intersect_ratio_of_complement`, which
283 provides the complementary information about the intersection of the areas
284 being analyzed.
287 Parameters:
289 a (numpy.ndarray): A 2D numpy array with dtype :py:obj:`bool`, that
290 corresponds to the **ground-truth object**
292 b (numpy.ndarray): A 2D numpy array with dtype :py:obj:`bool`, that
293 corresponds to the probe object that will be compared to the ground-truth
296 Returns:
298 float: The floating point number that corresponds to the overlap ratio. The
299 float value lies inside the interval :math:`[0, 1]`.
301 """
303 return (a & b).sum().astype(float) / a.sum().astype(float)
306def intersect_ratio_of_complement(a, b):
307 """Calculates the intersection ratio between the complement of ground-truth and a probe
309 This function calculates the intersection ratio between *the complement* of a
310 ground-truth mask (:math:`A`; probably generated from an annotation) and a
311 probe mask (:math:`B`), returning the ratio of overlap when the probe is
312 compared to the ground-truth data:
314 .. math::
316 R(A,B) = \\frac{|A^c \\cap B|}{|A|} = B \\setminus A
319 So, if the probe is totally inside the ground-truth data, then the output of
320 this function is ``0.0``, otherwise, if areas are exclusive for example, then
321 this function outputs greater than zero. The output of this function should
322 be analyzed against the output of :py:func:`intersect_ratio`, which provides
323 the complementary information about the intersection of the areas being
324 analyzed.
326 Parameters:
328 a (numpy.ndarray): A 2D numpy array with dtype :py:obj:`bool`, that
329 corresponds to the **ground-truth object**
331 b (numpy.ndarray): A 2D numpy array with dtype :py:obj:`bool`, that
332 corresponds to the probe object that will be compared to the ground-truth
335 Returns:
337 float: The floating point number that corresponds to the overlap ratio
338 between the probe area and the *complement* of the ground-truth area.
339 There are no bounds for the float value on the right side:
340 :math:`[0, +\\infty)`.
342 """
344 return ((~a) & b).sum().astype(float) / a.sum().astype(float)