footprints.py 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110
  1. import os
  2. import warnings
  3. from collections.abc import Sequence
  4. from numbers import Integral
  5. import numpy as np
  6. from .. import draw
  7. from skimage import morphology
  8. from .._shared.utils import deprecate_func
  9. # Precomputed ball and disk decompositions were saved as 2D arrays where the
  10. # radius of the desired decomposition is used to index into the first axis of
  11. # the array. The values at a given radius corresponds to the number of
  12. # repetitions of 3 different types elementary of structuring elements.
  13. #
  14. # See _nsphere_series_decomposition for full details.
  15. _nsphere_decompositions = {}
  16. _nsphere_decompositions[2] = np.load(
  17. os.path.join(os.path.dirname(__file__), 'disk_decompositions.npy')
  18. )
  19. _nsphere_decompositions[3] = np.load(
  20. os.path.join(os.path.dirname(__file__), 'ball_decompositions.npy')
  21. )
  22. def _footprint_is_sequence(footprint):
  23. if hasattr(footprint, '__array_interface__'):
  24. return False
  25. def _validate_sequence_element(t):
  26. return (
  27. isinstance(t, Sequence)
  28. and len(t) == 2
  29. and hasattr(t[0], '__array_interface__')
  30. and isinstance(t[1], Integral)
  31. )
  32. if isinstance(footprint, Sequence):
  33. if not all(_validate_sequence_element(t) for t in footprint):
  34. raise ValueError(
  35. "All elements of footprint sequence must be a 2-tuple where "
  36. "the first element of the tuple is an ndarray and the second "
  37. "is an integer indicating the number of iterations."
  38. )
  39. else:
  40. raise ValueError("footprint must be either an ndarray or Sequence")
  41. return True
  42. def _shape_from_sequence(footprints, require_odd_size=False):
  43. """Determine the shape of composite footprint
  44. In the future if we only want to support odd-sized square, we may want to
  45. change this to require_odd_size
  46. """
  47. if not _footprint_is_sequence(footprints):
  48. raise ValueError("expected a sequence of footprints")
  49. ndim = footprints[0][0].ndim
  50. shape = [0] * ndim
  51. def _odd_size(size, require_odd_size):
  52. if require_odd_size and size % 2 == 0:
  53. raise ValueError("expected all footprint elements to have odd size")
  54. for d in range(ndim):
  55. fp, nreps = footprints[0]
  56. _odd_size(fp.shape[d], require_odd_size)
  57. shape[d] = fp.shape[d] + (nreps - 1) * (fp.shape[d] - 1)
  58. for fp, nreps in footprints[1:]:
  59. _odd_size(fp.shape[d], require_odd_size)
  60. shape[d] += nreps * (fp.shape[d] - 1)
  61. return tuple(shape)
  62. def footprint_from_sequence(footprints):
  63. """Convert a footprint sequence into an equivalent ndarray.
  64. Parameters
  65. ----------
  66. footprints : tuple of 2-tuples
  67. A sequence of footprint tuples where the first element of each tuple
  68. is an array corresponding to a footprint and the second element is the
  69. number of times it is to be applied. Currently, all footprints should
  70. have odd size.
  71. Returns
  72. -------
  73. footprint : ndarray
  74. An single array equivalent to applying the sequence of ``footprints``.
  75. """
  76. # Create a single pixel image of sufficient size and apply binary dilation.
  77. shape = _shape_from_sequence(footprints)
  78. imag = np.zeros(shape, dtype=bool)
  79. imag[tuple(s // 2 for s in shape)] = 1
  80. return morphology.dilation(imag, footprints)
  81. def footprint_rectangle(shape, *, dtype=np.uint8, decomposition=None):
  82. """Generate a rectangular or hyper-rectangular footprint.
  83. Generates, depending on the length and dimensions requested with `shape`,
  84. a square, rectangle, cube, cuboid, or even higher-dimensional versions
  85. of these shapes.
  86. Parameters
  87. ----------
  88. shape : tuple[int, ...]
  89. The length of the footprint in each dimension. The length of the
  90. sequence determines the number of dimensions of the footprint.
  91. dtype : data-type, optional
  92. The data type of the footprint.
  93. decomposition : {None, 'separable', 'sequence'}, optional
  94. If None, a single array is returned. For 'sequence', a tuple of smaller
  95. footprints is returned. Applying this series of smaller footprints will
  96. give an identical result to a single, larger footprint, but often with
  97. better computational performance. See Notes for more details.
  98. With 'separable', this function uses separable 1D footprints for each
  99. axis. Whether 'sequence' or 'separable' is computationally faster may
  100. be architecture-dependent.
  101. Returns
  102. -------
  103. footprint : array or tuple[tuple[ndarray, int], ...]
  104. A footprint consisting only of ones, i.e. every pixel belongs to the
  105. neighborhood. When `decomposition` is None, this is just an array.
  106. Otherwise, this will be a tuple whose length is equal to the number of
  107. unique structuring elements to apply (see Examples for more detail).
  108. Examples
  109. --------
  110. >>> import skimage as ski
  111. >>> ski.morphology.footprint_rectangle((3, 5))
  112. array([[1, 1, 1, 1, 1],
  113. [1, 1, 1, 1, 1],
  114. [1, 1, 1, 1, 1]], dtype=uint8)
  115. Decomposition will return multiple footprints that combine into a simple
  116. footprint of the requested shape.
  117. >>> ski.morphology.footprint_rectangle((9, 9), decomposition="sequence")
  118. ((array([[1, 1, 1],
  119. [1, 1, 1],
  120. [1, 1, 1]], dtype=uint8),
  121. 4),)
  122. `"sequence"` makes sure that the decomposition only returns 1D footprints.
  123. >>> ski.morphology.footprint_rectangle((3, 5), decomposition="separable")
  124. ((array([[1],
  125. [1],
  126. [1]], dtype=uint8),
  127. 1),
  128. (array([[1, 1, 1, 1, 1]], dtype=uint8), 1))
  129. Generate a 5-dimensional hypercube with 3 samples in each dimension
  130. >>> ski.morphology.footprint_rectangle((3,) * 5).shape
  131. (3, 3, 3, 3, 3)
  132. """
  133. has_even_width = any(width % 2 == 0 for width in shape)
  134. if decomposition == "sequence" and has_even_width:
  135. warnings.warn(
  136. "decomposition='sequence' is only supported for uneven footprints, "
  137. "falling back to decomposition='separable'",
  138. stacklevel=2,
  139. )
  140. decomposition = "sequence_fallback"
  141. def partial_footprint(dim, width):
  142. shape_ = (1,) * dim + (width,) + (1,) * (len(shape) - dim - 1)
  143. fp = (np.ones(shape_, dtype=dtype), 1)
  144. return fp
  145. if decomposition is None:
  146. footprint = np.ones(shape, dtype=dtype)
  147. elif decomposition in ("separable", "sequence_fallback"):
  148. footprint = tuple(
  149. partial_footprint(dim, width) for dim, width in enumerate(shape)
  150. )
  151. elif decomposition == "sequence":
  152. min_width = min(shape)
  153. sq_reps = _decompose_size(min_width, 3)
  154. footprint = [(np.ones((3,) * len(shape), dtype=dtype), sq_reps)]
  155. for dim, width in enumerate(shape):
  156. if width > min_width:
  157. nextra = width - min_width + 1
  158. component = partial_footprint(dim, nextra)
  159. footprint.append(component)
  160. footprint = tuple(footprint)
  161. else:
  162. raise ValueError(f"Unrecognized decomposition: {decomposition}")
  163. return footprint
  164. @deprecate_func(
  165. deprecated_version="0.25",
  166. removed_version="0.27",
  167. hint="Use `skimage.morphology.footprint_rectangle` instead.",
  168. )
  169. def square(width, dtype=np.uint8, *, decomposition=None):
  170. """Generates a flat, square-shaped footprint.
  171. Every pixel along the perimeter has a chessboard distance
  172. no greater than radius (radius=floor(width/2)) pixels.
  173. Parameters
  174. ----------
  175. width : int
  176. The width and height of the square.
  177. Other Parameters
  178. ----------------
  179. dtype : data-type, optional
  180. The data type of the footprint.
  181. decomposition : {None, 'separable', 'sequence'}, optional
  182. If None, a single array is returned. For 'sequence', a tuple of smaller
  183. footprints is returned. Applying this series of smaller footprints will
  184. give an identical result to a single, larger footprint, but often with
  185. better computational performance. See Notes for more details.
  186. With 'separable', this function uses separable 1D footprints for each
  187. axis. Whether 'sequence' or 'separable' is computationally faster may
  188. be architecture-dependent.
  189. Returns
  190. -------
  191. footprint : ndarray or tuple
  192. The footprint where elements of the neighborhood are 1 and 0 otherwise.
  193. When `decomposition` is None, this is just a numpy.ndarray. Otherwise,
  194. this will be a tuple whose length is equal to the number of unique
  195. structuring elements to apply (see Notes for more detail)
  196. Notes
  197. -----
  198. When `decomposition` is not None, each element of the `footprint`
  199. tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a
  200. footprint array and the number of iterations it is to be applied.
  201. For binary morphology, using ``decomposition='sequence'`` or
  202. ``decomposition='separable'`` were observed to give better performance than
  203. ``decomposition=None``, with the magnitude of the performance increase
  204. rapidly increasing with footprint size. For grayscale morphology with
  205. square footprints, it is recommended to use ``decomposition=None`` since
  206. the internal SciPy functions that are called already have a fast
  207. implementation based on separable 1D sliding windows.
  208. The 'sequence' decomposition mode only supports odd valued `width`. If
  209. `width` is even, the sequence used will be identical to the 'separable'
  210. mode.
  211. """
  212. footprint = footprint_rectangle(
  213. shape=(width, width), dtype=dtype, decomposition=decomposition
  214. )
  215. return footprint
  216. def _decompose_size(size, kernel_size=3):
  217. """Determine number of repeated iterations for a `kernel_size` kernel.
  218. Returns how many repeated morphology operations with an element of size
  219. `kernel_size` is equivalent to a morphology with a single kernel of size
  220. `n`.
  221. """
  222. if kernel_size % 2 != 1:
  223. raise ValueError("only odd length kernel_size is supported")
  224. return 1 + (size - kernel_size) // (kernel_size - 1)
  225. @deprecate_func(
  226. deprecated_version="0.25",
  227. removed_version="0.27",
  228. hint="Use `skimage.morphology.footprint_rectangle` instead.",
  229. )
  230. def rectangle(nrows, ncols, dtype=np.uint8, *, decomposition=None):
  231. """Generates a flat, rectangular-shaped footprint.
  232. Every pixel in the rectangle generated for a given width and given height
  233. belongs to the neighborhood.
  234. Parameters
  235. ----------
  236. nrows : int
  237. The number of rows of the rectangle.
  238. ncols : int
  239. The number of columns of the rectangle.
  240. Other Parameters
  241. ----------------
  242. dtype : data-type, optional
  243. The data type of the footprint.
  244. decomposition : {None, 'separable', 'sequence'}, optional
  245. If None, a single array is returned. For 'sequence', a tuple of smaller
  246. footprints is returned. Applying this series of smaller footprints will
  247. given an identical result to a single, larger footprint, but often with
  248. better computational performance. See Notes for more details.
  249. With 'separable', this function uses separable 1D footprints for each
  250. axis. Whether 'sequence' or 'separable' is computationally faster may
  251. be architecture-dependent.
  252. Returns
  253. -------
  254. footprint : ndarray or tuple
  255. A footprint consisting only of ones, i.e. every pixel belongs to the
  256. neighborhood. When `decomposition` is None, this is just a
  257. numpy.ndarray. Otherwise, this will be a tuple whose length is equal to
  258. the number of unique structuring elements to apply (see Notes for more
  259. detail)
  260. Notes
  261. -----
  262. When `decomposition` is not None, each element of the `footprint`
  263. tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a
  264. footprint array and the number of iterations it is to be applied.
  265. For binary morphology, using ``decomposition='sequence'``
  266. was observed to give better performance, with the magnitude of the
  267. performance increase rapidly increasing with footprint size. For grayscale
  268. morphology with rectangular footprints, it is recommended to use
  269. ``decomposition=None`` since the internal SciPy functions that are called
  270. already have a fast implementation based on separable 1D sliding windows.
  271. The `sequence` decomposition mode only supports odd valued `nrows` and
  272. `ncols`. If either `nrows` or `ncols` is even, the sequence used will be
  273. identical to ``decomposition='separable'``.
  274. - The use of ``width`` and ``height`` has been deprecated in
  275. version 0.18.0. Use ``nrows`` and ``ncols`` instead.
  276. """
  277. footprint = footprint_rectangle(
  278. shape=(nrows, ncols), dtype=dtype, decomposition=decomposition
  279. )
  280. return footprint
  281. def diamond(radius, dtype=np.uint8, *, decomposition=None):
  282. """Generates a flat, diamond-shaped footprint.
  283. A pixel is part of the neighborhood (i.e. labeled 1) if
  284. the city block/Manhattan distance between it and the center of
  285. the neighborhood is no greater than radius.
  286. Parameters
  287. ----------
  288. radius : int
  289. The radius of the diamond-shaped footprint.
  290. Other Parameters
  291. ----------------
  292. dtype : data-type, optional
  293. The data type of the footprint.
  294. decomposition : {None, 'sequence'}, optional
  295. If None, a single array is returned. For 'sequence', a tuple of smaller
  296. footprints is returned. Applying this series of smaller footprints will
  297. given an identical result to a single, larger footprint, but with
  298. better computational performance. See Notes for more details.
  299. Returns
  300. -------
  301. footprint : ndarray or tuple
  302. The footprint where elements of the neighborhood are 1 and 0 otherwise.
  303. When `decomposition` is None, this is just a numpy.ndarray. Otherwise,
  304. this will be a tuple whose length is equal to the number of unique
  305. structuring elements to apply (see Notes for more detail)
  306. Notes
  307. -----
  308. When `decomposition` is not None, each element of the `footprint`
  309. tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a
  310. footprint array and the number of iterations it is to be applied.
  311. For either binary or grayscale morphology, using
  312. ``decomposition='sequence'`` was observed to have a performance benefit,
  313. with the magnitude of the benefit increasing with increasing footprint
  314. size.
  315. """
  316. if decomposition is None:
  317. L = np.arange(0, radius * 2 + 1)
  318. I, J = np.meshgrid(L, L)
  319. footprint = np.array(
  320. np.abs(I - radius) + np.abs(J - radius) <= radius, dtype=dtype
  321. )
  322. elif decomposition == 'sequence':
  323. fp = diamond(1, dtype=dtype, decomposition=None)
  324. nreps = _decompose_size(2 * radius + 1, fp.shape[0])
  325. footprint = ((fp, nreps),)
  326. else:
  327. raise ValueError(f"Unrecognized decomposition: {decomposition}")
  328. return footprint
  329. def _nsphere_series_decomposition(radius, ndim, dtype=np.uint8):
  330. """Generate a sequence of footprints approximating an n-sphere.
  331. Morphological operations with an n-sphere (hypersphere) footprint can be
  332. approximated by applying a series of smaller footprints of extent 3 along
  333. each axis. Specific solutions for this are given in [1]_ for the case of
  334. 2D disks with radius 2 through 10.
  335. Here we used n-dimensional extensions of the "square", "diamond" and
  336. "t-shaped" elements from that publication. All of these elementary elements
  337. have size ``(3,) * ndim``. We numerically computed the number of
  338. repetitions of each element that gives the closest match to the disk
  339. (in 2D) or ball (in 3D) computed with ``decomposition=None``.
  340. The approach can be extended to higher dimensions, but we have only stored
  341. results for 2D and 3D at this point.
  342. Empirically, the shapes at large radius approach a hexadecagon
  343. (16-sides [2]_) in 2D and a rhombicuboctahedron (26-faces, [3]_) in 3D.
  344. References
  345. ----------
  346. .. [1] Park, H and Chin R.T. Decomposition of structuring elements for
  347. optimal implementation of morphological operations. In Proceedings:
  348. 1997 IEEE Workshop on Nonlinear Signal and Image Processing, London,
  349. UK.
  350. https://www.iwaenc.org/proceedings/1997/nsip97/pdf/scan/ns970226.pdf
  351. .. [2] https://en.wikipedia.org/wiki/Hexadecagon
  352. .. [3] https://en.wikipedia.org/wiki/Rhombicuboctahedron
  353. """
  354. if radius == 1:
  355. # for radius 1 just use the exact shape (3,) * ndim solution
  356. kwargs = dict(dtype=dtype, strict_radius=False, decomposition=None)
  357. if ndim == 2:
  358. return ((disk(1, **kwargs), 1),)
  359. elif ndim == 3:
  360. return ((ball(1, **kwargs), 1),)
  361. # load precomputed decompositions
  362. if ndim not in _nsphere_decompositions:
  363. raise ValueError(
  364. "sequence decompositions are only currently available for "
  365. "2d disks or 3d balls"
  366. )
  367. precomputed_decompositions = _nsphere_decompositions[ndim]
  368. max_radius = precomputed_decompositions.shape[0]
  369. if radius > max_radius:
  370. raise ValueError(
  371. f"precomputed {ndim}D decomposition unavailable for "
  372. f"radius > {max_radius}"
  373. )
  374. num_t_series, num_diamond, num_square = precomputed_decompositions[radius]
  375. sequence = []
  376. if num_t_series > 0:
  377. # shape (3,) * ndim "T-shaped" footprints
  378. all_t = _t_shaped_element_series(ndim=ndim, dtype=dtype)
  379. [sequence.append((t, num_t_series)) for t in all_t]
  380. if num_diamond > 0:
  381. d = np.zeros((3,) * ndim, dtype=dtype)
  382. sl = [slice(1, 2)] * ndim
  383. for ax in range(ndim):
  384. sl[ax] = slice(None)
  385. d[tuple(sl)] = 1
  386. sl[ax] = slice(1, 2)
  387. sequence.append((d, num_diamond))
  388. if num_square > 0:
  389. sq = np.ones((3,) * ndim, dtype=dtype)
  390. sequence.append((sq, num_square))
  391. return tuple(sequence)
  392. def _t_shaped_element_series(ndim=2, dtype=np.uint8):
  393. """A series of T-shaped structuring elements.
  394. In the 2D case this is a T-shaped element and its rotation at multiples of
  395. 90 degrees. This series is used in efficient decompositions of disks of
  396. various radius as published in [1]_.
  397. The generalization to the n-dimensional case can be performed by having the
  398. "top" of the T to extend in (ndim - 1) dimensions and then producing a
  399. series of rotations such that the bottom end of the T points along each of
  400. ``2 * ndim`` orthogonal directions.
  401. """
  402. if ndim == 2:
  403. # The n-dimensional case produces the same set of footprints, but
  404. # the 2D example is retained here for clarity.
  405. t0 = np.array([[1, 1, 1], [0, 1, 0], [0, 1, 0]], dtype=dtype)
  406. t90 = np.rot90(t0, 1)
  407. t180 = np.rot90(t0, 2)
  408. t270 = np.rot90(t0, 3)
  409. return t0, t90, t180, t270
  410. else:
  411. # ndimensional generalization of the 2D case above
  412. all_t = []
  413. for ax in range(ndim):
  414. for idx in [0, 2]:
  415. t = np.zeros((3,) * ndim, dtype=dtype)
  416. sl = [slice(None)] * ndim
  417. sl[ax] = slice(idx, idx + 1)
  418. t[tuple(sl)] = 1
  419. sl = [slice(1, 2)] * ndim
  420. sl[ax] = slice(None)
  421. t[tuple(sl)] = 1
  422. all_t.append(t)
  423. return tuple(all_t)
  424. def disk(radius, dtype=np.uint8, *, strict_radius=True, decomposition=None):
  425. """Generates a flat, disk-shaped footprint.
  426. A pixel is within the neighborhood if the Euclidean distance between
  427. it and the origin is no greater than radius (This is only approximately
  428. True, when `decomposition == 'sequence'`).
  429. Parameters
  430. ----------
  431. radius : int
  432. The radius of the disk-shaped footprint.
  433. Other Parameters
  434. ----------------
  435. dtype : data-type, optional
  436. The data type of the footprint.
  437. strict_radius : bool, optional
  438. If False, extend the radius by 0.5. This allows the circle to expand
  439. further within a cube that remains of size ``2 * radius + 1`` along
  440. each axis. This parameter is ignored if decomposition is not None.
  441. decomposition : {None, 'sequence', 'crosses'}, optional
  442. If None, a single array is returned. For 'sequence', a tuple of smaller
  443. footprints is returned. Applying this series of smaller footprints will
  444. given a result equivalent to a single, larger footprint, but with
  445. better computational performance. For disk footprints, the 'sequence'
  446. or 'crosses' decompositions are not always exactly equivalent to
  447. ``decomposition=None``. See Notes for more details.
  448. Returns
  449. -------
  450. footprint : ndarray
  451. The footprint where elements of the neighborhood are 1 and 0 otherwise.
  452. Notes
  453. -----
  454. When `decomposition` is not None, each element of the `footprint`
  455. tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a
  456. footprint array and the number of iterations it is to be applied.
  457. The disk produced by the ``decomposition='sequence'`` mode may not be
  458. identical to that with ``decomposition=None``. A disk footprint can be
  459. approximated by applying a series of smaller footprints of extent 3 along
  460. each axis. Specific solutions for this are given in [1]_ for the case of
  461. 2D disks with radius 2 through 10. Here, we numerically computed the number
  462. of repetitions of each element that gives the closest match to the disk
  463. computed with kwargs ``strict_radius=False, decomposition=None``.
  464. Empirically, the series decomposition at large radius approaches a
  465. hexadecagon (a 16-sided polygon [2]_). In [3]_, the authors demonstrate
  466. that a hexadecagon is the closest approximation to a disk that can be
  467. achieved for decomposition with footprints of shape (3, 3).
  468. The disk produced by the ``decomposition='crosses'`` is often but not
  469. always identical to that with ``decomposition=None``. It tends to give a
  470. closer approximation than ``decomposition='sequence'``, at a performance
  471. that is fairly comparable. The individual cross-shaped elements are not
  472. limited to extent (3, 3) in size. Unlike the 'seqeuence' decomposition, the
  473. 'crosses' decomposition can also accurately approximate the shape of disks
  474. with ``strict_radius=True``. The method is based on an adaption of
  475. algorithm 1 given in [4]_.
  476. References
  477. ----------
  478. .. [1] Park, H and Chin R.T. Decomposition of structuring elements for
  479. optimal implementation of morphological operations. In Proceedings:
  480. 1997 IEEE Workshop on Nonlinear Signal and Image Processing, London,
  481. UK.
  482. https://www.iwaenc.org/proceedings/1997/nsip97/pdf/scan/ns970226.pdf
  483. .. [2] https://en.wikipedia.org/wiki/Hexadecagon
  484. .. [3] Vanrell, M and Vitrià, J. Optimal 3 × 3 decomposable disks for
  485. morphological transformations. Image and Vision Computing, Vol. 15,
  486. Issue 11, 1997.
  487. :DOI:`10.1016/S0262-8856(97)00026-7`
  488. .. [4] Li, D. and Ritter, G.X. Decomposition of Separable and Symmetric
  489. Convex Templates. Proc. SPIE 1350, Image Algebra and Morphological
  490. Image Processing, (1 November 1990).
  491. :DOI:`10.1117/12.23608`
  492. """
  493. if decomposition is None:
  494. L = np.arange(-radius, radius + 1)
  495. X, Y = np.meshgrid(L, L)
  496. if not strict_radius:
  497. radius += 0.5
  498. return np.array((X**2 + Y**2) <= radius**2, dtype=dtype)
  499. elif decomposition == 'sequence':
  500. sequence = _nsphere_series_decomposition(radius, ndim=2, dtype=dtype)
  501. elif decomposition == 'crosses':
  502. fp = disk(radius, dtype, strict_radius=strict_radius, decomposition=None)
  503. sequence = _cross_decomposition(fp)
  504. return sequence
  505. def _cross(r0, r1, dtype=np.uint8):
  506. """Cross-shaped structuring element of shape (r0, r1).
  507. Only the central row and column are ones.
  508. """
  509. s0 = int(2 * r0 + 1)
  510. s1 = int(2 * r1 + 1)
  511. c = np.zeros((s0, s1), dtype=dtype)
  512. if r1 != 0:
  513. c[r0, :] = 1
  514. if r0 != 0:
  515. c[:, r1] = 1
  516. return c
  517. def _cross_decomposition(footprint, dtype=np.uint8):
  518. """Decompose a symmetric convex footprint into cross-shaped elements.
  519. This is a decomposition of the footprint into a sequence of
  520. (possibly asymmetric) cross-shaped elements. This technique was proposed in
  521. [1]_ and corresponds roughly to algorithm 1 of that publication (some
  522. details had to be modified to get reliable operation).
  523. .. [1] Li, D. and Ritter, G.X. Decomposition of Separable and Symmetric
  524. Convex Templates. Proc. SPIE 1350, Image Algebra and Morphological
  525. Image Processing, (1 November 1990).
  526. :DOI:`10.1117/12.23608`
  527. """
  528. quadrant = footprint[footprint.shape[0] // 2 :, footprint.shape[1] // 2 :]
  529. col_sums = quadrant.sum(0, dtype=int)
  530. col_sums = np.concatenate((col_sums, np.asarray([0], dtype=int)))
  531. i_prev = 0
  532. idx = {}
  533. sum0 = 0
  534. for i in range(col_sums.size - 1):
  535. if col_sums[i] > col_sums[i + 1]:
  536. if i == 0:
  537. continue
  538. key = (col_sums[i_prev] - col_sums[i], i - i_prev)
  539. sum0 += key[0]
  540. if key not in idx:
  541. idx[key] = 1
  542. else:
  543. idx[key] += 1
  544. i_prev = i
  545. n = quadrant.shape[0] - 1 - sum0
  546. if n > 0:
  547. key = (n, 0)
  548. idx[key] = idx.get(key, 0) + 1
  549. return tuple([(_cross(r0, r1, dtype), n) for (r0, r1), n in idx.items()])
  550. def ellipse(width, height, dtype=np.uint8, *, decomposition=None):
  551. """Generates a flat, ellipse-shaped footprint.
  552. Every pixel along the perimeter of ellipse satisfies
  553. the equation ``(x/width+1)**2 + (y/height+1)**2 = 1``.
  554. Parameters
  555. ----------
  556. width : int
  557. The width of the ellipse-shaped footprint.
  558. height : int
  559. The height of the ellipse-shaped footprint.
  560. Other Parameters
  561. ----------------
  562. dtype : data-type, optional
  563. The data type of the footprint.
  564. decomposition : {None, 'crosses'}, optional
  565. If None, a single array is returned. For 'sequence', a tuple of smaller
  566. footprints is returned. Applying this series of smaller footprints will
  567. given an identical result to a single, larger footprint, but with
  568. better computational performance. See Notes for more details.
  569. Returns
  570. -------
  571. footprint : ndarray
  572. The footprint where elements of the neighborhood are 1 and 0 otherwise.
  573. The footprint will have shape ``(2 * height + 1, 2 * width + 1)``.
  574. Notes
  575. -----
  576. When `decomposition` is not None, each element of the `footprint`
  577. tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a
  578. footprint array and the number of iterations it is to be applied.
  579. The ellipse produced by the ``decomposition='crosses'`` is often but not
  580. always identical to that with ``decomposition=None``. The method is based
  581. on an adaption of algorithm 1 given in [1]_.
  582. References
  583. ----------
  584. .. [1] Li, D. and Ritter, G.X. Decomposition of Separable and Symmetric
  585. Convex Templates. Proc. SPIE 1350, Image Algebra and Morphological
  586. Image Processing, (1 November 1990).
  587. :DOI:`10.1117/12.23608`
  588. Examples
  589. --------
  590. >>> from skimage.morphology import footprints
  591. >>> footprints.ellipse(5, 3)
  592. array([[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
  593. [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  594. [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  595. [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  596. [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  597. [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  598. [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0]], dtype=uint8)
  599. """
  600. if decomposition is None:
  601. footprint = np.zeros((2 * height + 1, 2 * width + 1), dtype=dtype)
  602. rows, cols = draw.ellipse(height, width, height + 1, width + 1)
  603. footprint[rows, cols] = 1
  604. return footprint
  605. elif decomposition == 'crosses':
  606. fp = ellipse(width, height, dtype, decomposition=None)
  607. sequence = _cross_decomposition(fp)
  608. return sequence
  609. @deprecate_func(
  610. deprecated_version="0.25",
  611. removed_version="0.27",
  612. hint="Use `skimage.morphology.footprint_rectangle` instead.",
  613. )
  614. def cube(width, dtype=np.uint8, *, decomposition=None):
  615. """Generates a cube-shaped footprint.
  616. This is the 3D equivalent of a square.
  617. Every pixel along the perimeter has a chessboard distance
  618. no greater than radius (radius=floor(width/2)) pixels.
  619. Parameters
  620. ----------
  621. width : int
  622. The width, height and depth of the cube.
  623. Other Parameters
  624. ----------------
  625. dtype : data-type, optional
  626. The data type of the footprint.
  627. decomposition : {None, 'separable', 'sequence'}, optional
  628. If None, a single array is returned. For 'sequence', a tuple of smaller
  629. footprints is returned. Applying this series of smaller footprints will
  630. given an identical result to a single, larger footprint, but often with
  631. better computational performance. See Notes for more details.
  632. Returns
  633. -------
  634. footprint : ndarray or tuple
  635. The footprint where elements of the neighborhood are 1 and 0 otherwise.
  636. When `decomposition` is None, this is just a numpy.ndarray. Otherwise,
  637. this will be a tuple whose length is equal to the number of unique
  638. structuring elements to apply (see Notes for more detail)
  639. Notes
  640. -----
  641. When `decomposition` is not None, each element of the `footprint`
  642. tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a
  643. footprint array and the number of iterations it is to be applied.
  644. For binary morphology, using ``decomposition='sequence'``
  645. was observed to give better performance, with the magnitude of the
  646. performance increase rapidly increasing with footprint size. For grayscale
  647. morphology with square footprints, it is recommended to use
  648. ``decomposition=None`` since the internal SciPy functions that are called
  649. already have a fast implementation based on separable 1D sliding windows.
  650. The 'sequence' decomposition mode only supports odd valued `width`. If
  651. `width` is even, the sequence used will be identical to the 'separable'
  652. mode.
  653. """
  654. footprint = footprint_rectangle(
  655. shape=(width, width, width), dtype=dtype, decomposition=decomposition
  656. )
  657. return footprint
  658. def octahedron(radius, dtype=np.uint8, *, decomposition=None):
  659. """Generates a octahedron-shaped footprint.
  660. This is the 3D equivalent of a diamond.
  661. A pixel is part of the neighborhood (i.e. labeled 1) if
  662. the city block/Manhattan distance between it and the center of
  663. the neighborhood is no greater than radius.
  664. Parameters
  665. ----------
  666. radius : int
  667. The radius of the octahedron-shaped footprint.
  668. Other Parameters
  669. ----------------
  670. dtype : data-type, optional
  671. The data type of the footprint.
  672. decomposition : {None, 'sequence'}, optional
  673. If None, a single array is returned. For 'sequence', a tuple of smaller
  674. footprints is returned. Applying this series of smaller footprints will
  675. given an identical result to a single, larger footprint, but with
  676. better computational performance. See Notes for more details.
  677. Returns
  678. -------
  679. footprint : ndarray or tuple
  680. The footprint where elements of the neighborhood are 1 and 0 otherwise.
  681. When `decomposition` is None, this is just a numpy.ndarray. Otherwise,
  682. this will be a tuple whose length is equal to the number of unique
  683. structuring elements to apply (see Notes for more detail)
  684. Notes
  685. -----
  686. When `decomposition` is not None, each element of the `footprint`
  687. tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a
  688. footprint array and the number of iterations it is to be applied.
  689. For either binary or grayscale morphology, using
  690. ``decomposition='sequence'`` was observed to have a performance benefit,
  691. with the magnitude of the benefit increasing with increasing footprint
  692. size.
  693. """
  694. # note that in contrast to diamond(), this method allows non-integer radii
  695. if decomposition is None:
  696. n = 2 * radius + 1
  697. Z, Y, X = np.mgrid[
  698. -radius : radius : n * 1j,
  699. -radius : radius : n * 1j,
  700. -radius : radius : n * 1j,
  701. ]
  702. s = np.abs(X) + np.abs(Y) + np.abs(Z)
  703. footprint = np.array(s <= radius, dtype=dtype)
  704. elif decomposition == 'sequence':
  705. fp = octahedron(1, dtype=dtype, decomposition=None)
  706. nreps = _decompose_size(2 * radius + 1, fp.shape[0])
  707. footprint = ((fp, nreps),)
  708. else:
  709. raise ValueError(f"Unrecognized decomposition: {decomposition}")
  710. return footprint
  711. def ball(radius, dtype=np.uint8, *, strict_radius=True, decomposition=None):
  712. """Generates a ball-shaped footprint.
  713. This is the 3D equivalent of a disk.
  714. A pixel is within the neighborhood if the Euclidean distance between
  715. it and the origin is no greater than radius.
  716. Parameters
  717. ----------
  718. radius : float
  719. The radius of the ball-shaped footprint.
  720. Other Parameters
  721. ----------------
  722. dtype : data-type, optional
  723. The data type of the footprint.
  724. strict_radius : bool, optional
  725. If False, extend the radius by 0.5. This allows the circle to expand
  726. further within a cube that remains of size ``2 * radius + 1`` along
  727. each axis. This parameter is ignored if decomposition is not None.
  728. decomposition : {None, 'sequence'}, optional
  729. If None, a single array is returned. For 'sequence', a tuple of smaller
  730. footprints is returned. Applying this series of smaller footprints will
  731. given a result equivalent to a single, larger footprint, but with
  732. better computational performance. For ball footprints, the sequence
  733. decomposition is not exactly equivalent to decomposition=None.
  734. See Notes for more details.
  735. Returns
  736. -------
  737. footprint : ndarray or tuple
  738. The footprint where elements of the neighborhood are 1 and 0 otherwise.
  739. Notes
  740. -----
  741. The disk produced by the decomposition='sequence' mode is not identical
  742. to that with decomposition=None. Here we extend the approach taken in [1]_
  743. for disks to the 3D case, using 3-dimensional extensions of the "square",
  744. "diamond" and "t-shaped" elements from that publication. All of these
  745. elementary elements have size ``(3,) * ndim``. We numerically computed the
  746. number of repetitions of each element that gives the closest match to the
  747. ball computed with kwargs ``strict_radius=False, decomposition=None``.
  748. Empirically, the equivalent composite footprint to the sequence
  749. decomposition approaches a rhombicuboctahedron (26-faces [2]_).
  750. References
  751. ----------
  752. .. [1] Park, H and Chin R.T. Decomposition of structuring elements for
  753. optimal implementation of morphological operations. In Proceedings:
  754. 1997 IEEE Workshop on Nonlinear Signal and Image Processing, London,
  755. UK.
  756. https://www.iwaenc.org/proceedings/1997/nsip97/pdf/scan/ns970226.pdf
  757. .. [2] https://en.wikipedia.org/wiki/Rhombicuboctahedron
  758. """
  759. if decomposition is None:
  760. n = 2 * radius + 1
  761. Z, Y, X = np.mgrid[
  762. -radius : radius : n * 1j,
  763. -radius : radius : n * 1j,
  764. -radius : radius : n * 1j,
  765. ]
  766. s = X**2 + Y**2 + Z**2
  767. if not strict_radius:
  768. radius += 0.5
  769. return np.array(s <= radius * radius, dtype=dtype)
  770. elif decomposition == 'sequence':
  771. sequence = _nsphere_series_decomposition(radius, ndim=3, dtype=dtype)
  772. else:
  773. raise ValueError(f"Unrecognized decomposition: {decomposition}")
  774. return sequence
  775. def octagon(m, n, dtype=np.uint8, *, decomposition=None):
  776. """Generates an octagon shaped footprint.
  777. For a given size of (m) horizontal and vertical sides
  778. and a given (n) height or width of slanted sides octagon is generated.
  779. The slanted sides are 45 or 135 degrees to the horizontal axis
  780. and hence the widths and heights are equal. The overall size of the
  781. footprint along a single axis will be ``m + 2 * n``.
  782. Parameters
  783. ----------
  784. m : int
  785. The size of the horizontal and vertical sides.
  786. n : int
  787. The height or width of the slanted sides.
  788. Other Parameters
  789. ----------------
  790. dtype : data-type, optional
  791. The data type of the footprint.
  792. decomposition : {None, 'sequence'}, optional
  793. If None, a single array is returned. For 'sequence', a tuple of smaller
  794. footprints is returned. Applying this series of smaller footprints will
  795. given an identical result to a single, larger footprint, but with
  796. better computational performance. See Notes for more details.
  797. Returns
  798. -------
  799. footprint : ndarray or tuple
  800. The footprint where elements of the neighborhood are 1 and 0 otherwise.
  801. When `decomposition` is None, this is just a numpy.ndarray. Otherwise,
  802. this will be a tuple whose length is equal to the number of unique
  803. structuring elements to apply (see Notes for more detail)
  804. Notes
  805. -----
  806. When `decomposition` is not None, each element of the `footprint`
  807. tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a
  808. footprint array and the number of iterations it is to be applied.
  809. For either binary or grayscale morphology, using
  810. ``decomposition='sequence'`` was observed to have a performance benefit,
  811. with the magnitude of the benefit increasing with increasing footprint
  812. size.
  813. """
  814. if m == n == 0:
  815. raise ValueError("m and n cannot both be zero")
  816. # TODO?: warn about even footprint size when m is even
  817. if decomposition is None:
  818. from . import convex_hull_image
  819. footprint = np.zeros((m + 2 * n, m + 2 * n))
  820. footprint[0, n] = 1
  821. footprint[n, 0] = 1
  822. footprint[0, m + n - 1] = 1
  823. footprint[m + n - 1, 0] = 1
  824. footprint[-1, n] = 1
  825. footprint[n, -1] = 1
  826. footprint[-1, m + n - 1] = 1
  827. footprint[m + n - 1, -1] = 1
  828. footprint = convex_hull_image(footprint).astype(dtype)
  829. elif decomposition == 'sequence':
  830. # special handling for edge cases with small m and/or n
  831. if m <= 2 and n <= 2:
  832. return ((octagon(m, n, dtype=dtype, decomposition=None), 1),)
  833. # general approach for larger m and/or n
  834. if m == 0:
  835. m = 2
  836. n -= 1
  837. sequence = []
  838. if m > 1:
  839. sequence += list(
  840. footprint_rectangle((m, m), dtype=dtype, decomposition='sequence')
  841. )
  842. if n > 0:
  843. sequence += [(diamond(1, dtype=dtype, decomposition=None), n)]
  844. footprint = tuple(sequence)
  845. else:
  846. raise ValueError(f"Unrecognized decomposition: {decomposition}")
  847. return footprint
  848. def star(a, dtype=np.uint8):
  849. """Generates a star shaped footprint.
  850. Start has 8 vertices and is an overlap of square of size `2*a + 1`
  851. with its 45 degree rotated version.
  852. The slanted sides are 45 or 135 degrees to the horizontal axis.
  853. Parameters
  854. ----------
  855. a : int
  856. Parameter deciding the size of the star structural element. The side
  857. of the square array returned is `2*a + 1 + 2*floor(a / 2)`.
  858. Other Parameters
  859. ----------------
  860. dtype : data-type, optional
  861. The data type of the footprint.
  862. Returns
  863. -------
  864. footprint : ndarray
  865. The footprint where elements of the neighborhood are 1 and 0 otherwise.
  866. """
  867. from . import convex_hull_image
  868. if a == 1:
  869. bfilter = np.zeros((3, 3), dtype)
  870. bfilter[:] = 1
  871. return bfilter
  872. m = 2 * a + 1
  873. n = a // 2
  874. footprint_square = np.zeros((m + 2 * n, m + 2 * n))
  875. footprint_square[n : m + n, n : m + n] = 1
  876. c = (m + 2 * n - 1) // 2
  877. footprint_rotated = np.zeros((m + 2 * n, m + 2 * n))
  878. footprint_rotated[0, c] = footprint_rotated[-1, c] = 1
  879. footprint_rotated[c, 0] = footprint_rotated[c, -1] = 1
  880. footprint_rotated = convex_hull_image(footprint_rotated).astype(int)
  881. footprint = footprint_square + footprint_rotated
  882. footprint[footprint > 0] = 1
  883. return footprint.astype(dtype)
  884. def mirror_footprint(footprint):
  885. """Mirror each dimension in the footprint.
  886. Parameters
  887. ----------
  888. footprint : ndarray or tuple
  889. The input footprint or sequence of footprints
  890. Returns
  891. -------
  892. inverted : ndarray or tuple
  893. The footprint, mirrored along each dimension.
  894. Examples
  895. --------
  896. >>> footprint = np.array([[0, 0, 0],
  897. ... [0, 1, 1],
  898. ... [0, 1, 1]], np.uint8)
  899. >>> mirror_footprint(footprint)
  900. array([[1, 1, 0],
  901. [1, 1, 0],
  902. [0, 0, 0]], dtype=uint8)
  903. """
  904. if _footprint_is_sequence(footprint):
  905. return tuple((mirror_footprint(fp), n) for fp, n in footprint)
  906. footprint = np.asarray(footprint)
  907. return footprint[(slice(None, None, -1),) * footprint.ndim]
  908. def pad_footprint(footprint, *, pad_end=True):
  909. """Pad the footprint to an odd size along each dimension.
  910. Parameters
  911. ----------
  912. footprint : ndarray or tuple
  913. The input footprint or sequence of footprints
  914. pad_end : bool, optional
  915. If ``True``, pads at the end of each dimension (right side), otherwise
  916. pads on the front (left side).
  917. Returns
  918. -------
  919. padded : ndarray or tuple
  920. The footprint, padded to an odd size along each dimension.
  921. Examples
  922. --------
  923. >>> footprint = np.array([[0, 0],
  924. ... [1, 1],
  925. ... [1, 1]], np.uint8)
  926. >>> pad_footprint(footprint)
  927. array([[0, 0, 0],
  928. [1, 1, 0],
  929. [1, 1, 0]], dtype=uint8)
  930. """
  931. if _footprint_is_sequence(footprint):
  932. return tuple((pad_footprint(fp, pad_end=pad_end), n) for fp, n in footprint)
  933. footprint = np.asarray(footprint)
  934. padding = []
  935. for sz in footprint.shape:
  936. padding.append(((0, 1) if pad_end else (1, 0)) if sz % 2 == 0 else (0, 0))
  937. return np.pad(footprint, padding)