-
Notifications
You must be signed in to change notification settings - Fork 0
/
compute_morphometry_features.py
217 lines (171 loc) · 7.72 KB
/
compute_morphometry_features.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import warnings
from collections import OrderedDict
import numpy as np
def compute_morphometry_features(im_label, rprops=None):
"""
Calculate morphometry features for each object
Parameters
----------
im_label : array_like
A labeled mask image wherein intensity of a pixel is the ID of the
object it belongs to. Non-zero values are considered to be foreground
objects.
rprops : output of skimage.measure.regionprops, optional
rprops = skimage.measure.regionprops( im_label ). If rprops is not
passed then it will be computed inside which will increase the
computation time.
Returns
-------
fdata: pandas.DataFrame
A pandas dataframe containing the morphometry features for each
object/label listed below.
Notes
-----
List of morphometry features computed by this function:
Orientation.Orientation : float
Angle between the horizontal axis 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.
Size.Area : int
Number of pixels the object occupies.
Size.ConvexHullArea : int
Number of pixels of convex hull image, which is the smallest convex
polygon that encloses the region.
Size.MajorAxisLength : float
The length of the major axis of the ellipse that has the same
normalized second central moments as the object.
Size.MinorAxisLength : float
The length of the minor axis of the ellipse that has the same
normalized second central moments as the region.
Size.Perimeter : float
Perimeter of object which approximates the contour as a line
through the centers of border pixels using a 4-connectivity.
Shape.Circularity: float
A measure of how similar the shape of an object is to the circle
Shape.Eccentricity : float
A measure of aspect ratio computed to be the eccentricity of the
ellipse that has the same second-moments as the object region.
Eccentricity of an ellipse 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.
Shape.EquivalentDiameter : float
The diameter of a circle with the same area as the object.
Shape.Extent : float
Ratio of area of the object to its axis-aligned bounding box.
Shape.FractalDimension : float
Minkowski–Bouligand dimension, aka. the box-counting dimension. It
is a measure of boundary complexity. See
https://en.wikipedia.org/wiki/Minkowski%E2%80%93Bouligand_dimension
Shape.MinorMajorAxisRatio : float
A measure of aspect ratio. Ratio of minor to major axis of the ellipse
that has the same second-moments as the object region
Shape.Solidity : float
A measure of convexity computed as the ratio of the number of pixels
in the object to that of its convex hull.
Shape.HuMoments-k : float
Where k ranges from 1-7 are the 7 Hu moments features. The first six
moments are translation, scale and rotation invariant, while the
seventh moment flips its sign if the shape is a mirror image.
See https://learnopencv.com/shape-matching-using-hu-moments-c-python/
Shape.WeightedHuMoments-k : float
Same as Hu moments, but instead of using the binary mask, using the
intensity image.
"""
import pandas as pd
from skimage.measure import regionprops
# compute object properties if not provided
if rprops is None:
rprops = regionprops(im_label)
intensity_wtd = rprops[0]._intensity_image is not None
# List of feature names
featname_map = OrderedDict({
'Orientation.Orientation': 'orientation',
'Size.Area': 'area',
'Size.ConvexHullArea': 'convex_area',
'Size.MajorAxisLength': 'major_axis_length',
'Size.MinorAxisLength': 'minor_axis_length',
'Size.Perimeter': 'perimeter',
'Shape.Circularity': None,
'Shape.Eccentricity': 'eccentricity',
'Shape.EquivalentDiameter': 'equivalent_diameter',
'Shape.Extent': 'extent',
'Shape.FractalDimension': None,
'Shape.MinorMajorAxisRatio': None,
'Shape.Solidity': 'solidity',
})
hu_cols = ['Shape.HuMoments%d' % k for k in range(1, 8)]
featname_map.update({col: None for col in hu_cols})
if intensity_wtd:
wtd_hu_cols = [col.replace('.Hu', '.WeightedHu') for col in hu_cols]
featname_map.update({col: None for col in wtd_hu_cols})
feature_list = featname_map.keys()
mapped_feats = [k for k, v in featname_map.items() if v is not None]
# create pandas data frame containing the features for each object
numFeatures = len(feature_list)
numLabels = len(rprops)
fdata = pd.DataFrame(np.zeros((numLabels, numFeatures)),
columns=feature_list)
for i, nprop in enumerate(rprops):
# assign some features from sklearn as-is
for name in mapped_feats:
fdata.at[i, name] = nprop[featname_map[name]]
# compute Circularity
numerator = 4 * np.pi * nprop.area
denominator = nprop.perimeter ** 2
if denominator > 0:
fdata.at[i, 'Shape.Circularity'] = numerator / denominator
# compute fractal dimension (boundary complexity)
fdata.at[i, 'Shape.FractalDimension'] = _fractal_dimension(nprop.image)
# compute Minor to Major axis ratio
if nprop.major_axis_length > 0:
fdata.at[i, 'Shape.MinorMajorAxisRatio'] = \
nprop.minor_axis_length / nprop.major_axis_length
else:
fdata.at[i, 'Shape.MinorMajorAxisRatio'] = 1
# Hu moments, raw and possibly also intensity-weighted
fdata.loc[i, hu_cols] = nprop.moments_hu
if intensity_wtd:
fdata.loc[i, wtd_hu_cols] = nprop.weighted_moments_hu
return fdata
def _fractal_dimension(Z):
"""
Calculate the fractal dimension of an object (boundary complexity).
Source: https://gist.github.com/rougier/e5eafc276a4e54f516ed5559df4242c0
From https://en.wikipedia.org/wiki/Minkowski–Bouligand_dimension ...
In fractal geometry, the Minkowski–Bouligand dimension, also known as
Minkowski dimension or box-counting dimension, is a way of determining the
fractal dimension of a set S in a Euclidean space Rn, or more generally in
a metric space (X, d).
"""
# Only for 2d binary image
assert len(Z.shape) == 2
Z = Z > 0
# From https://github.com/rougier/numpy-100 (#87)
def boxcount(arr, k):
S = np.add.reduceat(
np.add.reduceat(arr, np.arange(0, arr.shape[0], k), axis=0),
np.arange(0, arr.shape[1], k),
axis=1)
# We count non-empty (0) and non-full boxes (k*k)
return len(np.where((S > 0) & (S < k * k))[0])
# Minimal dimension of image
p = min(Z.shape)
# Greatest power of 2 less than or equal to p
n = 2 ** np.floor(np.log(p) / np.log(2))
# Extract the exponent
n = int(np.log(n) / np.log(2))
# Build successive box sizes (from 2**n down to 2**1)
sizes = 2 ** np.arange(n, 1, -1)
# Actual box counting with decreasing size
counts = []
for size in sizes:
counts.append(boxcount(Z, size))
# Fit the successive log(sizes) with log (counts)
with warnings.catch_warnings():
warnings.simplefilter('ignore', np.RankWarning)
# coeffs = np.polyfit(np.log(sizes), np.log(counts), 1)
try:
coeffs = np.polyfit(np.log(sizes), np.log(counts), 1)
return -coeffs[0]
except TypeError:
return 0