"""Computation of event contour from event mask"""
from collections import deque
import numbers
import numpy as np
# equivalent to
# from skimage.measure import find_contours
from ..external.skimage.measure import find_contours
class NoValidContourFoundError(BaseException):
pass
class LazyContourList(object):
def __init__(self, masks, max_events=1000):
"""A list-like object that computes contours upon indexing
Parameters
----------
masks: array-like
3D array of masks, may be an HDF5 dataset or any other
structure that supports indexing
max_events: int
maximum number of contours to keep in the contour list;
set to 0/False/None to cache all contours
.. versionchanged:: 0.58.3
Added the `max_events` parameter which now makes this class
a lazy, least-recently-used contour list. To achieve the
old behavior (which may fill up your memory), set
`max_events=None`.
"""
self.masks = masks
self.contours = deque(maxlen=max_events or None)
self.indices = deque(maxlen=max_events or None)
#: used for hashing in ancillary features
self.identifier = str(masks[0][:].tobytes())
self.shape = len(masks), np.nan, 2
def __getitem__(self, idx):
"""Compute contour(s) if not already in self.contours"""
if not isinstance(idx, numbers.Integral):
# slicing!
indices = np.arange(len(self))[idx]
output = []
# populate the output list
for evid in indices:
output.append(self.__getitem__(evid))
return output
else:
try:
# Is the contour already available?
idx_q = self.indices.index(idx)
except ValueError:
# The contour is not there. Compute it.
try:
cont = get_contour(self.masks[idx])
except BaseException as e:
e.args = (f"Event {idx}, {e.args[0]}",)
raise
else:
# Get the contour from deque
cont = self.contours[idx_q]
# If we got here, it means that we have computed a contour
# successfully. Store it in the deque.
self.contours.append(cont)
self.indices.append(idx)
return cont
def __len__(self):
return len(self.masks)
[docs]
def get_contour(mask):
"""Compute the image contour from a mask
The contour is computed in a very inefficient way using scikit-image
and a conversion of float coordinates to pixel coordinates.
Parameters
----------
mask: binary ndarray of shape (M,N) or (K,M,N)
The mask outlining the pixel positions of the event.
If a 3d array is given, then `K` indexes the individual
contours.
Returns
-------
cont: ndarray or list of K ndarrays of shape (J,2)
A 2D array that holds the contour of an event (in pixels)
e.g. obtained using `mm.contour` where `mm` is an instance
of `RTDCBase`. The first and second columns of `cont`
correspond to the x- and y-coordinates of the contour.
"""
if isinstance(mask, np.ndarray) and len(mask.shape) == 2:
mask = [mask]
ret_list = False
else:
ret_list = True
contours = []
for mi in mask:
# This is only 10% slower than doing:
# conts, _ = cv2.findContours(np.array(mi, dtype=np.uint8),
# cv2.RETR_EXTERNAL,
# cv2.CHAIN_APPROX_NONE)
# c2 = conts[0].reshape(-1, 2)
conts = find_contours(mi.transpose(),
level=.9999,
positive_orientation="low",
fully_connected="high")
# get the longest contour
c0 = sorted(conts, key=lambda x: len(x))[-1]
# round all coordinates to pixel values
c1 = np.asarray(np.round(c0), int)
# remove duplicates
c2 = remove_duplicates(c1)
if len(c2) == 0:
raise NoValidContourFoundError("No contour found!")
contours.append(c2)
if ret_list:
return contours
else:
return contours[0]
def get_contour_lazily(mask):
"""Like :func:`get_contour`, but computes contours on demand
Parameters
----------
mask: binary ndarray of shape (M,N) or (K,M,N)
The mask outlining the pixel positions of the event.
If a 3d array is given, then `K` indexes the individual
contours.
Returns
-------
cont: ndarray or LazyContourList of K ndarrays of shape (J,2)
A 2D array that holds the contour of an event (in pixels)
e.g. obtained using `mm.contour` where `mm` is an instance
of `RTDCBase`. The first and second columns of `cont`
correspond to the x- and y-coordinates of the contour.
"""
if isinstance(mask, np.ndarray) and len(mask.shape) == 2:
# same behavior as `get_contour`
cont = get_contour(mask=mask)
else:
cont = LazyContourList(masks=mask)
return cont
def remove_duplicates(cont):
"""Remove duplicates in a circular contour"""
x = np.resize(cont, (len(cont) + 1, 2))
selection = np.ones(len(x), dtype=bool)
selection[1:] = ~np.prod((x[1:] == x[:-1]), axis=1, dtype=bool)
return x[selection][:-1]