_expand_labels.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import numpy as np
  2. from scipy.ndimage import distance_transform_edt
  3. def expand_labels(label_image, distance=1, spacing=1):
  4. """Expand labels in label image by ``distance`` pixels without overlapping.
  5. Given a label image, ``expand_labels`` grows label regions (connected components)
  6. outwards by up to ``distance`` units without overflowing into neighboring regions.
  7. More specifically, each background pixel that is within Euclidean distance
  8. of <= ``distance`` pixels of a connected component is assigned the label of that
  9. connected component. The `spacing` parameter can be used to specify the spacing
  10. rate of the distance transform used to calculate the Euclidean distance for anisotropic
  11. images.
  12. Where multiple connected components are within ``distance`` pixels of a background
  13. pixel, the label value of the closest connected component will be assigned (see
  14. Notes for the case of multiple labels at equal distance).
  15. Parameters
  16. ----------
  17. label_image : ndarray of dtype int
  18. label image
  19. distance : float
  20. Euclidean distance in pixels by which to grow the labels. Default is one.
  21. spacing : float, or sequence of float, optional
  22. Spacing of elements along each dimension. If a sequence, must be of length
  23. equal to the input rank; if a single number, this is used for all axes. If
  24. not specified, a grid spacing of unity is implied.
  25. Returns
  26. -------
  27. enlarged_labels : ndarray of dtype int
  28. Labeled array, where all connected regions have been enlarged
  29. Notes
  30. -----
  31. Where labels are spaced more than ``distance`` pixels are apart, this is
  32. equivalent to a morphological dilation with a disc or hyperball of radius ``distance``.
  33. However, in contrast to a morphological dilation, ``expand_labels`` will
  34. not expand a label region into a neighboring region.
  35. This implementation of ``expand_labels`` is derived from CellProfiler [1]_, where
  36. it is known as module "IdentifySecondaryObjects (Distance-N)" [2]_.
  37. There is an important edge case when a pixel has the same distance to
  38. multiple regions, as it is not defined which region expands into that
  39. space. Here, the exact behavior depends on the upstream implementation
  40. of ``scipy.ndimage.distance_transform_edt``.
  41. See Also
  42. --------
  43. :func:`skimage.measure.label`, :func:`skimage.segmentation.watershed`, :func:`skimage.morphology.dilation`
  44. References
  45. ----------
  46. .. [1] https://cellprofiler.org
  47. .. [2] https://github.com/CellProfiler/CellProfiler/blob/082930ea95add7b72243a4fa3d39ae5145995e9c/cellprofiler/modules/identifysecondaryobjects.py#L559
  48. Examples
  49. --------
  50. >>> labels = np.array([0, 1, 0, 0, 0, 0, 2])
  51. >>> expand_labels(labels, distance=1)
  52. array([1, 1, 1, 0, 0, 2, 2])
  53. Labels will not overwrite each other:
  54. >>> expand_labels(labels, distance=3)
  55. array([1, 1, 1, 1, 2, 2, 2])
  56. In case of ties, behavior is undefined, but currently resolves to the
  57. label closest to ``(0,) * ndim`` in lexicographical order.
  58. >>> labels_tied = np.array([0, 1, 0, 2, 0])
  59. >>> expand_labels(labels_tied, 1)
  60. array([1, 1, 1, 2, 2])
  61. >>> labels2d = np.array(
  62. ... [[0, 1, 0, 0],
  63. ... [2, 0, 0, 0],
  64. ... [0, 3, 0, 0]]
  65. ... )
  66. >>> expand_labels(labels2d, 1)
  67. array([[2, 1, 1, 0],
  68. [2, 2, 0, 0],
  69. [2, 3, 3, 0]])
  70. >>> expand_labels(labels2d, 1, spacing=[1, 0.5])
  71. array([[1, 1, 1, 1],
  72. [2, 2, 2, 0],
  73. [3, 3, 3, 3]])
  74. """
  75. distances, nearest_label_coords = distance_transform_edt(
  76. label_image == 0, sampling=spacing, return_indices=True
  77. )
  78. labels_out = np.zeros_like(label_image)
  79. dilate_mask = distances <= distance
  80. # build the coordinates to find nearest labels,
  81. # in contrast to [1] this implementation supports label arrays
  82. # of any dimension
  83. masked_nearest_label_coords = [
  84. dimension_indices[dilate_mask] for dimension_indices in nearest_label_coords
  85. ]
  86. nearest_labels = label_image[tuple(masked_nearest_label_coords)]
  87. labels_out[dilate_mask] = nearest_labels
  88. return labels_out