_flood_fill.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. """flood_fill.py - in place flood fill algorithm
  2. This module provides a function to fill all equal (or within tolerance) values
  3. connected to a given seed point with a different value.
  4. """
  5. import numpy as np
  6. from ..util import crop
  7. from ._flood_fill_cy import _flood_fill_equal, _flood_fill_tolerance
  8. from ._util import (
  9. _offsets_to_raveled_neighbors,
  10. _resolve_neighborhood,
  11. _set_border_values,
  12. )
  13. from .._shared.dtype import numeric_dtype_min_max
  14. def flood_fill(
  15. image,
  16. seed_point,
  17. new_value,
  18. *,
  19. footprint=None,
  20. connectivity=None,
  21. tolerance=None,
  22. in_place=False,
  23. ):
  24. """Perform flood filling on an image.
  25. Starting at a specific `seed_point`, connected points equal or within
  26. `tolerance` of the seed value are found, then set to `new_value`.
  27. Parameters
  28. ----------
  29. image : ndarray
  30. An n-dimensional array.
  31. seed_point : tuple or int
  32. The point in `image` used as the starting point for the flood fill. If
  33. the image is 1D, this point may be given as an integer.
  34. new_value : `image` type
  35. New value to set the entire fill. This must be chosen in agreement
  36. with the dtype of `image`.
  37. footprint : ndarray, optional
  38. The footprint (structuring element) used to determine the neighborhood
  39. of each evaluated pixel. It must contain only 1's and 0's, have the
  40. same number of dimensions as `image`. If not given, all adjacent pixels
  41. are considered as part of the neighborhood (fully connected).
  42. connectivity : int, optional
  43. A number used to determine the neighborhood of each evaluated pixel.
  44. Adjacent pixels whose squared distance from the center is less than or
  45. equal to `connectivity` are considered neighbors. Ignored if
  46. `footprint` is not None.
  47. tolerance : float or int, optional
  48. If None (default), adjacent values must be strictly equal to the
  49. value of `image` at `seed_point` to be filled. This is fastest.
  50. If a tolerance is provided, adjacent points with values within plus or
  51. minus tolerance from the seed point are filled (inclusive).
  52. in_place : bool, optional
  53. If True, flood filling is applied to `image` in place. If False, the
  54. flood filled result is returned without modifying the input `image`
  55. (default).
  56. Returns
  57. -------
  58. filled : ndarray
  59. An array with the same shape as `image` is returned, with values in
  60. areas connected to and equal (or within tolerance of) the seed point
  61. replaced with `new_value`.
  62. Notes
  63. -----
  64. The conceptual analogy of this operation is the 'paint bucket' tool in many
  65. raster graphics programs.
  66. Examples
  67. --------
  68. >>> from skimage.morphology import flood_fill
  69. >>> image = np.zeros((4, 7), dtype=int)
  70. >>> image[1:3, 1:3] = 1
  71. >>> image[3, 0] = 1
  72. >>> image[1:3, 4:6] = 2
  73. >>> image[3, 6] = 3
  74. >>> image
  75. array([[0, 0, 0, 0, 0, 0, 0],
  76. [0, 1, 1, 0, 2, 2, 0],
  77. [0, 1, 1, 0, 2, 2, 0],
  78. [1, 0, 0, 0, 0, 0, 3]])
  79. Fill connected ones with 5, with full connectivity (diagonals included):
  80. >>> flood_fill(image, (1, 1), 5)
  81. array([[0, 0, 0, 0, 0, 0, 0],
  82. [0, 5, 5, 0, 2, 2, 0],
  83. [0, 5, 5, 0, 2, 2, 0],
  84. [5, 0, 0, 0, 0, 0, 3]])
  85. Fill connected ones with 5, excluding diagonal points (connectivity 1):
  86. >>> flood_fill(image, (1, 1), 5, connectivity=1)
  87. array([[0, 0, 0, 0, 0, 0, 0],
  88. [0, 5, 5, 0, 2, 2, 0],
  89. [0, 5, 5, 0, 2, 2, 0],
  90. [1, 0, 0, 0, 0, 0, 3]])
  91. Fill with a tolerance:
  92. >>> flood_fill(image, (0, 0), 5, tolerance=1)
  93. array([[5, 5, 5, 5, 5, 5, 5],
  94. [5, 5, 5, 5, 2, 2, 5],
  95. [5, 5, 5, 5, 2, 2, 5],
  96. [5, 5, 5, 5, 5, 5, 3]])
  97. """
  98. mask = flood(
  99. image,
  100. seed_point,
  101. footprint=footprint,
  102. connectivity=connectivity,
  103. tolerance=tolerance,
  104. )
  105. if not in_place:
  106. image = image.copy()
  107. image[mask] = new_value
  108. return image
  109. def flood(image, seed_point, *, footprint=None, connectivity=None, tolerance=None):
  110. """Mask corresponding to a flood fill.
  111. Starting at a specific `seed_point`, connected points equal or within
  112. `tolerance` of the seed value are found.
  113. Parameters
  114. ----------
  115. image : ndarray
  116. An n-dimensional array.
  117. seed_point : tuple or int
  118. The point in `image` used as the starting point for the flood fill. If
  119. the image is 1D, this point may be given as an integer.
  120. footprint : ndarray, optional
  121. The footprint (structuring element) used to determine the neighborhood
  122. of each evaluated pixel. It must contain only 1's and 0's, have the
  123. same number of dimensions as `image`. If not given, all adjacent pixels
  124. are considered as part of the neighborhood (fully connected).
  125. connectivity : int, optional
  126. A number used to determine the neighborhood of each evaluated pixel.
  127. Adjacent pixels whose squared distance from the center is less than or
  128. equal to `connectivity` are considered neighbors. Ignored if
  129. `footprint` is not None.
  130. tolerance : float or int, optional
  131. If None (default), adjacent values must be strictly equal to the
  132. initial value of `image` at `seed_point`. This is fastest. If a value
  133. is given, a comparison will be done at every point and if within
  134. tolerance of the initial value will also be filled (inclusive).
  135. Returns
  136. -------
  137. mask : ndarray
  138. A Boolean array with the same shape as `image` is returned, with True
  139. values for areas connected to and equal (or within tolerance of) the
  140. seed point. All other values are False.
  141. Notes
  142. -----
  143. The conceptual analogy of this operation is the 'paint bucket' tool in many
  144. raster graphics programs. This function returns just the mask
  145. representing the fill.
  146. If indices are desired rather than masks for memory reasons, the user can
  147. simply run `numpy.nonzero` on the result, save the indices, and discard
  148. this mask.
  149. Examples
  150. --------
  151. >>> from skimage.morphology import flood
  152. >>> image = np.zeros((4, 7), dtype=int)
  153. >>> image[1:3, 1:3] = 1
  154. >>> image[3, 0] = 1
  155. >>> image[1:3, 4:6] = 2
  156. >>> image[3, 6] = 3
  157. >>> image
  158. array([[0, 0, 0, 0, 0, 0, 0],
  159. [0, 1, 1, 0, 2, 2, 0],
  160. [0, 1, 1, 0, 2, 2, 0],
  161. [1, 0, 0, 0, 0, 0, 3]])
  162. Fill connected ones with 5, with full connectivity (diagonals included):
  163. >>> mask = flood(image, (1, 1))
  164. >>> image_flooded = image.copy()
  165. >>> image_flooded[mask] = 5
  166. >>> image_flooded
  167. array([[0, 0, 0, 0, 0, 0, 0],
  168. [0, 5, 5, 0, 2, 2, 0],
  169. [0, 5, 5, 0, 2, 2, 0],
  170. [5, 0, 0, 0, 0, 0, 3]])
  171. Fill connected ones with 5, excluding diagonal points (connectivity 1):
  172. >>> mask = flood(image, (1, 1), connectivity=1)
  173. >>> image_flooded = image.copy()
  174. >>> image_flooded[mask] = 5
  175. >>> image_flooded
  176. array([[0, 0, 0, 0, 0, 0, 0],
  177. [0, 5, 5, 0, 2, 2, 0],
  178. [0, 5, 5, 0, 2, 2, 0],
  179. [1, 0, 0, 0, 0, 0, 3]])
  180. Fill with a tolerance:
  181. >>> mask = flood(image, (0, 0), tolerance=1)
  182. >>> image_flooded = image.copy()
  183. >>> image_flooded[mask] = 5
  184. >>> image_flooded
  185. array([[5, 5, 5, 5, 5, 5, 5],
  186. [5, 5, 5, 5, 2, 2, 5],
  187. [5, 5, 5, 5, 2, 2, 5],
  188. [5, 5, 5, 5, 5, 5, 3]])
  189. """
  190. # Correct start point in ravelled image - only copy if non-contiguous
  191. image = np.asarray(image)
  192. if image.flags.f_contiguous is True:
  193. order = 'F'
  194. elif image.flags.c_contiguous is True:
  195. order = 'C'
  196. else:
  197. image = np.ascontiguousarray(image)
  198. order = 'C'
  199. # Shortcut for rank zero
  200. if 0 in image.shape:
  201. return np.zeros(image.shape, dtype=bool)
  202. # Convenience for 1d input
  203. try:
  204. iter(seed_point)
  205. except TypeError:
  206. seed_point = (seed_point,)
  207. seed_value = image[seed_point]
  208. seed_point = tuple(np.asarray(seed_point) % image.shape)
  209. footprint = _resolve_neighborhood(
  210. footprint, connectivity, image.ndim, enforce_adjacency=False
  211. )
  212. center = tuple(s // 2 for s in footprint.shape)
  213. # Compute padding width as the maximum offset to neighbors on each axis.
  214. # Generates a 2-tuple of (pad_start, pad_end) for each axis.
  215. pad_width = [
  216. (np.max(np.abs(idx - c)),) * 2 for idx, c in zip(np.nonzero(footprint), center)
  217. ]
  218. # Must annotate borders
  219. working_image = np.pad(
  220. image, pad_width, mode='constant', constant_values=image.min()
  221. )
  222. # Stride-aware neighbors - works for both C- and Fortran-contiguity
  223. ravelled_seed_idx = np.ravel_multi_index(
  224. [i + pad_start for i, (pad_start, pad_end) in zip(seed_point, pad_width)],
  225. working_image.shape,
  226. order=order,
  227. )
  228. neighbor_offsets = _offsets_to_raveled_neighbors(
  229. working_image.shape, footprint, center=center, order=order
  230. )
  231. # Use a set of flags; see _flood_fill_cy.pyx for meanings
  232. flags = np.zeros(working_image.shape, dtype=np.uint8, order=order)
  233. _set_border_values(flags, value=2, border_width=pad_width)
  234. try:
  235. if tolerance is not None:
  236. tolerance = abs(tolerance)
  237. # Account for over- & underflow problems with seed_value ± tolerance
  238. # in a way that works with NumPy 1 & 2
  239. min_value, max_value = numeric_dtype_min_max(seed_value.dtype)
  240. low_tol = max(min_value.item(), seed_value.item() - tolerance)
  241. high_tol = min(max_value.item(), seed_value.item() + tolerance)
  242. _flood_fill_tolerance(
  243. working_image.ravel(order),
  244. flags.ravel(order),
  245. neighbor_offsets,
  246. ravelled_seed_idx,
  247. seed_value,
  248. low_tol,
  249. high_tol,
  250. )
  251. else:
  252. _flood_fill_equal(
  253. working_image.ravel(order),
  254. flags.ravel(order),
  255. neighbor_offsets,
  256. ravelled_seed_idx,
  257. seed_value,
  258. )
  259. except TypeError:
  260. if working_image.dtype == np.float16:
  261. # Provide the user with clearer error message
  262. raise TypeError(
  263. "dtype of `image` is float16 which is not "
  264. "supported, try upcasting to float32"
  265. )
  266. else:
  267. raise
  268. # Output what the user requested; view does not create a new copy.
  269. return crop(flags, pad_width, copy=False).view(bool)