107 lines
3.9 KiB
Python
107 lines
3.9 KiB
Python
"""
|
|
expand_labels is derived from code that was
|
|
originally part of CellProfiler, code licensed under BSD license.
|
|
Website: http://www.cellprofiler.org
|
|
|
|
Copyright (c) 2020 Broad Institute
|
|
All rights reserved.
|
|
|
|
Original authors: CellProfiler team
|
|
|
|
"""
|
|
import numpy as np
|
|
from scipy.ndimage import distance_transform_edt
|
|
|
|
|
|
def expand_labels(label_image, distance=1):
|
|
"""Expand labels in label image by ``distance`` pixels without overlapping.
|
|
|
|
Given a label image, ``expand_labels`` grows label regions (connected components)
|
|
outwards by up to ``distance`` pixels without overflowing into neighboring regions.
|
|
More specifically, each background pixel that is within Euclidean distance
|
|
of <= ``distance`` pixels of a connected component is assigned the label of that
|
|
connected component.
|
|
Where multiple connected components are within ``distance`` pixels of a background
|
|
pixel, the label value of the closest connected component will be assigned (see
|
|
Notes for the case of multiple labels at equal distance).
|
|
|
|
Parameters
|
|
----------
|
|
label_image : ndarray of dtype int
|
|
label image
|
|
distance : float
|
|
Euclidean distance in pixels by which to grow the labels. Default is one.
|
|
|
|
Returns
|
|
-------
|
|
enlarged_labels : ndarray of dtype int
|
|
Labeled array, where all connected regions have been enlarged
|
|
|
|
Notes
|
|
-----
|
|
Where labels are spaced more than ``distance`` pixels are apart, this is
|
|
equivalent to a morphological dilation with a disc or hyperball of radius ``distance``.
|
|
However, in contrast to a morphological dilation, ``expand_labels`` will
|
|
not expand a label region into a neighboring region.
|
|
|
|
This implementation of ``expand_labels`` is derived from CellProfiler [1]_, where
|
|
it is known as module "IdentifySecondaryObjects (Distance-N)" [2]_.
|
|
|
|
There is an important edge case when a pixel has the same distance to
|
|
multiple regions, as it is not defined which region expands into that
|
|
space. Here, the exact behavior depends on the upstream implementation
|
|
of ``scipy.ndimage.distance_transform_edt``.
|
|
|
|
See Also
|
|
--------
|
|
:func:`skimage.measure.label`, :func:`skimage.segmentation.watershed`, :func:`skimage.morphology.dilation`
|
|
|
|
References
|
|
----------
|
|
.. [1] https://cellprofiler.org
|
|
.. [2] https://github.com/CellProfiler/CellProfiler/blob/082930ea95add7b72243a4fa3d39ae5145995e9c/cellprofiler/modules/identifysecondaryobjects.py#L559
|
|
|
|
Examples
|
|
--------
|
|
>>> labels = np.array([0, 1, 0, 0, 0, 0, 2])
|
|
>>> expand_labels(labels, distance=1)
|
|
array([1, 1, 1, 0, 0, 2, 2])
|
|
|
|
Labels will not overwrite each other:
|
|
|
|
>>> expand_labels(labels, distance=3)
|
|
array([1, 1, 1, 1, 2, 2, 2])
|
|
|
|
In case of ties, behavior is undefined, but currently resolves to the
|
|
label closest to ``(0,) * ndim`` in lexicographical order.
|
|
|
|
>>> labels_tied = np.array([0, 1, 0, 2, 0])
|
|
>>> expand_labels(labels_tied, 1)
|
|
array([1, 1, 1, 2, 2])
|
|
>>> labels2d = np.array(
|
|
... [[0, 1, 0, 0],
|
|
... [2, 0, 0, 0],
|
|
... [0, 3, 0, 0]]
|
|
... )
|
|
>>> expand_labels(labels2d, 1)
|
|
array([[2, 1, 1, 0],
|
|
[2, 2, 0, 0],
|
|
[2, 3, 3, 0]])
|
|
"""
|
|
|
|
distances, nearest_label_coords = distance_transform_edt(
|
|
label_image == 0, return_indices=True
|
|
)
|
|
labels_out = np.zeros_like(label_image)
|
|
dilate_mask = distances <= distance
|
|
# build the coordinates to find nearest labels,
|
|
# in contrast to [1] this implementation supports label arrays
|
|
# of any dimension
|
|
masked_nearest_label_coords = [
|
|
dimension_indices[dilate_mask]
|
|
for dimension_indices in nearest_label_coords
|
|
]
|
|
nearest_labels = label_image[tuple(masked_nearest_label_coords)]
|
|
labels_out[dilate_mask] = nearest_labels
|
|
return labels_out
|