# coding: utf-8 from math import sqrt, atan2, pi as PI import numpy as np from scipy import ndimage from skimage.morphology import convex_hull_image from . import _moments __all__ = ['regionprops'] 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', # 'ConvexHull', 'ConvexImage', 'Coordinates', 'Eccentricity', 'EquivDiameter', 'EulerNumber', 'Extent', # 'Extrema', 'FilledArea', 'FilledImage', 'HuMoments', 'Image', 'MajorAxisLength', 'MaxIntensity', 'MeanIntensity', 'MinIntensity', 'MinorAxisLength', 'Moments', 'NormalizedMoments', 'Orientation', 'Perimeter', # 'PixelIdxList', # 'PixelList', 'Solidity', # 'SubarrayIdx' 'WeightedCentralMoments', 'WeightedCentroid', 'WeightedHuMoments', 'WeightedMoments', 'WeightedNormalizedMoments' ) def regionprops(label_image, properties=['Area', 'Centroid'], intensity_image=None): """Measure properties of labelled image regions. Parameters ---------- 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: * 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). intensity_image : (N, M) ndarray, optional Intensity image with same size as labelled image. Default is None. 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. 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] http://en.wikipedia.org/wiki/Image_moment Examples -------- >>> from skimage.data import coins >>> from skimage.morphology import label >>> img = coins() > 110 >>> label_img = label(img) >>> props = regionprops(label_img) >>> 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') # determine all properties if nothing specified if properties == 'all': properties = PROPS props = [] 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) 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 def perimeter(image, neighbourhood=4): """Calculate total perimeter of all objects in binary image. Parameters ---------- image : array binary image neighbourhood : 4 or 8, optional neighbourhood connectivity for border pixel determination, default 4 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 """ if neighbourhood == 4: strel = STREL_4 else: strel = STREL_8 eroded_image = ndimage.binary_erosion(image, strel) border_image = image - eroded_image # perimeter contribution: corresponding values in convolved image perimeter_weights = { 1: (5, 7, 15, 17, 25, 27), sqrt(2): (21, 33), 1 + sqrt(2) / 2: (13, 23) } perimeter_image = ndimage.convolve(border_image, np.array([[10, 2, 10], [ 2, 1, 2], [10, 2, 10]])) total_perimeter = 0 for weight, values in perimeter_weights.items(): num_values = 0 for value in values: num_values += np.sum(perimeter_image == value) total_perimeter += num_values * weight return total_perimeter