/
_moments.py
472 lines (393 loc) · 15.9 KB
/
_moments.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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# coding: utf-8
from __future__ import division
import numpy as np
from .._shared.utils import assert_nD
from . import _moments_cy
import itertools
from warnings import warn
def moments_coords(coords, order=3):
"""Calculate all raw image moments up to a certain order.
The following properties can be calculated from raw image moments:
* Area as: ``M[0, 0]``.
* Centroid as: {``M[1, 0] / M[0, 0]``, ``M[0, 1] / M[0, 0]``}.
Note that raw moments are neither translation, scale nor rotation
invariant.
Parameters
----------
coords : (N, D) double or uint8 array
Array of N points that describe an image of D dimensionality in
Cartesian space.
order : int, optional
Maximum order of moments. Default is 3.
Returns
-------
M : (``order + 1``, ``order + 1``, ...) array
Raw image moments. (D dimensions)
References
----------
.. [1] Johannes Kilian. Simple Image Analysis By Moments. Durham
University, version 0.2, Durham, 2001.
Examples
--------
>>> coords = np.array([[row, col]
... for row in range(13, 17)
... for col in range(14, 18)], dtype=np.double)
>>> M = moments_coords(coords)
>>> centroid_row = M[1, 0] / M[0, 0]
>>> centroid_col = M[0, 1] / M[0, 0]
>>> centroid_row, centroid_col
(14.5, 15.5)
"""
return moments_coords_central(coords, 0, order=order)
def moments_coords_central(coords, center=None, order=3):
"""Calculate all central image moments up to a certain order.
The following properties can be calculated from raw image moments:
* Area as: ``M[0, 0]``.
* Centroid as: {``M[1, 0] / M[0, 0]``, ``M[0, 1] / M[0, 0]``}.
Note that raw moments are neither translation, scale nor rotation
invariant.
Parameters
----------
coords : (N, D) double or uint8 array
Array of N points that describe an image of D dimensionality in
Cartesian space. A tuple of coordinates as returned by
``np.nonzero`` is also accepted as input.
center : tuple of float, optional
Coordinates of the image centroid. This will be computed if it
is not provided.
order : int, optional
Maximum order of moments. Default is 3.
Returns
-------
Mc : (``order + 1``, ``order + 1``, ...) array
Central image moments. (D dimensions)
References
----------
.. [1] Johannes Kilian. Simple Image Analysis By Moments. Durham
University, version 0.2, Durham, 2001.
Examples
--------
>>> coords = np.array([[row, col]
... for row in range(13, 17)
... for col in range(14, 18)])
>>> moments_coords_central(coords)
array([[ 16., 0., 20., 0.],
[ 0., 0., 0., 0.],
[ 20., 0., 25., 0.],
[ 0., 0., 0., 0.]])
As seen above, for symmetric objects, odd-order moments (columns 1 and 3,
rows 1 and 3) are zero when centered on the centroid, or center of mass,
of the object (the default). If we break the symmetry by adding a new
point, this no longer holds:
>>> coords2 = np.concatenate((coords, [[17, 17]]), axis=0)
>>> np.round(moments_coords_central(coords2), 2)
array([[ 17. , 0. , 22.12, -2.49],
[ 0. , 3.53, 1.73, 7.4 ],
[ 25.88, 6.02, 36.63, 8.83],
[ 4.15, 19.17, 14.8 , 39.6 ]])
Image moments and central image moments are equivalent (by definition)
when the center is (0, 0):
>>> np.allclose(moments_coords(coords),
... moments_coords_central(coords, (0, 0)))
True
"""
if isinstance(coords, tuple):
# This format corresponds to coordinate tuples as returned by
# e.g. np.nonzero: (row_coords, column_coords).
# We represent them as an npoints x ndim array.
coords = np.transpose(coords)
assert_nD(coords, 2)
ndim = coords.shape[1]
if center is None:
center = np.mean(coords, axis=0)
# center the coordinates
coords = coords.astype(float) - center
# generate all possible exponents for each axis in the given set of points
# produces a matrix of shape (N, D, order + 1)
coords = coords[..., np.newaxis] ** np.arange(order + 1)
# add extra dimensions for proper broadcasting
coords = coords.reshape(coords.shape + (1,) * (ndim - 1))
calc = 1
for axis in range(ndim):
# isolate each point's axis
isolated_axis = coords[:, axis]
# rotate orientation of matrix for proper broadcasting
isolated_axis = np.moveaxis(isolated_axis, 1, 1 + axis)
# calculate the moments for each point, one axis at a time
calc = calc * isolated_axis
# sum all individual point moments to get our final answer
Mc = np.sum(calc, axis=0)
return Mc
def moments(image, order=3):
"""Calculate all raw image moments up to a certain order.
The following properties can be calculated from raw image moments:
* Area as: ``M[0, 0]``.
* Centroid as: {``M[1, 0] / M[0, 0]``, ``M[0, 1] / M[0, 0]``}.
Note that raw moments are neither translation, scale nor rotation
invariant.
Parameters
----------
image : nD double or uint8 array
Rasterized shape as image.
order : int, optional
Maximum order of moments. Default is 3.
Returns
-------
m : (``order + 1``, ``order + 1``) array
Raw image moments.
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
--------
>>> image = np.zeros((20, 20), dtype=np.double)
>>> image[13:17, 13:17] = 1
>>> M = moments(image)
>>> cr = M[1, 0] / M[0, 0]
>>> cc = M[0, 1] / M[0, 0]
>>> cr, cc
(14.5, 14.5)
"""
return moments_central(image, (0,) * image.ndim, order=order)
def moments_central(image, center=None, cc=None, order=3, **kwargs):
"""Calculate all central image moments up to a certain order.
The center coordinates (cr, cc) can be calculated from the raw moments as:
{``M[1, 0] / M[0, 0]``, ``M[0, 1] / M[0, 0]``}.
Note that central moments are translation invariant but not scale and
rotation invariant.
Parameters
----------
image : nD double or uint8 array
Rasterized shape as image.
center : tuple of float, optional
Coordinates of the image centroid. This will be computed if it
is not provided.
order : int, optional
The maximum order of moments computed.
Other Parameters
----------------
cr : double
DEPRECATED: Center row coordinate for 2D image.
cc : double
DEPRECATED: Center column coordinate for 2D image.
Returns
-------
mu : (``order + 1``, ``order + 1``) array
Central image moments.
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
--------
>>> image = np.zeros((20, 20), dtype=np.double)
>>> image[13:17, 13:17] = 1
>>> M = moments(image)
>>> cr = M[1, 0] / M[0, 0]
>>> cc = M[0, 1] / M[0, 0]
>>> moments_central(image, (cr, cc))
array([[ 16., 0., 20., 0.],
[ 0., 0., 0., 0.],
[ 20., 0., 25., 0.],
[ 0., 0., 0., 0.]])
"""
if cc is not None: # using deprecated interface
message = ('Using deprecated 2D-only, xy-coordinate interface to '
'moments_central. This interface will be removed in '
'scikit-image 0.16. Use '
'moments_central(image, center=(cr, cc), order=3).')
warn(message)
if 'cr' in kwargs and center is None:
center = (kwargs['cr'], cc)
else:
center = (center, cc)
return moments_central(image, center=center, order=order).T
if center is None:
center = centroid(image)
calc = image.astype(float)
for dim, dim_length in enumerate(image.shape):
delta = np.arange(dim_length, dtype=float) - center[dim]
powers_of_delta = delta[:, np.newaxis] ** np.arange(order + 1)
calc = np.rollaxis(calc, dim, image.ndim)
calc = np.dot(calc, powers_of_delta)
calc = np.rollaxis(calc, -1, dim)
return calc
def moments_normalized(mu, order=3):
"""Calculate all normalized central image moments up to a certain order.
Note that normalized central moments are translation and scale invariant
but not rotation invariant.
Parameters
----------
mu : (M,[ ...,] M) array
Central image moments, where M must be greater than or equal
to ``order``.
order : int, optional
Maximum order of moments. Default is 3.
Returns
-------
nu : (``order + 1``,[ ...,] ``order + 1``) array
Normalized central image moments.
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
--------
>>> image = np.zeros((20, 20), dtype=np.double)
>>> image[13:17, 13:17] = 1
>>> m = moments(image)
>>> cr = m[0, 1] / m[0, 0]
>>> cc = m[1, 0] / m[0, 0]
>>> mu = moments_central(image, cr, cc)
>>> moments_normalized(mu)
array([[ nan, nan, 0.078125 , 0. ],
[ nan, 0. , 0. , 0. ],
[ 0.078125 , 0. , 0.00610352, 0. ],
[ 0. , 0. , 0. , 0. ]])
"""
if np.any(np.array(mu.shape) <= order):
raise ValueError("Shape of image moments must be >= `order`")
nu = np.zeros_like(mu)
mu0 = mu.ravel()[0]
for powers in itertools.product(range(order + 1), repeat=mu.ndim):
if sum(powers) < 2:
nu[powers] = np.nan
else:
nu[powers] = mu[powers] / (mu0 ** (sum(powers) / nu.ndim + 1))
return nu
def moments_hu(nu):
"""Calculate Hu's set of image moments (2D-only).
Note that this set of moments is proofed to be translation, scale and
rotation invariant.
Parameters
----------
nu : (M, M) array
Normalized central image moments, where M must be > 4.
Returns
-------
nu : (7,) array
Hu's set of image moments.
References
----------
.. [1] M. K. Hu, "Visual Pattern Recognition by Moment Invariants",
IRE Trans. Info. Theory, vol. IT-8, pp. 179-187, 1962
.. [2] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing:
Core Algorithms. Springer-Verlag, London, 2009.
.. [3] B. Jähne. Digital Image Processing. Springer-Verlag,
Berlin-Heidelberg, 6. edition, 2005.
.. [4] T. H. Reiss. Recognizing Planar Objects Using Invariant Image
Features, from Lecture notes in computer science, p. 676. Springer,
Berlin, 1993.
.. [5] http://en.wikipedia.org/wiki/Image_moment
"""
return _moments_cy.moments_hu(nu.astype(np.double))
def centroid(image):
"""Return the (weighted) centroid of an image.
Parameters
----------
image : array
The input image.
Returns
-------
center : tuple of float, length ``image.ndim``
The centroid of the (nonzero) pixels in ``image``.
"""
M = moments_central(image, center=(0,) * image.ndim, order=1)
center = (M[tuple(np.eye(image.ndim, dtype=int))] # array of weighted sums
# for each axis
/ M[(0,) * image.ndim]) # weighted sum of all points
return center
def inertia_tensor(image, mu=None):
"""Compute the inertia tensor of the input image.
Parameters
----------
image : array
The input image.
mu : array, optional
The pre-computed central moments of ``image``. The inertia tensor
computation requires the central moments of the image. If an
application requires both the central moments and the inertia tensor
(for example, `skimage.measure.regionprops`), then it is more
efficient to pre-compute them and pass them to the inertia tensor
call.
Returns
-------
T : array, shape ``(image.ndim, image.ndim)``
The inertia tensor of the input image. :math:`T_{i, j}` contains
the covariance of image intensity along axes :math:`i` and :math:`j`.
References
----------
.. [1] https://en.wikipedia.org/wiki/Moment_of_inertia#Inertia_tensor
.. [2] Bernd Jähne. Spatio-Temporal Image Processing: Theory and
Scientific Applications. (Chapter 8: Tensor Methods) Springer, 1993.
"""
if mu is None:
mu = moments_central(image, order=2) # don't need higher-order moments
mu0 = mu[(0,) * image.ndim]
result = np.zeros((image.ndim, image.ndim))
# nD expression to get coordinates ([2, 0], [0, 2]) (2D),
# ([2, 0, 0], [0, 2, 0], [0, 0, 2]) (3D), etc.
corners2 = tuple(2 * np.eye(image.ndim, dtype=int))
d = np.diag(result)
d.flags.writeable = True
# See https://ocw.mit.edu/courses/aeronautics-and-astronautics/
# 16-07-dynamics-fall-2009/lecture-notes/MIT16_07F09_Lec26.pdf
# Iii is the sum of second-order moments of every axis *except* i, not the
# second order moment of axis i.
# See also https://github.com/scikit-image/scikit-image/issues/3229
d[:] = (np.sum(mu[corners2]) - mu[corners2]) / mu0
for dims in itertools.combinations(range(image.ndim), 2):
mu_index = np.zeros(image.ndim, dtype=int)
mu_index[list(dims)] = 1
result[dims] = -mu[tuple(mu_index)] / mu0
result.T[dims] = -mu[tuple(mu_index)] / mu0
return result
def inertia_tensor_eigvals(image, mu=None, T=None):
"""Compute the eigenvalues of the inertia tensor of the image.
The inertia tensor measures covariance of the image intensity along
the image axes. (See `inertia_tensor`.) The relative magnitude of the
eigenvalues of the tensor is thus a measure of the elongation of a
(bright) object in the image.
Parameters
----------
image : array
The input image.
mu : array, optional
The pre-computed central moments of ``image``.
T : array, shape ``(image.ndim, image.ndim)``
The pre-computed inertia tensor. If ``T`` is given, ``mu`` and
``image`` are ignored.
Returns
-------
eigvals : list of float, length ``image.ndim``
The eigenvalues of the inertia tensor of ``image``, in descending
order.
Notes
-----
Computing the eigenvalues requires the inertia tensor of the input image.
This is much faster if the central moments (``mu``) are provided, or,
alternatively, one can provide the inertia tensor (``T``) directly.
"""
if T is None:
T = inertia_tensor(image, mu)
eigvals = np.linalg.eigvalsh(T)
return sorted(eigvals, reverse=True)