Commit 8d2d3306 authored by André Anjos's avatar André Anjos 💬

Implements distance metrics for ROI annotations

parent a45a0d7c
Pipeline #5199 failed with stages
in 2 minutes and 27 seconds
......@@ -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)
......@@ -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)
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