988 lines
34 KiB
Python
988 lines
34 KiB
Python
|
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 <numpy-images-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()
|