_regionprops_utils.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. from math import sqrt
  2. from numbers import Real
  3. import numpy as np
  4. from scipy import ndimage as ndi
  5. STREL_4 = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], dtype=np.uint8)
  6. STREL_8 = np.ones((3, 3), dtype=np.uint8)
  7. # Coefficients from
  8. # Ohser J., Nagel W., Schladitz K. (2002) The Euler Number of Discretized Sets
  9. # - On the Choice of Adjacency in Homogeneous Lattices.
  10. # In: Mecke K., Stoyan D. (eds) Morphology of Condensed Matter. Lecture Notes
  11. # in Physics, vol 600. Springer, Berlin, Heidelberg.
  12. # The value of coefficients correspond to the contributions to the Euler number
  13. # of specific voxel configurations, which are themselves encoded thanks to a
  14. # LUT. Computing the Euler number from the addition of the contributions of
  15. # local configurations is possible thanks to an integral geometry formula
  16. # (see the paper by Ohser et al. for more details).
  17. EULER_COEFS2D_4 = [0, 1, 0, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, 0, 0, 0]
  18. EULER_COEFS2D_8 = [0, 0, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, 0, 0, -1, 0]
  19. EULER_COEFS3D_26 = np.array(
  20. [
  21. 0,
  22. 1,
  23. 1,
  24. 0,
  25. 1,
  26. 0,
  27. -2,
  28. -1,
  29. 1,
  30. -2,
  31. 0,
  32. -1,
  33. 0,
  34. -1,
  35. -1,
  36. 0,
  37. 1,
  38. 0,
  39. -2,
  40. -1,
  41. -2,
  42. -1,
  43. -1,
  44. -2,
  45. -6,
  46. -3,
  47. -3,
  48. -2,
  49. -3,
  50. -2,
  51. 0,
  52. -1,
  53. 1,
  54. -2,
  55. 0,
  56. -1,
  57. -6,
  58. -3,
  59. -3,
  60. -2,
  61. -2,
  62. -1,
  63. -1,
  64. -2,
  65. -3,
  66. 0,
  67. -2,
  68. -1,
  69. 0,
  70. -1,
  71. -1,
  72. 0,
  73. -3,
  74. -2,
  75. 0,
  76. -1,
  77. -3,
  78. 0,
  79. -2,
  80. -1,
  81. 0,
  82. 1,
  83. 1,
  84. 0,
  85. 1,
  86. -2,
  87. -6,
  88. -3,
  89. 0,
  90. -1,
  91. -3,
  92. -2,
  93. -2,
  94. -1,
  95. -3,
  96. 0,
  97. -1,
  98. -2,
  99. -2,
  100. -1,
  101. 0,
  102. -1,
  103. -3,
  104. -2,
  105. -1,
  106. 0,
  107. 0,
  108. -1,
  109. -3,
  110. 0,
  111. 0,
  112. 1,
  113. -2,
  114. -1,
  115. 1,
  116. 0,
  117. -2,
  118. -1,
  119. -3,
  120. 0,
  121. -3,
  122. 0,
  123. 0,
  124. 1,
  125. -1,
  126. 4,
  127. 0,
  128. 3,
  129. 0,
  130. 3,
  131. 1,
  132. 2,
  133. -1,
  134. -2,
  135. -2,
  136. -1,
  137. -2,
  138. -1,
  139. 1,
  140. 0,
  141. 0,
  142. 3,
  143. 1,
  144. 2,
  145. 1,
  146. 2,
  147. 2,
  148. 1,
  149. 1,
  150. -6,
  151. -2,
  152. -3,
  153. -2,
  154. -3,
  155. -1,
  156. 0,
  157. 0,
  158. -3,
  159. -1,
  160. -2,
  161. -1,
  162. -2,
  163. -2,
  164. -1,
  165. -2,
  166. -3,
  167. -1,
  168. 0,
  169. -1,
  170. 0,
  171. 4,
  172. 3,
  173. -3,
  174. 0,
  175. 0,
  176. 1,
  177. 0,
  178. 1,
  179. 3,
  180. 2,
  181. 0,
  182. -3,
  183. -1,
  184. -2,
  185. -3,
  186. 0,
  187. 0,
  188. 1,
  189. -1,
  190. 0,
  191. 0,
  192. -1,
  193. -2,
  194. 1,
  195. -1,
  196. 0,
  197. -1,
  198. -2,
  199. -2,
  200. -1,
  201. 0,
  202. 1,
  203. 3,
  204. 2,
  205. -2,
  206. 1,
  207. -1,
  208. 0,
  209. 1,
  210. 2,
  211. 2,
  212. 1,
  213. 0,
  214. -3,
  215. -3,
  216. 0,
  217. -1,
  218. -2,
  219. 0,
  220. 1,
  221. -1,
  222. 0,
  223. -2,
  224. 1,
  225. 0,
  226. -1,
  227. -1,
  228. 0,
  229. -1,
  230. -2,
  231. 0,
  232. 1,
  233. -2,
  234. -1,
  235. 3,
  236. 2,
  237. -2,
  238. 1,
  239. 1,
  240. 2,
  241. -1,
  242. 0,
  243. 2,
  244. 1,
  245. -1,
  246. 0,
  247. -2,
  248. 1,
  249. -2,
  250. 1,
  251. 1,
  252. 2,
  253. -2,
  254. 3,
  255. -1,
  256. 2,
  257. -1,
  258. 2,
  259. 0,
  260. 1,
  261. 0,
  262. -1,
  263. -1,
  264. 0,
  265. -1,
  266. 0,
  267. 2,
  268. 1,
  269. -1,
  270. 2,
  271. 0,
  272. 1,
  273. 0,
  274. 1,
  275. 1,
  276. 0,
  277. ]
  278. )
  279. def euler_number(image, connectivity=None):
  280. """Calculate the Euler characteristic in binary image.
  281. For 2D objects, the Euler number is the number of objects minus the number
  282. of holes. For 3D objects, the Euler number is obtained as the number of
  283. objects plus the number of holes, minus the number of tunnels, or loops.
  284. Parameters
  285. ----------
  286. image : (M, N[, P]) ndarray
  287. Input image. If image is not binary, all values greater than zero
  288. are considered as the object.
  289. connectivity : int, optional
  290. Maximum number of orthogonal hops to consider a pixel/voxel
  291. as a neighbor.
  292. Accepted values are ranging from 1 to input.ndim. If ``None``, a full
  293. connectivity of ``input.ndim`` is used.
  294. 4 or 8 neighborhoods are defined for 2D images (connectivity 1 and 2,
  295. respectively).
  296. 6 or 26 neighborhoods are defined for 3D images, (connectivity 1 and 3,
  297. respectively). Connectivity 2 is not defined.
  298. Returns
  299. -------
  300. euler_number : int
  301. Euler characteristic of the set of all objects in the image.
  302. Notes
  303. -----
  304. The Euler characteristic is an integer number that describes the
  305. topology of the set of all objects in the input image. If object is
  306. 4-connected, then background is 8-connected, and conversely.
  307. The computation of the Euler characteristic is based on an integral
  308. geometry formula in discretized space. In practice, a neighborhood
  309. configuration is constructed, and a LUT is applied for each
  310. configuration. The coefficients used are the ones of Ohser et al.
  311. It can be useful to compute the Euler characteristic for several
  312. connectivities. A large relative difference between results
  313. for different connectivities suggests that the image resolution
  314. (with respect to the size of objects and holes) is too low.
  315. References
  316. ----------
  317. .. [1] S. Rivollier. Analyse d’image geometrique et morphometrique par
  318. diagrammes de forme et voisinages adaptatifs generaux. PhD thesis,
  319. 2010. Ecole Nationale Superieure des Mines de Saint-Etienne.
  320. https://tel.archives-ouvertes.fr/tel-00560838
  321. .. [2] Ohser J., Nagel W., Schladitz K. (2002) The Euler Number of
  322. Discretized Sets - On the Choice of Adjacency in Homogeneous
  323. Lattices. In: Mecke K., Stoyan D. (eds) Morphology of Condensed
  324. Matter. Lecture Notes in Physics, vol 600. Springer, Berlin,
  325. Heidelberg.
  326. Examples
  327. --------
  328. >>> import numpy as np
  329. >>> import skimage as ski
  330. >>> SAMPLE = np.zeros((100,100,100));
  331. >>> SAMPLE[40:60, 40:60, 40:60]=1
  332. >>> ski.measure.euler_number(SAMPLE) # doctest: +ELLIPSIS
  333. 1...
  334. >>> SAMPLE[45:55,45:55,45:55] = 0;
  335. >>> ski.measure.euler_number(SAMPLE) # doctest: +ELLIPSIS
  336. 2...
  337. >>> SAMPLE = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0],
  338. ... [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
  339. ... [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
  340. ... [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
  341. ... [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
  342. ... [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
  343. ... [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
  344. ... [1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0],
  345. ... [0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1],
  346. ... [0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1]])
  347. >>> ski.measure.euler_number(SAMPLE)
  348. 0
  349. >>> ski.measure.euler_number(SAMPLE, connectivity=1)
  350. 2
  351. """
  352. # as image can be a label image, transform it to binary
  353. image = (image > 0).astype(int)
  354. image = np.pad(image, pad_width=1, mode='constant')
  355. # check connectivity
  356. if connectivity is None:
  357. connectivity = image.ndim
  358. # config variable is an adjacency configuration. A coefficient given by
  359. # variable coefs is attributed to each configuration in order to get
  360. # the Euler characteristic.
  361. if image.ndim == 2:
  362. config = np.array([[0, 0, 0], [0, 1, 4], [0, 2, 8]])
  363. if connectivity == 1:
  364. coefs = EULER_COEFS2D_4
  365. else:
  366. coefs = EULER_COEFS2D_8
  367. bins = 16
  368. else: # 3D images
  369. if connectivity == 2:
  370. raise NotImplementedError(
  371. 'For 3D images, Euler number is implemented '
  372. 'for connectivities 1 and 3 only'
  373. )
  374. config = np.array(
  375. [
  376. [[0, 0, 0], [0, 0, 0], [0, 0, 0]],
  377. [[0, 0, 0], [0, 1, 4], [0, 2, 8]],
  378. [[0, 0, 0], [0, 16, 64], [0, 32, 128]],
  379. ]
  380. )
  381. if connectivity == 1:
  382. coefs = EULER_COEFS3D_26[::-1]
  383. else:
  384. coefs = EULER_COEFS3D_26
  385. bins = 256
  386. # XF has values in the 0-255 range in 3D, and in the 0-15 range in 2D,
  387. # with one unique value for each binary configuration of the
  388. # 27-voxel cube in 3D / 8-pixel square in 2D, up to symmetries
  389. XF = ndi.convolve(image, config, mode='constant', cval=0)
  390. h = np.bincount(XF.ravel(), minlength=bins)
  391. if image.ndim == 2:
  392. return coefs @ h
  393. else:
  394. return int(0.125 * coefs @ h)
  395. def perimeter(image, neighborhood=4):
  396. """Calculate total perimeter of all objects in binary image.
  397. Parameters
  398. ----------
  399. image : (M, N) ndarray
  400. Binary input image.
  401. neighborhood : 4 or 8, optional
  402. Neighborhood connectivity for border pixel determination. It is used to
  403. compute the contour. A higher neighborhood widens the border on which
  404. the perimeter is computed.
  405. Returns
  406. -------
  407. perimeter : float
  408. Total perimeter of all objects in binary image.
  409. References
  410. ----------
  411. .. [1] K. Benkrid, D. Crookes. Design and FPGA Implementation of
  412. a Perimeter Estimator. The Queen's University of Belfast.
  413. http://www.cs.qub.ac.uk/~d.crookes/webpubs/papers/perimeter.doc
  414. Examples
  415. --------
  416. >>> import skimage as ski
  417. >>> # coins image (binary)
  418. >>> img_coins = ski.data.coins() > 110
  419. >>> # total perimeter of all objects in the image
  420. >>> ski.measure.perimeter(img_coins, neighborhood=4) # doctest: +ELLIPSIS
  421. 7796.867...
  422. >>> ski.measure.perimeter(img_coins, neighborhood=8) # doctest: +ELLIPSIS
  423. 8806.268...
  424. """
  425. if image.ndim != 2:
  426. raise NotImplementedError('`perimeter` supports 2D images only')
  427. if neighborhood == 4:
  428. strel = STREL_4
  429. else:
  430. strel = STREL_8
  431. image = image.astype(np.uint8)
  432. eroded_image = ndi.binary_erosion(image, strel, border_value=0)
  433. border_image = image - eroded_image
  434. perimeter_weights = np.zeros(50, dtype=np.float64)
  435. perimeter_weights[[5, 7, 15, 17, 25, 27]] = 1
  436. perimeter_weights[[21, 33]] = sqrt(2)
  437. perimeter_weights[[13, 23]] = (1 + sqrt(2)) / 2
  438. perimeter_image = ndi.convolve(
  439. border_image,
  440. np.array([[10, 2, 10], [2, 1, 2], [10, 2, 10]]),
  441. mode='constant',
  442. cval=0,
  443. )
  444. # You can also write
  445. # return perimeter_weights[perimeter_image].sum()
  446. # but that was measured as taking much longer than bincount + np.dot (5x
  447. # as much time)
  448. perimeter_histogram = np.bincount(perimeter_image.ravel(), minlength=50)
  449. total_perimeter = perimeter_histogram @ perimeter_weights
  450. return total_perimeter
  451. def perimeter_crofton(image, directions=4):
  452. """Calculate total Crofton perimeter of all objects in binary image.
  453. Parameters
  454. ----------
  455. image : (M, N) ndarray
  456. Input image. If image is not binary, all values greater than zero
  457. are considered as the object.
  458. directions : 2 or 4, optional
  459. Number of directions used to approximate the Crofton perimeter. By
  460. default, 4 is used: it should be more accurate than 2.
  461. Computation time is the same in both cases.
  462. Returns
  463. -------
  464. perimeter : float
  465. Total perimeter of all objects in binary image.
  466. Notes
  467. -----
  468. This measure is based on Crofton formula [1], which is a measure from
  469. integral geometry. It is defined for general curve length evaluation via
  470. a double integral along all directions. In a discrete
  471. space, 2 or 4 directions give a quite good approximation, 4 being more
  472. accurate than 2 for more complex shapes.
  473. Similar to :func:`~.measure.perimeter`, this function returns an
  474. approximation of the perimeter in continuous space.
  475. References
  476. ----------
  477. .. [1] https://en.wikipedia.org/wiki/Crofton_formula
  478. .. [2] S. Rivollier. Analyse d’image geometrique et morphometrique par
  479. diagrammes de forme et voisinages adaptatifs generaux. PhD thesis,
  480. 2010.
  481. Ecole Nationale Superieure des Mines de Saint-Etienne.
  482. https://tel.archives-ouvertes.fr/tel-00560838
  483. Examples
  484. --------
  485. >>> import skimage as ski
  486. >>> # coins image (binary)
  487. >>> img_coins = ski.data.coins() > 110
  488. >>> # total perimeter of all objects in the image
  489. >>> ski.measure.perimeter_crofton(img_coins, directions=2) # doctest: +ELLIPSIS
  490. 8144.578...
  491. >>> ski.measure.perimeter_crofton(img_coins, directions=4) # doctest: +ELLIPSIS
  492. 7837.077...
  493. """
  494. if image.ndim != 2:
  495. raise NotImplementedError('`perimeter_crofton` supports 2D images only')
  496. # as image could be a label image, transform it to binary image
  497. image = (image > 0).astype(np.uint8)
  498. image = np.pad(image, pad_width=1, mode='constant')
  499. XF = ndi.convolve(
  500. image, np.array([[0, 0, 0], [0, 1, 4], [0, 2, 8]]), mode='constant', cval=0
  501. )
  502. h = np.bincount(XF.ravel(), minlength=16)
  503. # definition of the LUT
  504. if directions == 2:
  505. coefs = [
  506. 0,
  507. np.pi / 2,
  508. 0,
  509. 0,
  510. 0,
  511. np.pi / 2,
  512. 0,
  513. 0,
  514. np.pi / 2,
  515. np.pi,
  516. 0,
  517. 0,
  518. np.pi / 2,
  519. np.pi,
  520. 0,
  521. 0,
  522. ]
  523. else:
  524. coefs = [
  525. 0,
  526. np.pi / 4 * (1 + 1 / (np.sqrt(2))),
  527. np.pi / (4 * np.sqrt(2)),
  528. np.pi / (2 * np.sqrt(2)),
  529. 0,
  530. np.pi / 4 * (1 + 1 / (np.sqrt(2))),
  531. 0,
  532. np.pi / (4 * np.sqrt(2)),
  533. np.pi / 4,
  534. np.pi / 2,
  535. np.pi / (4 * np.sqrt(2)),
  536. np.pi / (4 * np.sqrt(2)),
  537. np.pi / 4,
  538. np.pi / 2,
  539. 0,
  540. 0,
  541. ]
  542. total_perimeter = coefs @ h
  543. return total_perimeter
  544. def _normalize_spacing(spacing, ndims):
  545. """Normalize spacing parameter.
  546. The `spacing` parameter should be a sequence of numbers matching
  547. the image dimensions. If `spacing` is a scalar, assume equal
  548. spacing along all dimensions.
  549. Parameters
  550. ----------
  551. spacing : Any
  552. User-provided `spacing` keyword.
  553. ndims : int
  554. Number of image dimensions.
  555. Returns
  556. -------
  557. spacing : array
  558. Corrected spacing.
  559. Raises
  560. ------
  561. ValueError
  562. If `spacing` is invalid.
  563. """
  564. spacing = np.array(spacing)
  565. if spacing.shape == ():
  566. spacing = np.broadcast_to(spacing, shape=(ndims,))
  567. elif spacing.shape != (ndims,):
  568. raise ValueError(
  569. f"spacing isn't a scalar nor a sequence of shape {(ndims,)}, got {spacing}."
  570. )
  571. if not all(isinstance(s, Real) for s in spacing):
  572. raise TypeError(
  573. f"Element of spacing isn't float or integer type, got {spacing}."
  574. )
  575. if not all(np.isfinite(spacing)):
  576. raise ValueError(
  577. f"Invalid spacing parameter. All elements must be finite, got {spacing}."
  578. )
  579. return spacing