scikit-image
1429 строк · 50.1 Кб
1import inspect2import sys3from functools import wraps4from math import atan25from math import pi as PI6from math import sqrt7from warnings import warn8
9import numpy as np10from scipy import ndimage as ndi11from scipy.spatial.distance import pdist12
13from . import _moments14from ._find_contours import find_contours15from ._marching_cubes_lewiner import marching_cubes16from ._regionprops_utils import (17euler_number,18perimeter,19perimeter_crofton,20_normalize_spacing,21)
22
23__all__ = ['regionprops', 'euler_number', 'perimeter', 'perimeter_crofton']24
25
26# All values in this PROPS dict correspond to current scikit-image property
27# names. The keys in this PROPS dict correspond to older names used in prior
28# releases. For backwards compatibility, these older names will continue to
29# work, but will not be documented.
30PROPS = {31'Area': 'area',32'BoundingBox': 'bbox',33'BoundingBoxArea': 'area_bbox',34'bbox_area': 'area_bbox',35'CentralMoments': 'moments_central',36'Centroid': 'centroid',37'ConvexArea': 'area_convex',38'convex_area': 'area_convex',39# 'ConvexHull',40'ConvexImage': 'image_convex',41'convex_image': 'image_convex',42'Coordinates': 'coords',43'Eccentricity': 'eccentricity',44'EquivDiameter': 'equivalent_diameter_area',45'equivalent_diameter': 'equivalent_diameter_area',46'EulerNumber': 'euler_number',47'Extent': 'extent',48# 'Extrema',49'FeretDiameter': 'feret_diameter_max',50'FeretDiameterMax': 'feret_diameter_max',51'FilledArea': 'area_filled',52'filled_area': 'area_filled',53'FilledImage': 'image_filled',54'filled_image': 'image_filled',55'HuMoments': 'moments_hu',56'Image': 'image',57'InertiaTensor': 'inertia_tensor',58'InertiaTensorEigvals': 'inertia_tensor_eigvals',59'IntensityImage': 'image_intensity',60'intensity_image': 'image_intensity',61'Label': 'label',62'LocalCentroid': 'centroid_local',63'local_centroid': 'centroid_local',64'MajorAxisLength': 'axis_major_length',65'major_axis_length': 'axis_major_length',66'MaxIntensity': 'intensity_max',67'max_intensity': 'intensity_max',68'MeanIntensity': 'intensity_mean',69'mean_intensity': 'intensity_mean',70'MinIntensity': 'intensity_min',71'min_intensity': 'intensity_min',72'std_intensity': 'intensity_std',73'MinorAxisLength': 'axis_minor_length',74'minor_axis_length': 'axis_minor_length',75'Moments': 'moments',76'NormalizedMoments': 'moments_normalized',77'Orientation': 'orientation',78'Perimeter': 'perimeter',79'CroftonPerimeter': 'perimeter_crofton',80# 'PixelIdxList',81# 'PixelList',82'Slice': 'slice',83'Solidity': 'solidity',84# 'SubarrayIdx'85'WeightedCentralMoments': 'moments_weighted_central',86'weighted_moments_central': 'moments_weighted_central',87'WeightedCentroid': 'centroid_weighted',88'weighted_centroid': 'centroid_weighted',89'WeightedHuMoments': 'moments_weighted_hu',90'weighted_moments_hu': 'moments_weighted_hu',91'WeightedLocalCentroid': 'centroid_weighted_local',92'weighted_local_centroid': 'centroid_weighted_local',93'WeightedMoments': 'moments_weighted',94'weighted_moments': 'moments_weighted',95'WeightedNormalizedMoments': 'moments_weighted_normalized',96'weighted_moments_normalized': 'moments_weighted_normalized',97}
98
99COL_DTYPES = {100'area': float,101'area_bbox': float,102'area_convex': float,103'area_filled': float,104'axis_major_length': float,105'axis_minor_length': float,106'bbox': int,107'centroid': float,108'centroid_local': float,109'centroid_weighted': float,110'centroid_weighted_local': float,111'coords': object,112'coords_scaled': object,113'eccentricity': float,114'equivalent_diameter_area': float,115'euler_number': int,116'extent': float,117'feret_diameter_max': float,118'image': object,119'image_convex': object,120'image_filled': object,121'image_intensity': object,122'inertia_tensor': float,123'inertia_tensor_eigvals': float,124'intensity_max': float,125'intensity_mean': float,126'intensity_min': float,127'intensity_std': float,128'label': int,129'moments': float,130'moments_central': float,131'moments_hu': float,132'moments_normalized': float,133'moments_weighted': float,134'moments_weighted_central': float,135'moments_weighted_hu': float,136'moments_weighted_normalized': float,137'num_pixels': int,138'orientation': float,139'perimeter': float,140'perimeter_crofton': float,141'slice': object,142'solidity': float,143}
144
145OBJECT_COLUMNS = [col for col, dtype in COL_DTYPES.items() if dtype == object]146
147PROP_VALS = set(PROPS.values())148
149_require_intensity_image = (150'image_intensity',151'intensity_max',152'intensity_mean',153'intensity_min',154'intensity_std',155'moments_weighted',156'moments_weighted_central',157'centroid_weighted',158'centroid_weighted_local',159'moments_weighted_hu',160'moments_weighted_normalized',161)
162
163
164def _infer_number_of_required_args(func):165"""Infer the number of required arguments for a function166
167Parameters
168----------
169func : callable
170The function that is being inspected.
171
172Returns
173-------
174n_args : int
175The number of required arguments of func.
176"""
177argspec = inspect.getfullargspec(func)178n_args = len(argspec.args)179if argspec.defaults is not None:180n_args -= len(argspec.defaults)181return n_args182
183
184def _infer_regionprop_dtype(func, *, intensity, ndim):185"""Infer the dtype of a region property calculated by func.186
187If a region property function always returns the same shape and type of
188output regardless of input size, then the dtype is the dtype of the
189returned array. Otherwise, the property has object dtype.
190
191Parameters
192----------
193func : callable
194Function to be tested. The signature should be array[bool] -> Any if
195intensity is False, or *(array[bool], array[float]) -> Any otherwise.
196intensity : bool
197Whether the regionprop is calculated on an intensity image.
198ndim : int
199The number of dimensions for which to check func.
200
201Returns
202-------
203dtype : NumPy data type
204The data type of the returned property.
205"""
206mask_1 = np.ones((1,) * ndim, dtype=bool)207mask_1 = np.pad(mask_1, (0, 1), constant_values=False)208mask_2 = np.ones((2,) * ndim, dtype=bool)209mask_2 = np.pad(mask_2, (1, 0), constant_values=False)210propmasks = [mask_1, mask_2]211
212rng = np.random.default_rng()213
214if intensity and _infer_number_of_required_args(func) == 2:215
216def _func(mask):217return func(mask, rng.random(mask.shape))218
219else:220_func = func221props1, props2 = map(_func, propmasks)222if (223np.isscalar(props1)224and np.isscalar(props2)225or np.array(props1).shape == np.array(props2).shape226):227dtype = np.array(props1).dtype.type228else:229dtype = np.object_230return dtype231
232
233def _cached(f):234@wraps(f)235def wrapper(obj):236cache = obj._cache237prop = f.__name__238
239if not obj._cache_active:240return f(obj)241
242if prop not in cache:243cache[prop] = f(obj)244
245return cache[prop]246
247return wrapper248
249
250def only2d(method):251@wraps(method)252def func2d(self, *args, **kwargs):253if self._ndim > 2:254raise NotImplementedError(255f"Property {method.__name__} is not implemented for 3D images"256)257return method(self, *args, **kwargs)258
259return func2d260
261
262def _inertia_eigvals_to_axes_lengths_3D(inertia_tensor_eigvals):263"""Compute ellipsoid axis lengths from inertia tensor eigenvalues.264
265Parameters
266---------
267inertia_tensor_eigvals : sequence of float
268A sequence of 3 floating point eigenvalues, sorted in descending order.
269
270Returns
271-------
272axis_lengths : list of float
273The ellipsoid axis lengths sorted in descending order.
274
275Notes
276-----
277Let a >= b >= c be the ellipsoid semi-axes and s1 >= s2 >= s3 be the
278inertia tensor eigenvalues.
279
280The inertia tensor eigenvalues are given for a solid ellipsoid in [1]_.
281s1 = 1 / 5 * (a**2 + b**2)
282s2 = 1 / 5 * (a**2 + c**2)
283s3 = 1 / 5 * (b**2 + c**2)
284
285Rearranging to solve for a, b, c in terms of s1, s2, s3 gives
286a = math.sqrt(5 / 2 * ( s1 + s2 - s3))
287b = math.sqrt(5 / 2 * ( s1 - s2 + s3))
288c = math.sqrt(5 / 2 * (-s1 + s2 + s3))
289
290We can then simply replace sqrt(5/2) by sqrt(10) to get the full axes
291lengths rather than the semi-axes lengths.
292
293References
294----------
295..[1] https://en.wikipedia.org/wiki/List_of_moments_of_inertia#List_of_3D_inertia_tensors # noqa
296"""
297axis_lengths = []298for ax in range(2, -1, -1):299w = sum(v * -1 if i == ax else v for i, v in enumerate(inertia_tensor_eigvals))300axis_lengths.append(sqrt(10 * w))301return axis_lengths302
303
304class RegionProperties:305"""Please refer to `skimage.measure.regionprops` for more information306on the available region properties.
307"""
308
309def __init__(310self,311slice,312label,313label_image,314intensity_image,315cache_active,316*,317extra_properties=None,318spacing=None,319offset=None,320):321if intensity_image is not None:322ndim = label_image.ndim323if not (324intensity_image.shape[:ndim] == label_image.shape325and intensity_image.ndim in [ndim, ndim + 1]326):327raise ValueError(328'Label and intensity image shapes must match,'329' except for channel (last) axis.'330)331multichannel = label_image.shape < intensity_image.shape332else:333multichannel = False334
335self.label = label336if offset is None:337offset = np.zeros((label_image.ndim,), dtype=int)338self._offset = np.array(offset)339
340self._slice = slice341self.slice = slice342self._label_image = label_image343self._intensity_image = intensity_image344
345self._cache_active = cache_active346self._cache = {}347self._ndim = label_image.ndim348self._multichannel = multichannel349self._spatial_axes = tuple(range(self._ndim))350if spacing is None:351spacing = np.full(self._ndim, 1.0)352self._spacing = _normalize_spacing(spacing, self._ndim)353self._pixel_area = np.prod(self._spacing)354
355self._extra_properties = {}356if extra_properties is not None:357for func in extra_properties:358name = func.__name__359if hasattr(self, name):360msg = (361f"Extra property '{name}' is shadowed by existing "362f"property and will be inaccessible. Consider "363f"renaming it."364)365warn(msg)366self._extra_properties = {func.__name__: func for func in extra_properties}367
368def __getattr__(self, attr):369if self._intensity_image is None and attr in _require_intensity_image:370raise AttributeError(371f"Attribute '{attr}' unavailable when `intensity_image` "372f"has not been specified."373)374if attr in self._extra_properties:375func = self._extra_properties[attr]376n_args = _infer_number_of_required_args(func)377# determine whether func requires intensity image378if n_args == 2:379if self._intensity_image is not None:380if self._multichannel:381multichannel_list = [382func(self.image, self.image_intensity[..., i])383for i in range(self.image_intensity.shape[-1])384]385return np.stack(multichannel_list, axis=-1)386else:387return func(self.image, self.image_intensity)388else:389raise AttributeError(390f'intensity image required to calculate {attr}'391)392elif n_args == 1:393return func(self.image)394else:395raise AttributeError(396f'Custom regionprop function\'s number of arguments must '397f'be 1 or 2, but {attr} takes {n_args} arguments.'398)399elif attr in PROPS and attr.lower() == attr:400if (401self._intensity_image is None402and PROPS[attr] in _require_intensity_image403):404raise AttributeError(405f"Attribute '{attr}' unavailable when `intensity_image` "406f"has not been specified."407)408# retrieve deprecated property (excluding old CamelCase ones)409return getattr(self, PROPS[attr])410else:411raise AttributeError(f"'{type(self)}' object has no attribute '{attr}'")412
413def __setattr__(self, name, value):414if name in PROPS:415super().__setattr__(PROPS[name], value)416else:417super().__setattr__(name, value)418
419@property420@_cached421def num_pixels(self):422return np.sum(self.image)423
424@property425@_cached426def area(self):427return np.sum(self.image) * self._pixel_area428
429@property430def bbox(self):431"""432Returns
433-------
434A tuple of the bounding box's start coordinates for each dimension,
435followed by the end coordinates for each dimension
436"""
437return tuple(438[self.slice[i].start for i in range(self._ndim)]439+ [self.slice[i].stop for i in range(self._ndim)]440)441
442@property443def area_bbox(self):444return self.image.size * self._pixel_area445
446@property447def centroid(self):448return tuple(self.coords_scaled.mean(axis=0))449
450@property451@_cached452def area_convex(self):453return np.sum(self.image_convex) * self._pixel_area454
455@property456@_cached457def image_convex(self):458from ..morphology.convex_hull import convex_hull_image459
460return convex_hull_image(self.image)461
462@property463def coords_scaled(self):464indices = np.argwhere(self.image)465object_offset = np.array([self.slice[i].start for i in range(self._ndim)])466return (object_offset + indices) * self._spacing + self._offset467
468@property469def coords(self):470indices = np.argwhere(self.image)471object_offset = np.array([self.slice[i].start for i in range(self._ndim)])472return object_offset + indices + self._offset473
474@property475@only2d476def eccentricity(self):477l1, l2 = self.inertia_tensor_eigvals478if l1 == 0:479return 0480return sqrt(1 - l2 / l1)481
482@property483def equivalent_diameter_area(self):484return (2 * self._ndim * self.area / PI) ** (1 / self._ndim)485
486@property487def euler_number(self):488if self._ndim not in [2, 3]:489raise NotImplementedError(490'Euler number is implemented for ' '2D or 3D images only'491)492return euler_number(self.image, self._ndim)493
494@property495def extent(self):496return self.area / self.area_bbox497
498@property499def feret_diameter_max(self):500identity_convex_hull = np.pad(501self.image_convex, 2, mode='constant', constant_values=0502)503if self._ndim == 2:504coordinates = np.vstack(505find_contours(identity_convex_hull, 0.5, fully_connected='high')506)507elif self._ndim == 3:508coordinates, _, _, _ = marching_cubes(identity_convex_hull, level=0.5)509distances = pdist(coordinates * self._spacing, 'sqeuclidean')510return sqrt(np.max(distances))511
512@property513def area_filled(self):514return np.sum(self.image_filled) * self._pixel_area515
516@property517@_cached518def image_filled(self):519structure = np.ones((3,) * self._ndim)520return ndi.binary_fill_holes(self.image, structure)521
522@property523@_cached524def image(self):525return self._label_image[self.slice] == self.label526
527@property528@_cached529def inertia_tensor(self):530mu = self.moments_central531return _moments.inertia_tensor(self.image, mu, spacing=self._spacing)532
533@property534@_cached535def inertia_tensor_eigvals(self):536return _moments.inertia_tensor_eigvals(self.image, T=self.inertia_tensor)537
538@property539@_cached540def image_intensity(self):541if self._intensity_image is None:542raise AttributeError('No intensity image specified.')543image = (544self.image545if not self._multichannel546else np.expand_dims(self.image, self._ndim)547)548return self._intensity_image[self.slice] * image549
550def _image_intensity_double(self):551return self.image_intensity.astype(np.float64, copy=False)552
553@property554def centroid_local(self):555M = self.moments556M0 = M[(0,) * self._ndim]557
558def _get_element(axis):559return (0,) * axis + (1,) + (0,) * (self._ndim - 1 - axis)560
561return np.asarray(562tuple(M[_get_element(axis)] / M0 for axis in range(self._ndim))563)564
565@property566def intensity_max(self):567vals = self.image_intensity[self.image]568return np.max(vals, axis=0).astype(np.float64, copy=False)569
570@property571def intensity_mean(self):572return np.mean(self.image_intensity[self.image], axis=0)573
574@property575def intensity_min(self):576vals = self.image_intensity[self.image]577return np.min(vals, axis=0).astype(np.float64, copy=False)578
579@property580def intensity_std(self):581vals = self.image_intensity[self.image]582return np.std(vals, axis=0)583
584@property585def axis_major_length(self):586if self._ndim == 2:587l1 = self.inertia_tensor_eigvals[0]588return 4 * sqrt(l1)589elif self._ndim == 3:590# equivalent to _inertia_eigvals_to_axes_lengths_3D(ev)[0]591ev = self.inertia_tensor_eigvals592return sqrt(10 * (ev[0] + ev[1] - ev[2]))593else:594raise ValueError("axis_major_length only available in 2D and 3D")595
596@property597def axis_minor_length(self):598if self._ndim == 2:599l2 = self.inertia_tensor_eigvals[-1]600return 4 * sqrt(l2)601elif self._ndim == 3:602# equivalent to _inertia_eigvals_to_axes_lengths_3D(ev)[-1]603ev = self.inertia_tensor_eigvals604return sqrt(10 * (-ev[0] + ev[1] + ev[2]))605else:606raise ValueError("axis_minor_length only available in 2D and 3D")607
608@property609@_cached610def moments(self):611M = _moments.moments(self.image.astype(np.uint8), 3, spacing=self._spacing)612return M613
614@property615@_cached616def moments_central(self):617mu = _moments.moments_central(618self.image.astype(np.uint8),619self.centroid_local,620order=3,621spacing=self._spacing,622)623return mu624
625@property626@only2d627def moments_hu(self):628if any(s != 1.0 for s in self._spacing):629raise NotImplementedError('`moments_hu` supports spacing = (1, 1) only')630return _moments.moments_hu(self.moments_normalized)631
632@property633@_cached634def moments_normalized(self):635return _moments.moments_normalized(636self.moments_central, 3, spacing=self._spacing637)638
639@property640@only2d641def orientation(self):642a, b, b, c = self.inertia_tensor.flat643if a - c == 0:644if b < 0:645return PI / 4.0646else:647return -PI / 4.0648else:649return 0.5 * atan2(-2 * b, c - a)650
651@property652@only2d653def perimeter(self):654if len(np.unique(self._spacing)) != 1:655raise NotImplementedError('`perimeter` supports isotropic spacings only')656return perimeter(self.image, 4) * self._spacing[0]657
658@property659@only2d660def perimeter_crofton(self):661if len(np.unique(self._spacing)) != 1:662raise NotImplementedError('`perimeter` supports isotropic spacings only')663return perimeter_crofton(self.image, 4) * self._spacing[0]664
665@property666def solidity(self):667return self.area / self.area_convex668
669@property670def centroid_weighted(self):671ctr = self.centroid_weighted_local672return tuple(673idx + slc.start * spc674for idx, slc, spc in zip(ctr, self.slice, self._spacing)675)676
677@property678def centroid_weighted_local(self):679M = self.moments_weighted680M0 = M[(0,) * self._ndim]681
682def _get_element(axis):683return (0,) * axis + (1,) + (0,) * (self._ndim - 1 - axis)684
685return np.asarray(686tuple(M[_get_element(axis)] / M0 for axis in range(self._ndim))687)688
689@property690@_cached691def moments_weighted(self):692image = self._image_intensity_double()693if self._multichannel:694moments = np.stack(695[696_moments.moments(image[..., i], order=3, spacing=self._spacing)697for i in range(image.shape[-1])698],699axis=-1,700)701else:702moments = _moments.moments(image, order=3, spacing=self._spacing)703return moments704
705@property706@_cached707def moments_weighted_central(self):708ctr = self.centroid_weighted_local709image = self._image_intensity_double()710if self._multichannel:711moments_list = [712_moments.moments_central(713image[..., i], center=ctr[..., i], order=3, spacing=self._spacing714)715for i in range(image.shape[-1])716]717moments = np.stack(moments_list, axis=-1)718else:719moments = _moments.moments_central(720image, ctr, order=3, spacing=self._spacing721)722return moments723
724@property725@only2d726def moments_weighted_hu(self):727if not (np.array(self._spacing) == np.array([1, 1])).all():728raise NotImplementedError('`moments_hu` supports spacing = (1, 1) only')729nu = self.moments_weighted_normalized730if self._multichannel:731nchannels = self._intensity_image.shape[-1]732return np.stack(733[_moments.moments_hu(nu[..., i]) for i in range(nchannels)],734axis=-1,735)736else:737return _moments.moments_hu(nu)738
739@property740@_cached741def moments_weighted_normalized(self):742mu = self.moments_weighted_central743if self._multichannel:744nchannels = self._intensity_image.shape[-1]745return np.stack(746[747_moments.moments_normalized(748mu[..., i], order=3, spacing=self._spacing749)750for i in range(nchannels)751],752axis=-1,753)754else:755return _moments.moments_normalized(mu, order=3, spacing=self._spacing)756
757def __iter__(self):758props = PROP_VALS759
760if self._intensity_image is None:761unavailable_props = _require_intensity_image762props = props.difference(unavailable_props)763
764return iter(sorted(props))765
766def __getitem__(self, key):767value = getattr(self, key, None)768if value is not None:769return value770else: # backwards compatibility771return getattr(self, PROPS[key])772
773def __eq__(self, other):774if not isinstance(other, RegionProperties):775return False776
777for key in PROP_VALS:778try:779# so that NaNs are equal780np.testing.assert_equal(781getattr(self, key, None), getattr(other, key, None)782)783except AssertionError:784return False785
786return True787
788
789# For compatibility with code written prior to 0.16
790_RegionProperties = RegionProperties791
792
793def _props_to_dict(regions, properties=('label', 'bbox'), separator='-'):794"""Convert image region properties list into a column dictionary.795
796Parameters
797----------
798regions : (K,) list
799List of RegionProperties objects as returned by :func:`regionprops`.
800properties : tuple or list of str, optional
801Properties that will be included in the resulting dictionary
802For a list of available properties, please see :func:`regionprops`.
803Users should remember to add "label" to keep track of region
804identities.
805separator : str, optional
806For non-scalar properties not listed in OBJECT_COLUMNS, each element
807will appear in its own column, with the index of that element separated
808from the property name by this separator. For example, the inertia
809tensor of a 2D region will appear in four columns:
810``inertia_tensor-0-0``, ``inertia_tensor-0-1``, ``inertia_tensor-1-0``,
811and ``inertia_tensor-1-1`` (where the separator is ``-``).
812
813Object columns are those that cannot be split in this way because the
814number of columns would change depending on the object. For example,
815``image`` and ``coords``.
816
817Returns
818-------
819out_dict : dict
820Dictionary mapping property names to an array of values of that
821property, one value per region. This dictionary can be used as input to
822pandas ``DataFrame`` to map property names to columns in the frame and
823regions to rows.
824
825Notes
826-----
827Each column contains either a scalar property, an object property, or an
828element in a multidimensional array.
829
830Properties with scalar values for each region, such as "eccentricity", will
831appear as a float or int array with that property name as key.
832
833Multidimensional properties *of fixed size* for a given image dimension,
834such as "centroid" (every centroid will have three elements in a 3D image,
835no matter the region size), will be split into that many columns, with the
836name {property_name}{separator}{element_num} (for 1D properties),
837{property_name}{separator}{elem_num0}{separator}{elem_num1} (for 2D
838properties), and so on.
839
840For multidimensional properties that don't have a fixed size, such as
841"image" (the image of a region varies in size depending on the region
842size), an object array will be used, with the corresponding property name
843as the key.
844
845Examples
846--------
847>>> from skimage import data, util, measure
848>>> image = data.coins()
849>>> label_image = measure.label(image > 110, connectivity=image.ndim)
850>>> proplist = regionprops(label_image, image)
851>>> props = _props_to_dict(proplist, properties=['label', 'inertia_tensor',
852... 'inertia_tensor_eigvals'])
853>>> props # doctest: +ELLIPSIS +SKIP
854{'label': array([ 1, 2, ...]), ...
855'inertia_tensor-0-0': array([ 4.012...e+03, 8.51..., ...]), ...
856...,
857'inertia_tensor_eigvals-1': array([ 2.67...e+02, 2.83..., ...])}
858
859The resulting dictionary can be directly passed to pandas, if installed, to
860obtain a clean DataFrame:
861
862>>> import pandas as pd # doctest: +SKIP
863>>> data = pd.DataFrame(props) # doctest: +SKIP
864>>> data.head() # doctest: +SKIP
865label inertia_tensor-0-0 ... inertia_tensor_eigvals-1
8660 1 4012.909888 ... 267.065503
8671 2 8.514739 ... 2.834806
8682 3 0.666667 ... 0.000000
8693 4 0.000000 ... 0.000000
8704 5 0.222222 ... 0.111111
871
872"""
873
874out = {}875n = len(regions)876for prop in properties:877r = regions[0]878# Copy the original property name so the output will have the879# user-provided property name in the case of deprecated names.880orig_prop = prop881# determine the current property name for any deprecated property.882prop = PROPS.get(prop, prop)883rp = getattr(r, prop)884if prop in COL_DTYPES:885dtype = COL_DTYPES[prop]886else:887func = r._extra_properties[prop]888dtype = _infer_regionprop_dtype(889func,890intensity=r._intensity_image is not None,891ndim=r.image.ndim,892)893
894# scalars and objects are dedicated one column per prop895# array properties are raveled into multiple columns896# for more info, refer to notes 1897if np.isscalar(rp) or prop in OBJECT_COLUMNS or dtype is np.object_:898column_buffer = np.empty(n, dtype=dtype)899for i in range(n):900column_buffer[i] = regions[i][prop]901out[orig_prop] = np.copy(column_buffer)902else:903# precompute property column names and locations904modified_props = []905locs = []906for ind in np.ndindex(np.shape(rp)):907modified_props.append(separator.join(map(str, (orig_prop,) + ind)))908locs.append(ind if len(ind) > 1 else ind[0])909
910# fill temporary column data_array911n_columns = len(locs)912column_data = np.empty((n, n_columns), dtype=dtype)913for k in range(n):914# we coerce to a numpy array to ensure structures like915# tuple-of-arrays expand correctly into columns916rp = np.asarray(regions[k][prop])917for i, loc in enumerate(locs):918column_data[k, i] = rp[loc]919
920# add the columns to the output dictionary921for i, modified_prop in enumerate(modified_props):922out[modified_prop] = column_data[:, i]923return out924
925
926def regionprops_table(927label_image,928intensity_image=None,929properties=('label', 'bbox'),930*,931cache=True,932separator='-',933extra_properties=None,934spacing=None,935):936"""Compute image properties and return them as a pandas-compatible table.937
938The table is a dictionary mapping column names to value arrays. See Notes
939section below for details.
940
941.. versionadded:: 0.16
942
943Parameters
944----------
945label_image : (M, N[, P]) ndarray
946Labeled input image. Labels with value 0 are ignored.
947intensity_image : (M, N[, P][, C]) ndarray, optional
948Intensity (i.e., input) image with same size as labeled image, plus
949optionally an extra dimension for multichannel data. The channel dimension,
950if present, must be the last axis. Default is None.
951
952.. versionchanged:: 0.18.0
953The ability to provide an extra dimension for channels was added.
954properties : tuple or list of str, optional
955Properties that will be included in the resulting dictionary
956For a list of available properties, please see :func:`regionprops`.
957Users should remember to add "label" to keep track of region
958identities.
959cache : bool, optional
960Determine whether to cache calculated properties. The computation is
961much faster for cached properties, whereas the memory consumption
962increases.
963separator : str, optional
964For non-scalar properties not listed in OBJECT_COLUMNS, each element
965will appear in its own column, with the index of that element separated
966from the property name by this separator. For example, the inertia
967tensor of a 2D region will appear in four columns:
968``inertia_tensor-0-0``, ``inertia_tensor-0-1``, ``inertia_tensor-1-0``,
969and ``inertia_tensor-1-1`` (where the separator is ``-``).
970
971Object columns are those that cannot be split in this way because the
972number of columns would change depending on the object. For example,
973``image`` and ``coords``.
974extra_properties : Iterable of callables
975Add extra property computation functions that are not included with
976skimage. The name of the property is derived from the function name,
977the dtype is inferred by calling the function on a small sample.
978If the name of an extra property clashes with the name of an existing
979property the extra property will not be visible and a UserWarning is
980issued. A property computation function must take a region mask as its
981first argument. If the property requires an intensity image, it must
982accept the intensity image as the second argument.
983spacing: tuple of float, shape (ndim,)
984The pixel spacing along each axis of the image.
985
986Returns
987-------
988out_dict : dict
989Dictionary mapping property names to an array of values of that
990property, one value per region. This dictionary can be used as input to
991pandas ``DataFrame`` to map property names to columns in the frame and
992regions to rows. If the image has no regions,
993the arrays will have length 0, but the correct type.
994
995Notes
996-----
997Each column contains either a scalar property, an object property, or an
998element in a multidimensional array.
999
1000Properties with scalar values for each region, such as "eccentricity", will
1001appear as a float or int array with that property name as key.
1002
1003Multidimensional properties *of fixed size* for a given image dimension,
1004such as "centroid" (every centroid will have three elements in a 3D image,
1005no matter the region size), will be split into that many columns, with the
1006name {property_name}{separator}{element_num} (for 1D properties),
1007{property_name}{separator}{elem_num0}{separator}{elem_num1} (for 2D
1008properties), and so on.
1009
1010For multidimensional properties that don't have a fixed size, such as
1011"image" (the image of a region varies in size depending on the region
1012size), an object array will be used, with the corresponding property name
1013as the key.
1014
1015Examples
1016--------
1017>>> from skimage import data, util, measure
1018>>> image = data.coins()
1019>>> label_image = measure.label(image > 110, connectivity=image.ndim)
1020>>> props = measure.regionprops_table(label_image, image,
1021... properties=['label', 'inertia_tensor',
1022... 'inertia_tensor_eigvals'])
1023>>> props # doctest: +ELLIPSIS +SKIP
1024{'label': array([ 1, 2, ...]), ...
1025'inertia_tensor-0-0': array([ 4.012...e+03, 8.51..., ...]), ...
1026...,
1027'inertia_tensor_eigvals-1': array([ 2.67...e+02, 2.83..., ...])}
1028
1029The resulting dictionary can be directly passed to pandas, if installed, to
1030obtain a clean DataFrame:
1031
1032>>> import pandas as pd # doctest: +SKIP
1033>>> data = pd.DataFrame(props) # doctest: +SKIP
1034>>> data.head() # doctest: +SKIP
1035label inertia_tensor-0-0 ... inertia_tensor_eigvals-1
10360 1 4012.909888 ... 267.065503
10371 2 8.514739 ... 2.834806
10382 3 0.666667 ... 0.000000
10393 4 0.000000 ... 0.000000
10404 5 0.222222 ... 0.111111
1041
1042[5 rows x 7 columns]
1043
1044If we want to measure a feature that does not come as a built-in
1045property, we can define custom functions and pass them as
1046``extra_properties``. For example, we can create a custom function
1047that measures the intensity quartiles in a region:
1048
1049>>> from skimage import data, util, measure
1050>>> import numpy as np
1051>>> def quartiles(regionmask, intensity):
1052... return np.percentile(intensity[regionmask], q=(25, 50, 75))
1053>>>
1054>>> image = data.coins()
1055>>> label_image = measure.label(image > 110, connectivity=image.ndim)
1056>>> props = measure.regionprops_table(label_image, intensity_image=image,
1057... properties=('label',),
1058... extra_properties=(quartiles,))
1059>>> import pandas as pd # doctest: +SKIP
1060>>> pd.DataFrame(props).head() # doctest: +SKIP
1061label quartiles-0 quartiles-1 quartiles-2
10620 1 117.00 123.0 130.0
10631 2 111.25 112.0 114.0
10642 3 111.00 111.0 111.0
10653 4 111.00 111.5 112.5
10664 5 112.50 113.0 114.0
1067
1068"""
1069regions = regionprops(1070label_image,1071intensity_image=intensity_image,1072cache=cache,1073extra_properties=extra_properties,1074spacing=spacing,1075)1076if extra_properties is not None:1077properties = list(properties) + [prop.__name__ for prop in extra_properties]1078if len(regions) == 0:1079ndim = label_image.ndim1080label_image = np.zeros((3,) * ndim, dtype=int)1081label_image[(1,) * ndim] = 11082if intensity_image is not None:1083intensity_image = np.zeros(1084label_image.shape + intensity_image.shape[ndim:],1085dtype=intensity_image.dtype,1086)1087regions = regionprops(1088label_image,1089intensity_image=intensity_image,1090cache=cache,1091extra_properties=extra_properties,1092spacing=spacing,1093)1094
1095out_d = _props_to_dict(regions, properties=properties, separator=separator)1096return {k: v[:0] for k, v in out_d.items()}1097
1098return _props_to_dict(regions, properties=properties, separator=separator)1099
1100
1101def regionprops(1102label_image,1103intensity_image=None,1104cache=True,1105*,1106extra_properties=None,1107spacing=None,1108offset=None,1109):1110r"""Measure properties of labeled image regions.1111
1112Parameters
1113----------
1114label_image : (M, N[, P]) ndarray
1115Labeled input image. Labels with value 0 are ignored.
1116
1117.. versionchanged:: 0.14.1
1118Previously, ``label_image`` was processed by ``numpy.squeeze`` and
1119so any number of singleton dimensions was allowed. This resulted in
1120inconsistent handling of images with singleton dimensions. To
1121recover the old behaviour, use
1122``regionprops(np.squeeze(label_image), ...)``.
1123intensity_image : (M, N[, P][, C]) ndarray, optional
1124Intensity (i.e., input) image with same size as labeled image, plus
1125optionally an extra dimension for multichannel data. Currently,
1126this extra channel dimension, if present, must be the last axis.
1127Default is None.
1128
1129.. versionchanged:: 0.18.0
1130The ability to provide an extra dimension for channels was added.
1131cache : bool, optional
1132Determine whether to cache calculated properties. The computation is
1133much faster for cached properties, whereas the memory consumption
1134increases.
1135extra_properties : Iterable of callables
1136Add extra property computation functions that are not included with
1137skimage. The name of the property is derived from the function name,
1138the dtype is inferred by calling the function on a small sample.
1139If the name of an extra property clashes with the name of an existing
1140property the extra property will not be visible and a UserWarning is
1141issued. A property computation function must take a region mask as its
1142first argument. If the property requires an intensity image, it must
1143accept the intensity image as the second argument.
1144spacing: tuple of float, shape (ndim,)
1145The pixel spacing along each axis of the image.
1146offset : array-like of int, shape `(label_image.ndim,)`, optional
1147Coordinates of the origin ("top-left" corner) of the label image.
1148Normally this is ([0, ]0, 0), but it might be different if one wants
1149to obtain regionprops of subvolumes within a larger volume.
1150
1151Returns
1152-------
1153properties : list of RegionProperties
1154Each item describes one labeled region, and can be accessed using the
1155attributes listed below.
1156
1157Notes
1158-----
1159The following properties can be accessed as attributes or keys:
1160
1161**area** : float
1162Area of the region i.e. number of pixels of the region scaled by pixel-area.
1163**area_bbox** : float
1164Area of the bounding box i.e. number of pixels of bounding box scaled by pixel-area.
1165**area_convex** : float
1166Area of the convex hull image, which is the smallest convex
1167polygon that encloses the region.
1168**area_filled** : float
1169Area of the region with all the holes filled in.
1170**axis_major_length** : float
1171The length of the major axis of the ellipse that has the same
1172normalized second central moments as the region.
1173**axis_minor_length** : float
1174The length of the minor axis of the ellipse that has the same
1175normalized second central moments as the region.
1176**bbox** : tuple
1177Bounding box ``(min_row, min_col, max_row, max_col)``.
1178Pixels belonging to the bounding box are in the half-open interval
1179``[min_row; max_row)`` and ``[min_col; max_col)``.
1180**centroid** : array
1181Centroid coordinate tuple ``(row, col)``.
1182**centroid_local** : array
1183Centroid coordinate tuple ``(row, col)``, relative to region bounding
1184box.
1185**centroid_weighted** : array
1186Centroid coordinate tuple ``(row, col)`` weighted with intensity
1187image.
1188**centroid_weighted_local** : array
1189Centroid coordinate tuple ``(row, col)``, relative to region bounding
1190box, weighted with intensity image.
1191**coords_scaled** : (K, 2) ndarray
1192Coordinate list ``(row, col)`` of the region scaled by ``spacing``.
1193**coords** : (K, 2) ndarray
1194Coordinate list ``(row, col)`` of the region.
1195**eccentricity** : float
1196Eccentricity of the ellipse that has the same second-moments as the
1197region. The eccentricity is the ratio of the focal distance
1198(distance between focal points) over the major axis length.
1199The value is in the interval [0, 1).
1200When it is 0, the ellipse becomes a circle.
1201**equivalent_diameter_area** : float
1202The diameter of a circle with the same area as the region.
1203**euler_number** : int
1204Euler characteristic of the set of non-zero pixels.
1205Computed as number of connected components subtracted by number of
1206holes (input.ndim connectivity). In 3D, number of connected
1207components plus number of holes subtracted by number of tunnels.
1208**extent** : float
1209Ratio of pixels in the region to pixels in the total bounding box.
1210Computed as ``area / (rows * cols)``
1211**feret_diameter_max** : float
1212Maximum Feret's diameter computed as the longest distance between
1213points around a region's convex hull contour as determined by
1214``find_contours``. [5]_
1215**image** : (H, J) ndarray
1216Sliced binary region image which has the same size as bounding box.
1217**image_convex** : (H, J) ndarray
1218Binary convex hull image which has the same size as bounding box.
1219**image_filled** : (H, J) ndarray
1220Binary region image with filled holes which has the same size as
1221bounding box.
1222**image_intensity** : ndarray
1223Image inside region bounding box.
1224**inertia_tensor** : ndarray
1225Inertia tensor of the region for the rotation around its mass.
1226**inertia_tensor_eigvals** : tuple
1227The eigenvalues of the inertia tensor in decreasing order.
1228**intensity_max** : float
1229Value with the greatest intensity in the region.
1230**intensity_mean** : float
1231Value with the mean intensity in the region.
1232**intensity_min** : float
1233Value with the least intensity in the region.
1234**intensity_std** : float
1235Standard deviation of the intensity in the region.
1236**label** : int
1237The label in the labeled input image.
1238**moments** : (3, 3) ndarray
1239Spatial moments up to 3rd order::
1240
1241m_ij = sum{ array(row, col) * row^i * col^j }
1242
1243where the sum is over the `row`, `col` coordinates of the region.
1244**moments_central** : (3, 3) ndarray
1245Central moments (translation invariant) up to 3rd order::
1246
1247mu_ij = sum{ array(row, col) * (row - row_c)^i * (col - col_c)^j }
1248
1249where the sum is over the `row`, `col` coordinates of the region,
1250and `row_c` and `col_c` are the coordinates of the region's centroid.
1251**moments_hu** : tuple
1252Hu moments (translation, scale and rotation invariant).
1253**moments_normalized** : (3, 3) ndarray
1254Normalized moments (translation and scale invariant) up to 3rd order::
1255
1256nu_ij = mu_ij / m_00^[(i+j)/2 + 1]
1257
1258where `m_00` is the zeroth spatial moment.
1259**moments_weighted** : (3, 3) ndarray
1260Spatial moments of intensity image up to 3rd order::
1261
1262wm_ij = sum{ array(row, col) * row^i * col^j }
1263
1264where the sum is over the `row`, `col` coordinates of the region.
1265**moments_weighted_central** : (3, 3) ndarray
1266Central moments (translation invariant) of intensity image up to
12673rd order::
1268
1269wmu_ij = sum{ array(row, col) * (row - row_c)^i * (col - col_c)^j }
1270
1271where the sum is over the `row`, `col` coordinates of the region,
1272and `row_c` and `col_c` are the coordinates of the region's weighted
1273centroid.
1274**moments_weighted_hu** : tuple
1275Hu moments (translation, scale and rotation invariant) of intensity
1276image.
1277**moments_weighted_normalized** : (3, 3) ndarray
1278Normalized moments (translation and scale invariant) of intensity
1279image up to 3rd order::
1280
1281wnu_ij = wmu_ij / wm_00^[(i+j)/2 + 1]
1282
1283where ``wm_00`` is the zeroth spatial moment (intensity-weighted area).
1284**num_pixels** : int
1285Number of foreground pixels.
1286**orientation** : float
1287Angle between the 0th axis (rows) and the major
1288axis of the ellipse that has the same second moments as the region,
1289ranging from `-pi/2` to `pi/2` counter-clockwise.
1290**perimeter** : float
1291Perimeter of object which approximates the contour as a line
1292through the centers of border pixels using a 4-connectivity.
1293**perimeter_crofton** : float
1294Perimeter of object approximated by the Crofton formula in 4
1295directions.
1296**slice** : tuple of slices
1297A slice to extract the object from the source image.
1298**solidity** : float
1299Ratio of pixels in the region to pixels of the convex hull image.
1300
1301Each region also supports iteration, so that you can do::
1302
1303for prop in region:
1304print(prop, region[prop])
1305
1306See Also
1307--------
1308label
1309
1310References
1311----------
1312.. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing:
1313Core Algorithms. Springer-Verlag, London, 2009.
1314.. [2] B. Jähne. Digital Image Processing. Springer-Verlag,
1315Berlin-Heidelberg, 6. edition, 2005.
1316.. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image
1317Features, from Lecture notes in computer science, p. 676. Springer,
1318Berlin, 1993.
1319.. [4] https://en.wikipedia.org/wiki/Image_moment
1320.. [5] W. Pabst, E. Gregorová. Characterization of particles and particle
1321systems, pp. 27-28. ICT Prague, 2007.
1322https://old.vscht.cz/sil/keramika/Characterization_of_particles/CPPS%20_English%20version_.pdf
1323
1324Examples
1325--------
1326>>> from skimage import data, util
1327>>> from skimage.measure import label, regionprops
1328>>> img = util.img_as_ubyte(data.coins()) > 110
1329>>> label_img = label(img, connectivity=img.ndim)
1330>>> props = regionprops(label_img)
1331>>> # centroid of first labeled object
1332>>> props[0].centroid
1333(22.72987986048314, 81.91228523446583)
1334>>> # centroid of first labeled object
1335>>> props[0]['centroid']
1336(22.72987986048314, 81.91228523446583)
1337
1338Add custom measurements by passing functions as ``extra_properties``
1339
1340>>> from skimage import data, util
1341>>> from skimage.measure import label, regionprops
1342>>> import numpy as np
1343>>> img = util.img_as_ubyte(data.coins()) > 110
1344>>> label_img = label(img, connectivity=img.ndim)
1345>>> def pixelcount(regionmask):
1346... return np.sum(regionmask)
1347>>> props = regionprops(label_img, extra_properties=(pixelcount,))
1348>>> props[0].pixelcount
13497741
1350>>> props[1]['pixelcount']
135142
1352
1353"""
1354
1355if label_image.ndim not in (2, 3):1356raise TypeError('Only 2-D and 3-D images supported.')1357
1358if not np.issubdtype(label_image.dtype, np.integer):1359if np.issubdtype(label_image.dtype, bool):1360raise TypeError(1361'Non-integer image types are ambiguous: '1362'use skimage.measure.label to label the connected '1363'components of label_image, '1364'or label_image.astype(np.uint8) to interpret '1365'the True values as a single label.'1366)1367else:1368raise TypeError('Non-integer label_image types are ambiguous')1369
1370if offset is None:1371offset_arr = np.zeros((label_image.ndim,), dtype=int)1372else:1373offset_arr = np.asarray(offset)1374if offset_arr.ndim != 1 or offset_arr.size != label_image.ndim:1375raise ValueError(1376'Offset should be an array-like of integers '1377'of shape (label_image.ndim,); '1378f'{offset} was provided.'1379)1380
1381regions = []1382
1383objects = ndi.find_objects(label_image)1384for i, sl in enumerate(objects):1385if sl is None:1386continue1387
1388label = i + 11389
1390props = RegionProperties(1391sl,1392label,1393label_image,1394intensity_image,1395cache,1396spacing=spacing,1397extra_properties=extra_properties,1398offset=offset_arr,1399)1400regions.append(props)1401
1402return regions1403
1404
1405def _parse_docs():1406import re1407import textwrap1408
1409doc = regionprops.__doc__ or ''1410arg_regex = r'\*\*(\w+)\*\* \:.*?\n(.*?)(?=\n [\*\S]+)'1411if sys.version_info >= (3, 13):1412arg_regex = r'\*\*(\w+)\*\* \:.*?\n(.*?)(?=\n[\*\S]+)'1413
1414matches = re.finditer(arg_regex, doc, flags=re.DOTALL)1415prop_doc = {m.group(1): textwrap.dedent(m.group(2)) for m in matches}1416
1417return prop_doc1418
1419
1420def _install_properties_docs():1421prop_doc = _parse_docs()1422
1423for p in [member for member in dir(RegionProperties) if not member.startswith('_')]:1424getattr(RegionProperties, p).__doc__ = prop_doc[p]1425
1426
1427if __debug__:1428# don't install docstrings when in optimized/non-debug mode1429_install_properties_docs()1430