From 7fda9000c700ec5509fc3d5a3b61c89b74e51d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 6 Aug 2013 10:51:50 +0200 Subject: [PATCH 1/6] Add cached_property decorator --- skimage/_shared/utils.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index ea631716..ae82bfbc 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -5,7 +5,7 @@ import sys from . import six -__all__ = ['deprecated', 'get_bound_method_class'] +__all__ = ['deprecated', 'cached_property', 'get_bound_method_class'] class deprecated(object): @@ -57,6 +57,38 @@ class deprecated(object): return wrapped +class cached_property(object): + """Decorator to use a function as a cached property. + + The function is only called the first time and each successive call returns + the cached result of the first call. + + class Foo(object): + + @cached_property + def foo(self): + return "Cached" + + Adapted from . + + """ + + def __init__(self, func, name=None, doc=None): + self.__name__ = name or func.__name__ + self.__module__ = func.__module__ + self.__doc__ = doc or func.__doc__ + self.func = func + + def __get__(self, obj, type=None): + if obj is None: + return self + value = obj.__dict__.get(self.__name__, _missing) + if value is _missing: + value = self.func(obj) + obj.__dict__[self.__name__] = value + return value + + def get_bound_method_class(m): """Return the class for a bound method. From 918332c4c6fc6a0d4090002c1ba158f231a3ab3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 6 Aug 2013 14:27:37 +0200 Subject: [PATCH 2/6] Refactor regionprops --- doc/examples/plot_regionprops.py | 24 +- skimage/measure/_regionprops.py | 773 ++++++++++++---------- skimage/measure/tests/test_regionprops.py | 109 ++- 3 files changed, 486 insertions(+), 420 deletions(-) diff --git a/doc/examples/plot_regionprops.py b/doc/examples/plot_regionprops.py index f96b3b8c..f675d11c 100644 --- a/doc/examples/plot_regionprops.py +++ b/doc/examples/plot_regionprops.py @@ -24,29 +24,23 @@ image[rr,cc] = 1 image = rotate(image, angle=15, order=0) label_img = label(image) -props = regionprops(label_img, [ - 'BoundingBox', - 'Centroid', - 'Orientation', - 'MajorAxisLength', - 'MinorAxisLength' -]) +regions = regionprops(label_img) plt.imshow(image) -for prop in props: - x0 = prop['Centroid'][1] - y0 = prop['Centroid'][0] - x1 = x0 + math.cos(prop['Orientation']) * 0.5 * prop['MajorAxisLength'] - y1 = y0 - math.sin(prop['Orientation']) * 0.5 * prop['MajorAxisLength'] - x2 = x0 - math.sin(prop['Orientation']) * 0.5 * prop['MinorAxisLength'] - y2 = y0 - math.cos(prop['Orientation']) * 0.5 * prop['MinorAxisLength'] +for props in regions: + y0, x0 = props.centroid + orientation = props.orientation + x1 = x0 + math.cos(orientation) * 0.5 * props.major_axis_length + y1 = y0 - math.sin(orientation) * 0.5 * props.major_axis_length + x2 = x0 - math.sin(orientation) * 0.5 * props.minor_axis_length + y2 = y0 - math.cos(orientation) * 0.5 * props.minor_axis_length plt.plot((x0, x1), (y0, y1), '-r', linewidth=2.5) plt.plot((x0, x2), (y0, y2), '-r', linewidth=2.5) plt.plot(x0, y0, '.g', markersize=15) - minr, minc, maxr, maxc = prop['BoundingBox'] + minr, minc, maxr, maxc = props.bbox bx = (minc, maxc, maxc, minc, minc) by = (minr, minr, maxr, maxr, minr) plt.plot(bx, by, '-b', linewidth=2.5) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 72d1e2f4..810a44a0 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -1,4 +1,5 @@ # coding: utf-8 +import warnings from math import sqrt, atan2, pi as PI import numpy as np from scipy import ndimage @@ -14,47 +15,308 @@ STREL_4 = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]) STREL_8 = np.ones((3, 3), 'int8') -PROPS = ( - 'Area', - 'BoundingBox', - 'CentralMoments', - 'Centroid', - 'ConvexArea', +PROPS = { + 'Area': 'area', + 'BoundingBox': 'bbox', + 'CentralMoments': 'central_moments', + 'Centroid': 'centroid', + 'ConvexArea': 'convex_area', # 'ConvexHull', - 'ConvexImage', - 'Coordinates', - 'Eccentricity', - 'EquivDiameter', - 'EulerNumber', - 'Extent', + 'ConvexImage': 'convex_image', + 'Coordinates': 'coords', + 'Eccentricity': 'eccentricity', + 'EquivDiameter': 'equivalent_diameter', + 'EulerNumber': 'euler_number', + 'Extent': 'extent', # 'Extrema', - 'FilledArea', - 'FilledImage', - 'HuMoments', - 'Image', - 'MajorAxisLength', - 'MaxIntensity', - 'MeanIntensity', - 'MinIntensity', - 'MinorAxisLength', - 'Moments', - 'NormalizedMoments', - 'Orientation', - 'Perimeter', + 'FilledArea': 'filled_area', + 'FilledImage': 'filled_image', + 'HuMoments': 'hu_moments', + 'Image': 'image', + 'MajorAxisLength': 'major_axis_length', + 'MaxIntensity': 'max_intensity', + 'MeanIntensity': 'mean_intensity', + 'MinIntensity': 'min_intensity', + 'MinorAxisLength': 'minor_axis_length', + 'Moments': 'moments', + 'NormalizedMoments': 'normalized_moments', + 'Orientation': 'orientation', + 'Perimeter': 'perimeter', # 'PixelIdxList', # 'PixelList', - 'Solidity', + 'Solidity': 'solidity', # 'SubarrayIdx' - 'WeightedCentralMoments', - 'WeightedCentroid', - 'WeightedHuMoments', - 'WeightedMoments', - 'WeightedNormalizedMoments' -) + 'WeightedCentralMoments': 'weighted_central_moments', + 'WeightedCentroid': 'weighted_centroid', + 'WeightedHuMoments': 'weighted_hu_moments', + 'WeightedMoments': 'weighted_moments', + 'WeightedNormalizedMoments': 'weighted_normalized_moments' +} -def regionprops(label_image, properties=['Area', 'Centroid'], - intensity_image=None): +class cached_property(object): + """Decorator to use a function as a cached property. + + The function is only called the first time and each successive call returns + the cached result of the first call. + + class Foo(object): + + @cached_property + def foo(self): + return "Cached" + + class Foo(object): + + def __init__(self): + self.cache_active = False + + @cached_property + def foo(self): + return "Not cached" + + Adapted from . + + """ + + def __init__(self, func, name=None, doc=None): + self.__name__ = name or func.__name__ + self.__module__ = func.__module__ + self.__doc__ = doc or func.__doc__ + self.func = func + + def __get__(self, obj, type=None): + if obj is None: + return self + + # call every time, if cache is not active + if not obj.__dict__.get('cache_active', True): + return self.func(obj) + + # try to retrieve from cache or call and store result in cache + try: + value = obj.__dict__[self.__name__] + except KeyError: + value = self.func(obj) + obj.__dict__[self.__name__] = value + return value + + +class _RegionProperties(object): + + def __init__(self, slice, label, label_image, intensity_image, + cache_active): + self._slice = slice + self.label = label + self._label_image = label_image + self._intensity_image = intensity_image + self.cache_active = cache_active + + @cached_property + def area(self): + """Number of pixels of region.""" + + return self.moments[0, 0] + + @cached_property + def bbox(self): + """Bounding box `(min_row, min_col, max_row, max_col)`""" + + return (self._slice[0].start, self._slice[1].start, + self._slice[0].stop, self._slice[1].stop) + + @cached_property + def centroid(self): + row, col = self.local_centroid + return row + self._slice[0].start, col + self._slice[1].start + + @cached_property + def central_moments(self): + row, col = self.local_centroid + return _moments.central_moments(self._image_double, row, col, 3) + + @cached_property + def convex_area(self): + return np.sum(self.convex_image) + + @cached_property + def convex_image(self): + return convex_hull_image(self.image) + + @cached_property + def coords(self): + rr, cc = np.nonzero(self.image) + return np.vstack((rr + self._slice[0].start, + cc + self._slice[1].start)).T + + @cached_property + def eccentricity(self): + l1, l2 = self.inertia_tensor_eigvals + if l1 == 0: + return 0 + return sqrt(1 - l2 / l1) + + @cached_property + def equivalent_diameter(self): + return sqrt(4 * self.moments[0, 0] / PI) + + @cached_property + def euler_number(self): + euler_array = self.filled_image != self.image + _, num = ndimage.label(euler_array, STREL_8) + return -num + + @cached_property + def extent(self): + rows, cols = self.image.shape + return self.moments[0, 0] / (rows * cols) + + @cached_property + def filled_area(self): + return np.sum(self.filled_image) + + @cached_property + def filled_image(self): + return ndimage.binary_fill_holes(self.image, STREL_8) + + @cached_property + def hu_moments(self): + return _moments.hu_moments(self.normalized_moments) + + @cached_property + def image(self): + return self._label_image[self._slice] == self.label + + @cached_property + def _image_double(self): + return self.image.astype(np.double) + + @cached_property + def inertia_tensor(self): + mu = self.central_moments + a = mu[2, 0] / mu[0, 0] + b = -mu[1, 1] / mu[0, 0] + c = mu[0, 2] / mu[0, 0] + return np.array([[a, b], [b, c]]) + + @cached_property + def inertia_tensor_eigvals(self): + a, b, b, c = self.inertia_tensor.flat + # eigen values of inertia tensor + l1 = (a + c) / 2 + sqrt(4 * b ** 2 + (a - c) ** 2) / 2 + l2 = (a + c) / 2 - sqrt(4 * b ** 2 + (a - c) ** 2) / 2 + return l1, l2 + + @cached_property + def intensity_image(self): + if self._intensity_image is None: + raise AttributeError('No intensity image specified.') + return self._intensity_image[self._slice] * self.image + + @cached_property + def _intensity_image_double(self): + return self.intensity_image.astype(np.double) + + @cached_property + def moments(self): + return _moments.central_moments(self._image_double, 0, 0, 3) + + @cached_property + def local_centroid(self): + m = self.moments + row = m[0, 1] / m[0, 0] + col = m[1, 0] / m[0, 0] + return row, col + + @cached_property + def max_intensity(self): + return np.max(self.intensity_image[self.image]) + + @cached_property + def mean_intensity(self): + return np.mean(self.intensity_image[self.image]) + + @cached_property + def min_intensity(self): + return np.min(self.intensity_image[self.image]) + + @cached_property + def major_axis_length(self): + l1, _ = self.inertia_tensor_eigvals + return 4 * sqrt(l1) + + @cached_property + def minor_axis_length(self): + _, l2 = self.inertia_tensor_eigvals + return 4 * sqrt(l2) + + @cached_property + def normalized_moments(self): + return _moments.normalized_moments(self.central_moments, 3) + + @cached_property + def orientation(self): + a, b, b, c = self.inertia_tensor.flat + b = -b + if a - c == 0: + if b > 0: + return -PI / 4. + else: + return PI / 4. + else: + return - 0.5 * atan2(2 * b, (a - c)) + + @cached_property + def perimeter(self): + return perimeter(self.image, 4) + + @cached_property + def solidity(self): + return self.moments[0, 0] / np.sum(self.convex_image) + + @cached_property + def weighted_central_moments(self): + row, col = self.weighted_local_centroid + return _moments.central_moments(self._intensity_image_double, + row, col, 3) + + @cached_property + def weighted_centroid(self): + row, col = self.weighted_local_centroid + return row + self._slice[0].start, col + self._slice[1].start + + @cached_property + def weighted_local_centroid(self): + m = self.weighted_moments + row = m[0, 1] / m[0, 0] + col = m[1, 0] / m[0, 0] + return row, col + + @cached_property + def weighted_hu_moments(self): + return _moments.hu_moments(self.weighted_normalized_moments) + + @cached_property + def weighted_moments(self): + return _moments.central_moments(self._intensity_image_double, 0, 0, 3) + + @cached_property + def weighted_normalized_moments(self): + return _moments.normalized_moments(self.weighted_central_moments, 3) + + def __getitem__(self, key): + value = getattr(self, key, None) + if value is not None: + return value + else: # backwards compatability + warnings.warn('Usage of deprecated property name.', + category=DeprecationWarning) + return getattr(self, PROPS[key]) + + + +def regionprops(label_image, properties=None, + intensity_image=None, cache=True): """Measure properties of labelled image regions. Parameters @@ -62,150 +324,128 @@ def regionprops(label_image, properties=['Area', 'Centroid'], label_image : (N, M) ndarray Labelled input image. properties : {'all', list} - Shape measurements to be determined for each labelled image region. - Default is `['Area', 'Centroid']`. The following properties can be - determined: + **Deprecated parameter** - * Area : int - Number of pixels of region. - - * BoundingBox : tuple - Bounding box `(min_row, min_col, max_row, max_col)` - - * CentralMoments : (3, 3) ndarray - Central moments (translation invariant) up to 3rd order. - - mu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } - - where the sum is over the `x`, `y` coordinates of the region, - and `x_c` and `y_c` are the coordinates of the region's centroid. - - * Centroid : array - Centroid coordinate tuple `(row, col)`. - - * ConvexArea : int - Number of pixels of convex hull image. - - * ConvexImage : (H, J) ndarray - Binary convex hull image which has the same size as bounding box. - - * Coordinates : (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 distance between its - minor and major axis length. The value is between 0 and 1. - - * EquivDiameter : float - The diameter of a circle with the same area as the region. - - * EulerNumber : int - Euler number 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)` - - * FilledArea : int - Number of pixels of filled region. - - * FilledImage : (H, J) ndarray - Binary region image with filled holes which has the same size as - bounding box. - - * HuMoments : tuple - Hu moments (translation, scale and rotation invariant). - - * Image : (H, J) ndarray - Sliced binary region image which has the same size as bounding box. - - * MajorAxisLength : float - The length of the major axis of the ellipse that has the same - normalized second central moments as the region. - - * MaxIntensity: float - Value with the greatest intensity in the region. - - * MeanIntensity: float - Value with the mean intensity in the region. - - * MinIntensity: float - Value with the least intensity in the region. - - * MinorAxisLength : 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_ji = sum{ array(x, y) * x^j * y^i } - - where the sum is over the `x`, `y` coordinates of the region. - - * NormalizedMoments : (3, 3) ndarray - Normalized moments (translation and scale invariant) up to 3rd - order. - - nu_ji = mu_ji / m_00^[(i+j)/2 + 1] - - where `m_00` is the zeroth spatial moment. - - * Orientation : float - Angle between the X-axis and the major axis of the ellipse that has - the same second-moments as the region. Ranging from `-pi/2` to - `pi/2` in counter-clockwise direction. - - * Perimeter : float - Perimeter of object which approximates the contour as a line - through the centers of border pixels using a 4-connectivity. - - * Solidity : float - Ratio of pixels in the region to pixels of the convex hull image. - - * WeightedCentralMoments : (3, 3) ndarray - Central moments (translation invariant) of intensity image up to - 3rd order. - - wmu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } - - where the sum is over the `x`, `y` coordinates of the region, - and `x_c` and `y_c` are the coordinates of the region's centroid. - - * WeightedCentroid : array - Centroid coordinate tuple `(row, col)` weighted with intensity - image. - - * WeightedHuMoments : tuple - Hu moments (translation, scale and rotation invariant) of intensity - image. - - * WeightedMoments : (3, 3) ndarray - Spatial moments of intensity image up to 3rd order. - - wm_ji = sum{ array(x, y) * x^j * y^i } - - where the sum is over the `x`, `y` coordinates of the region. - - * WeightedNormalizedMoments : (3, 3) ndarray - Normalized moments (translation and scale invariant) of intensity - image up to 3rd order. - - wnu_ji = wmu_ji / wm_00^[(i+j)/2 + 1] - - where `wm_00` is the zeroth spatial moment (intensity-weighted - area). + This parameter is not needed any more since all properties are + determined dynamically. intensity_image : (N, M) ndarray, optional Intensity image with same size as labelled 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. Returns ------- - properties : list of dicts - List containing a property dict for each region. The property dicts - contain all the specified properties plus a 'Label' field. + properties : list + List containing a properties for each region. The properties of each + region can be accessed as attributes and keys. + + Notes + ----- + The following properties can be accessed as attributes or keys: + + area : int + Number of pixels of region. + bbox : tuple + Bounding box `(min_row, min_col, max_row, max_col)` + central_moments : (3, 3) ndarray + Central moments (translation invariant) up to 3rd order:: + + mu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } + + where the sum is over the `x`, `y` coordinates of the region, + and `x_c` and `y_c` are the coordinates of the region's centroid. + centroid : array + Centroid coordinate tuple `(row, col)`. + convex_area : int + Number of pixels of convex hull image. + 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 distance between its + minor and major axis length. The value is between 0 and 1. + equivalent_diameter : float + The diameter of a circle with the same area as the region. + euler_number : int + Euler number 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 filled region. + filled_image : (H, J) ndarray + Binary region image with filled holes which has the same size as + bounding box. + hu_moments : tuple + Hu moments (translation, scale and rotation invariant). + image : (H, J) ndarray + Sliced binary region image which has the same size as 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. + min_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_ji = sum{ array(x, y) * x^j * y^i } + + where the sum is over the `x`, `y` coordinates of the region. + normalized_moments : (3, 3) ndarray + Normalized moments (translation and scale invariant) up to 3rd order:: + + nu_ji = mu_ji / m_00^[(i+j)/2 + 1] + + where `m_00` is the zeroth spatial moment. + orientation : float + Angle between the X-axis and the major axis of the ellipse that has + the same second-moments as the region. Ranging from `-pi/2` to + `pi/2` in counter-clockwise direction. + perimeter : float + Perimeter of object which approximates the contour as a line + through the centers of border pixels using a 4-connectivity. + solidity : float + Ratio of pixels in the region to pixels of the convex hull image. + weighted_central_moments : (3, 3) ndarray + Central moments (translation invariant) of intensity image up to + 3rd order:: + + wmu_ji = sum{ array(x, y) * (x - x_c)^j * (y - y_c)^i } + + where the sum is over the `x`, `y` coordinates of the region, + and `x_c` and `y_c` are the coordinates of the region's centroid. + weighted_centroid : array + Centroid coordinate tuple `(row, col)` weighted with intensity + image. + weighted_hu_moments : tuple + Hu moments (translation, scale and rotation invariant) of intensity + image. + weighted_moments : (3, 3) ndarray + Spatial moments of intensity image up to 3rd order:: + + wm_ji = sum{ array(x, y) * x^j * y^i } + + where the sum is over the `x`, `y` coordinates of the region. + weighted_normalized_moments : (3, 3) ndarray + Normalized moments (translation and scale invariant) of intensity + image up to 3rd order:: + + wnu_ji = wmu_ji / wm_00^[(i+j)/2 + 1] + + where `wm_00` is the zeroth spatial moment (intensity-weighted area). References ---------- @@ -225,194 +465,29 @@ def regionprops(label_image, properties=['Area', 'Centroid'], >>> img = coins() > 110 >>> label_img = label(img) >>> props = regionprops(label_img) - >>> props[0]['Centroid'] # centroid of first labelled object + >>> props[0].centroid # centroid of first labelled object + >>> props[0]['centroid'] # centroid of first labelled object """ if not np.issubdtype(label_image.dtype, 'int'): - raise TypeError('labelled image must be of integer dtype') + raise TypeError('Labelled image must be of integer dtype.') - # determine all properties if nothing specified - if properties == 'all': - properties = PROPS + if properties is not None: + warnings.warn('The ``properties`` argument is deprecated and is ' + 'not needed any more as properties are ' + 'determined dynamically.', + category=DeprecationWarning) - props = [] + regions = [] objects = ndimage.find_objects(label_image) for i, sl in enumerate(objects): label = i + 1 - # create property dict for current label - obj_props = {} - props.append(obj_props) + props = _RegionProperties(sl, label, label_image, + intensity_image, cache) + regions.append(props) - obj_props['Label'] = label - - array = (label_image[sl] == label).astype('double') - - # upper left corner of object bbox - r0 = sl[0].start - c0 = sl[1].start - - m = _moments.central_moments(array, 0, 0, 3) - # centroid - cr = m[0, 1] / m[0, 0] - cc = m[1, 0] / m[0, 0] - mu = _moments.central_moments(array, cr, cc, 3) - - # elements of the inertia tensor [a b; b c] - a = mu[2, 0] / mu[0, 0] - b = mu[1, 1] / mu[0, 0] - c = mu[0, 2] / mu[0, 0] - # eigen values of inertia tensor - l1 = (a + c) / 2 + sqrt(4 * b ** 2 + (a - c) ** 2) / 2 - l2 = (a + c) / 2 - sqrt(4 * b ** 2 + (a - c) ** 2) / 2 - - # cached results which are used by several properties - _filled_image = None - _convex_image = None - _nu = None - - if 'Area' in properties: - obj_props['Area'] = m[0, 0] - - if 'BoundingBox' in properties: - obj_props['BoundingBox'] = (r0, c0, sl[0].stop, sl[1].stop) - - if 'Centroid' in properties: - obj_props['Centroid'] = cr + r0, cc + c0 - - if 'CentralMoments' in properties: - obj_props['CentralMoments'] = mu - - if 'ConvexArea' in properties: - if _convex_image is None: - _convex_image = convex_hull_image(array) - obj_props['ConvexArea'] = np.sum(_convex_image) - - if 'ConvexImage' in properties: - if _convex_image is None: - _convex_image = convex_hull_image(array) - obj_props['ConvexImage'] = _convex_image - - if 'Coordinates' in properties: - rr, cc = np.nonzero(array) - obj_props['Coordinates'] = np.vstack((rr + r0, cc + c0)).T - - if 'Eccentricity' in properties: - if l1 == 0: - obj_props['Eccentricity'] = 0 - else: - obj_props['Eccentricity'] = sqrt(1 - l2 / l1) - - if 'EquivDiameter' in properties: - obj_props['EquivDiameter'] = sqrt(4 * m[0, 0] / PI) - - if 'EulerNumber' in properties: - if _filled_image is None: - _filled_image = ndimage.binary_fill_holes(array, STREL_8) - euler_array = _filled_image != array - _, num = ndimage.label(euler_array, STREL_8) - obj_props['EulerNumber'] = - num - - if 'Extent' in properties: - obj_props['Extent'] = m[0, 0] / (array.shape[0] * array.shape[1]) - - if 'HuMoments' in properties: - if _nu is None: - _nu = _moments.normalized_moments(mu, 3) - obj_props['HuMoments'] = _moments.hu_moments(_nu) - - if 'Image' in properties: - obj_props['Image'] = array - - if 'FilledArea' in properties: - if _filled_image is None: - _filled_image = ndimage.binary_fill_holes(array, STREL_8) - obj_props['FilledArea'] = np.sum(_filled_image) - - if 'FilledImage' in properties: - if _filled_image is None: - _filled_image = ndimage.binary_fill_holes(array, STREL_8) - obj_props['FilledImage'] = _filled_image - - if 'MajorAxisLength' in properties: - obj_props['MajorAxisLength'] = 4 * sqrt(l1) - - if 'MinorAxisLength' in properties: - obj_props['MinorAxisLength'] = 4 * sqrt(l2) - - if 'Moments' in properties: - obj_props['Moments'] = m - - if 'NormalizedMoments' in properties: - if _nu is None: - _nu = _moments.normalized_moments(mu, 3) - obj_props['NormalizedMoments'] = _nu - - if 'Orientation' in properties: - if a - c == 0: - if b > 0: - obj_props['Orientation'] = -PI / 4. - else: - obj_props['Orientation'] = PI / 4. - else: - obj_props['Orientation'] = - 0.5 * atan2(2 * b, (a - c)) - - if 'Perimeter' in properties: - obj_props['Perimeter'] = perimeter(array, 4) - - if 'Solidity' in properties: - if _convex_image is None: - _convex_image = convex_hull_image(array) - obj_props['Solidity'] = m[0, 0] / np.sum(_convex_image) - - if intensity_image is not None: - weighted_array = array * intensity_image[sl] - - wm = _moments.central_moments(weighted_array, 0, 0, 3) - # weighted centroid - wcr = wm[0, 1] / wm[0, 0] - wcc = wm[1, 0] / wm[0, 0] - wmu = _moments.central_moments(weighted_array, wcr, wcc, 3) - - # cached results which are used by several properties - _wnu = None - _vals = None - - if 'MaxIntensity' in properties: - if _vals is None: - _vals = weighted_array[array.astype('bool')] - obj_props['MaxIntensity'] = np.max(_vals) - - if 'MeanIntensity' in properties: - if _vals is None: - _vals = weighted_array[array.astype('bool')] - obj_props['MeanIntensity'] = np.mean(_vals) - - if 'MinIntensity' in properties: - if _vals is None: - _vals = weighted_array[array.astype('bool')] - obj_props['MinIntensity'] = np.min(_vals) - - if 'WeightedCentralMoments' in properties: - obj_props['WeightedCentralMoments'] = wmu - - if 'WeightedCentroid' in properties: - obj_props['WeightedCentroid'] = wcr + r0, wcc + c0 - - if 'WeightedHuMoments' in properties: - if _wnu is None: - _wnu = _moments.normalized_moments(wmu, 3) - obj_props['WeightedHuMoments'] = _moments.hu_moments(_wnu) - - if 'WeightedMoments' in properties: - obj_props['WeightedMoments'] = wm - - if 'WeightedNormalizedMoments' in properties: - if _wnu is None: - _wnu = _moments.normalized_moments(wmu, 3) - obj_props['WeightedNormalizedMoments'] = _wnu - - return props + return regions def perimeter(image, neighbourhood=4): diff --git a/skimage/measure/tests/test_regionprops.py b/skimage/measure/tests/test_regionprops.py index c1396670..c515a3fd 100644 --- a/skimage/measure/tests/test_regionprops.py +++ b/skimage/measure/tests/test_regionprops.py @@ -22,33 +22,33 @@ INTENSITY_SAMPLE = SAMPLE.copy() INTENSITY_SAMPLE[1, 9:11] = 2 +def test_all_props(): + regions = regionprops(SAMPLE, 'all', INTENSITY_SAMPLE)[0] + for prop in PROPS: + regions[prop] + + def test_unsupported_dtype(): assert_raises(TypeError, regionprops, np.zeros((10, 10), dtype=np.double)) -def test_all_props(): - props = regionprops(SAMPLE, 'all', INTENSITY_SAMPLE)[0] - for prop in PROPS: - assert prop in props - - def test_area(): - area = regionprops(SAMPLE, ['Area'])[0]['Area'] + area = regionprops(SAMPLE)[0].area assert area == np.sum(SAMPLE) def test_bbox(): - bbox = regionprops(SAMPLE, ['BoundingBox'])[0]['BoundingBox'] + bbox = regionprops(SAMPLE)[0].bbox assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1])) SAMPLE_mod = SAMPLE.copy() SAMPLE_mod[:, -1] = 0 - bbox = regionprops(SAMPLE_mod, ['BoundingBox'])[0]['BoundingBox'] + bbox = regionprops(SAMPLE_mod)[0].bbox assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1]-1)) def test_central_moments(): - mu = regionprops(SAMPLE, ['CentralMoments'])[0]['CentralMoments'] + mu = regionprops(SAMPLE)[0].central_moments # determined with OpenCV assert_almost_equal(mu[0,2], 436.00000000000045) # different from OpenCV results, bug in OpenCV @@ -61,19 +61,19 @@ def test_central_moments(): def test_centroid(): - centroid = regionprops(SAMPLE, ['Centroid'])[0]['Centroid'] + centroid = regionprops(SAMPLE)[0].centroid # determined with MATLAB assert_array_almost_equal(centroid, (5.66666666666666, 9.444444444444444)) def test_convex_area(): - area = regionprops(SAMPLE, ['ConvexArea'])[0]['ConvexArea'] + area = regionprops(SAMPLE)[0].convex_area # determined with MATLAB assert area == 124 def test_convex_image(): - img = regionprops(SAMPLE, ['ConvexImage'])[0]['ConvexImage'] + img = regionprops(SAMPLE)[0].convex_image # determined with MATLAB ref = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0], @@ -94,43 +94,43 @@ def test_coordinates(): sample = np.zeros((10, 10), dtype=np.int8) coords = np.array([[3, 2], [3, 3], [3, 4]]) sample[coords[:, 0], coords[:, 1]] = 1 - prop_coords = regionprops(sample, ['Coordinates'])[0]['Coordinates'] + prop_coords = regionprops(sample)[0].coords assert_array_equal(prop_coords, coords) def test_eccentricity(): - eps = regionprops(SAMPLE, ['Eccentricity'])[0]['Eccentricity'] + eps = regionprops(SAMPLE)[0].eccentricity assert_almost_equal(eps, 0.814629313427) img = np.zeros((5, 5), dtype=np.int) img[2, 2] = 1 - eps = regionprops(img, ['Eccentricity'])[0]['Eccentricity'] + eps = regionprops(img)[0].eccentricity assert_almost_equal(eps, 0) def test_equiv_diameter(): - diameter = regionprops(SAMPLE, ['EquivDiameter'])[0]['EquivDiameter'] + diameter = regionprops(SAMPLE)[0].equivalent_diameter # determined with MATLAB assert_almost_equal(diameter, 9.57461472963) def test_euler_number(): - en = regionprops(SAMPLE, ['EulerNumber'])[0]['EulerNumber'] + en = regionprops(SAMPLE)[0].euler_number assert en == 0 SAMPLE_mod = SAMPLE.copy() SAMPLE_mod[7, -3] = 0 - en = regionprops(SAMPLE_mod, ['EulerNumber'])[0]['EulerNumber'] + en = regionprops(SAMPLE_mod)[0].euler_number assert en == -1 def test_extent(): - extent = regionprops(SAMPLE, ['Extent'])[0]['Extent'] + extent = regionprops(SAMPLE)[0].extent assert_almost_equal(extent, 0.4) def test_hu_moments(): - hu = regionprops(SAMPLE, ['HuMoments'])[0]['HuMoments'] + hu = regionprops(SAMPLE)[0].hu_moments ref = np.array([ 3.27117627e-01, 2.63869194e-02, @@ -145,59 +145,59 @@ def test_hu_moments(): def test_image(): - img = regionprops(SAMPLE, ['Image'])[0]['Image'] + img = regionprops(SAMPLE)[0].image assert_array_equal(img, SAMPLE) def test_filled_area(): - area = regionprops(SAMPLE, ['FilledArea'])[0]['FilledArea'] + area = regionprops(SAMPLE)[0].filled_area assert area == np.sum(SAMPLE) SAMPLE_mod = SAMPLE.copy() SAMPLE_mod[7, -3] = 0 - area = regionprops(SAMPLE_mod, ['FilledArea'])[0]['FilledArea'] + area = regionprops(SAMPLE_mod)[0].filled_area assert area == np.sum(SAMPLE) def test_filled_image(): - img = regionprops(SAMPLE, ['FilledImage'])[0]['FilledImage'] + img = regionprops(SAMPLE)[0].filled_image assert_array_equal(img, SAMPLE) def test_major_axis_length(): - length = regionprops(SAMPLE, ['MajorAxisLength'])[0]['MajorAxisLength'] + length = regionprops(SAMPLE)[0].major_axis_length # MATLAB has different interpretation of ellipse than found in literature, # here implemented as found in literature assert_almost_equal(length, 16.7924234999) def test_max_intensity(): - intensity = regionprops(SAMPLE, ['MaxIntensity'], INTENSITY_SAMPLE - )[0]['MaxIntensity'] + intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].max_intensity assert_almost_equal(intensity, 2) def test_mean_intensity(): - intensity = regionprops(SAMPLE, ['MeanIntensity'], INTENSITY_SAMPLE - )[0]['MeanIntensity'] + intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].mean_intensity assert_almost_equal(intensity, 1.02777777777777) def test_min_intensity(): - intensity = regionprops(SAMPLE, ['MinIntensity'], INTENSITY_SAMPLE - )[0]['MinIntensity'] + intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].min_intensity assert_almost_equal(intensity, 1) def test_minor_axis_length(): - length = regionprops(SAMPLE, ['MinorAxisLength'])[0]['MinorAxisLength'] + length = regionprops(SAMPLE)[0].minor_axis_length # MATLAB has different interpretation of ellipse than found in literature, # here implemented as found in literature assert_almost_equal(length, 9.739302807263) def test_moments(): - m = regionprops(SAMPLE, ['Moments'])[0]['Moments'] + m = regionprops(SAMPLE)[0].moments # determined with OpenCV assert_almost_equal(m[0,0], 72.0) assert_almost_equal(m[0,1], 408.0) @@ -212,7 +212,7 @@ def test_moments(): def test_normalized_moments(): - nu = regionprops(SAMPLE, ['NormalizedMoments'])[0]['NormalizedMoments'] + nu = regionprops(SAMPLE)[0].normalized_moments # determined with OpenCV assert_almost_equal(nu[0,2], 0.08410493827160502) assert_almost_equal(nu[1,1], -0.016846707818929982) @@ -223,29 +223,26 @@ def test_normalized_moments(): def test_orientation(): - orientation = regionprops(SAMPLE, ['Orientation'])[0]['Orientation'] + orientation = regionprops(SAMPLE)[0].orientation # determined with MATLAB assert_almost_equal(orientation, 0.10446844651921) # test correct quadrant determination - orientation2 = regionprops(SAMPLE.T, ['Orientation'])[0]['Orientation'] + orientation2 = regionprops(SAMPLE.T)[0].orientation assert_almost_equal(orientation2, math.pi / 2 - orientation) # test diagonal regions diag = np.eye(10, dtype=int) - orientation_diag = regionprops(diag, ['Orientation'])[0]['Orientation'] + orientation_diag = regionprops(diag)[0].orientation assert_almost_equal(orientation_diag, -math.pi / 4) - orientation_diag = regionprops(np.flipud(diag), ['Orientation'] - )[0]['Orientation'] + orientation_diag = regionprops(np.flipud(diag))[0].orientation assert_almost_equal(orientation_diag, math.pi / 4) - orientation_diag = regionprops(np.fliplr(diag), ['Orientation'] - )[0]['Orientation'] + orientation_diag = regionprops(np.fliplr(diag))[0].orientation assert_almost_equal(orientation_diag, math.pi / 4) - orientation_diag = regionprops(np.fliplr(np.flipud(diag)), ['Orientation'] - )[0]['Orientation'] + orientation_diag = regionprops(np.fliplr(np.flipud(diag)))[0].orientation assert_almost_equal(orientation_diag, -math.pi / 4) def test_perimeter(): - per = regionprops(SAMPLE, ['Perimeter'])[0]['Perimeter'] + per = regionprops(SAMPLE)[0].perimeter assert_almost_equal(per, 55.2487373415) per = perimeter(SAMPLE.astype('double'), neighbourhood=8) @@ -253,14 +250,14 @@ def test_perimeter(): def test_solidity(): - solidity = regionprops(SAMPLE, ['Solidity'])[0]['Solidity'] + solidity = regionprops(SAMPLE)[0].solidity # determined with MATLAB assert_almost_equal(solidity, 0.580645161290323) def test_weighted_central_moments(): - wmu = regionprops(SAMPLE, ['WeightedCentralMoments'], INTENSITY_SAMPLE - )[0]['WeightedCentralMoments'] + wmu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_central_moments ref = np.array( [[ 7.4000000000e+01, -2.1316282073e-13, 4.7837837838e+02, -7.5943608473e+02], @@ -276,14 +273,14 @@ def test_weighted_central_moments(): def test_weighted_centroid(): - centroid = regionprops(SAMPLE, ['WeightedCentroid'], INTENSITY_SAMPLE - )[0]['WeightedCentroid'] + centroid = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_centroid assert_array_almost_equal(centroid, (5.540540540540, 9.445945945945)) def test_weighted_hu_moments(): - whu = regionprops(SAMPLE, ['WeightedHuMoments'], INTENSITY_SAMPLE - )[0]['WeightedHuMoments'] + whu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_hu_moments ref = np.array([ 3.1750587329e-01, 2.1417517159e-02, @@ -297,8 +294,8 @@ def test_weighted_hu_moments(): def test_weighted_moments(): - wm = regionprops(SAMPLE, ['WeightedMoments'], INTENSITY_SAMPLE - )[0]['WeightedMoments'] + wm = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_moments ref = np.array( [[ 7.4000000000e+01, 4.1000000000e+02, 2.7500000000e+03, 1.9778000000e+04], @@ -313,8 +310,8 @@ def test_weighted_moments(): def test_weighted_normalized_moments(): - wnu = regionprops(SAMPLE, ['WeightedNormalizedMoments'], INTENSITY_SAMPLE - )[0]['WeightedNormalizedMoments'] + wnu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE + )[0].weighted_normalized_moments ref = np.array( [[ np.nan, np.nan, 0.0873590903, -0.0161217406], [ np.nan, -0.0160405109, -0.0031421072, -0.0031376984], From d94c25efde905581306a3cf230ce7639819124d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 6 Aug 2013 14:28:28 +0200 Subject: [PATCH 3/6] Remove cached_property class from shared utils --- skimage/_shared/utils.py | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/skimage/_shared/utils.py b/skimage/_shared/utils.py index ae82bfbc..5e2f7785 100644 --- a/skimage/_shared/utils.py +++ b/skimage/_shared/utils.py @@ -57,38 +57,6 @@ class deprecated(object): return wrapped -class cached_property(object): - """Decorator to use a function as a cached property. - - The function is only called the first time and each successive call returns - the cached result of the first call. - - class Foo(object): - - @cached_property - def foo(self): - return "Cached" - - Adapted from . - - """ - - def __init__(self, func, name=None, doc=None): - self.__name__ = name or func.__name__ - self.__module__ = func.__module__ - self.__doc__ = doc or func.__doc__ - self.func = func - - def __get__(self, obj, type=None): - if obj is None: - return self - value = obj.__dict__.get(self.__name__, _missing) - if value is _missing: - value = self.func(obj) - obj.__dict__[self.__name__] = value - return value - - def get_bound_method_class(m): """Return the class for a bound method. From 9f8e904b6671723b3fbbf2748737cc1a09f3916d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 6 Aug 2013 14:30:51 +0200 Subject: [PATCH 4/6] Add note about backwards compatibility to TODO.txt --- TODO.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.txt b/TODO.txt index 2067d036..7ad40875 100644 --- a/TODO.txt +++ b/TODO.txt @@ -3,6 +3,7 @@ Version 0.10 * Remove deprecated functions: - ``skimage.filter.rank.*`` * Remove deprecated parameter ``epsilon`` of ``skimage.viewer.LineProfile`` +* Remove backwards-compatability of ``skimage.measure.regionprops`` Version 0.9 ----------- From 01871c153d0425ed4af5fc5e6bc6192c86b2459c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 6 Aug 2013 14:34:36 +0200 Subject: [PATCH 5/6] Remove legacy doc strings --- skimage/measure/_regionprops.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 810a44a0..85c11416 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -114,14 +114,10 @@ class _RegionProperties(object): @cached_property def area(self): - """Number of pixels of region.""" - return self.moments[0, 0] @cached_property def bbox(self): - """Bounding box `(min_row, min_col, max_row, max_col)`""" - return (self._slice[0].start, self._slice[1].start, self._slice[0].stop, self._slice[1].stop) From 92e70547fa7d2cb7c12d66bff3fbe28db00dc766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Sch=C3=B6nberger?= Date: Tue, 6 Aug 2013 14:39:38 +0200 Subject: [PATCH 6/6] Use asbolute import for Cython lib --- skimage/measure/_regionprops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skimage/measure/_regionprops.py b/skimage/measure/_regionprops.py index 85c11416..90d4a25c 100644 --- a/skimage/measure/_regionprops.py +++ b/skimage/measure/_regionprops.py @@ -5,7 +5,7 @@ import numpy as np from scipy import ndimage from skimage.morphology import convex_hull_image -from . import _moments +from skimage.measure import _moments __all__ = ['regionprops']