grayreconstruct.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import numpy as np
  2. from .._shared.utils import _supported_float_type
  3. from ..filters._rank_order import rank_order
  4. from ._grayreconstruct import reconstruction_loop
  5. def reconstruction(seed, mask, method='dilation', footprint=None, offset=None):
  6. """Perform a morphological reconstruction of an image.
  7. Morphological reconstruction by dilation is similar to basic morphological
  8. dilation: high-intensity values will replace nearby low-intensity values.
  9. The basic dilation operator, however, uses a footprint to
  10. determine how far a value in the input image can spread. In contrast,
  11. reconstruction uses two images: a "seed" image, which specifies the values
  12. that spread, and a "mask" image, which gives the maximum allowed value at
  13. each pixel. The mask image, like the footprint, limits the spread
  14. of high-intensity values. Reconstruction by erosion is simply the inverse:
  15. low-intensity values spread from the seed image and are limited by the mask
  16. image, which represents the minimum allowed value.
  17. Alternatively, you can think of reconstruction as a way to isolate the
  18. connected regions of an image. For dilation, reconstruction connects
  19. regions marked by local maxima in the seed image: neighboring pixels
  20. less-than-or-equal-to those seeds are connected to the seeded region.
  21. Local maxima with values larger than the seed image will get truncated to
  22. the seed value.
  23. Parameters
  24. ----------
  25. seed : ndarray
  26. The seed image (a.k.a. marker image), which specifies the values that
  27. are dilated or eroded.
  28. mask : ndarray
  29. The maximum (dilation) / minimum (erosion) allowed value at each pixel.
  30. method : {'dilation'|'erosion'}, optional
  31. Perform reconstruction by dilation or erosion. In dilation (or
  32. erosion), the seed image is dilated (or eroded) until limited by the
  33. mask image. For dilation, each seed value must be less than or equal
  34. to the corresponding mask value; for erosion, the reverse is true.
  35. Default is 'dilation'.
  36. footprint : ndarray, optional
  37. The neighborhood expressed as an n-D array of 1's and 0's.
  38. Default is the n-D square of radius equal to 1 (i.e. a 3x3 square
  39. for 2D images, a 3x3x3 cube for 3D images, etc.)
  40. offset : ndarray, optional
  41. The coordinates of the center of the footprint.
  42. Default is located on the geometrical center of the footprint, in that
  43. case footprint dimensions must be odd.
  44. Returns
  45. -------
  46. reconstructed : ndarray
  47. The result of morphological reconstruction.
  48. Examples
  49. --------
  50. >>> import numpy as np
  51. >>> from skimage.morphology import reconstruction
  52. First, we create a sinusoidal mask image with peaks at middle and ends.
  53. >>> x = np.linspace(0, 4 * np.pi)
  54. >>> y_mask = np.cos(x)
  55. Then, we create a seed image initialized to the minimum mask value (for
  56. reconstruction by dilation, min-intensity values don't spread) and add
  57. "seeds" to the left and right peak, but at a fraction of peak value (1).
  58. >>> y_seed = y_mask.min() * np.ones_like(x)
  59. >>> y_seed[0] = 0.5
  60. >>> y_seed[-1] = 0
  61. >>> y_rec = reconstruction(y_seed, y_mask)
  62. The reconstructed image (or curve, in this case) is exactly the same as the
  63. mask image, except that the peaks are truncated to 0.5 and 0. The middle
  64. peak disappears completely: Since there were no seed values in this peak
  65. region, its reconstructed value is truncated to the surrounding value (-1).
  66. As a more practical example, we try to extract the bright features of an
  67. image by subtracting a background image created by reconstruction.
  68. >>> y, x = np.mgrid[:20:0.5, :20:0.5]
  69. >>> bumps = np.sin(x) + np.sin(y)
  70. To create the background image, set the mask image to the original image,
  71. and the seed image to the original image with an intensity offset, `h`.
  72. >>> h = 0.3
  73. >>> seed = bumps - h
  74. >>> background = reconstruction(seed, bumps)
  75. The resulting reconstructed image looks exactly like the original image,
  76. but with the peaks of the bumps cut off. Subtracting this reconstructed
  77. image from the original image leaves just the peaks of the bumps
  78. >>> hdome = bumps - background
  79. This operation is known as the h-dome of the image and leaves features
  80. of height `h` in the subtracted image.
  81. Notes
  82. -----
  83. The algorithm is taken from [1]_. Applications for grayscale reconstruction
  84. are discussed in [2]_ and [3]_.
  85. References
  86. ----------
  87. .. [1] Robinson, "Efficient morphological reconstruction: a downhill
  88. filter", Pattern Recognition Letters 25 (2004) 1759-1767.
  89. .. [2] Vincent, L., "Morphological Grayscale Reconstruction in Image
  90. Analysis: Applications and Efficient Algorithms", IEEE Transactions
  91. on Image Processing (1993)
  92. .. [3] Soille, P., "Morphological Image Analysis: Principles and
  93. Applications", Chapter 6, 2nd edition (2003), ISBN 3540429883.
  94. """
  95. assert tuple(seed.shape) == tuple(mask.shape)
  96. if method == 'dilation' and np.any(seed > mask):
  97. raise ValueError(
  98. "Intensity of seed image must be less than that "
  99. "of the mask image for reconstruction by dilation."
  100. )
  101. elif method == 'erosion' and np.any(seed < mask):
  102. raise ValueError(
  103. "Intensity of seed image must be greater than that "
  104. "of the mask image for reconstruction by erosion."
  105. )
  106. if footprint is None:
  107. footprint = np.ones([3] * seed.ndim, dtype=bool)
  108. else:
  109. footprint = footprint.astype(bool, copy=True)
  110. if offset is None:
  111. if not all([d % 2 == 1 for d in footprint.shape]):
  112. raise ValueError("Footprint dimensions must all be odd")
  113. offset = np.array([d // 2 for d in footprint.shape])
  114. else:
  115. if offset.ndim != footprint.ndim:
  116. raise ValueError("Offset and footprint ndims must be equal.")
  117. if not all([(0 <= o < d) for o, d in zip(offset, footprint.shape)]):
  118. raise ValueError("Offset must be included inside footprint")
  119. # Cross out the center of the footprint
  120. footprint[tuple(slice(d, d + 1) for d in offset)] = False
  121. # Make padding for edges of reconstructed image so we can ignore boundaries
  122. dims = np.zeros(seed.ndim + 1, dtype=int)
  123. dims[1:] = np.array(seed.shape) + (np.array(footprint.shape) - 1)
  124. dims[0] = 2
  125. inside_slices = tuple(slice(o, o + s) for o, s in zip(offset, seed.shape))
  126. # Set padded region to minimum image intensity and mask along first axis so
  127. # we can interleave image and mask pixels when sorting.
  128. if method == 'dilation':
  129. pad_value = np.min(seed)
  130. elif method == 'erosion':
  131. pad_value = np.max(seed)
  132. else:
  133. raise ValueError(
  134. "Reconstruction method can be one of 'erosion' "
  135. f"or 'dilation'. Got '{method}'."
  136. )
  137. float_dtype = _supported_float_type(mask.dtype)
  138. images = np.full(dims, pad_value, dtype=float_dtype)
  139. images[(0, *inside_slices)] = seed
  140. images[(1, *inside_slices)] = mask
  141. # determine whether image is large enough to require 64-bit integers
  142. isize = images.size
  143. # use -isize so we get a signed dtype rather than an unsigned one
  144. signed_int_dtype = np.result_type(np.min_scalar_type(-isize), np.int32)
  145. # the corresponding unsigned type has same char, but uppercase
  146. unsigned_int_dtype = np.dtype(signed_int_dtype.char.upper())
  147. # Create a list of strides across the array to get the neighbors within
  148. # a flattened array
  149. value_stride = np.array(images.strides[1:]) // images.dtype.itemsize
  150. image_stride = images.strides[0] // images.dtype.itemsize
  151. footprint_mgrid = np.mgrid[
  152. [slice(-o, d - o) for d, o in zip(footprint.shape, offset)]
  153. ]
  154. footprint_offsets = footprint_mgrid[:, footprint].transpose()
  155. nb_strides = np.array(
  156. [
  157. np.sum(value_stride * footprint_offset)
  158. for footprint_offset in footprint_offsets
  159. ],
  160. signed_int_dtype,
  161. )
  162. images = images.reshape(-1)
  163. # Erosion goes smallest to largest; dilation goes largest to smallest.
  164. index_sorted = np.argsort(images).astype(signed_int_dtype, copy=False)
  165. if method == 'dilation':
  166. index_sorted = index_sorted[::-1]
  167. # Make a linked list of pixels sorted by value. -1 is the list terminator.
  168. prev = np.full(isize, -1, signed_int_dtype)
  169. next = np.full(isize, -1, signed_int_dtype)
  170. prev[index_sorted[1:]] = index_sorted[:-1]
  171. next[index_sorted[:-1]] = index_sorted[1:]
  172. # Cython inner-loop compares the rank of pixel values.
  173. if method == 'dilation':
  174. value_rank, value_map = rank_order(images)
  175. elif method == 'erosion':
  176. value_rank, value_map = rank_order(-images)
  177. value_map = -value_map
  178. start = index_sorted[0]
  179. value_rank = value_rank.astype(unsigned_int_dtype, copy=False)
  180. reconstruction_loop(value_rank, prev, next, nb_strides, start, image_stride)
  181. # Reshape reconstructed image to original image shape and remove padding.
  182. rec_img = value_map[value_rank[:image_stride]]
  183. rec_img.shape = np.array(seed.shape) + (np.array(footprint.shape) - 1)
  184. return rec_img[inside_slices]