scikit-image
360 строк · 11.5 Кб
1import itertools2
3import numpy as np4import pytest5from scipy import ndimage as ndi6
7from skimage import draw8from skimage._shared import testing9from skimage._shared.testing import assert_allclose, assert_almost_equal, assert_equal10from skimage._shared.utils import _supported_float_type11from skimage.measure import (12centroid,13inertia_tensor,14inertia_tensor_eigvals,15moments,16moments_central,17moments_coords,18moments_coords_central,19moments_hu,20moments_normalized,21)
22
23
24def compare_moments(m1, m2, thresh=1e-8):25"""Compare two moments arrays.26
27Compares only values in the upper-left triangle of m1, m2 since
28values below the diagonal exceed the specified order and are not computed
29when the analytical computation is used.
30
31Also, there the first-order central moments will be exactly zero with the
32analytical calculation, but will not be zero due to limited floating point
33precision when using a numerical computation. Here we just specify the
34tolerance as a fraction of the maximum absolute value in the moments array.
35"""
36m1 = m1.copy()37m2 = m2.copy()38
39# make sure location of any NaN values match and then ignore the NaN values40# in the subsequent comparisons41nan_idx1 = np.where(np.isnan(m1.ravel()))[0]42nan_idx2 = np.where(np.isnan(m2.ravel()))[0]43assert len(nan_idx1) == len(nan_idx2)44assert np.all(nan_idx1 == nan_idx2)45m1[np.isnan(m1)] = 046m2[np.isnan(m2)] = 047
48max_val = np.abs(m1[m1 != 0]).max()49for orders in itertools.product(*((range(m1.shape[0]),) * m1.ndim)):50if sum(orders) > m1.shape[0] - 1:51m1[orders] = 052m2[orders] = 053continue54abs_diff = abs(m1[orders] - m2[orders])55rel_diff = abs_diff / max_val56assert rel_diff < thresh57
58
59@pytest.mark.parametrize('anisotropic', [False, True, None])60def test_moments(anisotropic):61image = np.zeros((20, 20), dtype=np.float64)62image[14, 14] = 163image[15, 15] = 164image[14, 15] = 0.565image[15, 14] = 0.566if anisotropic:67spacing = (1.4, 2)68else:69spacing = (1, 1)70if anisotropic is None:71m = moments(image)72else:73m = moments(image, spacing=spacing)74assert_equal(m[0, 0], 3)75assert_almost_equal(m[1, 0] / m[0, 0], 14.5 * spacing[0])76assert_almost_equal(m[0, 1] / m[0, 0], 14.5 * spacing[1])77
78
79@pytest.mark.parametrize('anisotropic', [False, True, None])80def test_moments_central(anisotropic):81image = np.zeros((20, 20), dtype=np.float64)82image[14, 14] = 183image[15, 15] = 184image[14, 15] = 0.585image[15, 14] = 0.586if anisotropic:87spacing = (2, 1)88else:89spacing = (1, 1)90if anisotropic is None:91mu = moments_central(image, (14.5, 14.5))92# check for proper centroid computation93mu_calc_centroid = moments_central(image)94else:95mu = moments_central(96image, (14.5 * spacing[0], 14.5 * spacing[1]), spacing=spacing97)98# check for proper centroid computation99mu_calc_centroid = moments_central(image, spacing=spacing)100
101compare_moments(mu, mu_calc_centroid, thresh=1e-14)102
103# shift image by dx=2, dy=2104image2 = np.zeros((20, 20), dtype=np.double)105image2[16, 16] = 1106image2[17, 17] = 1107image2[16, 17] = 0.5108image2[17, 16] = 0.5109if anisotropic is None:110mu2 = moments_central(image2, (14.5 + 2, 14.5 + 2))111else:112mu2 = moments_central(113image2, ((14.5 + 2) * spacing[0], (14.5 + 2) * spacing[1]), spacing=spacing114)115# central moments must be translation invariant116compare_moments(mu, mu2, thresh=1e-14)117
118
119def test_moments_coords():120image = np.zeros((20, 20), dtype=np.float64)121image[13:17, 13:17] = 1122mu_image = moments(image)123
124coords = np.array(125[[r, c] for r in range(13, 17) for c in range(13, 17)], dtype=np.float64126)127mu_coords = moments_coords(coords)128assert_almost_equal(mu_coords, mu_image)129
130
131@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])132def test_moments_coords_dtype(dtype):133image = np.zeros((20, 20), dtype=dtype)134image[13:17, 13:17] = 1135
136expected_dtype = _supported_float_type(dtype)137mu_image = moments(image)138assert mu_image.dtype == expected_dtype139
140coords = np.array(141[[r, c] for r in range(13, 17) for c in range(13, 17)], dtype=dtype142)143mu_coords = moments_coords(coords)144assert mu_coords.dtype == expected_dtype145
146assert_almost_equal(mu_coords, mu_image)147
148
149def test_moments_central_coords():150image = np.zeros((20, 20), dtype=np.float64)151image[13:17, 13:17] = 1152mu_image = moments_central(image, (14.5, 14.5))153
154coords = np.array(155[[r, c] for r in range(13, 17) for c in range(13, 17)], dtype=np.float64156)157mu_coords = moments_coords_central(coords, (14.5, 14.5))158assert_almost_equal(mu_coords, mu_image)159
160# ensure that center is being calculated normally161mu_coords_calc_centroid = moments_coords_central(coords)162assert_almost_equal(mu_coords_calc_centroid, mu_coords)163
164# shift image by dx=3 dy=3165image = np.zeros((20, 20), dtype=np.float64)166image[16:20, 16:20] = 1167mu_image = moments_central(image, (14.5, 14.5))168
169coords = np.array(170[[r, c] for r in range(16, 20) for c in range(16, 20)], dtype=np.float64171)172mu_coords = moments_coords_central(coords, (14.5, 14.5))173assert_almost_equal(mu_coords, mu_image)174
175
176def test_moments_normalized():177image = np.zeros((20, 20), dtype=np.float64)178image[13:17, 13:17] = 1179mu = moments_central(image, (14.5, 14.5))180nu = moments_normalized(mu)181# shift image by dx=-2, dy=-2 and scale non-zero extent by 0.5182image2 = np.zeros((20, 20), dtype=np.float64)183# scale amplitude by 0.7184image2[11:13, 11:13] = 0.7185mu2 = moments_central(image2, (11.5, 11.5))186nu2 = moments_normalized(mu2)187# central moments must be translation and scale invariant188assert_almost_equal(nu, nu2, decimal=1)189
190
191@pytest.mark.parametrize('anisotropic', [False, True])192def test_moments_normalized_spacing(anisotropic):193image = np.zeros((20, 20), dtype=np.double)194image[13:17, 13:17] = 1195
196if not anisotropic:197spacing1 = (1, 1)198spacing2 = (3, 3)199else:200spacing1 = (1, 2)201spacing2 = (2, 4)202
203mu = moments_central(image, spacing=spacing1)204nu = moments_normalized(mu, spacing=spacing1)205
206mu2 = moments_central(image, spacing=spacing2)207nu2 = moments_normalized(mu2, spacing=spacing2)208
209# result should be invariant to absolute scale of spacing210compare_moments(nu, nu2)211
212
213def test_moments_normalized_3d():214image = draw.ellipsoid(1, 1, 10)215mu_image = moments_central(image)216nu = moments_normalized(mu_image)217assert nu[0, 0, 2] > nu[0, 2, 0]218assert_almost_equal(nu[0, 2, 0], nu[2, 0, 0])219
220coords = np.where(image)221mu_coords = moments_coords_central(coords)222assert_almost_equal(mu_image, mu_coords)223
224
225@pytest.mark.parametrize('dtype', [np.uint8, np.int32, np.float32, np.float64])226@pytest.mark.parametrize('order', [1, 2, 3, 4])227@pytest.mark.parametrize('ndim', [2, 3, 4])228def test_analytical_moments_calculation(dtype, order, ndim):229if ndim == 2:230shape = (256, 256)231elif ndim == 3:232shape = (64, 64, 64)233else:234shape = (16,) * ndim235rng = np.random.default_rng(1234)236if np.dtype(dtype).kind in 'iu':237x = rng.integers(0, 256, shape, dtype=dtype)238else:239x = rng.standard_normal(shape, dtype=dtype)240# setting center=None will use the analytical expressions241m1 = moments_central(x, center=None, order=order)242# providing explicit centroid will bypass the analytical code path243m2 = moments_central(x, center=centroid(x), order=order)244# ensure numeric and analytical central moments are close245# TODO: np 2 failed w/ thresh = 1e-4246thresh = 1.5e-4 if x.dtype == np.float32 else 1e-9247compare_moments(m1, m2, thresh=thresh)248
249
250def test_moments_normalized_invalid():251with testing.raises(ValueError):252moments_normalized(np.zeros((3, 3)), 3)253with testing.raises(ValueError):254moments_normalized(np.zeros((3, 3)), 4)255
256
257def test_moments_hu():258image = np.zeros((20, 20), dtype=np.float64)259image[13:15, 13:17] = 1260mu = moments_central(image, (13.5, 14.5))261nu = moments_normalized(mu)262hu = moments_hu(nu)263# shift image by dx=2, dy=3, scale by 0.5 and rotate by 90deg264image2 = np.zeros((20, 20), dtype=np.float64)265image2[11, 11:13] = 1266image2 = image2.T267mu2 = moments_central(image2, (11.5, 11))268nu2 = moments_normalized(mu2)269hu2 = moments_hu(nu2)270# central moments must be translation and scale invariant271assert_almost_equal(hu, hu2, decimal=1)272
273
274@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])275def test_moments_dtype(dtype):276image = np.zeros((20, 20), dtype=dtype)277image[13:15, 13:17] = 1278
279expected_dtype = _supported_float_type(dtype)280mu = moments_central(image, (13.5, 14.5))281assert mu.dtype == expected_dtype282
283nu = moments_normalized(mu)284assert nu.dtype == expected_dtype285
286hu = moments_hu(nu)287assert hu.dtype == expected_dtype288
289
290@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])291def test_centroid(dtype):292image = np.zeros((20, 20), dtype=dtype)293image[14, 14:16] = 1294image[15, 14:16] = 1 / 3295image_centroid = centroid(image)296if dtype == np.float16:297rtol = 1e-3298elif dtype == np.float32:299rtol = 1e-5300else:301rtol = 1e-7302assert_allclose(image_centroid, (14.25, 14.5), rtol=rtol)303
304
305@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])306def test_inertia_tensor_2d(dtype):307image = np.zeros((40, 40), dtype=dtype)308image[15:25, 5:35] = 1 # big horizontal rectangle (aligned with axis 1)309expected_dtype = _supported_float_type(image.dtype)310
311T = inertia_tensor(image)312assert T.dtype == expected_dtype313assert T[0, 0] > T[1, 1]314np.testing.assert_allclose(T[0, 1], 0)315
316v0, v1 = inertia_tensor_eigvals(image, T=T)317assert v0.dtype == expected_dtype318assert v1.dtype == expected_dtype319np.testing.assert_allclose(np.sqrt(v0 / v1), 3, rtol=0.01, atol=0.05)320
321
322def test_inertia_tensor_3d():323image = draw.ellipsoid(10, 5, 3)324T0 = inertia_tensor(image)325eig0, V0 = np.linalg.eig(T0)326# principal axis of ellipse = eigenvector of smallest eigenvalue327v0 = V0[:, np.argmin(eig0)]328
329assert np.allclose(v0, [1, 0, 0]) or np.allclose(-v0, [1, 0, 0])330
331imrot = ndi.rotate(image.astype(float), 30, axes=(0, 1), order=1)332Tr = inertia_tensor(imrot)333eigr, Vr = np.linalg.eig(Tr)334vr = Vr[:, np.argmin(eigr)]335
336# Check that axis has rotated by expected amount337pi, cos, sin = np.pi, np.cos, np.sin338R = np.array(339[[cos(pi / 6), -sin(pi / 6), 0], [sin(pi / 6), cos(pi / 6), 0], [0, 0, 1]]340)341expected_vr = R @ v0342assert np.allclose(vr, expected_vr, atol=1e-3, rtol=0.01) or np.allclose(343-vr, expected_vr, atol=1e-3, rtol=0.01344)345
346
347def test_inertia_tensor_eigvals():348# Floating point precision problems could make a positive349# semidefinite matrix have an eigenvalue that is very slightly350# negative. Check that we have caught and fixed this problem.351image = np.array(352[353[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],354[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],355[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],356]357)358# mu = np.array([[3, 0, 98], [0, 14, 0], [2, 0, 98]])359eigvals = inertia_tensor_eigvals(image=image)360assert min(eigvals) >= 0361