from warnings import warn from math import sqrt, atan2, pi as PI import numpy as np from scipy import ndimage as ndi from ._label import label from . import _moments from functools import wraps __all__ = ['regionprops', 'perimeter'] STREL_4 = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], dtype=np.uint8) STREL_8 = np.ones((3, 3), dtype=np.uint8) STREL_26_3D = np.ones((3, 3, 3), dtype=np.uint8) PROPS = { 'Area': 'area', 'BoundingBox': 'bbox', 'BoundingBoxArea': 'bbox_area', 'CentralMoments': 'moments_central', 'Centroid': 'centroid', 'ConvexArea': 'convex_area', # 'ConvexHull', 'ConvexImage': 'convex_image', 'Coordinates': 'coords', 'Eccentricity': 'eccentricity', 'EquivDiameter': 'equivalent_diameter', 'EulerNumber': 'euler_number', 'Extent': 'extent', # 'Extrema', 'FilledArea': 'filled_area', 'FilledImage': 'filled_image', 'HuMoments': 'moments_hu', 'Image': 'image', 'InertiaTensor': 'inertia_tensor', 'InertiaTensorEigvals': 'inertia_tensor_eigvals', 'IntensityImage': 'intensity_image', 'Label': 'label', 'LocalCentroid': 'local_centroid', 'MajorAxisLength': 'major_axis_length', 'MaxIntensity': 'max_intensity', 'MeanIntensity': 'mean_intensity', 'MinIntensity': 'min_intensity', 'MinorAxisLength': 'minor_axis_length', 'Moments': 'moments', 'NormalizedMoments': 'moments_normalized', 'Orientation': 'orientation', 'Perimeter': 'perimeter', # 'PixelIdxList', # 'PixelList', 'Slice': 'slice', 'Solidity': 'solidity', # 'SubarrayIdx' 'WeightedCentralMoments': 'weighted_moments_central', 'WeightedCentroid': 'weighted_centroid', 'WeightedHuMoments': 'weighted_moments_hu', 'WeightedLocalCentroid': 'weighted_local_centroid', 'WeightedMoments': 'weighted_moments', 'WeightedNormalizedMoments': 'weighted_moments_normalized' } OBJECT_COLUMNS = { 'image', 'coords', 'convex_image', 'slice', 'filled_image', 'intensity_image' } COL_DTYPES = { 'area': int, 'bbox': int, 'bbox_area': int, 'moments_central': float, 'centroid': float, 'convex_area': int, 'convex_image': object, 'coords': object, 'eccentricity': float, 'equivalent_diameter': float, 'euler_number': int, 'extent': float, 'filled_area': int, 'filled_image': object, 'moments_hu': float, 'image': object, 'inertia_tensor': float, 'inertia_tensor_eigvals': float, 'intensity_image': object, 'label': int, 'local_centroid': float, 'major_axis_length': float, 'max_intensity': int, 'mean_intensity': float, 'min_intensity': int, 'minor_axis_length': float, 'moments': float, 'moments_normalized': float, 'orientation': float, 'perimeter': float, 'slice': object, 'solidity': float, 'weighted_moments_central': float, 'weighted_centroid': float, 'weighted_moments_hu': float, 'weighted_local_centroid': float, 'weighted_moments': float, 'weighted_moments_normalized': float } PROP_VALS = set(PROPS.values()) def _cached(f): @wraps(f) def wrapper(obj): cache = obj._cache prop = f.__name__ if not ((prop in cache) and obj._cache_active): cache[prop] = f(obj) return cache[prop] return wrapper def only2d(method): @wraps(method) def func2d(self, *args, **kwargs): if self._ndim > 2: raise NotImplementedError('Property %s is not implemented for ' '3D images' % method.__name__) return method(self, *args, **kwargs) return func2d class RegionProperties: """Please refer to `skimage.measure.regionprops` for more information on the available region properties. """ def __init__(self, slice, label, label_image, intensity_image, cache_active): if intensity_image is not None: if not intensity_image.shape == label_image.shape: raise ValueError('Label and intensity image must have the' ' same shape.') self.label = label self._slice = slice self.slice = slice self._label_image = label_image self._intensity_image = intensity_image self._cache_active = cache_active self._cache = {} self._ndim = label_image.ndim @property @_cached def area(self): return np.sum(self.image) @property def bbox(self): """ Returns ------- A tuple of the bounding box's start coordinates for each dimension, followed by the end coordinates for each dimension """ return tuple([self.slice[i].start for i in range(self._ndim)] + [self.slice[i].stop for i in range(self._ndim)]) @property def bbox_area(self): return self.image.size @property def centroid(self): return tuple(self.coords.mean(axis=0)) @property @_cached def convex_area(self): return np.sum(self.convex_image) @property @_cached def convex_image(self): from ..morphology.convex_hull import convex_hull_image return convex_hull_image(self.image) @property def coords(self): indices = np.nonzero(self.image) return np.vstack([indices[i] + self.slice[i].start for i in range(self._ndim)]).T @property @only2d def eccentricity(self): l1, l2 = self.inertia_tensor_eigvals if l1 == 0: return 0 return sqrt(1 - l2 / l1) @property def equivalent_diameter(self): if self._ndim == 2: return sqrt(4 * self.area / PI) elif self._ndim == 3: return (6 * self.area / PI) ** (1. / 3) @property def euler_number(self): euler_array = self.filled_image != self.image _, num = label(euler_array, connectivity=self._ndim, return_num=True, background=0) return -num + 1 @property def extent(self): return self.area / self.image.size @property def filled_area(self): return np.sum(self.filled_image) @property @_cached def filled_image(self): structure = np.ones((3,) * self._ndim) return ndi.binary_fill_holes(self.image, structure) @property @_cached def image(self): return self._label_image[self.slice] == self.label @property @_cached def inertia_tensor(self): mu = self.moments_central return _moments.inertia_tensor(self.image, mu) @property @_cached def inertia_tensor_eigvals(self): return _moments.inertia_tensor_eigvals(self.image, T=self.inertia_tensor) @property @_cached def intensity_image(self): if self._intensity_image is None: raise AttributeError('No intensity image specified.') return self._intensity_image[self.slice] * self.image def _intensity_image_double(self): return self.intensity_image.astype(np.double) @property def local_centroid(self): M = self.moments return tuple(M[tuple(np.eye(self._ndim, dtype=int))] / M[(0,) * self._ndim]) @property def max_intensity(self): return np.max(self.intensity_image[self.image]) @property def mean_intensity(self): return np.mean(self.intensity_image[self.image]) @property def min_intensity(self): return np.min(self.intensity_image[self.image]) @property def major_axis_length(self): l1 = self.inertia_tensor_eigvals[0] return 4 * sqrt(l1) @property def minor_axis_length(self): l2 = self.inertia_tensor_eigvals[-1] return 4 * sqrt(l2) @property @_cached def moments(self): M = _moments.moments(self.image.astype(np.uint8), 3) return M @property @_cached def moments_central(self): mu = _moments.moments_central(self.image.astype(np.uint8), self.local_centroid, order=3) return mu @property @only2d def moments_hu(self): return _moments.moments_hu(self.moments_normalized) @property @_cached def moments_normalized(self): return _moments.moments_normalized(self.moments_central, 3) @property @only2d def orientation(self): a, b, b, c = self.inertia_tensor.flat if a - c == 0: if b < 0: return -PI / 4. else: return PI / 4. else: return 0.5 * atan2(-2 * b, c - a) @property @only2d def perimeter(self): return perimeter(self.image, 4) @property def solidity(self): return self.area / self.convex_area @property def weighted_centroid(self): ctr = self.weighted_local_centroid return tuple(idx + slc.start for idx, slc in zip(ctr, self.slice)) @property def weighted_local_centroid(self): M = self.weighted_moments return (M[tuple(np.eye(self._ndim, dtype=int))] / M[(0,) * self._ndim]) @property @_cached def weighted_moments(self): return _moments.moments(self._intensity_image_double(), 3) @property @_cached def weighted_moments_central(self): ctr = self.weighted_local_centroid return _moments.moments_central(self._intensity_image_double(), center=ctr, order=3) @property @only2d def weighted_moments_hu(self): return _moments.moments_hu(self.weighted_moments_normalized) @property @_cached def weighted_moments_normalized(self): return _moments.moments_normalized(self.weighted_moments_central, 3) def __iter__(self): props = PROP_VALS if self._intensity_image is None: unavailable_props = ('intensity_image', 'max_intensity', 'mean_intensity', 'min_intensity', 'weighted_moments', 'weighted_moments_central', 'weighted_centroid', 'weighted_local_centroid', 'weighted_moments_hu', 'weighted_moments_normalized') props = props.difference(unavailable_props) return iter(sorted(props)) def __getitem__(self, key): value = getattr(self, key, None) if value is not None: return value else: # backwards compatibility return getattr(self, PROPS[key]) def __eq__(self, other): if not isinstance(other, RegionProperties): return False for key in PROP_VALS: try: # so that NaNs are equal np.testing.assert_equal(getattr(self, key, None), getattr(other, key, None)) except AssertionError: return False return True # For compatibility with code written prior to 0.16 _RegionProperties = RegionProperties def _props_to_dict(regions, properties=('label', 'bbox'), separator='-'): """Convert image region properties list into a column dictionary. Parameters ---------- regions : (N,) list List of RegionProperties objects as returned by :func:`regionprops`. properties : tuple or list of str, optional Properties that will be included in the resulting dictionary For a list of available properties, please see :func:`regionprops`. Users should remember to add "label" to keep track of region identities. separator : str, optional For non-scalar properties not listed in OBJECT_COLUMNS, each element will appear in its own column, with the index of that element separated from the property name by this separator. For example, the inertia tensor of a 2D region will appear in four columns: ``inertia_tensor-0-0``, ``inertia_tensor-0-1``, ``inertia_tensor-1-0``, and ``inertia_tensor-1-1`` (where the separator is ``-``). Object columns are those that cannot be split in this way because the number of columns would change depending on the object. For example, ``image`` and ``coords``. Returns ------- out_dict : dict Dictionary mapping property names to an array of values of that property, one value per region. This dictionary can be used as input to pandas ``DataFrame`` to map property names to columns in the frame and regions to rows. Notes ----- Each column contains either a scalar property, an object property, or an element in a multidimensional array. Properties with scalar values for each region, such as "eccentricity", will appear as a float or int array with that property name as key. Multidimensional properties *of fixed size* for a given image dimension, such as "centroid" (every centroid will have three elements in a 3D image, no matter the region size), will be split into that many columns, with the name {property_name}{separator}{element_num} (for 1D properties), {property_name}{separator}{elem_num0}{separator}{elem_num1} (for 2D properties), and so on. For multidimensional properties that don't have a fixed size, such as "image" (the image of a region varies in size depending on the region size), an object array will be used, with the corresponding property name as the key. Examples -------- >>> from skimage import data, util, measure >>> image = data.coins() >>> label_image = measure.label(image > 110, connectivity=image.ndim) >>> proplist = regionprops(label_image, image) >>> props = _props_to_dict(proplist, properties=['label', 'inertia_tensor', ... 'inertia_tensor_eigvals']) >>> props # doctest: +ELLIPSIS +SKIP {'label': array([ 1, 2, ...]), ... 'inertia_tensor-0-0': array([ 4.012...e+03, 8.51..., ...]), ... ..., 'inertia_tensor_eigvals-1': array([ 2.67...e+02, 2.83..., ...])} The resulting dictionary can be directly passed to pandas, if installed, to obtain a clean DataFrame: >>> import pandas as pd # doctest: +SKIP >>> data = pd.DataFrame(props) # doctest: +SKIP >>> data.head() # doctest: +SKIP label inertia_tensor-0-0 ... inertia_tensor_eigvals-1 0 1 4012.909888 ... 267.065503 1 2 8.514739 ... 2.834806 2 3 0.666667 ... 0.000000 3 4 0.000000 ... 0.000000 4 5 0.222222 ... 0.111111 """ out = {} n = len(regions) for prop in properties: dtype = COL_DTYPES[prop] column_buffer = np.zeros(n, dtype=dtype) r = regions[0][prop] # scalars and objects are dedicated one column per prop # array properties are raveled into multiple columns # for more info, refer to notes 1 if np.isscalar(r) or prop in OBJECT_COLUMNS: for i in range(n): column_buffer[i] = regions[i][prop] out[prop] = np.copy(column_buffer) else: if isinstance(r, np.ndarray): shape = r.shape else: shape = (len(r),) for ind in np.ndindex(shape): for k in range(n): loc = ind if len(ind) > 1 else ind[0] column_buffer[k] = regions[k][prop][loc] modified_prop = separator.join(map(str, (prop,) + ind)) out[modified_prop] = np.copy(column_buffer) return out def regionprops_table(label_image, intensity_image=None, properties=('label', 'bbox'), *, cache=True, separator='-'): """Compute image properties and return them as a pandas-compatible table. The table is a dictionary mapping column names to value arrays. See Notes section below for details. Parameters ---------- label_image : (N, M) ndarray Labeled input image. Labels with value 0 are ignored. intensity_image : (N, M) ndarray, optional Intensity (i.e., input) image with same size as labeled image. Default is None. properties : tuple or list of str, optional Properties that will be included in the resulting dictionary For a list of available properties, please see :func:`regionprops`. Users should remember to add "label" to keep track of region identities. cache : bool, optional Determine whether to cache calculated properties. The computation is much faster for cached properties, whereas the memory consumption increases. separator : str, optional For non-scalar properties not listed in OBJECT_COLUMNS, each element will appear in its own column, with the index of that element separated from the property name by this separator. For example, the inertia tensor of a 2D region will appear in four columns: ``inertia_tensor-0-0``, ``inertia_tensor-0-1``, ``inertia_tensor-1-0``, and ``inertia_tensor-1-1`` (where the separator is ``-``). Object columns are those that cannot be split in this way because the number of columns would change depending on the object. For example, ``image`` and ``coords``. Returns ------- out_dict : dict Dictionary mapping property names to an array of values of that property, one value per region. This dictionary can be used as input to pandas ``DataFrame`` to map property names to columns in the frame and regions to rows. If the image has no regions, the arrays will have length 0, but the correct type. Notes ----- Each column contains either a scalar property, an object property, or an element in a multidimensional array. Properties with scalar values for each region, such as "eccentricity", will appear as a float or int array with that property name as key. Multidimensional properties *of fixed size* for a given image dimension, such as "centroid" (every centroid will have three elements in a 3D image, no matter the region size), will be split into that many columns, with the name {property_name}{separator}{element_num} (for 1D properties), {property_name}{separator}{elem_num0}{separator}{elem_num1} (for 2D properties), and so on. For multidimensional properties that don't have a fixed size, such as "image" (the image of a region varies in size depending on the region size), an object array will be used, with the corresponding property name as the key. Examples -------- >>> from skimage import data, util, measure >>> image = data.coins() >>> label_image = measure.label(image > 110, connectivity=image.ndim) >>> props = regionprops_table(label_image, image, ... properties=['label', 'inertia_tensor', ... 'inertia_tensor_eigvals']) >>> props # doctest: +ELLIPSIS +SKIP {'label': array([ 1, 2, ...]), ... 'inertia_tensor-0-0': array([ 4.012...e+03, 8.51..., ...]), ... ..., 'inertia_tensor_eigvals-1': array([ 2.67...e+02, 2.83..., ...])} The resulting dictionary can be directly passed to pandas, if installed, to obtain a clean DataFrame: >>> import pandas as pd # doctest: +SKIP >>> data = pd.DataFrame(props) # doctest: +SKIP >>> data.head() # doctest: +SKIP label inertia_tensor-0-0 ... inertia_tensor_eigvals-1 0 1 4012.909888 ... 267.065503 1 2 8.514739 ... 2.834806 2 3 0.666667 ... 0.000000 3 4 0.000000 ... 0.000000 4 5 0.222222 ... 0.111111 [5 rows x 7 columns] """ regions = regionprops(label_image, intensity_image=intensity_image, cache=cache) if len(regions) == 0: label_image = np.zeros((3,) * label_image.ndim, dtype=int) label_image[(1,) * label_image.ndim] = 1 if intensity_image is not None: intensity_image = np.zeros(label_image.shape, dtype=intensity_image.dtype) regions = regionprops(label_image, intensity_image=intensity_image, cache=cache) out_d = _props_to_dict(regions, properties=properties, separator=separator) return {k: v[:0] for k, v in out_d.items()} return _props_to_dict(regions, properties=properties, separator=separator) def regionprops(label_image, intensity_image=None, cache=True, coordinates=None): r"""Measure properties of labeled image regions. Parameters ---------- label_image : (N, M) ndarray Labeled input image. Labels with value 0 are ignored. .. versionchanged:: 0.14.1 Previously, ``label_image`` was processed by ``numpy.squeeze`` and so any number of singleton dimensions was allowed. This resulted in inconsistent handling of images with singleton dimensions. To recover the old behaviour, use ``regionprops(np.squeeze(label_image), ...)``. intensity_image : (N, M) ndarray, optional Intensity (i.e., input) image with same size as labeled image. Default is None. cache : bool, optional Determine whether to cache calculated properties. The computation is much faster for cached properties, whereas the memory consumption increases. coordinates : DEPRECATED This argument is deprecated and will be removed in a future version of scikit-image. See :ref:`Coordinate conventions ` for more details. .. deprecated:: 0.16.0 Use "rc" coordinates everywhere. It may be sufficient to call ``numpy.transpose`` on your label image to get the same values as 0.15 and earlier. However, for some properties, the transformation will be less trivial. For example, the new orientation is :math:`\frac{\pi}{2}` plus the old orientation. Returns ------- properties : list of RegionProperties Each item describes one labeled region, and can be accessed using the attributes listed below. Notes ----- The following properties can be accessed as attributes or keys: **area** : int Number of pixels of the region. **bbox** : tuple Bounding box ``(min_row, min_col, max_row, max_col)``. Pixels belonging to the bounding box are in the half-open interval ``[min_row; max_row)`` and ``[min_col; max_col)``. **bbox_area** : int Number of pixels of bounding box. **centroid** : array Centroid coordinate tuple ``(row, col)``. **convex_area** : int Number of pixels of convex hull image, which is the smallest convex polygon that encloses the region. **convex_image** : (H, J) ndarray Binary convex hull image which has the same size as bounding box. **coords** : (N, 2) ndarray Coordinate list ``(row, col)`` of the region. **eccentricity** : float Eccentricity of the ellipse that has the same second-moments as the region. The eccentricity is the ratio of the focal distance (distance between focal points) over the major axis length. The value is in the interval [0, 1). When it is 0, the ellipse becomes a circle. **equivalent_diameter** : float The diameter of a circle with the same area as the region. **euler_number** : int Euler characteristic of region. Computed as number of objects (= 1) subtracted by number of holes (8-connectivity). **extent** : float Ratio of pixels in the region to pixels in the total bounding box. Computed as ``area / (rows * cols)`` **filled_area** : int Number of pixels of the region will all the holes filled in. Describes the area of the filled_image. **filled_image** : (H, J) ndarray Binary region image with filled holes which has the same size as bounding box. **image** : (H, J) ndarray Sliced binary region image which has the same size as bounding box. **inertia_tensor** : ndarray Inertia tensor of the region for the rotation around its mass. **inertia_tensor_eigvals** : tuple The eigenvalues of the inertia tensor in decreasing order. **intensity_image** : ndarray Image inside region bounding box. **label** : int The label in the labeled input image. **local_centroid** : array Centroid coordinate tuple ``(row, col)``, relative to region bounding box. **major_axis_length** : float The length of the major axis of the ellipse that has the same normalized second central moments as the region. **max_intensity** : float Value with the greatest intensity in the region. **mean_intensity** : float Value with the mean intensity in the region. **min_intensity** : float Value with the least intensity in the region. **minor_axis_length** : float The length of the minor axis of the ellipse that has the same normalized second central moments as the region. **moments** : (3, 3) ndarray Spatial moments up to 3rd order:: m_ij = sum{ array(row, col) * row^i * col^j } where the sum is over the `row`, `col` coordinates of the region. **moments_central** : (3, 3) ndarray Central moments (translation invariant) up to 3rd order:: mu_ij = sum{ array(row, col) * (row - row_c)^i * (col - col_c)^j } where the sum is over the `row`, `col` coordinates of the region, and `row_c` and `col_c` are the coordinates of the region's centroid. **moments_hu** : tuple Hu moments (translation, scale and rotation invariant). **moments_normalized** : (3, 3) ndarray Normalized moments (translation and scale invariant) up to 3rd order:: nu_ij = mu_ij / m_00^[(i+j)/2 + 1] where `m_00` is the zeroth spatial moment. **orientation** : float Angle between the 0th axis (rows) and the major axis of the ellipse that has the same second moments as the region, ranging from `-pi/2` to `pi/2` counter-clockwise. **perimeter** : float Perimeter of object which approximates the contour as a line through the centers of border pixels using a 4-connectivity. **slice** : tuple of slices A slice to extract the object from the source image. **solidity** : float Ratio of pixels in the region to pixels of the convex hull image. **weighted_centroid** : array Centroid coordinate tuple ``(row, col)`` weighted with intensity image. **weighted_local_centroid** : array Centroid coordinate tuple ``(row, col)``, relative to region bounding box, weighted with intensity image. **weighted_moments** : (3, 3) ndarray Spatial moments of intensity image up to 3rd order:: wm_ij = sum{ array(row, col) * row^i * col^j } where the sum is over the `row`, `col` coordinates of the region. **weighted_moments_central** : (3, 3) ndarray Central moments (translation invariant) of intensity image up to 3rd order:: wmu_ij = sum{ array(row, col) * (row - row_c)^i * (col - col_c)^j } where the sum is over the `row`, `col` coordinates of the region, and `row_c` and `col_c` are the coordinates of the region's weighted centroid. **weighted_moments_hu** : tuple Hu moments (translation, scale and rotation invariant) of intensity image. **weighted_moments_normalized** : (3, 3) ndarray Normalized moments (translation and scale invariant) of intensity image up to 3rd order:: wnu_ij = wmu_ij / wm_00^[(i+j)/2 + 1] where ``wm_00`` is the zeroth spatial moment (intensity-weighted area). Each region also supports iteration, so that you can do:: for prop in region: print(prop, region[prop]) See Also -------- label References ---------- .. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: Core Algorithms. Springer-Verlag, London, 2009. .. [2] B. Jähne. Digital Image Processing. Springer-Verlag, Berlin-Heidelberg, 6. edition, 2005. .. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image Features, from Lecture notes in computer science, p. 676. Springer, Berlin, 1993. .. [4] https://en.wikipedia.org/wiki/Image_moment Examples -------- >>> from skimage import data, util >>> from skimage.measure import label >>> img = util.img_as_ubyte(data.coins()) > 110 >>> label_img = label(img, connectivity=img.ndim) >>> props = regionprops(label_img) >>> # centroid of first labeled object >>> props[0].centroid (22.72987986048314, 81.91228523446583) >>> # centroid of first labeled object >>> props[0]['centroid'] (22.72987986048314, 81.91228523446583) """ if label_image.ndim not in (2, 3): raise TypeError('Only 2-D and 3-D images supported.') if not np.issubdtype(label_image.dtype, np.integer): if np.issubdtype(label_image.dtype, np.bool_): raise TypeError( 'Non-integer image types are ambiguous: ' 'use skimage.measure.label to label the connected' 'components of label_image,' 'or label_image.astype(np.uint8) to interpret' 'the True values as a single label.') else: raise TypeError( 'Non-integer label_image types are ambiguous') if coordinates is not None: if coordinates == 'rc': msg = ('The coordinates keyword argument to skimage.measure.' 'regionprops is deprecated. All features are now computed ' 'in rc (row-column) coordinates. Please remove ' '`coordinates="rc"` from all calls to regionprops before ' 'updating scikit-image.') warn(msg, stacklevel=2, category=FutureWarning) else: msg = ('Values other than "rc" for the "coordinates" argument ' 'to skimage.measure.regionprops are no longer supported. ' 'You should update your code to use "rc" coordinates and ' 'stop using the "coordinates" argument, or use skimage ' 'version 0.15.x or earlier.') raise ValueError(msg) regions = [] objects = ndi.find_objects(label_image) for i, sl in enumerate(objects): if sl is None: continue label = i + 1 props = RegionProperties(sl, label, label_image, intensity_image, cache) regions.append(props) return regions def perimeter(image, neighbourhood=4): """Calculate total perimeter of all objects in binary image. Parameters ---------- image : (N, M) ndarray 2D binary image. neighbourhood : 4 or 8, optional Neighborhood connectivity for border pixel determination. It is used to compute the contour. A higher neighbourhood widens the border on which the perimeter is computed. Returns ------- perimeter : float Total perimeter of all objects in binary image. References ---------- .. [1] K. Benkrid, D. Crookes. Design and FPGA Implementation of a Perimeter Estimator. The Queen's University of Belfast. http://www.cs.qub.ac.uk/~d.crookes/webpubs/papers/perimeter.doc Examples -------- >>> from skimage import data, util >>> from skimage.measure import label >>> # coins image (binary) >>> img_coins = data.coins() > 110 >>> # total perimeter of all objects in the image >>> perimeter(img_coins, neighbourhood=4) # doctest: +ELLIPSIS 7796.867... >>> perimeter(img_coins, neighbourhood=8) # doctest: +ELLIPSIS 8806.268... """ if image.ndim != 2: raise NotImplementedError('`perimeter` supports 2D images only') if neighbourhood == 4: strel = STREL_4 else: strel = STREL_8 image = image.astype(np.uint8) eroded_image = ndi.binary_erosion(image, strel, border_value=0) border_image = image - eroded_image perimeter_weights = np.zeros(50, dtype=np.double) perimeter_weights[[5, 7, 15, 17, 25, 27]] = 1 perimeter_weights[[21, 33]] = sqrt(2) perimeter_weights[[13, 23]] = (1 + sqrt(2)) / 2 perimeter_image = ndi.convolve(border_image, np.array([[10, 2, 10], [ 2, 1, 2], [10, 2, 10]]), mode='constant', cval=0) # You can also write # return perimeter_weights[perimeter_image].sum() # but that was measured as taking much longer than bincount + np.dot (5x # as much time) perimeter_histogram = np.bincount(perimeter_image.ravel(), minlength=50) total_perimeter = perimeter_histogram @ perimeter_weights return total_perimeter def _parse_docs(): import re import textwrap doc = regionprops.__doc__ or '' matches = re.finditer(r'\*\*(\w+)\*\* \:.*?\n(.*?)(?=\n [\*\S]+)', doc, flags=re.DOTALL) prop_doc = {m.group(1): textwrap.dedent(m.group(2)) for m in matches} return prop_doc def _install_properties_docs(): prop_doc = _parse_docs() for p in [member for member in dir(RegionProperties) if not member.startswith('_')]: getattr(RegionProperties, p).__doc__ = prop_doc[p] if __debug__: # don't install docstrings when in optimized/non-debug mode _install_properties_docs()