utils.py 9.17 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
#!/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
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201


def show_image(image):
  """Shows a single image

  Parameters:

    image (numpy.ndarray): A 2D numpy.ndarray compose of 8-bit unsigned
      integers containing the original image

  """

  from PIL import Image
  img = Image.fromarray(image)
  img.show()


def show_mask_over_image(image, mask, color='red'):
  """Plots the mask over the image of a finger, for debugging purposes

  Parameters:

    image (numpy.ndarray): A 2D numpy.ndarray compose of 8-bit unsigned
      integers containing the original image

    mask (numpy.ndarray): A 2D numpy.ndarray compose of boolean values
      containing the calculated mask

  """

  from PIL import Image

  img = Image.fromarray(image).convert(mode='RGBA')
  msk = Image.fromarray((~mask).astype('uint8')*80)
  red = Image.new('RGBA', img.size, color=color)
  img.paste(red, mask=msk)
  img.show()
202 203 204 205 206 207 208 209 210


def jaccard_index(a, b):
  """Calculates the intersection over union for two masks

  This function calculates the Jaccard index:

  .. math::

211 212
     J(A,B) &= \\frac{|A \cap B|}{|A \\cup B|} \\\\
            &= \\frac{|A \cap B|}{|A|+|B|-|A \\cup B|}
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243


  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::

244
     R(A,B) = \\frac{|A \\cap B|}{|A|}
245 246 247

  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
248
  this function returns ``0.0``. The output of this function should be analyzed
249 250 251 252 253 254 255
  against the output of :py:func:`intersect_ratio_of_complement`, which
  provides the complementary information about the intersection of the areas
  being analyzed.


  Parameters:

256 257
    a (numpy.ndarray): A 2D numpy array with dtype :py:obj:`bool`, that
      corresponds to the **ground-truth object**
258

259 260
    b (numpy.ndarray): A 2D numpy array with dtype :py:obj:`bool`, that
      corresponds to the probe object that will be compared to the ground-truth
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283


  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::

284
     R(A,B) = \\frac{|A^c \\cap B|}{|A|} = B \\setminus A
285 286 287 288 289 290 291 292 293 294 295


  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:

296 297
    a (numpy.ndarray): A 2D numpy array with dtype :py:obj:`bool`, that
      corresponds to the **ground-truth object**
298

299 300
    b (numpy.ndarray): A 2D numpy array with dtype :py:obj:`bool`, that
      corresponds to the probe object that will be compared to the ground-truth
301 302 303 304 305 306 307


  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:
308
    :math:`[0, +\\infty)`.
309 310 311 312

  """

  return ((~a) & b).sum().astype(float) / a.sum().astype(float)