_regionprops.py 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498
  1. import inspect
  2. import sys
  3. from functools import wraps
  4. from math import atan2, sqrt
  5. from math import pi as PI
  6. from warnings import warn
  7. import numpy as np
  8. from scipy import ndimage as ndi
  9. from scipy.spatial.distance import pdist
  10. from . import _moments
  11. from ._find_contours import find_contours
  12. from ._marching_cubes_lewiner import marching_cubes
  13. from ._regionprops_utils import (
  14. _normalize_spacing,
  15. euler_number,
  16. perimeter,
  17. perimeter_crofton,
  18. )
  19. __all__ = ['regionprops', 'euler_number', 'perimeter', 'perimeter_crofton']
  20. # All values in this PROPS dict correspond to current scikit-image property
  21. # names. The keys in this PROPS dict correspond to deprecated names used in
  22. # prior releases
  23. PROPS = {
  24. 'Area': 'area',
  25. 'BoundingBox': 'bbox',
  26. 'BoundingBoxArea': 'area_bbox',
  27. 'bbox_area': 'area_bbox',
  28. 'CentralMoments': 'moments_central',
  29. 'Centroid': 'centroid',
  30. 'ConvexArea': 'area_convex',
  31. 'convex_area': 'area_convex',
  32. # 'ConvexHull',
  33. 'ConvexImage': 'image_convex',
  34. 'convex_image': 'image_convex',
  35. 'Coordinates': 'coords',
  36. 'Eccentricity': 'eccentricity',
  37. 'EquivDiameter': 'equivalent_diameter_area',
  38. 'equivalent_diameter': 'equivalent_diameter_area',
  39. 'EulerNumber': 'euler_number',
  40. 'Extent': 'extent',
  41. # 'Extrema',
  42. 'FeretDiameter': 'feret_diameter_max',
  43. 'FeretDiameterMax': 'feret_diameter_max',
  44. 'FilledArea': 'area_filled',
  45. 'filled_area': 'area_filled',
  46. 'FilledImage': 'image_filled',
  47. 'filled_image': 'image_filled',
  48. 'HuMoments': 'moments_hu',
  49. 'Image': 'image',
  50. 'InertiaTensor': 'inertia_tensor',
  51. 'InertiaTensorEigvals': 'inertia_tensor_eigvals',
  52. 'IntensityImage': 'image_intensity',
  53. 'intensity_image': 'image_intensity',
  54. 'Label': 'label',
  55. 'LocalCentroid': 'centroid_local',
  56. 'local_centroid': 'centroid_local',
  57. 'MajorAxisLength': 'axis_major_length',
  58. 'major_axis_length': 'axis_major_length',
  59. 'MaxIntensity': 'intensity_max',
  60. 'max_intensity': 'intensity_max',
  61. 'MeanIntensity': 'intensity_mean',
  62. 'mean_intensity': 'intensity_mean',
  63. 'MinIntensity': 'intensity_min',
  64. 'min_intensity': 'intensity_min',
  65. 'std_intensity': 'intensity_std',
  66. 'MinorAxisLength': 'axis_minor_length',
  67. 'minor_axis_length': 'axis_minor_length',
  68. 'Moments': 'moments',
  69. 'NormalizedMoments': 'moments_normalized',
  70. 'Orientation': 'orientation',
  71. 'Perimeter': 'perimeter',
  72. 'CroftonPerimeter': 'perimeter_crofton',
  73. # 'PixelIdxList',
  74. # 'PixelList',
  75. 'Slice': 'slice',
  76. 'Solidity': 'solidity',
  77. # 'SubarrayIdx'
  78. 'WeightedCentralMoments': 'moments_weighted_central',
  79. 'weighted_moments_central': 'moments_weighted_central',
  80. 'WeightedCentroid': 'centroid_weighted',
  81. 'weighted_centroid': 'centroid_weighted',
  82. 'WeightedHuMoments': 'moments_weighted_hu',
  83. 'weighted_moments_hu': 'moments_weighted_hu',
  84. 'WeightedLocalCentroid': 'centroid_weighted_local',
  85. 'weighted_local_centroid': 'centroid_weighted_local',
  86. 'WeightedMoments': 'moments_weighted',
  87. 'weighted_moments': 'moments_weighted',
  88. 'WeightedNormalizedMoments': 'moments_weighted_normalized',
  89. 'weighted_moments_normalized': 'moments_weighted_normalized',
  90. }
  91. COL_DTYPES = {
  92. 'area': float,
  93. 'area_bbox': float,
  94. 'area_convex': float,
  95. 'area_filled': float,
  96. 'axis_major_length': float,
  97. 'axis_minor_length': float,
  98. 'bbox': int,
  99. 'centroid': float,
  100. 'centroid_local': float,
  101. 'centroid_weighted': float,
  102. 'centroid_weighted_local': float,
  103. 'coords': object,
  104. 'coords_scaled': object,
  105. 'eccentricity': float,
  106. 'equivalent_diameter_area': float,
  107. 'euler_number': int,
  108. 'extent': float,
  109. 'feret_diameter_max': float,
  110. 'image': object,
  111. 'image_convex': object,
  112. 'image_filled': object,
  113. 'image_intensity': object,
  114. 'inertia_tensor': float,
  115. 'inertia_tensor_eigvals': float,
  116. 'intensity_max': float,
  117. 'intensity_mean': float,
  118. 'intensity_median': float,
  119. 'intensity_min': float,
  120. 'intensity_std': float,
  121. 'label': int,
  122. 'moments': float,
  123. 'moments_central': float,
  124. 'moments_hu': float,
  125. 'moments_normalized': float,
  126. 'moments_weighted': float,
  127. 'moments_weighted_central': float,
  128. 'moments_weighted_hu': float,
  129. 'moments_weighted_normalized': float,
  130. 'num_pixels': int,
  131. 'orientation': float,
  132. 'perimeter': float,
  133. 'perimeter_crofton': float,
  134. 'slice': object,
  135. 'solidity': float,
  136. }
  137. OBJECT_COLUMNS = [col for col, dtype in COL_DTYPES.items() if dtype == object]
  138. PROP_VALS = set(PROPS.values())
  139. _require_intensity_image = (
  140. 'image_intensity',
  141. 'intensity_max',
  142. 'intensity_mean',
  143. 'intensity_median',
  144. 'intensity_min',
  145. 'intensity_std',
  146. 'moments_weighted',
  147. 'moments_weighted_central',
  148. 'centroid_weighted',
  149. 'centroid_weighted_local',
  150. 'moments_weighted_hu',
  151. 'moments_weighted_normalized',
  152. )
  153. def _infer_number_of_required_args(func):
  154. """Infer the number of required arguments for a given function.
  155. Parameters
  156. ----------
  157. func : callable
  158. The function that is being inspected.
  159. Returns
  160. -------
  161. n_args : int
  162. The number of required arguments for `func`.
  163. """
  164. argspec = inspect.getfullargspec(func)
  165. n_args = len(argspec.args)
  166. if argspec.defaults is not None:
  167. n_args -= len(argspec.defaults)
  168. return n_args
  169. def _infer_regionprop_dtype(func, *, intensity, ndim):
  170. """Infer the dtype of a region property calculated by `func`.
  171. If a region property function always returns the same shape and type of
  172. output regardless of input size, then the dtype is the dtype of the
  173. returned array. Otherwise, the property has object dtype.
  174. Parameters
  175. ----------
  176. func : callable
  177. Function to be tested. The signature should be array[bool] -> Any if
  178. `intensity` is False, or *(array[bool], array[float]) -> Any otherwise.
  179. intensity : bool
  180. Whether the regionprop is calculated using an intensity image.
  181. ndim : int
  182. The number of dimensions for which to check `func`.
  183. Returns
  184. -------
  185. dtype : NumPy data type
  186. The data type of the returned property.
  187. """
  188. mask_1 = np.ones((1,) * ndim, dtype=bool)
  189. mask_1 = np.pad(mask_1, (0, 1), constant_values=False)
  190. mask_2 = np.ones((2,) * ndim, dtype=bool)
  191. mask_2 = np.pad(mask_2, (1, 0), constant_values=False)
  192. propmasks = [mask_1, mask_2]
  193. rng = np.random.default_rng()
  194. if intensity and _infer_number_of_required_args(func) == 2:
  195. def _func(mask):
  196. return func(mask, rng.random(mask.shape))
  197. else:
  198. _func = func
  199. props1, props2 = map(_func, propmasks)
  200. if (
  201. np.isscalar(props1)
  202. and np.isscalar(props2)
  203. or np.array(props1).shape == np.array(props2).shape
  204. ):
  205. dtype = np.array(props1).dtype.type
  206. else:
  207. dtype = np.object_
  208. return dtype
  209. def _cached(f):
  210. @wraps(f)
  211. def wrapper(obj):
  212. cache = obj._cache
  213. prop = f.__name__
  214. if not obj._cache_active:
  215. return f(obj)
  216. if prop not in cache:
  217. cache[prop] = f(obj)
  218. return cache[prop]
  219. return wrapper
  220. def only2d(method):
  221. @wraps(method)
  222. def func2d(self, *args, **kwargs):
  223. if self._ndim > 2:
  224. raise NotImplementedError(
  225. f"Property {method.__name__} is not implemented for 3D images"
  226. )
  227. return method(self, *args, **kwargs)
  228. return func2d
  229. def _inertia_eigvals_to_axes_lengths_3D(inertia_tensor_eigvals):
  230. """Compute ellipsoid axis lengths from inertia tensor eigenvalues.
  231. Parameters
  232. ----------
  233. inertia_tensor_eigvals : sequence of float
  234. A sequence of 3 floating point eigenvalues, sorted in descending order.
  235. Returns
  236. -------
  237. axis_lengths : list of float
  238. The ellipsoid axis lengths sorted in descending order.
  239. Notes
  240. -----
  241. Let a >= b >= c be the ellipsoid semi-axes and s1 >= s2 >= s3 be the
  242. inertia tensor eigenvalues.
  243. The inertia tensor eigenvalues are given for a solid ellipsoid in [1]_.
  244. s1 = 1 / 5 * (a**2 + b**2)
  245. s2 = 1 / 5 * (a**2 + c**2)
  246. s3 = 1 / 5 * (b**2 + c**2)
  247. Rearranging to solve for a, b, c in terms of s1, s2, s3 gives
  248. a = math.sqrt(5 / 2 * ( s1 + s2 - s3))
  249. b = math.sqrt(5 / 2 * ( s1 - s2 + s3))
  250. c = math.sqrt(5 / 2 * (-s1 + s2 + s3))
  251. We can then simply replace sqrt(5/2) by sqrt(10) to get the full axes
  252. lengths rather than the semi-axes lengths.
  253. References
  254. ----------
  255. .. [1] https://en.wikipedia.org/wiki/List_of_moments_of_inertia#List_of_3D_inertia_tensors
  256. """
  257. axis_lengths = []
  258. for ax in range(2, -1, -1):
  259. w = sum(v * -1 if i == ax else v for i, v in enumerate(inertia_tensor_eigvals))
  260. w = max(0, w) # numerical errors can lead to small negative values
  261. axis_lengths.append(sqrt(10 * w))
  262. return axis_lengths
  263. class RegionProperties:
  264. """Provides properties of a labeled image region.
  265. Please refer to `skimage.measure.regionprops` for more information
  266. on the available region properties.
  267. Examples
  268. --------
  269. >>> RegionProperties(
  270. ... slice=(slice(0, 2), slice(0, 4)),
  271. ... label=2,
  272. ... label_image=np.array([[0, 1, 1, 2, 0], [2, 2, 2, 2, 0]]),
  273. ... intensity_image=None,
  274. ... cache_active=False,
  275. ... )
  276. <RegionProperties: label=2, bbox=(0, 0, 2, 4)>
  277. """
  278. def __init__(
  279. self,
  280. slice,
  281. label,
  282. label_image,
  283. intensity_image,
  284. cache_active,
  285. *,
  286. extra_properties=None,
  287. spacing=None,
  288. offset=None,
  289. ):
  290. if intensity_image is not None:
  291. ndim = label_image.ndim
  292. if not (
  293. intensity_image.shape[:ndim] == label_image.shape
  294. and intensity_image.ndim in [ndim, ndim + 1]
  295. ):
  296. raise ValueError(
  297. 'Label and intensity image shapes must match,'
  298. ' except for channel (last) axis.'
  299. )
  300. multichannel = label_image.shape < intensity_image.shape
  301. else:
  302. multichannel = False
  303. self.label = label
  304. if offset is None:
  305. offset = np.zeros((label_image.ndim,), dtype=int)
  306. self._offset = np.array(offset)
  307. self._slice = slice
  308. self.slice = slice
  309. self._label_image = label_image
  310. self._intensity_image = intensity_image
  311. self._cache_active = cache_active
  312. self._cache = {}
  313. self._ndim = label_image.ndim
  314. self._multichannel = multichannel
  315. self._spatial_axes = tuple(range(self._ndim))
  316. if spacing is None:
  317. spacing = np.full(self._ndim, 1.0)
  318. self._spacing = _normalize_spacing(spacing, self._ndim)
  319. self._pixel_area = np.prod(self._spacing)
  320. self._extra_properties = {}
  321. if extra_properties is not None:
  322. for func in extra_properties:
  323. name = func.__name__
  324. if hasattr(self, name):
  325. msg = (
  326. f"Extra property '{name}' is shadowed by existing "
  327. f"property and will be inaccessible. Consider "
  328. f"renaming it."
  329. )
  330. warn(msg)
  331. self._extra_properties = {func.__name__: func for func in extra_properties}
  332. def __getattr__(self, attr):
  333. if attr == "__setstate__":
  334. # When deserializing this object with pickle, `__setstate__`
  335. # is accessed before any other attributes like `self._intensity_image`
  336. # are available which leads to a RecursionError when trying to
  337. # access them later on in this function. So guard against this by
  338. # provoking the default AttributeError (gh-6465).
  339. return self.__getattribute__(attr)
  340. if self._intensity_image is None and attr in _require_intensity_image:
  341. raise AttributeError(
  342. f"Attribute '{attr}' unavailable when `intensity_image` "
  343. f"has not been specified."
  344. )
  345. if attr in self._extra_properties:
  346. func = self._extra_properties[attr]
  347. n_args = _infer_number_of_required_args(func)
  348. # determine whether func requires intensity image
  349. if n_args == 2:
  350. if self._intensity_image is not None:
  351. if self._multichannel:
  352. multichannel_list = [
  353. func(self.image, self.image_intensity[..., i])
  354. for i in range(self.image_intensity.shape[-1])
  355. ]
  356. return np.stack(multichannel_list, axis=-1)
  357. else:
  358. return func(self.image, self.image_intensity)
  359. else:
  360. raise AttributeError(
  361. f'intensity image required to calculate {attr}'
  362. )
  363. elif n_args == 1:
  364. return func(self.image)
  365. else:
  366. raise AttributeError(
  367. f'Custom regionprop function\'s number of arguments must '
  368. f'be 1 or 2, but {attr} takes {n_args} arguments.'
  369. )
  370. elif attr in PROPS and attr.lower() == attr:
  371. if (
  372. self._intensity_image is None
  373. and PROPS[attr] in _require_intensity_image
  374. ):
  375. raise AttributeError(
  376. f"Attribute '{attr}' unavailable when `intensity_image` "
  377. f"has not been specified."
  378. )
  379. warn(
  380. f"`RegionProperties.{attr}` is deprecated starting in "
  381. "version 0.26 and will be removed in version 2.0. Use "
  382. f"`RegionProperties.{PROPS[attr]}` instead. ",
  383. category=FutureWarning,
  384. stacklevel=2,
  385. )
  386. # retrieve deprecated property (excluding old CamelCase ones)
  387. return getattr(self, PROPS[attr])
  388. # Fallback to default behavior, potentially raising an attribute error
  389. return self.__getattribute__(attr)
  390. def __setattr__(self, name, value):
  391. if name in PROPS:
  392. super().__setattr__(PROPS[name], value)
  393. else:
  394. super().__setattr__(name, value)
  395. @property
  396. @_cached
  397. def num_pixels(self):
  398. return np.sum(self.image)
  399. @property
  400. @_cached
  401. def area(self):
  402. return np.sum(self.image) * self._pixel_area
  403. @property
  404. def bbox(self):
  405. """
  406. Returns
  407. -------
  408. A tuple of the bounding box's start coordinates for each dimension,
  409. followed by the end coordinates for each dimension.
  410. """
  411. return tuple(
  412. [self.slice[i].start for i in range(self._ndim)]
  413. + [self.slice[i].stop for i in range(self._ndim)]
  414. )
  415. @property
  416. def area_bbox(self):
  417. return self.image.size * self._pixel_area
  418. @property
  419. def centroid(self):
  420. return tuple(self.coords_scaled.mean(axis=0))
  421. @property
  422. @_cached
  423. def area_convex(self):
  424. return np.sum(self.image_convex) * self._pixel_area
  425. @property
  426. @_cached
  427. def image_convex(self):
  428. from ..morphology.convex_hull import convex_hull_image
  429. return convex_hull_image(self.image)
  430. @property
  431. def coords_scaled(self):
  432. indices = np.argwhere(self.image)
  433. object_offset = np.array([self.slice[i].start for i in range(self._ndim)])
  434. return (object_offset + indices) * self._spacing + self._offset
  435. @property
  436. def coords(self):
  437. indices = np.argwhere(self.image)
  438. object_offset = np.array([self.slice[i].start for i in range(self._ndim)])
  439. return object_offset + indices + self._offset
  440. @property
  441. @only2d
  442. def eccentricity(self):
  443. l1, l2 = self.inertia_tensor_eigvals
  444. if l1 == 0:
  445. return 0
  446. return sqrt(1 - l2 / l1)
  447. @property
  448. def equivalent_diameter_area(self):
  449. return (2 * self._ndim * self.area / PI) ** (1 / self._ndim)
  450. @property
  451. def euler_number(self):
  452. if self._ndim not in [2, 3]:
  453. raise NotImplementedError(
  454. 'Euler number is implemented for 2D and 3D images only'
  455. )
  456. return euler_number(self.image, self._ndim)
  457. @property
  458. def extent(self):
  459. return self.area / self.area_bbox
  460. @property
  461. def feret_diameter_max(self):
  462. identity_convex_hull = np.pad(
  463. self.image_convex, 2, mode='constant', constant_values=0
  464. )
  465. if self._ndim == 2:
  466. coordinates = np.vstack(
  467. find_contours(identity_convex_hull, 0.5, fully_connected='high')
  468. )
  469. elif self._ndim == 3:
  470. coordinates, _, _, _ = marching_cubes(identity_convex_hull, level=0.5)
  471. distances = pdist(coordinates * self._spacing, 'sqeuclidean')
  472. return sqrt(np.max(distances))
  473. @property
  474. def area_filled(self):
  475. return np.sum(self.image_filled) * self._pixel_area
  476. @property
  477. @_cached
  478. def image_filled(self):
  479. structure = np.ones((3,) * self._ndim)
  480. return ndi.binary_fill_holes(self.image, structure)
  481. @property
  482. @_cached
  483. def image(self):
  484. return self._label_image[self.slice] == self.label
  485. @property
  486. @_cached
  487. def inertia_tensor(self):
  488. mu = self.moments_central
  489. return _moments.inertia_tensor(self.image, mu, spacing=self._spacing)
  490. @property
  491. @_cached
  492. def inertia_tensor_eigvals(self):
  493. return _moments.inertia_tensor_eigvals(self.image, T=self.inertia_tensor)
  494. @property
  495. @_cached
  496. def image_intensity(self):
  497. if self._intensity_image is None:
  498. raise AttributeError('No intensity image specified.')
  499. image = (
  500. self.image
  501. if not self._multichannel
  502. else np.expand_dims(self.image, self._ndim)
  503. )
  504. return self._intensity_image[self.slice] * image
  505. def _image_intensity_double(self):
  506. return self.image_intensity.astype(np.float64, copy=False)
  507. @property
  508. def centroid_local(self):
  509. M = self.moments
  510. M0 = M[(0,) * self._ndim]
  511. def _get_element(axis):
  512. return (0,) * axis + (1,) + (0,) * (self._ndim - 1 - axis)
  513. return np.asarray(
  514. tuple(M[_get_element(axis)] / M0 for axis in range(self._ndim))
  515. )
  516. @property
  517. def intensity_max(self):
  518. vals = self.image_intensity[self.image]
  519. return np.max(vals, axis=0).astype(np.float64, copy=False)
  520. @property
  521. def intensity_mean(self):
  522. return np.mean(self.image_intensity[self.image], axis=0)
  523. @property
  524. def intensity_median(self):
  525. return np.median(self.image_intensity[self.image], axis=0)
  526. @property
  527. def intensity_min(self):
  528. vals = self.image_intensity[self.image]
  529. return np.min(vals, axis=0).astype(np.float64, copy=False)
  530. @property
  531. def intensity_std(self):
  532. vals = self.image_intensity[self.image]
  533. return np.std(vals, axis=0)
  534. @property
  535. def axis_major_length(self):
  536. if self._ndim == 2:
  537. l1 = self.inertia_tensor_eigvals[0]
  538. return 4 * sqrt(l1)
  539. elif self._ndim == 3:
  540. # equivalent to _inertia_eigvals_to_axes_lengths_3D(ev)[0]
  541. ev = self.inertia_tensor_eigvals
  542. l2 = 10 * (ev[0] + ev[1] - ev[2])
  543. return sqrt(max(0, l2))
  544. else:
  545. raise ValueError("axis_major_length only available in 2D and 3D")
  546. @property
  547. def axis_minor_length(self):
  548. if self._ndim == 2:
  549. l2 = self.inertia_tensor_eigvals[-1]
  550. return 4 * sqrt(l2)
  551. elif self._ndim == 3:
  552. # equivalent to _inertia_eigvals_to_axes_lengths_3D(ev)[-1]
  553. ev = self.inertia_tensor_eigvals
  554. l2 = 10 * (-ev[0] + ev[1] + ev[2])
  555. # numerical errors can lead to small negative values
  556. return sqrt(max(0, l2))
  557. else:
  558. raise ValueError("axis_minor_length only available in 2D and 3D")
  559. @property
  560. @_cached
  561. def moments(self):
  562. M = _moments.moments(self.image.astype(np.uint8), 3, spacing=self._spacing)
  563. return M
  564. @property
  565. @_cached
  566. def moments_central(self):
  567. mu = _moments.moments_central(
  568. self.image.astype(np.uint8),
  569. self.centroid_local,
  570. order=3,
  571. spacing=self._spacing,
  572. )
  573. return mu
  574. @property
  575. @only2d
  576. def moments_hu(self):
  577. if any(s != 1.0 for s in self._spacing):
  578. raise NotImplementedError('`moments_hu` supports spacing = (1, 1) only')
  579. return _moments.moments_hu(self.moments_normalized)
  580. @property
  581. @_cached
  582. def moments_normalized(self):
  583. return _moments.moments_normalized(
  584. self.moments_central, 3, spacing=self._spacing
  585. )
  586. @property
  587. @only2d
  588. def orientation(self):
  589. a, b, b, c = self.inertia_tensor.flat
  590. if a - c == 0:
  591. if b < 0:
  592. return PI / 4.0
  593. else:
  594. return -PI / 4.0
  595. else:
  596. return 0.5 * atan2(-2 * b, c - a)
  597. @property
  598. @only2d
  599. def perimeter(self):
  600. if len(np.unique(self._spacing)) != 1:
  601. raise NotImplementedError('`perimeter` supports isotropic spacings only')
  602. return perimeter(self.image, 4) * self._spacing[0]
  603. @property
  604. @only2d
  605. def perimeter_crofton(self):
  606. if len(np.unique(self._spacing)) != 1:
  607. raise NotImplementedError('`perimeter` supports isotropic spacings only')
  608. return perimeter_crofton(self.image, 4) * self._spacing[0]
  609. @property
  610. def solidity(self):
  611. return self.area / self.area_convex
  612. @property
  613. def centroid_weighted(self):
  614. ctr = self.centroid_weighted_local
  615. return tuple(
  616. idx + slc.start * spc
  617. for idx, slc, spc in zip(ctr, self.slice, self._spacing)
  618. )
  619. @property
  620. def centroid_weighted_local(self):
  621. M = self.moments_weighted
  622. M0 = M[(0,) * self._ndim]
  623. def _get_element(axis):
  624. return (0,) * axis + (1,) + (0,) * (self._ndim - 1 - axis)
  625. return np.asarray(
  626. tuple(M[_get_element(axis)] / M0 for axis in range(self._ndim))
  627. )
  628. @property
  629. @_cached
  630. def moments_weighted(self):
  631. image = self._image_intensity_double()
  632. if self._multichannel:
  633. moments = np.stack(
  634. [
  635. _moments.moments(image[..., i], order=3, spacing=self._spacing)
  636. for i in range(image.shape[-1])
  637. ],
  638. axis=-1,
  639. )
  640. else:
  641. moments = _moments.moments(image, order=3, spacing=self._spacing)
  642. return moments
  643. @property
  644. @_cached
  645. def moments_weighted_central(self):
  646. ctr = self.centroid_weighted_local
  647. image = self._image_intensity_double()
  648. if self._multichannel:
  649. moments_list = [
  650. _moments.moments_central(
  651. image[..., i], center=ctr[..., i], order=3, spacing=self._spacing
  652. )
  653. for i in range(image.shape[-1])
  654. ]
  655. moments = np.stack(moments_list, axis=-1)
  656. else:
  657. moments = _moments.moments_central(
  658. image, ctr, order=3, spacing=self._spacing
  659. )
  660. return moments
  661. @property
  662. @only2d
  663. def moments_weighted_hu(self):
  664. if not (np.array(self._spacing) == np.array([1, 1])).all():
  665. raise NotImplementedError('`moments_hu` supports spacing = (1, 1) only')
  666. nu = self.moments_weighted_normalized
  667. if self._multichannel:
  668. nchannels = self._intensity_image.shape[-1]
  669. return np.stack(
  670. [_moments.moments_hu(nu[..., i]) for i in range(nchannels)],
  671. axis=-1,
  672. )
  673. else:
  674. return _moments.moments_hu(nu)
  675. @property
  676. @_cached
  677. def moments_weighted_normalized(self):
  678. mu = self.moments_weighted_central
  679. if self._multichannel:
  680. nchannels = self._intensity_image.shape[-1]
  681. return np.stack(
  682. [
  683. _moments.moments_normalized(
  684. mu[..., i], order=3, spacing=self._spacing
  685. )
  686. for i in range(nchannels)
  687. ],
  688. axis=-1,
  689. )
  690. else:
  691. return _moments.moments_normalized(mu, order=3, spacing=self._spacing)
  692. def __iter__(self):
  693. props = PROP_VALS
  694. if self._intensity_image is None:
  695. unavailable_props = _require_intensity_image
  696. props = props.difference(unavailable_props)
  697. return iter(sorted(props))
  698. def __getitem__(self, key):
  699. if key in PROPS:
  700. warn(
  701. f"`RegionProperties[{key!r}]` is deprecated starting in "
  702. "version 0.26 and will be removed in version 2.0. Use "
  703. f"`RegionProperties[{PROPS[key]!r}]` instead. ",
  704. category=FutureWarning,
  705. stacklevel=2,
  706. )
  707. key = PROPS[key]
  708. return getattr(self, key)
  709. def __eq__(self, other):
  710. if not isinstance(other, RegionProperties):
  711. return False
  712. for key in PROP_VALS:
  713. try:
  714. # so that NaNs are equal
  715. np.testing.assert_equal(
  716. getattr(self, key, None), getattr(other, key, None)
  717. )
  718. except AssertionError:
  719. return False
  720. return True
  721. def __repr__(self):
  722. cls_name = type(self).__qualname__
  723. out = f"<{cls_name}: label={self.label!r}, bbox={self.bbox}>"
  724. return out
  725. # For compatibility with code written prior to 0.16
  726. _RegionProperties = RegionProperties
  727. def _props_to_dict(regions, properties=('label', 'bbox'), separator='-'):
  728. """Convert image region properties list into a column dictionary.
  729. Parameters
  730. ----------
  731. regions : (K,) list
  732. List of RegionProperties objects as returned by :func:`regionprops`.
  733. properties : tuple or list of str, optional
  734. Properties that will be included in the resulting dictionary
  735. For a list of available properties, please see :func:`regionprops`.
  736. Users should remember to add "label" to keep track of region
  737. identities.
  738. separator : str, optional
  739. For non-scalar properties not listed in OBJECT_COLUMNS, each element
  740. will appear in its own column, with the index of that element separated
  741. from the property name by this separator. For example, the inertia
  742. tensor of a 2D region will appear in four columns:
  743. ``inertia_tensor-0-0``, ``inertia_tensor-0-1``, ``inertia_tensor-1-0``,
  744. and ``inertia_tensor-1-1`` (where the separator is ``-``).
  745. Object columns are those that cannot be split in this way because the
  746. number of columns would change depending on the object. For example,
  747. ``image`` and ``coords``.
  748. Returns
  749. -------
  750. out_dict : dict
  751. Dictionary mapping property names to an array of values of that
  752. property, one value per region. This dictionary can be used as input to
  753. pandas ``DataFrame`` to map property names to columns in the frame and
  754. regions to rows.
  755. Notes
  756. -----
  757. Each column contains either a scalar property, an object property, or an
  758. element in a multidimensional array.
  759. Properties with scalar values for each region, such as "eccentricity", will
  760. appear as a float or int array with that property name as key.
  761. Multidimensional properties *of fixed size* for a given image dimension,
  762. such as "centroid" (every centroid will have three elements in a 3D image,
  763. no matter the region size), will be split into that many columns, with the
  764. name {property_name}{separator}{element_num} (for 1D properties),
  765. {property_name}{separator}{elem_num0}{separator}{elem_num1} (for 2D
  766. properties), and so on.
  767. For multidimensional properties that don't have a fixed size, such as
  768. "image" (the image of a region varies in size depending on the region
  769. size), an object array will be used, with the corresponding property name
  770. as the key.
  771. Examples
  772. --------
  773. >>> from skimage import data, util, measure
  774. >>> image = data.coins()
  775. >>> label_image = measure.label(image > 110, connectivity=image.ndim)
  776. >>> proplist = regionprops(label_image, image)
  777. >>> props = _props_to_dict(proplist, properties=['label', 'inertia_tensor',
  778. ... 'inertia_tensor_eigvals'])
  779. >>> props # doctest: +ELLIPSIS +SKIP
  780. {'label': array([ 1, 2, ...]), ...
  781. 'inertia_tensor-0-0': array([ 4.012...e+03, 8.51..., ...]), ...
  782. ...,
  783. 'inertia_tensor_eigvals-1': array([ 2.67...e+02, 2.83..., ...])}
  784. The resulting dictionary can be directly passed to pandas, if installed, to
  785. obtain a clean DataFrame:
  786. >>> import pandas as pd # doctest: +SKIP
  787. >>> data = pd.DataFrame(props) # doctest: +SKIP
  788. >>> data.head() # doctest: +SKIP
  789. label inertia_tensor-0-0 ... inertia_tensor_eigvals-1
  790. 0 1 4012.909888 ... 267.065503
  791. 1 2 8.514739 ... 2.834806
  792. 2 3 0.666667 ... 0.000000
  793. 3 4 0.000000 ... 0.000000
  794. 4 5 0.222222 ... 0.111111
  795. """
  796. out = {}
  797. n = len(regions)
  798. for prop in properties:
  799. r = regions[0]
  800. # Copy the original property name so the output will have the
  801. # user-provided property name in the case of deprecated names.
  802. orig_prop = prop
  803. # determine the current property name for any deprecated property.
  804. prop = PROPS.get(prop, prop)
  805. rp = getattr(r, prop)
  806. if prop in COL_DTYPES:
  807. dtype = COL_DTYPES[prop]
  808. else:
  809. func = r._extra_properties[prop]
  810. dtype = _infer_regionprop_dtype(
  811. func,
  812. intensity=r._intensity_image is not None,
  813. ndim=r.image.ndim,
  814. )
  815. # scalars and objects are dedicated one column per prop
  816. # array properties are raveled into multiple columns
  817. # for more info, refer to notes 1
  818. if np.isscalar(rp) or prop in OBJECT_COLUMNS or dtype is np.object_:
  819. column_buffer = np.empty(n, dtype=dtype)
  820. for i in range(n):
  821. column_buffer[i] = regions[i][prop]
  822. out[orig_prop] = np.copy(column_buffer)
  823. else:
  824. # precompute property column names and locations
  825. modified_props = []
  826. locs = []
  827. for ind in np.ndindex(np.shape(rp)):
  828. modified_props.append(separator.join(map(str, (orig_prop,) + ind)))
  829. locs.append(ind if len(ind) > 1 else ind[0])
  830. # fill temporary column data_array
  831. n_columns = len(locs)
  832. column_data = np.empty((n, n_columns), dtype=dtype)
  833. for k in range(n):
  834. # we coerce to a numpy array to ensure structures like
  835. # tuple-of-arrays expand correctly into columns
  836. rp = np.asarray(regions[k][prop])
  837. for i, loc in enumerate(locs):
  838. column_data[k, i] = rp[loc]
  839. # add the columns to the output dictionary
  840. for i, modified_prop in enumerate(modified_props):
  841. out[modified_prop] = column_data[:, i]
  842. return out
  843. def regionprops_table(
  844. label_image,
  845. intensity_image=None,
  846. properties=('label', 'bbox'),
  847. *,
  848. cache=True,
  849. separator='-',
  850. extra_properties=None,
  851. spacing=None,
  852. ):
  853. """Compute region properties and return them as a pandas-compatible table.
  854. The return value is a dictionary mapping property names to value arrays.
  855. This dictionary can be used as input to ``pandas.DataFrame`` to result in
  856. a "tidy" [1]_ table with one region per row and one property per column.
  857. Use this function typically when you want to do downstream data analysis,
  858. or save region data to disk in a structured way. One downside of this
  859. function is that it breaks multi-dimensional properties into independent
  860. columns; for example, the region centroids of a 3D image end up in three
  861. different columns, one per dimension. If you need to do complex
  862. computations with the region properties, using
  863. :func:`skimage.measure.regionprops` might be more fitting.
  864. .. versionadded:: 0.16
  865. Parameters
  866. ----------
  867. label_image : (M, N[, P]) ndarray
  868. Label image. Labels with value 0 are ignored.
  869. intensity_image : (M, N[, P][, C]) ndarray, optional
  870. Intensity (input) image of same shape as label image, plus
  871. optionally an extra dimension for multichannel data. The channel dimension,
  872. if present, must be the last axis. Default is None.
  873. .. versionchanged:: 0.18.0
  874. The ability to provide an extra dimension for channels was added.
  875. properties : tuple or list of str, optional
  876. Properties that will be included in the resulting dictionary
  877. For a list of available properties, please see :func:`regionprops`.
  878. Users should remember to add "label" to keep track of region
  879. identities.
  880. cache : bool, optional
  881. Determine whether to cache calculated properties. The computation is
  882. much faster for cached properties, whereas the memory consumption
  883. increases.
  884. separator : str, optional
  885. For non-scalar properties not listed in OBJECT_COLUMNS, each element
  886. will appear in its own column, with the index of that element separated
  887. from the property name by this separator. For example, the inertia
  888. tensor of a 2D region will appear in four columns:
  889. ``inertia_tensor-0-0``, ``inertia_tensor-0-1``, ``inertia_tensor-1-0``,
  890. and ``inertia_tensor-1-1`` (where the separator is ``-``).
  891. Object columns are those that cannot be split in this way because the
  892. number of columns would change depending on the object. For example,
  893. ``image`` and ``coords``.
  894. extra_properties : iterable of callables
  895. Add extra property computation functions that are not included with
  896. skimage. The name of the property is derived from the function name,
  897. the dtype is inferred by calling the function on a small sample.
  898. If the name of an extra property clashes with the name of an existing
  899. property the extra property will not be visible and a UserWarning is
  900. issued. A property computation function must take a region mask as its
  901. first argument. If the property requires an intensity image, it must
  902. accept the intensity image as the second argument.
  903. spacing : tuple of float, shape (ndim,)
  904. The pixel spacing along each axis of the image.
  905. Returns
  906. -------
  907. out_dict : dict
  908. Dictionary mapping property names to an array of values of that
  909. property, one value per region. This dictionary can be used as input to
  910. pandas ``DataFrame`` to map property names to columns in the frame and
  911. regions to rows. If the image has no regions,
  912. the arrays will have length 0, but the correct type.
  913. Notes
  914. -----
  915. Each column contains either a scalar property, an object property, or an
  916. element in a multidimensional array.
  917. Properties with scalar values for each region, such as "eccentricity", will
  918. appear as a float or int array with that property name as key.
  919. Multidimensional properties *of fixed size* for a given image dimension,
  920. such as "centroid" (every centroid will have three elements in a 3D image,
  921. no matter the region size), will be split into that many columns, with the
  922. name {property_name}{separator}{element_num} (for 1D properties),
  923. {property_name}{separator}{elem_num0}{separator}{elem_num1} (for 2D
  924. properties), and so on.
  925. For multidimensional properties that don't have a fixed size, such as
  926. "image" (the image of a region varies in size depending on the region
  927. size), an object array will be used, with the corresponding property name
  928. as the key.
  929. References
  930. ----------
  931. .. [1] Wickham, H (2014) "Tidy Data" Journal of Statistical Software,
  932. 59(10), 1–23. https://doi.org/10.18637/jss.v059.i10
  933. https://vita.had.co.nz/papers/tidy-data.pdf
  934. Examples
  935. --------
  936. >>> from skimage import data, util, measure
  937. >>> image = data.coins()
  938. >>> label_image = measure.label(image > 110, connectivity=image.ndim)
  939. >>> props = measure.regionprops_table(label_image, image,
  940. ... properties=['label', 'inertia_tensor',
  941. ... 'inertia_tensor_eigvals'])
  942. >>> props # doctest: +ELLIPSIS +SKIP
  943. {'label': array([ 1, 2, ...]), ...
  944. 'inertia_tensor-0-0': array([ 4.012...e+03, 8.51..., ...]), ...
  945. ...,
  946. 'inertia_tensor_eigvals-1': array([ 2.67...e+02, 2.83..., ...])}
  947. The resulting dictionary can be directly passed to pandas, if installed, to
  948. obtain a clean DataFrame:
  949. >>> import pandas as pd # doctest: +SKIP
  950. >>> data = pd.DataFrame(props) # doctest: +SKIP
  951. >>> data.head() # doctest: +SKIP
  952. label inertia_tensor-0-0 ... inertia_tensor_eigvals-1
  953. 0 1 4012.909888 ... 267.065503
  954. 1 2 8.514739 ... 2.834806
  955. 2 3 0.666667 ... 0.000000
  956. 3 4 0.000000 ... 0.000000
  957. 4 5 0.222222 ... 0.111111
  958. [5 rows x 7 columns]
  959. If we want to measure a feature that does not come as a built-in
  960. property, we can define custom functions and pass them as
  961. ``extra_properties``. For example, we can create a custom function
  962. that measures the intensity quartiles in a region:
  963. >>> from skimage import data, util, measure
  964. >>> import numpy as np
  965. >>> def quartiles(regionmask, intensity):
  966. ... return np.percentile(intensity[regionmask], q=(25, 50, 75))
  967. >>>
  968. >>> image = data.coins()
  969. >>> label_image = measure.label(image > 110, connectivity=image.ndim)
  970. >>> props = measure.regionprops_table(label_image, intensity_image=image,
  971. ... properties=('label',),
  972. ... extra_properties=(quartiles,))
  973. >>> import pandas as pd # doctest: +SKIP
  974. >>> pd.DataFrame(props).head() # doctest: +SKIP
  975. label quartiles-0 quartiles-1 quartiles-2
  976. 0 1 117.00 123.0 130.0
  977. 1 2 111.25 112.0 114.0
  978. 2 3 111.00 111.0 111.0
  979. 3 4 111.00 111.5 112.5
  980. 4 5 112.50 113.0 114.0
  981. """
  982. regions = regionprops(
  983. label_image,
  984. intensity_image=intensity_image,
  985. cache=cache,
  986. extra_properties=extra_properties,
  987. spacing=spacing,
  988. )
  989. if extra_properties is not None:
  990. properties = list(properties) + [prop.__name__ for prop in extra_properties]
  991. if len(regions) == 0:
  992. ndim = label_image.ndim
  993. label_image = np.zeros((3,) * ndim, dtype=int)
  994. label_image[(1,) * ndim] = 1
  995. if intensity_image is not None:
  996. intensity_image = np.zeros(
  997. label_image.shape + intensity_image.shape[ndim:],
  998. dtype=intensity_image.dtype,
  999. )
  1000. regions = regionprops(
  1001. label_image,
  1002. intensity_image=intensity_image,
  1003. cache=cache,
  1004. extra_properties=extra_properties,
  1005. spacing=spacing,
  1006. )
  1007. out_d = _props_to_dict(regions, properties=properties, separator=separator)
  1008. return {k: v[:0] for k, v in out_d.items()}
  1009. return _props_to_dict(regions, properties=properties, separator=separator)
  1010. def regionprops(
  1011. label_image,
  1012. intensity_image=None,
  1013. cache=True,
  1014. *,
  1015. extra_properties=None,
  1016. spacing=None,
  1017. offset=None,
  1018. ):
  1019. r"""Measure properties of labeled image regions.
  1020. Region properties are evaluated on demand and come in diverse types. If
  1021. you want to do tabular data analysis of specific properties, consider
  1022. using :func:`skimage.measure.regionprops_table` instead.
  1023. Parameters
  1024. ----------
  1025. label_image : (M, N[, P]) ndarray
  1026. Label image. Labels with value 0 are ignored.
  1027. .. versionchanged:: 0.14.1
  1028. Previously, ``label_image`` was processed by ``numpy.squeeze`` and
  1029. so any number of singleton dimensions was allowed. This resulted in
  1030. inconsistent handling of images with singleton dimensions. To
  1031. recover the old behaviour, use
  1032. ``regionprops(np.squeeze(label_image), ...)``.
  1033. intensity_image : (M, N[, P][, C]) ndarray, optional
  1034. Intensity (input) image of same shape as label image, plus
  1035. optionally an extra dimension for multichannel data. Currently,
  1036. this extra channel dimension, if present, must be the last axis.
  1037. Default is None.
  1038. .. versionchanged:: 0.18.0
  1039. The ability to provide an extra dimension for channels was added.
  1040. cache : bool, optional
  1041. Determine whether to cache calculated properties. The computation is
  1042. much faster for cached properties, whereas the memory consumption
  1043. increases.
  1044. extra_properties : iterable of callables
  1045. Add extra property computation functions that are not included with
  1046. skimage. The name of the property is derived from the function name
  1047. and its dtype is inferred by calling the function on a small sample.
  1048. If the name of an extra property clashes with the name of an existing
  1049. property, the extra property will not be visible and a UserWarning will be
  1050. issued. A property computation function must take `label_image` as its
  1051. first argument. If the property requires an intensity image, it must
  1052. accept `intensity_image` as the second argument.
  1053. spacing : tuple of float, shape (ndim,)
  1054. The pixel spacing along each axis of the image.
  1055. offset : array-like of int, shape `(label_image.ndim,)`, optional
  1056. Coordinates of the origin ("top-left" corner) of the label image.
  1057. Normally this is ([0, ]0, 0), but it might be different if one wants
  1058. to obtain regionprops of subvolumes within a larger volume.
  1059. Returns
  1060. -------
  1061. properties : list of RegionProperties
  1062. Each item of the list corresponds to one labeled image region,
  1063. and can be accessed using the attributes listed below.
  1064. Notes
  1065. -----
  1066. The following properties can be accessed as attributes or keys:
  1067. **area** : float
  1068. Area of the region, i.e., number of pixels of the region scaled by
  1069. pixel area (as determined by `spacing`).
  1070. **area_bbox** : float
  1071. Area of the bounding box, i.e., number of pixels of the region's
  1072. bounding box scaled by pixel area (as determined by `spacing`).
  1073. **area_convex** : float
  1074. Area of the convex hull image, which is the smallest convex
  1075. polygon that encloses the region.
  1076. **area_filled** : float
  1077. Area of the region with all the holes filled in.
  1078. **axis_major_length** : float
  1079. The length of the major axis of the ellipse that has the same
  1080. normalized second central moments as the region.
  1081. **axis_minor_length** : float
  1082. The length of the minor axis of the ellipse that has the same
  1083. normalized second central moments as the region.
  1084. **bbox** : tuple
  1085. Bounding box ``(min_row, min_col, max_row, max_col)``.
  1086. Pixels belonging to the bounding box are in the half-open interval
  1087. ``[min_row; max_row)`` and ``[min_col; max_col)``.
  1088. **centroid** : array
  1089. Centroid coordinate tuple ``(row, col)``.
  1090. **centroid_local** : array
  1091. Centroid coordinate tuple ``(row, col)``, relative to region bounding
  1092. box.
  1093. **centroid_weighted** : array
  1094. Centroid coordinate tuple ``(row, col)`` weighted with intensity
  1095. image.
  1096. **centroid_weighted_local** : array
  1097. Centroid coordinate tuple ``(row, col)``, relative to region bounding
  1098. box, weighted with intensity image.
  1099. **coords_scaled** : (K, 2) ndarray
  1100. Coordinate list ``(row, col)`` of the region scaled by `spacing`.
  1101. **coords** : (K, 2) ndarray
  1102. Coordinate list ``(row, col)`` of the region.
  1103. **eccentricity** : float
  1104. Eccentricity of the ellipse that has the same second moments as the
  1105. region. The eccentricity is the ratio of the focal distance
  1106. (distance between focal points) over the major axis length.
  1107. The value is in the interval [0, 1).
  1108. When it is 0, the ellipse becomes a circle.
  1109. **equivalent_diameter_area** : float
  1110. The diameter of a circle with the same area as the region.
  1111. **euler_number** : int
  1112. Euler characteristic of the set of non-zero pixels.
  1113. Computed as number of connected components subtracted by number of
  1114. holes (input.ndim connectivity). In 3D, number of connected
  1115. components plus number of holes subtracted by number of tunnels.
  1116. **extent** : float
  1117. Ratio of pixels in the region to pixels in the total bounding box.
  1118. Computed as ``area / (rows * cols)``.
  1119. **feret_diameter_max** : float
  1120. Maximum Feret's diameter computed as the longest distance between
  1121. points around a region's convex hull contour as determined by
  1122. ``find_contours`` [5]_.
  1123. **image** : (H, J) ndarray
  1124. Binary region image sliced by the bounding box.
  1125. **image_convex** : (H, J) ndarray
  1126. Binary convex hull image sliced by bounding box.
  1127. **image_filled** : (H, J) ndarray
  1128. Binary region image with filled holes sliced by bounding box.
  1129. **image_intensity** : (H, J) ndarray
  1130. Intensity image sliced by bounding box.
  1131. **inertia_tensor** : ndarray
  1132. Inertia tensor of the region for the rotation around its mass.
  1133. **inertia_tensor_eigvals** : tuple
  1134. The eigenvalues of the inertia tensor in decreasing order.
  1135. **intensity_max** : float
  1136. Value of greatest intensity in the region.
  1137. **intensity_mean** : float
  1138. Average of intensity values in the region.
  1139. **intensity_median** : float
  1140. Value of median intensity in the region.
  1141. **intensity_min** : float
  1142. Value of lowest intensity in the region.
  1143. **intensity_std** : float
  1144. Standard deviation of intensity values in the region.
  1145. **label** : int
  1146. The region's label in the input label image.
  1147. **moments** : (3, 3) ndarray
  1148. Spatial moments up to 3rd order::
  1149. m_ij = sum{ array(row, col) * row^i * col^j }
  1150. where the sum is over the ``row, col`` coordinates of the region.
  1151. **moments_central** : (3, 3) ndarray
  1152. Central moments (translation invariant) up to 3rd order::
  1153. mu_ij = sum{ array(row, col) * (row - row_c)^i * (col - col_c)^j }
  1154. where the sum is over the ``row, col`` coordinates of the region,
  1155. and ``row_c`` and ``col_c`` are the coordinates of the region's centroid.
  1156. **moments_hu** : tuple
  1157. Hu moments (translation, scale and rotation invariant).
  1158. **moments_normalized** : (3, 3) ndarray
  1159. Normalized moments (translation and scale invariant) up to 3rd order::
  1160. nu_ij = mu_ij / m_00^[(i+j)/2 + 1]
  1161. where ``m_00`` is the zeroth spatial moment.
  1162. **moments_weighted** : (3, 3) array
  1163. Spatial moments of intensity image up to 3rd order::
  1164. wm_ij = sum{ array(row, col) * row^i * col^j }
  1165. where the sum is over the ``row, col`` coordinates of the region.
  1166. **moments_weighted_central** : (3, 3) ndarray
  1167. Central moments (translation invariant) of intensity image up to
  1168. 3rd order::
  1169. wmu_ij = sum{ array(row, col) * (row - row_c)^i * (col - col_c)^j }
  1170. where the sum is over the ``row, col`` coordinates of the region,
  1171. and ``row_c`` and ``col_c`` are the coordinates of the region's weighted
  1172. centroid.
  1173. **moments_weighted_hu** : tuple
  1174. Hu moments (translation, scale and rotation invariant) of intensity
  1175. image.
  1176. **moments_weighted_normalized** : (3, 3) ndarray
  1177. Normalized moments (translation and scale invariant) of intensity
  1178. image up to 3rd order::
  1179. wnu_ij = wmu_ij / wm_00^[(i+j)/2 + 1]
  1180. where ``wm_00`` is the zero-th spatial moment (intensity-weighted area).
  1181. **num_pixels** : int
  1182. Number of foreground pixels.
  1183. **orientation** : float
  1184. Angle between the 0th axis (rows) and the major
  1185. axis of the ellipse that has the same second moments as the region,
  1186. ranging from :math:`-\pi/2` to :math:`\pi/2` counter-clockwise.
  1187. **perimeter** : float
  1188. Perimeter of the region which approximates the contour as a line
  1189. through the centers of border pixels using a 4-connectivity.
  1190. **perimeter_crofton** : float
  1191. Perimeter of the region approximated by the Crofton formula in 4
  1192. directions.
  1193. **slice** : tuple of slices
  1194. A slice to extract the region from the input image.
  1195. **solidity** : float
  1196. Ratio of pixels in the region to pixels of the convex hull image.
  1197. `properties` also supports iteration, so that you can do::
  1198. for region in properties:
  1199. print(region, properties[region])
  1200. See Also
  1201. --------
  1202. label
  1203. References
  1204. ----------
  1205. .. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing:
  1206. Core Algorithms. Springer-Verlag, London, 2009.
  1207. .. [2] B. Jähne. Digital Image Processing. Springer-Verlag,
  1208. Berlin-Heidelberg, 6. edition, 2005.
  1209. .. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image
  1210. Features, from Lecture notes in computer science, p. 676. Springer,
  1211. Berlin, 1993.
  1212. .. [4] https://en.wikipedia.org/wiki/Image_moment
  1213. .. [5] W. Pabst, E. Gregorová. Characterization of particles and particle
  1214. systems, pp. 27-28. ICT Prague, 2007.
  1215. https://old.vscht.cz/sil/keramika/Characterization_of_particles/CPPS%20_English%20version_.pdf
  1216. Examples
  1217. --------
  1218. >>> import skimage as ski
  1219. >>> img = ski.util.img_as_ubyte(ski.data.coins()) > 110
  1220. >>> label_img = ski.measure.label(img, connectivity=img.ndim)
  1221. >>> props = ski.measure.regionprops(label_img)
  1222. >>> # centroid of first labeled region
  1223. >>> props[0].centroid
  1224. (22.72987986048314, 81.91228523446583)
  1225. >>> # centroid of first labeled region
  1226. >>> props[0]['centroid']
  1227. (22.72987986048314, 81.91228523446583)
  1228. Add custom measurements by passing functions as ``extra_properties``:
  1229. >>> import numpy as np
  1230. >>> import skimage as ski
  1231. >>> img = ski.util.img_as_ubyte(ski.data.coins()) > 110
  1232. >>> label_img = ski.measure.label(img, connectivity=img.ndim)
  1233. >>> def pixelcount(regionmask):
  1234. ... return np.sum(regionmask)
  1235. >>> props = ski.measure.regionprops(label_img, extra_properties=(pixelcount,))
  1236. >>> # pixelcount of first labeled region
  1237. >>> props[0].pixelcount
  1238. 7741
  1239. >>> # pixelcount of first labeled region
  1240. >>> props[1]['pixelcount']
  1241. 42
  1242. """
  1243. if label_image.ndim not in (2, 3):
  1244. raise TypeError('Only 2-D and 3-D images supported.')
  1245. if not np.issubdtype(label_image.dtype, np.integer):
  1246. if np.issubdtype(label_image.dtype, bool):
  1247. raise TypeError(
  1248. 'Non-integer image types are ambiguous: '
  1249. 'use skimage.measure.label to label the connected '
  1250. 'components of label_image, '
  1251. 'or label_image.astype(np.uint8) to interpret '
  1252. 'the True values as a single label.'
  1253. )
  1254. else:
  1255. raise TypeError('Non-integer label_image types are ambiguous')
  1256. if offset is None:
  1257. offset_arr = np.zeros((label_image.ndim,), dtype=int)
  1258. else:
  1259. offset_arr = np.asarray(offset)
  1260. if offset_arr.ndim != 1 or offset_arr.size != label_image.ndim:
  1261. raise ValueError(
  1262. 'Offset should be an array-like of integers '
  1263. 'of shape (label_image.ndim,); '
  1264. f'{offset} was provided.'
  1265. )
  1266. regions = []
  1267. objects = ndi.find_objects(label_image)
  1268. for i, sl in enumerate(objects):
  1269. if sl is None:
  1270. continue
  1271. label = i + 1
  1272. props = RegionProperties(
  1273. sl,
  1274. label,
  1275. label_image,
  1276. intensity_image,
  1277. cache,
  1278. spacing=spacing,
  1279. extra_properties=extra_properties,
  1280. offset=offset_arr,
  1281. )
  1282. regions.append(props)
  1283. return regions
  1284. def _parse_docs():
  1285. import re
  1286. import textwrap
  1287. doc = regionprops.__doc__ or ''
  1288. arg_regex = r'\*\*(\w+)\*\* \:.*?\n(.*?)(?=\n [\*\S]+)'
  1289. if sys.version_info >= (3, 13):
  1290. arg_regex = r'\*\*(\w+)\*\* \:.*?\n(.*?)(?=\n[\*\S]+)'
  1291. matches = re.finditer(arg_regex, doc, flags=re.DOTALL)
  1292. prop_doc = {m.group(1): textwrap.dedent(m.group(2)) for m in matches}
  1293. return prop_doc
  1294. def _install_properties_docs():
  1295. prop_doc = _parse_docs()
  1296. for p in [member for member in dir(RegionProperties) if not member.startswith('_')]:
  1297. getattr(RegionProperties, p).__doc__ = prop_doc[p]
  1298. if __debug__:
  1299. # don't install docstrings when in optimized/non-debug mode
  1300. _install_properties_docs()