diff --git a/bob/bio/vein/preprocessor/utils.py b/bob/bio/vein/preprocessor/utils.py index 20354ae219633bd61cb7349daab134a8cad3ec61..bc56d9e84ba3524e66b5d634822905424a8b3d17 100644 --- a/bob/bio/vein/preprocessor/utils.py +++ b/bob/bio/vein/preprocessor/utils.py @@ -199,3 +199,110 @@ def show_mask_over_image(image, mask, color='red'): red = Image.new('RGBA', img.size, color=color) img.paste(red, mask=msk) img.show() + + +def jaccard_index(a, b): + """Calculates the intersection over union for two masks + + This function calculates the Jaccard index: + + .. math:: + + J(A,B) = \frac{|A \cap B|}{|A \cup B|} = + \frac{|A \cap B|}{|A|+|B|-|A \cup B|} + + + Parameters: + + a (numpy.ndarray): A 2D numpy array with dtype :py:obj:bool + + b (numpy.ndarray): A 2D numpy array with dtype :py:obj:bool + + + Returns: + + float: The floating point number that corresponds to the Jaccard index. The + float value lies inside the interval :math:[0, 1]. If a and b are + equal, then the similarity is maximum and the value output is 1.0. If + the areas are exclusive, then the value output by this function is 0.0. + + """ + + return (a & b).sum().astype(float) / (a | b).sum().astype(float) + + +def intersect_ratio(a, b): + """Calculates the intersection ratio between a probe and ground-truth + + This function calculates the intersection ratio between a probe mask + (:math:B) and a ground-truth mask (:math:A; probably generated from an + annotation), and returns the ratio of overlap when the probe is compared to + the ground-truth data: + + .. math:: + + R(A,B) = \frac{|A \cap B|}{|A|} + + So, if the probe occupies the entirety of the ground-truth data, then the + output of this function is 1.0, otherwise, if areas are exclusive, then + this function returns 0.0. The output of this function should be analyzed + against the output of :py:func:intersect_ratio_of_complement, which + provides the complementary information about the intersection of the areas + being analyzed. + + + Parameters: + + a (numpy.ndarray): A 2D numpy array with dtype :py:obj:bool + + b (numpy.ndarray): A 2D numpy array with dtype :py:obj:bool + + + Returns: + + float: The floating point number that corresponds to the overlap ratio. The + float value lies inside the interval :math:[0, 1]. + + """ + + return (a & b).sum().astype(float) / a.sum().astype(float) + + +def intersect_ratio_of_complement(a, b): + """Calculates the intersection ratio between a probe and the ground-truth + complement + + This function calculates the intersection ratio between a probe mask + (:math:B) and *the complement* of a ground-truth mask (:math:A; probably + generated from an annotation), and returns the ratio of overlap when the + probe is compared to the ground-truth data: + + .. math:: + + R(A,B) = \frac{|A^c \cap B|}{|A|} = B \\ A + + + So, if the probe is totally inside the ground-truth data, then the output of + this function is 0.0, otherwise, if areas are exclusive for example, then + this function outputs greater than zero. The output of this function should + be analyzed against the output of :py:func:intersect_ratio, which provides + the complementary information about the intersection of the areas being + analyzed. + + Parameters: + + a (numpy.ndarray): A 2D numpy array with dtype :py:obj:bool + + b (numpy.ndarray): A 2D numpy array with dtype :py:obj:bool + + + Returns: + + float: The floating point number that corresponds to the overlap ratio + between the probe area and the *complement* of the ground-truth area. + There are no bounds for the float value on the right side: + :math:[0, +\inf]`. + + """ + + return ((~a) & b).sum().astype(float) / a.sum().astype(float) diff --git a/bob/bio/vein/tests/test.py b/bob/bio/vein/tests/test.py index 113da8ddd33232d6f9f82833b6ad165512ad0299..da4715cbc46b8d053d0298bc3bca0ea3ec3a6944 100644 --- a/bob/bio/vein/tests/test.py +++ b/bob/bio/vein/tests/test.py @@ -277,3 +277,57 @@ def test_mask_to_image(): assert 'int16' in str(e) else: raise AssertionError('Conversion to int16 did not trigger a TypeError') + + +def test_jaccard_index(): + + # Tests to verify the Jaccard index calculation is accurate + a = numpy.array([ + [False, False], + [True, True], + ]) + + b = numpy.array([ + [True, True], + [True, False], + ]) + + nose.tools.eq_(utils.jaccard_index(a, b), 1.0/4.0) + nose.tools.eq_(utils.jaccard_index(a, a), 1.0) + nose.tools.eq_(utils.jaccard_index(b, b), 1.0) + nose.tools.eq_(utils.jaccard_index(a, numpy.ones(a.shape, dtype=bool)), + 2.0/4.0) + nose.tools.eq_(utils.jaccard_index(a, numpy.zeros(a.shape, dtype=bool)), 0.0) + nose.tools.eq_(utils.jaccard_index(b, numpy.ones(b.shape, dtype=bool)), + 3.0/4.0) + nose.tools.eq_(utils.jaccard_index(b, numpy.zeros(b.shape, dtype=bool)), 0.0) + + +def test_intersection_ratio(): + + # Tests to verify the intersection ratio calculation is accurate + a = numpy.array([ + [False, False], + [True, True], + ]) + + b = numpy.array([ + [True, False], + [True, False], + ]) + + nose.tools.eq_(utils.intersect_ratio(a, b), 1.0/2.0) + nose.tools.eq_(utils.intersect_ratio(a, a), 1.0) + nose.tools.eq_(utils.intersect_ratio(b, b), 1.0) + nose.tools.eq_(utils.intersect_ratio(a, numpy.ones(a.shape, dtype=bool)), 1.0) + nose.tools.eq_(utils.intersect_ratio(a, numpy.zeros(a.shape, dtype=bool)), 0) + nose.tools.eq_(utils.intersect_ratio(b, numpy.ones(b.shape, dtype=bool)), 1.0) + nose.tools.eq_(utils.intersect_ratio(b, numpy.zeros(b.shape, dtype=bool)), 0) + + nose.tools.eq_(utils.intersect_ratio_of_complement(a, b), 1.0/2.0) + nose.tools.eq_(utils.intersect_ratio_of_complement(a, a), 0.0) + nose.tools.eq_(utils.intersect_ratio_of_complement(b, b), 0.0) + nose.tools.eq_(utils.intersect_ratio_of_complement(a, numpy.ones(a.shape, dtype=bool)), 1.0) + nose.tools.eq_(utils.intersect_ratio_of_complement(a, numpy.zeros(a.shape, dtype=bool)), 0) + nose.tools.eq_(utils.intersect_ratio_of_complement(b, numpy.ones(b.shape, dtype=bool)), 1.0) + nose.tools.eq_(utils.intersect_ratio_of_complement(b, numpy.zeros(b.shape, dtype=bool)), 0)