_skeletonize.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. """
  2. Algorithms for computing the skeleton of a binary image
  3. """
  4. import numpy as np
  5. from scipy import ndimage as ndi
  6. from .._shared.utils import check_nD
  7. from ..util import crop
  8. from ._skeletonize_lee_cy import _compute_thin_image
  9. from ._skeletonize_various_cy import (
  10. _fast_skeletonize,
  11. _skeletonize_loop,
  12. _table_lookup_index,
  13. )
  14. def skeletonize(image, *, method=None):
  15. """Compute the skeleton of the input image via thinning.
  16. Parameters
  17. ----------
  18. image : (M, N[, P]) ndarray of bool or int
  19. The image containing the objects to be skeletonized. Each connected component
  20. in the image is reduced to a single-pixel wide skeleton. The image is binarized
  21. prior to thinning; thus, adjacent objects of different intensities are
  22. considered as one. Zero or ``False`` values represent the background, nonzero
  23. or ``True`` values -- foreground.
  24. method : {'zhang', 'lee'}, optional
  25. Which algorithm to use. Zhang's algorithm [Zha84]_ only works for
  26. 2D images, and is the default for 2D. Lee's algorithm [Lee94]_
  27. works for 2D or 3D images and is the default for 3D.
  28. Returns
  29. -------
  30. skeleton : (M, N[, P]) ndarray of bool
  31. The thinned image.
  32. See Also
  33. --------
  34. medial_axis
  35. References
  36. ----------
  37. .. [Lee94] T.-C. Lee, R.L. Kashyap and C.-N. Chu, Building skeleton models
  38. via 3-D medial surface/axis thinning algorithms.
  39. Computer Vision, Graphics, and Image Processing, 56(6):462-478, 1994.
  40. .. [Zha84] A fast parallel algorithm for thinning digital patterns,
  41. T. Y. Zhang and C. Y. Suen, Communications of the ACM,
  42. March 1984, Volume 27, Number 3.
  43. Examples
  44. --------
  45. >>> X, Y = np.ogrid[0:9, 0:9]
  46. >>> ellipse = (1./3 * (X - 4)**2 + (Y - 4)**2 < 3**2).astype(bool)
  47. >>> ellipse.view(np.uint8)
  48. array([[0, 0, 0, 1, 1, 1, 0, 0, 0],
  49. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  50. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  51. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  52. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  53. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  54. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  55. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  56. [0, 0, 0, 1, 1, 1, 0, 0, 0]], dtype=uint8)
  57. >>> skel = skeletonize(ellipse)
  58. >>> skel.view(np.uint8)
  59. array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
  60. [0, 0, 0, 0, 0, 0, 0, 0, 0],
  61. [0, 0, 0, 0, 0, 0, 0, 0, 0],
  62. [0, 0, 0, 0, 1, 0, 0, 0, 0],
  63. [0, 0, 0, 0, 1, 0, 0, 0, 0],
  64. [0, 0, 0, 0, 1, 0, 0, 0, 0],
  65. [0, 0, 0, 0, 1, 0, 0, 0, 0],
  66. [0, 0, 0, 0, 0, 0, 0, 0, 0],
  67. [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
  68. """
  69. image = image.astype(bool, order="C", copy=False)
  70. if method not in {'zhang', 'lee', None}:
  71. raise ValueError(
  72. f'skeletonize method should be either "lee" or "zhang", ' f'got {method}.'
  73. )
  74. if image.ndim == 2 and (method is None or method == 'zhang'):
  75. skeleton = _skeletonize_zhang(image)
  76. elif image.ndim == 3 and method == 'zhang':
  77. raise ValueError('skeletonize method "zhang" only works for 2D ' 'images.')
  78. elif image.ndim == 3 or (image.ndim == 2 and method == 'lee'):
  79. skeleton = _skeletonize_lee(image)
  80. else:
  81. raise ValueError(
  82. f'skeletonize requires a 2D or 3D image as input, ' f'got {image.ndim}D.'
  83. )
  84. return skeleton
  85. def _skeletonize_zhang(image):
  86. """Return the skeleton of a 2D binary image.
  87. Thinning is used to reduce each connected component in a binary image
  88. to a single-pixel wide skeleton.
  89. Parameters
  90. ----------
  91. image : numpy.ndarray
  92. An image containing the objects to be skeletonized. Zeros or ``False``
  93. represent background, nonzero values or ``True`` are foreground.
  94. Returns
  95. -------
  96. skeleton : ndarray
  97. A matrix containing the thinned image.
  98. See Also
  99. --------
  100. medial_axis, skeletonize, thin
  101. Notes
  102. -----
  103. The algorithm [Zha84]_ works by making successive passes of the image,
  104. removing pixels on object borders. This continues until no
  105. more pixels can be removed. The image is correlated with a
  106. mask that assigns each pixel a number in the range [0...255]
  107. corresponding to each possible pattern of its 8 neighboring
  108. pixels. A look up table is then used to assign the pixels a
  109. value of 0, 1, 2 or 3, which are selectively removed during
  110. the iterations.
  111. Note that this algorithm will give different results than a
  112. medial axis transform, which is also often referred to as
  113. "skeletonization".
  114. References
  115. ----------
  116. .. [Zha84] A fast parallel algorithm for thinning digital patterns,
  117. T. Y. Zhang and C. Y. Suen, Communications of the ACM,
  118. March 1984, Volume 27, Number 3.
  119. Examples
  120. --------
  121. >>> X, Y = np.ogrid[0:9, 0:9]
  122. >>> ellipse = (1./3 * (X - 4)**2 + (Y - 4)**2 < 3**2).astype(bool)
  123. >>> ellipse.view(np.uint8)
  124. array([[0, 0, 0, 1, 1, 1, 0, 0, 0],
  125. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  126. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  127. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  128. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  129. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  130. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  131. [0, 0, 1, 1, 1, 1, 1, 0, 0],
  132. [0, 0, 0, 1, 1, 1, 0, 0, 0]], dtype=uint8)
  133. >>> skel = skeletonize(ellipse)
  134. >>> skel.view(np.uint8)
  135. array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
  136. [0, 0, 0, 0, 0, 0, 0, 0, 0],
  137. [0, 0, 0, 0, 0, 0, 0, 0, 0],
  138. [0, 0, 0, 0, 1, 0, 0, 0, 0],
  139. [0, 0, 0, 0, 1, 0, 0, 0, 0],
  140. [0, 0, 0, 0, 1, 0, 0, 0, 0],
  141. [0, 0, 0, 0, 1, 0, 0, 0, 0],
  142. [0, 0, 0, 0, 0, 0, 0, 0, 0],
  143. [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
  144. """
  145. if image.ndim != 2:
  146. raise ValueError("Zhang's skeletonize method requires a 2D array")
  147. return _fast_skeletonize(image)
  148. # --------- Skeletonization and thinning based on Guo and Hall 1989 ---------
  149. def _generate_thin_luts():
  150. """generate LUTs for thinning algorithm (for reference)"""
  151. def nabe(n):
  152. return np.array([n >> i & 1 for i in range(0, 9)]).astype(bool)
  153. def G1(n):
  154. s = 0
  155. bits = nabe(n)
  156. for i in (0, 2, 4, 6):
  157. if not (bits[i]) and (bits[i + 1] or bits[(i + 2) % 8]):
  158. s += 1
  159. return s == 1
  160. g1_lut = np.array([G1(n) for n in range(256)])
  161. def G2(n):
  162. n1, n2 = 0, 0
  163. bits = nabe(n)
  164. for k in (1, 3, 5, 7):
  165. if bits[k] or bits[k - 1]:
  166. n1 += 1
  167. if bits[k] or bits[(k + 1) % 8]:
  168. n2 += 1
  169. return min(n1, n2) in [2, 3]
  170. g2_lut = np.array([G2(n) for n in range(256)])
  171. g12_lut = g1_lut & g2_lut
  172. def G3(n):
  173. bits = nabe(n)
  174. return not ((bits[1] or bits[2] or not (bits[7])) and bits[0])
  175. def G3p(n):
  176. bits = nabe(n)
  177. return not ((bits[5] or bits[6] or not (bits[3])) and bits[4])
  178. g3_lut = np.array([G3(n) for n in range(256)])
  179. g3p_lut = np.array([G3p(n) for n in range(256)])
  180. g123_lut = g12_lut & g3_lut
  181. g123p_lut = g12_lut & g3p_lut
  182. return g123_lut, g123p_lut
  183. # fmt: off
  184. G123_LUT = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
  185. 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0,
  186. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1,
  187. 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  188. 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1,
  189. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
  190. 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0,
  191. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  192. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  193. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  194. 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
  195. 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0,
  196. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0,
  197. 0, 1, 1, 0, 0, 1, 0, 0, 0], dtype=bool)
  198. G123P_LUT = np.array([0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0,
  199. 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
  200. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  201. 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0,
  202. 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  203. 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  204. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
  205. 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
  206. 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  207. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  208. 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0,
  209. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1,
  210. 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  211. 0, 0, 0, 0, 0, 0, 0, 0, 0], dtype=bool)
  212. # fmt: on
  213. def thin(image, max_num_iter=None):
  214. """
  215. Perform morphological thinning of a binary image.
  216. Parameters
  217. ----------
  218. image : binary (M, N) ndarray
  219. The image to thin. If this input isn't already a binary image,
  220. it gets converted into one: In this case, zero values are considered
  221. background (False), nonzero values are considered foreground (True).
  222. max_num_iter : int, number of iterations, optional
  223. Regardless of the value of this parameter, the thinned image
  224. is returned immediately if an iteration produces no change.
  225. If this parameter is specified it thus sets an upper bound on
  226. the number of iterations performed.
  227. Returns
  228. -------
  229. out : ndarray of bool
  230. Thinned image.
  231. See Also
  232. --------
  233. skeletonize, medial_axis
  234. Notes
  235. -----
  236. This algorithm [1]_ works by making multiple passes over the image,
  237. removing pixels matching a set of criteria designed to thin
  238. connected regions while preserving eight-connected components and
  239. 2 x 2 squares [2]_. In each of the two sub-iterations the algorithm
  240. correlates the intermediate skeleton image with a neighborhood mask,
  241. then looks up each neighborhood in a lookup table indicating whether
  242. the central pixel should be deleted in that sub-iteration.
  243. References
  244. ----------
  245. .. [1] Z. Guo and R. W. Hall, "Parallel thinning with
  246. two-subiteration algorithms," Comm. ACM, vol. 32, no. 3,
  247. pp. 359-373, 1989. :DOI:`10.1145/62065.62074`
  248. .. [2] Lam, L., Seong-Whan Lee, and Ching Y. Suen, "Thinning
  249. Methodologies-A Comprehensive Survey," IEEE Transactions on
  250. Pattern Analysis and Machine Intelligence, Vol 14, No. 9,
  251. p. 879, 1992. :DOI:`10.1109/34.161346`
  252. Examples
  253. --------
  254. >>> square = np.zeros((7, 7), dtype=bool)
  255. >>> square[1:-1, 2:-2] = 1
  256. >>> square[0, 1] = 1
  257. >>> square.view(np.uint8)
  258. array([[0, 1, 0, 0, 0, 0, 0],
  259. [0, 0, 1, 1, 1, 0, 0],
  260. [0, 0, 1, 1, 1, 0, 0],
  261. [0, 0, 1, 1, 1, 0, 0],
  262. [0, 0, 1, 1, 1, 0, 0],
  263. [0, 0, 1, 1, 1, 0, 0],
  264. [0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
  265. >>> skel = thin(square)
  266. >>> skel.view(np.uint8)
  267. array([[0, 1, 0, 0, 0, 0, 0],
  268. [0, 0, 1, 0, 0, 0, 0],
  269. [0, 0, 0, 1, 0, 0, 0],
  270. [0, 0, 0, 1, 0, 0, 0],
  271. [0, 0, 0, 1, 0, 0, 0],
  272. [0, 0, 0, 0, 0, 0, 0],
  273. [0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
  274. """
  275. # check that image is 2d
  276. check_nD(image, 2)
  277. # convert image to uint8 with values in {0, 1}
  278. skel = np.asanyarray(image, dtype=bool).copy().view(np.uint8)
  279. # neighborhood mask
  280. mask = np.array([[8, 4, 2], [16, 0, 1], [32, 64, 128]], dtype=np.uint8)
  281. # iterate until convergence, up to the iteration limit
  282. max_num_iter = max_num_iter or np.inf
  283. num_iter = 0
  284. n_pts_old, n_pts_new = np.inf, np.sum(skel)
  285. while n_pts_old != n_pts_new and num_iter < max_num_iter:
  286. n_pts_old = n_pts_new
  287. # perform the two "subiterations" described in the paper
  288. for lut in [G123_LUT, G123P_LUT]:
  289. # correlate image with neighborhood mask
  290. N = ndi.correlate(skel, mask, mode='constant')
  291. # take deletion decision from this subiteration's LUT
  292. D = np.take(lut, N)
  293. # perform deletion
  294. skel[D] = 0
  295. n_pts_new = np.sum(skel) # count points after thinning
  296. num_iter += 1
  297. return skel.astype(bool)
  298. # --------- Skeletonization by medial axis transform --------
  299. _eight_connect = ndi.generate_binary_structure(2, 2)
  300. def medial_axis(image, mask=None, return_distance=False, *, rng=None):
  301. """Compute the medial axis transform of a binary image.
  302. Parameters
  303. ----------
  304. image : binary ndarray, shape (M, N)
  305. The image of the shape to skeletonize. If this input isn't already a
  306. binary image, it gets converted into one: In this case, zero values are
  307. considered background (False), nonzero values are considered
  308. foreground (True).
  309. mask : binary ndarray, shape (M, N), optional
  310. If a mask is given, only those elements in `image` with a true
  311. value in `mask` are used for computing the medial axis.
  312. return_distance : bool, optional
  313. If true, the distance transform is returned as well as the skeleton.
  314. rng : {`numpy.random.Generator`, int}, optional
  315. Pseudo-random number generator.
  316. By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`).
  317. If `rng` is an int, it is used to seed the generator.
  318. The PRNG determines the order in which pixels are processed for
  319. tiebreaking.
  320. .. versionadded:: 0.19
  321. Returns
  322. -------
  323. out : ndarray of bools
  324. Medial axis transform of the image
  325. dist : ndarray of ints, optional
  326. Distance transform of the image (only returned if `return_distance`
  327. is True)
  328. See Also
  329. --------
  330. skeletonize, thin
  331. Notes
  332. -----
  333. This algorithm computes the medial axis transform of an image
  334. as the ridges of its distance transform.
  335. The different steps of the algorithm are as follows
  336. * A lookup table is used, that assigns 0 or 1 to each configuration of
  337. the 3x3 binary square, whether the central pixel should be removed
  338. or kept. We want a point to be removed if it has more than one neighbor
  339. and if removing it does not change the number of connected components.
  340. * The distance transform to the background is computed, as well as
  341. the cornerness of the pixel.
  342. * The foreground (value of 1) points are ordered by
  343. the distance transform, then the cornerness.
  344. * A cython function is called to reduce the image to its skeleton. It
  345. processes pixels in the order determined at the previous step, and
  346. removes or maintains a pixel according to the lookup table. Because
  347. of the ordering, it is possible to process all pixels in only one
  348. pass.
  349. Examples
  350. --------
  351. >>> square = np.zeros((7, 7), dtype=bool)
  352. >>> square[1:-1, 2:-2] = 1
  353. >>> square.view(np.uint8)
  354. array([[0, 0, 0, 0, 0, 0, 0],
  355. [0, 0, 1, 1, 1, 0, 0],
  356. [0, 0, 1, 1, 1, 0, 0],
  357. [0, 0, 1, 1, 1, 0, 0],
  358. [0, 0, 1, 1, 1, 0, 0],
  359. [0, 0, 1, 1, 1, 0, 0],
  360. [0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
  361. >>> medial_axis(square).view(np.uint8)
  362. array([[0, 0, 0, 0, 0, 0, 0],
  363. [0, 0, 1, 0, 1, 0, 0],
  364. [0, 0, 0, 1, 0, 0, 0],
  365. [0, 0, 0, 1, 0, 0, 0],
  366. [0, 0, 0, 1, 0, 0, 0],
  367. [0, 0, 1, 0, 1, 0, 0],
  368. [0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
  369. """
  370. global _eight_connect
  371. if mask is None:
  372. masked_image = image.astype(bool)
  373. else:
  374. masked_image = image.astype(bool).copy()
  375. masked_image[~mask] = False
  376. #
  377. # Build lookup table - three conditions
  378. # 1. Keep only positive pixels (center_is_foreground array).
  379. # AND
  380. # 2. Keep if removing the pixel results in a different connectivity
  381. # (if the number of connected components is different with and
  382. # without the central pixel)
  383. # OR
  384. # 3. Keep if # pixels in neighborhood is 2 or less
  385. # Note that table is independent of image
  386. center_is_foreground = (np.arange(512) & 2**4).astype(bool)
  387. table = (
  388. center_is_foreground # condition 1.
  389. & (
  390. np.array(
  391. [
  392. ndi.label(_pattern_of(index), _eight_connect)[1]
  393. != ndi.label(_pattern_of(index & ~(2**4)), _eight_connect)[1]
  394. for index in range(512)
  395. ]
  396. ) # condition 2
  397. | np.array([np.sum(_pattern_of(index)) < 3 for index in range(512)])
  398. )
  399. # condition 3
  400. )
  401. # Build distance transform
  402. distance = ndi.distance_transform_edt(masked_image)
  403. if return_distance:
  404. store_distance = distance.copy()
  405. # Corners
  406. # The processing order along the edge is critical to the shape of the
  407. # resulting skeleton: if you process a corner first, that corner will
  408. # be eroded and the skeleton will miss the arm from that corner. Pixels
  409. # with fewer neighbors are more "cornery" and should be processed last.
  410. # We use a cornerness_table lookup table where the score of a
  411. # configuration is the number of background (0-value) pixels in the
  412. # 3x3 neighborhood
  413. cornerness_table = np.array(
  414. [9 - np.sum(_pattern_of(index)) for index in range(512)]
  415. )
  416. corner_score = _table_lookup(masked_image, cornerness_table)
  417. # Define arrays for inner loop
  418. i, j = np.mgrid[0 : image.shape[0], 0 : image.shape[1]]
  419. result = masked_image.copy()
  420. distance = distance[result]
  421. i = np.ascontiguousarray(i[result], dtype=np.intp)
  422. j = np.ascontiguousarray(j[result], dtype=np.intp)
  423. result = np.ascontiguousarray(result, np.uint8)
  424. # Determine the order in which pixels are processed.
  425. # We use a random # for tiebreaking. Assign each pixel in the image a
  426. # predictable, random # so that masking doesn't affect arbitrary choices
  427. # of skeletons
  428. #
  429. generator = np.random.default_rng(rng)
  430. tiebreaker = generator.permutation(np.arange(masked_image.sum()))
  431. order = np.lexsort((tiebreaker, corner_score[masked_image], distance))
  432. order = np.ascontiguousarray(order, dtype=np.int32)
  433. table = np.ascontiguousarray(table, dtype=np.uint8)
  434. # Remove pixels not belonging to the medial axis
  435. _skeletonize_loop(result, i, j, order, table)
  436. result = result.astype(bool)
  437. if mask is not None:
  438. result[~mask] = image[~mask]
  439. if return_distance:
  440. return result, store_distance
  441. else:
  442. return result
  443. def _pattern_of(index):
  444. """
  445. Return the pattern represented by an index value
  446. Byte decomposition of index
  447. """
  448. return np.array(
  449. [
  450. [index & 2**0, index & 2**1, index & 2**2],
  451. [index & 2**3, index & 2**4, index & 2**5],
  452. [index & 2**6, index & 2**7, index & 2**8],
  453. ],
  454. bool,
  455. )
  456. def _table_lookup(image, table):
  457. """
  458. Perform a morphological transform on an image, directed by its
  459. neighbors
  460. Parameters
  461. ----------
  462. image : ndarray
  463. A binary image
  464. table : ndarray
  465. A 512-element table giving the transform of each pixel given
  466. the values of that pixel and its 8-connected neighbors.
  467. Returns
  468. -------
  469. result : ndarray of same shape as `image`
  470. Transformed image
  471. Notes
  472. -----
  473. The pixels are numbered like this::
  474. 0 1 2
  475. 3 4 5
  476. 6 7 8
  477. The index at a pixel is the sum of 2**<pixel-number> for pixels
  478. that evaluate to true.
  479. """
  480. #
  481. # We accumulate into the indexer to get the index into the table
  482. # at each point in the image
  483. #
  484. if image.shape[0] < 3 or image.shape[1] < 3:
  485. image = image.astype(bool)
  486. indexer = np.zeros(image.shape, int)
  487. indexer[1:, 1:] += image[:-1, :-1] * 2**0
  488. indexer[1:, :] += image[:-1, :] * 2**1
  489. indexer[1:, :-1] += image[:-1, 1:] * 2**2
  490. indexer[:, 1:] += image[:, :-1] * 2**3
  491. indexer[:, :] += image[:, :] * 2**4
  492. indexer[:, :-1] += image[:, 1:] * 2**5
  493. indexer[:-1, 1:] += image[1:, :-1] * 2**6
  494. indexer[:-1, :] += image[1:, :] * 2**7
  495. indexer[:-1, :-1] += image[1:, 1:] * 2**8
  496. else:
  497. indexer = _table_lookup_index(np.ascontiguousarray(image, np.uint8))
  498. image = table[indexer]
  499. return image
  500. def _skeletonize_lee(image):
  501. """Compute the skeleton of a binary image.
  502. Thinning is used to reduce each connected component in a binary image
  503. to a single-pixel wide skeleton.
  504. Parameters
  505. ----------
  506. image : ndarray, 2D or 3D
  507. An image containing the objects to be skeletonized. Zeros or ``False``
  508. represent background, nonzero values or ``True`` are foreground.
  509. Returns
  510. -------
  511. skeleton : ndarray of bool
  512. The thinned image.
  513. See Also
  514. --------
  515. skeletonize, medial_axis
  516. Notes
  517. -----
  518. The method of [Lee94]_ uses an octree data structure to examine a 3x3x3
  519. neighborhood of a pixel. The algorithm proceeds by iteratively sweeping
  520. over the image, and removing pixels at each iteration until the image
  521. stops changing. Each iteration consists of two steps: first, a list of
  522. candidates for removal is assembled; then pixels from this list are
  523. rechecked sequentially, to better preserve connectivity of the image.
  524. The algorithm this function implements is different from the algorithms
  525. used by either `skeletonize` or `medial_axis`, thus for 2D images the
  526. results produced by this function are generally different.
  527. References
  528. ----------
  529. .. [Lee94] T.-C. Lee, R.L. Kashyap and C.-N. Chu, Building skeleton models
  530. via 3-D medial surface/axis thinning algorithms.
  531. Computer Vision, Graphics, and Image Processing, 56(6):462-478, 1994.
  532. """
  533. # make sure the image is 3D or 2D
  534. if image.ndim < 2 or image.ndim > 3:
  535. raise ValueError(
  536. "skeletonize can only handle 2D or 3D images; "
  537. f"got image.ndim = {image.ndim} instead."
  538. )
  539. image_o = image.astype(bool, order="C", copy=False)
  540. # make a 2D input image 3D and pad it w/ zeros to simplify dealing w/ boundaries
  541. # NB: careful here to not clobber the original *and* minimize copying
  542. if image.ndim == 2:
  543. image_o = image_o[np.newaxis, ...]
  544. image_o = np.pad(image_o, pad_width=1, mode='constant') # copies
  545. # do the computation
  546. image_o = _compute_thin_image(image_o)
  547. # crop it back and restore the original intensity range
  548. image_o = crop(image_o, crop_width=1)
  549. if image.ndim == 2:
  550. image_o = image_o[0]
  551. return image_o