scikit-image
1536 строк · 51.4 Кб
1import math
2
3import re
4import numpy as np
5import pytest
6import scipy.ndimage as ndi
7from numpy.testing import (
8assert_allclose,
9assert_almost_equal,
10assert_array_almost_equal,
11assert_array_equal,
12assert_equal,
13)
14
15from skimage import data, draw, transform
16from skimage._shared import testing
17from skimage.measure._regionprops import (
18COL_DTYPES,
19OBJECT_COLUMNS,
20PROPS,
21_inertia_eigvals_to_axes_lengths_3D,
22_parse_docs,
23_props_to_dict,
24_require_intensity_image,
25euler_number,
26perimeter,
27perimeter_crofton,
28regionprops,
29regionprops_table,
30)
31from skimage.segmentation import slic
32
33SAMPLE = np.array(
34[
35[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0],
36[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
37[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
38[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
39[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
40[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
41[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
42[1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0],
43[0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1],
44[0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1],
45]
46)
47INTENSITY_SAMPLE = SAMPLE.copy()
48INTENSITY_SAMPLE[1, 9:11] = 2
49INTENSITY_FLOAT_SAMPLE = INTENSITY_SAMPLE.copy().astype(np.float64) / 10.0
50INTENSITY_FLOAT_SAMPLE_MULTICHANNEL = INTENSITY_FLOAT_SAMPLE[..., np.newaxis] * [
511,
522,
533,
54]
55
56SAMPLE_MULTIPLE = np.eye(10, dtype=np.int32)
57SAMPLE_MULTIPLE[3:5, 7:8] = 2
58INTENSITY_SAMPLE_MULTIPLE = SAMPLE_MULTIPLE.copy() * 2.0
59
60SAMPLE_3D = np.zeros((6, 6, 6), dtype=np.uint8)
61SAMPLE_3D[1:3, 1:3, 1:3] = 1
62SAMPLE_3D[3, 2, 2] = 1
63INTENSITY_SAMPLE_3D = SAMPLE_3D.copy()
64
65
66def get_moment_function(img, spacing=(1, 1)):
67rows, cols = img.shape
68Y, X = np.meshgrid(
69np.linspace(0, rows * spacing[0], rows, endpoint=False),
70np.linspace(0, cols * spacing[1], cols, endpoint=False),
71indexing='ij',
72)
73return lambda p, q: np.sum(Y**p * X**q * img)
74
75
76def get_moment3D_function(img, spacing=(1, 1, 1)):
77slices, rows, cols = img.shape
78Z, Y, X = np.meshgrid(
79np.linspace(0, slices * spacing[0], slices, endpoint=False),
80np.linspace(0, rows * spacing[1], rows, endpoint=False),
81np.linspace(0, cols * spacing[2], cols, endpoint=False),
82indexing='ij',
83)
84return lambda p, q, r: np.sum(Z**p * Y**q * X**r * img)
85
86
87def get_central_moment_function(img, spacing=(1, 1)):
88rows, cols = img.shape
89Y, X = np.meshgrid(
90np.linspace(0, rows * spacing[0], rows, endpoint=False),
91np.linspace(0, cols * spacing[1], cols, endpoint=False),
92indexing='ij',
93)
94
95Mpq = get_moment_function(img, spacing=spacing)
96cY = Mpq(1, 0) / Mpq(0, 0)
97cX = Mpq(0, 1) / Mpq(0, 0)
98
99return lambda p, q: np.sum((Y - cY) ** p * (X - cX) ** q * img)
100
101
102def test_all_props():
103region = regionprops(SAMPLE, INTENSITY_SAMPLE)[0]
104for prop in PROPS:
105try:
106# access legacy name via dict
107assert_almost_equal(region[prop], getattr(region, PROPS[prop]))
108
109# skip property access tests for old CamelCase names
110# (we intentionally do not provide properties for these)
111if prop.lower() == prop:
112# access legacy name via attribute
113assert_almost_equal(getattr(region, prop), getattr(region, PROPS[prop]))
114
115except TypeError: # the `slice` property causes this
116pass
117
118
119def test_all_props_3d():
120region = regionprops(SAMPLE_3D, INTENSITY_SAMPLE_3D)[0]
121for prop in PROPS:
122try:
123assert_almost_equal(region[prop], getattr(region, PROPS[prop]))
124
125# skip property access tests for old CamelCase names
126# (we intentionally do not provide properties for these)
127if prop.lower() == prop:
128assert_almost_equal(getattr(region, prop), getattr(region, PROPS[prop]))
129
130except (NotImplementedError, TypeError):
131pass
132
133
134def test_num_pixels():
135num_pixels = regionprops(SAMPLE)[0].num_pixels
136assert num_pixels == 72
137
138num_pixels = regionprops(SAMPLE, spacing=(2, 1))[0].num_pixels
139assert num_pixels == 72
140
141
142def test_dtype():
143regionprops(np.zeros((10, 10), dtype=int))
144regionprops(np.zeros((10, 10), dtype=np.uint))
145with pytest.raises(TypeError):
146regionprops(np.zeros((10, 10), dtype=float))
147with pytest.raises(TypeError):
148regionprops(np.zeros((10, 10), dtype=np.float64))
149with pytest.raises(TypeError):
150regionprops(np.zeros((10, 10), dtype=bool))
151
152
153def test_ndim():
154regionprops(np.zeros((10, 10), dtype=int))
155regionprops(np.zeros((10, 10, 1), dtype=int))
156regionprops(np.zeros((10, 10, 10), dtype=int))
157regionprops(np.zeros((1, 1), dtype=int))
158regionprops(np.zeros((1, 1, 1), dtype=int))
159with pytest.raises(TypeError):
160regionprops(np.zeros((10, 10, 10, 2), dtype=int))
161
162
163def test_feret_diameter_max():
164# comparator result is based on SAMPLE from manually-inspected computations
165comparator_result = 18
166test_result = regionprops(SAMPLE)[0].feret_diameter_max
167assert np.abs(test_result - comparator_result) < 1
168comparator_result_spacing = 10
169test_result_spacing = regionprops(SAMPLE, spacing=[1, 0.1])[0].feret_diameter_max
170assert np.abs(test_result_spacing - comparator_result_spacing) < 1
171# square, test that maximum Feret diameter is sqrt(2) * square side
172img = np.zeros((20, 20), dtype=np.uint8)
173img[2:-2, 2:-2] = 1
174feret_diameter_max = regionprops(img)[0].feret_diameter_max
175assert np.abs(feret_diameter_max - 16 * np.sqrt(2)) < 1
176# Due to marching-squares with a level of .5 the diagonal goes from (0, 0.5) to (16, 15.5).
177assert np.abs(feret_diameter_max - np.sqrt(16**2 + (16 - 1) ** 2)) < 1e-6
178
179
180def test_feret_diameter_max_spacing():
181# comparator result is based on SAMPLE from manually-inspected computations
182comparator_result = 18
183test_result = regionprops(SAMPLE)[0].feret_diameter_max
184assert np.abs(test_result - comparator_result) < 1
185spacing = (2, 1)
186# square, test that maximum Feret diameter is sqrt(2) * square side
187img = np.zeros((20, 20), dtype=np.uint8)
188img[2:-2, 2:-2] = 1
189feret_diameter_max = regionprops(img, spacing=spacing)[0].feret_diameter_max
190# For anisotropic spacing the shift is applied to the smaller spacing.
191assert (
192np.abs(
193feret_diameter_max
194- np.sqrt(
195(spacing[0] * 16 - (spacing[0] <= spacing[1])) ** 2
196+ (spacing[1] * 16 - (spacing[1] < spacing[0])) ** 2
197)
198)
199< 1e-6
200)
201
202
203def test_feret_diameter_max_3d():
204img = np.zeros((20, 20), dtype=np.uint8)
205img[2:-2, 2:-2] = 1
206img_3d = np.dstack((img,) * 3)
207feret_diameter_max = regionprops(img_3d)[0].feret_diameter_max
208# Due to marching-cubes with a level of .5 -1=2*0.5 has to be subtracted from two axes.
209# There are three combinations (x-1, y-1, z), (x-1, y, z-1), (x, y-1, z-1). The option
210# yielding the longest diagonal is the computed max_feret_diameter.
211assert (
212np.abs(feret_diameter_max - np.sqrt((16 - 1) ** 2 + 16**2 + (3 - 1) ** 2))
213< 1e-6
214)
215spacing = (1, 2, 3)
216feret_diameter_max = regionprops(img_3d, spacing=spacing)[0].feret_diameter_max
217# The longest of the three options is the max_feret_diameter
218assert (
219np.abs(
220feret_diameter_max
221- np.sqrt(
222(spacing[0] * (16 - 1)) ** 2
223+ (spacing[1] * (16 - 0)) ** 2
224+ (spacing[2] * (3 - 1)) ** 2
225)
226)
227< 1e-6
228)
229assert (
230np.abs(
231feret_diameter_max
232- np.sqrt(
233(spacing[0] * (16 - 1)) ** 2
234+ (spacing[1] * (16 - 1)) ** 2
235+ (spacing[2] * (3 - 0)) ** 2
236)
237)
238> 1e-6
239)
240assert (
241np.abs(
242feret_diameter_max
243- np.sqrt(
244(spacing[0] * (16 - 0)) ** 2
245+ (spacing[1] * (16 - 1)) ** 2
246+ (spacing[2] * (3 - 1)) ** 2
247)
248)
249> 1e-6
250)
251
252
253@pytest.mark.parametrize(
254"sample,spacing",
255[
256(SAMPLE, None),
257(SAMPLE, 1),
258(SAMPLE, (1, 1)),
259(SAMPLE, (1, 2)),
260(SAMPLE_3D, None),
261(SAMPLE_3D, 1),
262(SAMPLE_3D, (2, 1, 3)),
263],
264)
265def test_area(sample, spacing):
266area = regionprops(sample, spacing=spacing)[0].area
267desired = np.sum(sample * (np.prod(spacing) if spacing else 1))
268assert area == desired
269
270
271def test_bbox():
272bbox = regionprops(SAMPLE)[0].bbox
273assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1]))
274
275bbox = regionprops(SAMPLE, spacing=(1, 2))[0].bbox
276assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1]))
277
278SAMPLE_mod = SAMPLE.copy()
279SAMPLE_mod[:, -1] = 0
280bbox = regionprops(SAMPLE_mod)[0].bbox
281assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1] - 1))
282bbox = regionprops(SAMPLE_mod, spacing=(3, 2))[0].bbox
283assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1] - 1))
284
285bbox = regionprops(SAMPLE_3D)[0].bbox
286assert_array_almost_equal(bbox, (1, 1, 1, 4, 3, 3))
287bbox = regionprops(SAMPLE_3D, spacing=(0.5, 2, 7))[0].bbox
288assert_array_almost_equal(bbox, (1, 1, 1, 4, 3, 3))
289
290
291def test_area_bbox():
292padded = np.pad(SAMPLE, 5, mode='constant')
293bbox_area = regionprops(padded)[0].area_bbox
294assert_array_almost_equal(bbox_area, SAMPLE.size)
295
296
297def test_area_bbox_spacing():
298spacing = (0.5, 3)
299padded = np.pad(SAMPLE, 5, mode='constant')
300bbox_area = regionprops(padded, spacing=spacing)[0].area_bbox
301assert_array_almost_equal(bbox_area, SAMPLE.size * np.prod(spacing))
302
303
304def test_moments_central():
305mu = regionprops(SAMPLE)[0].moments_central
306# determined with OpenCV
307assert_almost_equal(mu[2, 0], 436.00000000000045)
308# different from OpenCV results, bug in OpenCV
309assert_almost_equal(mu[3, 0], -737.333333333333)
310assert_almost_equal(mu[1, 1], -87.33333333333303)
311assert_almost_equal(mu[2, 1], -127.5555555555593)
312assert_almost_equal(mu[0, 2], 1259.7777777777774)
313assert_almost_equal(mu[1, 2], 2000.296296296291)
314assert_almost_equal(mu[0, 3], -760.0246913580195)
315
316# Verify central moment test functions
317centralMpq = get_central_moment_function(SAMPLE, spacing=(1, 1))
318assert_almost_equal(centralMpq(2, 0), mu[2, 0])
319assert_almost_equal(centralMpq(3, 0), mu[3, 0])
320assert_almost_equal(centralMpq(1, 1), mu[1, 1])
321assert_almost_equal(centralMpq(2, 1), mu[2, 1])
322assert_almost_equal(centralMpq(0, 2), mu[0, 2])
323assert_almost_equal(centralMpq(1, 2), mu[1, 2])
324assert_almost_equal(centralMpq(0, 3), mu[0, 3])
325
326
327def test_moments_central_spacing():
328# Test spacing against verified central moment test function
329spacing = (1.8, 0.8)
330centralMpq = get_central_moment_function(SAMPLE, spacing=spacing)
331
332mu = regionprops(SAMPLE, spacing=spacing)[0].moments_central
333assert_almost_equal(mu[2, 0], centralMpq(2, 0))
334assert_almost_equal(mu[3, 0], centralMpq(3, 0))
335assert_almost_equal(mu[1, 1], centralMpq(1, 1))
336assert_almost_equal(mu[2, 1], centralMpq(2, 1))
337assert_almost_equal(mu[0, 2], centralMpq(0, 2))
338assert_almost_equal(mu[1, 2], centralMpq(1, 2))
339assert_almost_equal(mu[0, 3], centralMpq(0, 3))
340
341
342def test_centroid():
343centroid = regionprops(SAMPLE)[0].centroid
344# determined with MATLAB
345assert_array_almost_equal(centroid, (5.66666666666666, 9.444444444444444))
346
347# Verify test moment function with spacing=(1, 1)
348Mpq = get_moment_function(SAMPLE, spacing=(1, 1))
349cY = Mpq(1, 0) / Mpq(0, 0)
350cX = Mpq(0, 1) / Mpq(0, 0)
351
352assert_array_almost_equal((cY, cX), centroid)
353
354
355def test_centroid_spacing():
356spacing = (1.8, 0.8)
357# Moment
358Mpq = get_moment_function(SAMPLE, spacing=spacing)
359cY = Mpq(1, 0) / Mpq(0, 0)
360cX = Mpq(0, 1) / Mpq(0, 0)
361
362centroid = regionprops(SAMPLE, spacing=spacing)[0].centroid
363assert_array_almost_equal(centroid, (cY, cX))
364
365
366def test_centroid_3d():
367centroid = regionprops(SAMPLE_3D)[0].centroid
368# determined by mean along axis 1 of SAMPLE_3D.nonzero()
369assert_array_almost_equal(centroid, (1.66666667, 1.55555556, 1.55555556))
370
371# Verify moment 3D test function
372Mpqr = get_moment3D_function(SAMPLE_3D, spacing=(1, 1, 1))
373cZ = Mpqr(1, 0, 0) / Mpqr(0, 0, 0)
374cY = Mpqr(0, 1, 0) / Mpqr(0, 0, 0)
375cX = Mpqr(0, 0, 1) / Mpqr(0, 0, 0)
376
377assert_array_almost_equal((cZ, cY, cX), centroid)
378
379
380@pytest.mark.parametrize(
381"spacing",
382[[2.1, 2.2, 2.3], [2.0, 2.0, 2.0], [2, 2, 2]],
383)
384def test_spacing_parameter_3d(spacing):
385"""Test the _normalize_spacing code."""
386
387# Test centroid3d spacing
388Mpqr = get_moment3D_function(SAMPLE_3D, spacing=spacing)
389cZ = Mpqr(1, 0, 0) / Mpqr(0, 0, 0)
390cY = Mpqr(0, 1, 0) / Mpqr(0, 0, 0)
391cX = Mpqr(0, 0, 1) / Mpqr(0, 0, 0)
392centroid = regionprops(SAMPLE_3D, spacing=spacing)[0].centroid
393assert_array_almost_equal(centroid, (cZ, cY, cX))
394
395
396@pytest.mark.parametrize(
397"spacing",
398[(1, 1j), 1 + 0j],
399)
400def test_spacing_parameter_complex_input(spacing):
401"""Test the _normalize_spacing code."""
402with pytest.raises(
403TypeError, match="Element of spacing isn't float or integer type, got"
404):
405regionprops(SAMPLE, spacing=spacing)[0].centroid
406
407
408@pytest.mark.parametrize(
409"spacing",
410[np.nan, np.inf, -np.inf],
411)
412def test_spacing_parameter_nan_inf(spacing):
413"""Test the _normalize_spacing code."""
414with pytest.raises(ValueError):
415regionprops(SAMPLE, spacing=spacing)[0].centroid
416
417
418@pytest.mark.parametrize("spacing", ([1], [[1, 1]], (1, 1, 1)))
419def test_spacing_mismtaching_shape(spacing):
420with pytest.raises(ValueError, match="spacing isn't a scalar nor a sequence"):
421regionprops(SAMPLE, spacing=spacing)[0].centroid
422
423
424@pytest.mark.parametrize("spacing", [[2.1, 2.2], [2.0, 2.0], [2, 2]])
425def test_spacing_parameter_2d(spacing):
426"""Test the _normalize_spacing code."""
427# Test weight centroid spacing
428Mpq = get_moment_function(INTENSITY_SAMPLE, spacing=spacing)
429cY = Mpq(0, 1) / Mpq(0, 0)
430cX = Mpq(1, 0) / Mpq(0, 0)
431centroid = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE, spacing=spacing)[
4320
433].centroid_weighted
434assert_almost_equal(centroid, (cX, cY))
435
436
437@pytest.mark.parametrize(
438"spacing",
439[["bad input"], ["bad input", 1, 2.1]],
440)
441def test_spacing_parameter_2d_bad_input(spacing):
442"""Test the _normalize_spacing code."""
443with pytest.raises(ValueError):
444regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE, spacing=spacing)[
4450
446].centroid_weighted
447
448
449def test_area_convex():
450area = regionprops(SAMPLE)[0].area_convex
451assert area == 125
452
453
454def test_area_convex_spacing():
455spacing = (1, 4)
456area = regionprops(SAMPLE, spacing=spacing)[0].area_convex
457assert area == 125 * np.prod(spacing)
458
459
460def test_image_convex():
461img = regionprops(SAMPLE)[0].image_convex
462ref = np.array(
463[
464[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0],
465[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
466[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
467[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
468[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
469[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
470[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
471[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
472[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
473[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
474]
475)
476assert_array_equal(img, ref)
477
478
479def test_coordinates():
480sample = np.zeros((10, 10), dtype=np.int8)
481coords = np.array([[3, 2], [3, 3], [3, 4]])
482sample[coords[:, 0], coords[:, 1]] = 1
483prop_coords = regionprops(sample)[0].coords
484assert_array_equal(prop_coords, coords)
485prop_coords = regionprops(sample, spacing=(0.5, 1.2))[0].coords
486assert_array_equal(prop_coords, coords)
487
488
489@pytest.mark.parametrize(
490"spacing",
491[None, 1, 2, (1, 1), (1, 0.5)],
492)
493def test_coordinates_scaled(spacing):
494sample = np.zeros((10, 10), dtype=np.int8)
495coords = np.array([[3, 2], [3, 3], [3, 4]])
496sample[coords[:, 0], coords[:, 1]] = 1
497prop_coords = regionprops(sample, spacing=spacing)[0].coords_scaled
498if spacing is None:
499desired_coords = coords
500else:
501desired_coords = coords * np.array(spacing)
502assert_array_equal(prop_coords, desired_coords)
503
504
505@pytest.mark.parametrize(
506"spacing",
507[None, 1, 2, (0.2, 3, 2.3)],
508)
509def test_coordinates_scaled_3d(spacing):
510sample = np.zeros((6, 6, 6), dtype=np.int8)
511coords = np.array([[1, 1, 1], [1, 2, 1], [1, 3, 1]])
512sample[coords[:, 0], coords[:, 1], coords[:, 2]] = 1
513prop_coords = regionprops(sample, spacing=spacing)[0].coords_scaled
514if spacing is None:
515desired_coords = coords
516else:
517desired_coords = coords * np.array(spacing)
518assert_array_equal(prop_coords, desired_coords)
519
520
521def test_slice():
522padded = np.pad(SAMPLE, ((2, 4), (5, 2)), mode='constant')
523nrow, ncol = SAMPLE.shape
524result = regionprops(padded)[0].slice
525expected = (slice(2, 2 + nrow), slice(5, 5 + ncol))
526assert_equal(result, expected)
527
528
529def test_slice_spacing():
530padded = np.pad(SAMPLE, ((2, 4), (5, 2)), mode='constant')
531nrow, ncol = SAMPLE.shape
532result = regionprops(padded)[0].slice
533expected = (slice(2, 2 + nrow), slice(5, 5 + ncol))
534spacing = (2, 0.2)
535result = regionprops(padded, spacing=spacing)[0].slice
536assert_equal(result, expected)
537
538
539def test_eccentricity():
540eps = regionprops(SAMPLE)[0].eccentricity
541assert_almost_equal(eps, 0.814629313427)
542
543eps = regionprops(SAMPLE, spacing=(1.5, 1.5))[0].eccentricity
544assert_almost_equal(eps, 0.814629313427)
545
546img = np.zeros((5, 5), dtype=int)
547img[2, 2] = 1
548eps = regionprops(img)[0].eccentricity
549assert_almost_equal(eps, 0)
550
551eps = regionprops(img, spacing=(3, 3))[0].eccentricity
552assert_almost_equal(eps, 0)
553
554
555def test_equivalent_diameter_area():
556diameter = regionprops(SAMPLE)[0].equivalent_diameter_area
557# determined with MATLAB
558assert_almost_equal(diameter, 9.57461472963)
559
560spacing = (1, 3)
561diameter = regionprops(SAMPLE, spacing=spacing)[0].equivalent_diameter_area
562equivalent_area = np.pi * (diameter / 2.0) ** 2
563assert_almost_equal(equivalent_area, SAMPLE.sum() * np.prod(spacing))
564
565
566def test_euler_number():
567for spacing in [(1, 1), (2.1, 0.9)]:
568en = regionprops(SAMPLE, spacing=spacing)[0].euler_number
569assert en == 0
570
571SAMPLE_mod = SAMPLE.copy()
572SAMPLE_mod[7, -3] = 0
573en = regionprops(SAMPLE_mod, spacing=spacing)[0].euler_number
574assert en == -1
575
576en = euler_number(SAMPLE, 1)
577assert en == 2
578
579en = euler_number(SAMPLE_mod, 1)
580assert en == 1
581
582en = euler_number(SAMPLE_3D, 1)
583assert en == 1
584
585en = euler_number(SAMPLE_3D, 3)
586assert en == 1
587
588# for convex body, Euler number is 1
589SAMPLE_3D_2 = np.zeros((100, 100, 100))
590SAMPLE_3D_2[40:60, 40:60, 40:60] = 1
591en = euler_number(SAMPLE_3D_2, 3)
592assert en == 1
593
594SAMPLE_3D_2[45:55, 45:55, 45:55] = 0
595en = euler_number(SAMPLE_3D_2, 3)
596assert en == 2
597
598
599def test_extent():
600extent = regionprops(SAMPLE)[0].extent
601assert_almost_equal(extent, 0.4)
602extent = regionprops(SAMPLE, spacing=(5, 0.2))[0].extent
603assert_almost_equal(extent, 0.4)
604
605
606def test_moments_hu():
607hu = regionprops(SAMPLE)[0].moments_hu
608ref = np.array(
609[
6103.27117627e-01,
6112.63869194e-02,
6122.35390060e-02,
6131.23151193e-03,
6141.38882330e-06,
615-2.72586158e-05,
616-6.48350653e-06,
617]
618)
619# bug in OpenCV caused in Central Moments calculation?
620assert_array_almost_equal(hu, ref)
621
622with testing.raises(NotImplementedError):
623regionprops(SAMPLE, spacing=(2, 1))[0].moments_hu
624
625
626def test_image():
627img = regionprops(SAMPLE)[0].image
628assert_array_equal(img, SAMPLE)
629
630img = regionprops(SAMPLE_3D)[0].image
631assert_array_equal(img, SAMPLE_3D[1:4, 1:3, 1:3])
632
633
634def test_label():
635label = regionprops(SAMPLE)[0].label
636assert_array_equal(label, 1)
637
638label = regionprops(SAMPLE_3D)[0].label
639assert_array_equal(label, 1)
640
641
642def test_area_filled():
643area = regionprops(SAMPLE)[0].area_filled
644assert area == np.sum(SAMPLE)
645
646
647def test_area_filled_zero():
648SAMPLE_mod = SAMPLE.copy()
649SAMPLE_mod[7, -3] = 0
650area = regionprops(SAMPLE_mod)[0].area_filled
651assert area == np.sum(SAMPLE)
652
653
654def test_area_filled_spacing():
655SAMPLE_mod = SAMPLE.copy()
656SAMPLE_mod[7, -3] = 0
657
658spacing = (2, 1.2)
659area = regionprops(SAMPLE, spacing=spacing)[0].area_filled
660assert area == np.sum(SAMPLE) * np.prod(spacing)
661
662area = regionprops(SAMPLE_mod, spacing=spacing)[0].area_filled
663assert area == np.sum(SAMPLE) * np.prod(spacing)
664
665
666def test_image_filled():
667img = regionprops(SAMPLE)[0].image_filled
668assert_array_equal(img, SAMPLE)
669img = regionprops(SAMPLE, spacing=(1, 4))[0].image_filled
670assert_array_equal(img, SAMPLE)
671
672
673def test_axis_major_length():
674length = regionprops(SAMPLE)[0].axis_major_length
675# MATLAB has different interpretation of ellipse than found in literature,
676# here implemented as found in literature
677target_length = 16.7924234999
678assert_almost_equal(length, target_length)
679
680length = regionprops(SAMPLE, spacing=(2, 2))[0].axis_major_length
681assert_almost_equal(length, 2 * target_length)
682
683from skimage.draw import ellipse
684
685img = np.zeros((20, 24), dtype=np.uint8)
686rr, cc = ellipse(11, 11, 7, 9, rotation=np.deg2rad(45))
687img[rr, cc] = 1
688
689target_length = regionprops(img, spacing=(1, 1))[0].axis_major_length
690length_wo_spacing = regionprops(img[::2], spacing=(1, 1))[0].axis_minor_length
691assert abs(length_wo_spacing - target_length) > 0.1
692length = regionprops(img[:, ::2], spacing=(1, 2))[0].axis_major_length
693assert_almost_equal(length, target_length, decimal=0)
694
695
696def test_intensity_max():
697intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE)[0].intensity_max
698assert_almost_equal(intensity, 2)
699
700
701def test_intensity_mean():
702intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE)[0].intensity_mean
703assert_almost_equal(intensity, 1.02777777777777)
704
705
706def test_intensity_min():
707intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE)[0].intensity_min
708assert_almost_equal(intensity, 1)
709
710
711def test_intensity_std():
712intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE)[0].intensity_std
713assert_almost_equal(intensity, 0.16433554953054486)
714
715
716def test_axis_minor_length():
717length = regionprops(SAMPLE)[0].axis_minor_length
718# MATLAB has different interpretation of ellipse than found in literature,
719# here implemented as found in literature
720target_length = 9.739302807263
721assert_almost_equal(length, target_length)
722
723length = regionprops(SAMPLE, spacing=(1.5, 1.5))[0].axis_minor_length
724assert_almost_equal(length, 1.5 * target_length)
725
726from skimage.draw import ellipse
727
728img = np.zeros((10, 12), dtype=np.uint8)
729rr, cc = ellipse(5, 6, 3, 5, rotation=np.deg2rad(30))
730img[rr, cc] = 1
731
732target_length = regionprops(img, spacing=(1, 1))[0].axis_minor_length
733length_wo_spacing = regionprops(img[::2], spacing=(1, 1))[0].axis_minor_length
734assert abs(length_wo_spacing - target_length) > 0.1
735length = regionprops(img[::2], spacing=(2, 1))[0].axis_minor_length
736assert_almost_equal(length, target_length, decimal=1)
737
738
739def test_moments():
740m = regionprops(SAMPLE)[0].moments
741# determined with OpenCV
742assert_almost_equal(m[0, 0], 72.0)
743assert_almost_equal(m[0, 1], 680.0)
744assert_almost_equal(m[0, 2], 7682.0)
745assert_almost_equal(m[0, 3], 95588.0)
746assert_almost_equal(m[1, 0], 408.0)
747assert_almost_equal(m[1, 1], 3766.0)
748assert_almost_equal(m[1, 2], 43882.0)
749assert_almost_equal(m[2, 0], 2748.0)
750assert_almost_equal(m[2, 1], 24836.0)
751assert_almost_equal(m[3, 0], 19776.0)
752
753# Verify moment test function
754Mpq = get_moment_function(SAMPLE, spacing=(1, 1))
755assert_almost_equal(Mpq(0, 0), m[0, 0])
756assert_almost_equal(Mpq(0, 1), m[0, 1])
757assert_almost_equal(Mpq(0, 2), m[0, 2])
758assert_almost_equal(Mpq(0, 3), m[0, 3])
759assert_almost_equal(Mpq(1, 0), m[1, 0])
760assert_almost_equal(Mpq(1, 1), m[1, 1])
761assert_almost_equal(Mpq(1, 2), m[1, 2])
762assert_almost_equal(Mpq(2, 0), m[2, 0])
763assert_almost_equal(Mpq(2, 1), m[2, 1])
764assert_almost_equal(Mpq(3, 0), m[3, 0])
765
766
767def test_moments_spacing():
768# Test moment on spacing
769spacing = (2, 0.3)
770m = regionprops(SAMPLE, spacing=spacing)[0].moments
771Mpq = get_moment_function(SAMPLE, spacing=spacing)
772assert_almost_equal(m[0, 0], Mpq(0, 0))
773assert_almost_equal(m[0, 1], Mpq(0, 1))
774assert_almost_equal(m[0, 2], Mpq(0, 2))
775assert_almost_equal(m[0, 3], Mpq(0, 3))
776assert_almost_equal(m[1, 0], Mpq(1, 0))
777assert_almost_equal(m[1, 1], Mpq(1, 1))
778assert_almost_equal(m[1, 2], Mpq(1, 2))
779assert_almost_equal(m[2, 0], Mpq(2, 0))
780assert_almost_equal(m[2, 1], Mpq(2, 1))
781assert_almost_equal(m[3, 0], Mpq(3, 0))
782
783
784def test_moments_normalized():
785nu = regionprops(SAMPLE)[0].moments_normalized
786
787# determined with OpenCV
788assert_almost_equal(nu[0, 2], 0.24301268861454037)
789assert_almost_equal(nu[0, 3], -0.017278118992041805)
790assert_almost_equal(nu[1, 1], -0.016846707818929982)
791assert_almost_equal(nu[1, 2], 0.045473992910668816)
792assert_almost_equal(nu[2, 0], 0.08410493827160502)
793assert_almost_equal(nu[2, 1], -0.002899800614433943)
794
795
796def test_moments_normalized_spacing():
797spacing = (3, 3)
798nu = regionprops(SAMPLE, spacing=spacing)[0].moments_normalized
799
800# Normalized moments are scale invariant.
801assert_almost_equal(nu[0, 2], 0.24301268861454037)
802assert_almost_equal(nu[0, 3], -0.017278118992041805)
803assert_almost_equal(nu[1, 1], -0.016846707818929982)
804assert_almost_equal(nu[1, 2], 0.045473992910668816)
805assert_almost_equal(nu[2, 0], 0.08410493827160502)
806assert_almost_equal(nu[2, 1], -0.002899800614433943)
807
808
809def test_orientation():
810orient = regionprops(SAMPLE)[0].orientation
811# determined with MATLAB
812target_orient = -1.4663278802756865
813assert_almost_equal(orient, target_orient)
814
815orient = regionprops(SAMPLE, spacing=(2, 2))[0].orientation
816assert_almost_equal(orient, target_orient)
817
818# test diagonal regions
819diag = np.eye(10, dtype=int)
820orient_diag = regionprops(diag)[0].orientation
821assert_almost_equal(orient_diag, math.pi / 4)
822orient_diag = regionprops(diag, spacing=(1, 2))[0].orientation
823assert_almost_equal(orient_diag, np.arccos(0.5 / np.sqrt(1 + 0.5**2)))
824orient_diag = regionprops(np.flipud(diag))[0].orientation
825assert_almost_equal(orient_diag, -math.pi / 4)
826orient_diag = regionprops(np.flipud(diag), spacing=(1, 2))[0].orientation
827assert_almost_equal(orient_diag, -np.arccos(0.5 / np.sqrt(1 + 0.5**2)))
828orient_diag = regionprops(np.fliplr(diag))[0].orientation
829assert_almost_equal(orient_diag, -math.pi / 4)
830orient_diag = regionprops(np.fliplr(diag), spacing=(1, 2))[0].orientation
831assert_almost_equal(orient_diag, -np.arccos(0.5 / np.sqrt(1 + 0.5**2)))
832orient_diag = regionprops(np.fliplr(np.flipud(diag)))[0].orientation
833assert_almost_equal(orient_diag, math.pi / 4)
834orient_diag = regionprops(np.fliplr(np.flipud(diag)), spacing=(1, 2))[0].orientation
835assert_almost_equal(orient_diag, np.arccos(0.5 / np.sqrt(1 + 0.5**2)))
836
837
838def test_orientation_continuity():
839# nearly diagonal array
840arr1 = np.array([[0, 0, 1, 1], [0, 0, 1, 0], [0, 1, 0, 0], [1, 0, 0, 0]])
841# diagonal array
842arr2 = np.array([[0, 0, 0, 2], [0, 0, 2, 0], [0, 2, 0, 0], [2, 0, 0, 0]])
843# nearly diagonal array
844arr3 = np.array([[0, 0, 0, 3], [0, 0, 3, 3], [0, 3, 0, 0], [3, 0, 0, 0]])
845image = np.hstack((arr1, arr2, arr3))
846props = regionprops(image)
847orientations = [prop.orientation for prop in props]
848np.testing.assert_allclose(orientations, orientations[1], rtol=0, atol=0.08)
849assert_almost_equal(orientations[0], -0.7144496360953664)
850assert_almost_equal(orientations[1], -0.7853981633974483)
851assert_almost_equal(orientations[2], -0.8563466906995303)
852
853# Test spacing
854spacing = (3.2, 1.2)
855wmu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE, spacing=spacing)[
8560
857].moments_weighted_central
858centralMpq = get_central_moment_function(INTENSITY_SAMPLE, spacing=spacing)
859assert_almost_equal(wmu[0, 0], centralMpq(0, 0))
860assert_almost_equal(wmu[0, 1], centralMpq(0, 1))
861assert_almost_equal(wmu[0, 2], centralMpq(0, 2))
862assert_almost_equal(wmu[0, 3], centralMpq(0, 3))
863assert_almost_equal(wmu[1, 0], centralMpq(1, 0))
864assert_almost_equal(wmu[1, 1], centralMpq(1, 1))
865assert_almost_equal(wmu[1, 2], centralMpq(1, 2))
866assert_almost_equal(wmu[1, 3], centralMpq(1, 3))
867assert_almost_equal(wmu[2, 0], centralMpq(2, 0))
868assert_almost_equal(wmu[2, 1], centralMpq(2, 1))
869assert_almost_equal(wmu[2, 2], centralMpq(2, 2))
870assert_almost_equal(wmu[2, 3], centralMpq(2, 3))
871assert_almost_equal(wmu[3, 0], centralMpq(3, 0))
872assert_almost_equal(wmu[3, 1], centralMpq(3, 1))
873assert_almost_equal(wmu[3, 2], centralMpq(3, 2))
874assert_almost_equal(wmu[3, 3], centralMpq(3, 3))
875
876
877def test_perimeter():
878per = regionprops(SAMPLE)[0].perimeter
879target_per = 55.2487373415
880assert_almost_equal(per, target_per)
881per = regionprops(SAMPLE, spacing=(2, 2))[0].perimeter
882assert_almost_equal(per, 2 * target_per)
883
884per = perimeter(SAMPLE.astype('double'), neighborhood=8)
885assert_almost_equal(per, 46.8284271247)
886
887with testing.raises(NotImplementedError):
888per = regionprops(SAMPLE, spacing=(2, 1))[0].perimeter
889
890
891def test_perimeter_crofton():
892per = regionprops(SAMPLE)[0].perimeter_crofton
893target_per_crof = 61.0800637973
894assert_almost_equal(per, target_per_crof)
895per = regionprops(SAMPLE, spacing=(2, 2))[0].perimeter_crofton
896assert_almost_equal(per, 2 * target_per_crof)
897
898per = perimeter_crofton(SAMPLE.astype('double'), directions=2)
899assert_almost_equal(per, 64.4026493985)
900
901with testing.raises(NotImplementedError):
902per = regionprops(SAMPLE, spacing=(2, 1))[0].perimeter_crofton
903
904
905def test_solidity():
906solidity = regionprops(SAMPLE)[0].solidity
907target_solidity = 0.576
908assert_almost_equal(solidity, target_solidity)
909
910solidity = regionprops(SAMPLE, spacing=(3, 9))[0].solidity
911assert_almost_equal(solidity, target_solidity)
912
913
914def test_multichannel_centroid_weighted_table():
915"""Test for https://github.com/scikit-image/scikit-image/issues/6860."""
916intensity_image = INTENSITY_FLOAT_SAMPLE_MULTICHANNEL
917rp0 = regionprops(SAMPLE, intensity_image=intensity_image[..., 0])[0]
918rp1 = regionprops(SAMPLE, intensity_image=intensity_image[..., 0:1])[0]
919rpm = regionprops(SAMPLE, intensity_image=intensity_image)[0]
920np.testing.assert_almost_equal(
921rp0.centroid_weighted, np.squeeze(rp1.centroid_weighted)
922)
923np.testing.assert_almost_equal(
924rp0.centroid_weighted, np.array(rpm.centroid_weighted)[:, 0]
925)
926assert np.shape(rp0.centroid_weighted) == (SAMPLE.ndim,)
927assert np.shape(rp1.centroid_weighted) == (SAMPLE.ndim, 1)
928assert np.shape(rpm.centroid_weighted) == (SAMPLE.ndim, intensity_image.shape[-1])
929
930table = regionprops_table(
931SAMPLE, intensity_image=intensity_image, properties=('centroid_weighted',)
932)
933# check the number of returned columns is correct
934assert len(table) == np.size(rpm.centroid_weighted)
935
936
937def test_moments_weighted_central():
938wmu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE)[
9390
940].moments_weighted_central
941ref = np.array(
942[
943[7.4000000000e01, 3.7303493627e-14, 1.2602837838e03, -7.6561796932e02],
944[-2.1316282073e-13, -8.7837837838e01, 2.1571526662e03, -4.2385971907e03],
945[4.7837837838e02, -1.4801314828e02, 6.6989799420e03, -9.9501164076e03],
946[-7.5943608473e02, -1.2714707125e03, 1.5304076361e04, -3.3156729271e04],
947]
948)
949
950assert_array_almost_equal(wmu, ref)
951
952# Verify test function
953centralMpq = get_central_moment_function(INTENSITY_SAMPLE, spacing=(1, 1))
954assert_almost_equal(centralMpq(0, 0), ref[0, 0])
955assert_almost_equal(centralMpq(0, 1), ref[0, 1])
956assert_almost_equal(centralMpq(0, 2), ref[0, 2])
957assert_almost_equal(centralMpq(0, 3), ref[0, 3])
958assert_almost_equal(centralMpq(1, 0), ref[1, 0])
959assert_almost_equal(centralMpq(1, 1), ref[1, 1])
960assert_almost_equal(centralMpq(1, 2), ref[1, 2])
961assert_almost_equal(centralMpq(1, 3), ref[1, 3])
962assert_almost_equal(centralMpq(2, 0), ref[2, 0])
963assert_almost_equal(centralMpq(2, 1), ref[2, 1])
964assert_almost_equal(centralMpq(2, 2), ref[2, 2])
965assert_almost_equal(centralMpq(2, 3), ref[2, 3])
966assert_almost_equal(centralMpq(3, 0), ref[3, 0])
967assert_almost_equal(centralMpq(3, 1), ref[3, 1])
968assert_almost_equal(centralMpq(3, 2), ref[3, 2])
969assert_almost_equal(centralMpq(3, 3), ref[3, 3])
970
971
972def test_centroid_weighted():
973sample_for_spacing = np.array(
974[
975[0, 0, 0, 0, 0, 0],
976[0, 0, 0, 0, 0, 0],
977[0, 0, 0, 0, 0, 0],
978[0, 0, 0, 1, 1, 1],
979[0, 0, 0, 1, 1, 1],
980[0, 0, 0, 1, 1, 1],
981]
982)
983target_centroid_wspacing = (4.0, 4.0)
984centroid = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE)[
9850
986].centroid_weighted
987target_centroid = (5.540540540540, 9.445945945945)
988assert_array_almost_equal(centroid, target_centroid)
989
990# Verify test function
991Mpq = get_moment_function(INTENSITY_SAMPLE, spacing=(1, 1))
992cY = Mpq(0, 1) / Mpq(0, 0)
993cX = Mpq(1, 0) / Mpq(0, 0)
994assert_almost_equal((cX, cY), centroid)
995
996# Test spacing
997spacing = (2, 2)
998Mpq = get_moment_function(INTENSITY_SAMPLE, spacing=spacing)
999cY = Mpq(0, 1) / Mpq(0, 0)
1000cX = Mpq(1, 0) / Mpq(0, 0)
1001centroid = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE, spacing=spacing)[
10020
1003].centroid_weighted
1004assert_almost_equal(centroid, (cX, cY))
1005assert_almost_equal(centroid, 2 * np.array(target_centroid))
1006centroid = regionprops(
1007sample_for_spacing, intensity_image=sample_for_spacing, spacing=spacing
1008)[0].centroid_weighted
1009assert_almost_equal(centroid, 2 * np.array(target_centroid_wspacing))
1010
1011spacing = (1.3, 0.7)
1012Mpq = get_moment_function(INTENSITY_SAMPLE, spacing=spacing)
1013cY = Mpq(0, 1) / Mpq(0, 0)
1014cX = Mpq(1, 0) / Mpq(0, 0)
1015centroid = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE, spacing=spacing)[
10160
1017].centroid_weighted
1018assert_almost_equal(centroid, (cX, cY))
1019centroid = regionprops(
1020sample_for_spacing, intensity_image=sample_for_spacing, spacing=spacing
1021)[0].centroid_weighted
1022assert_almost_equal(centroid, spacing * np.array(target_centroid_wspacing))
1023
1024
1025def test_moments_weighted_hu():
1026whu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE)[0].moments_weighted_hu
1027ref = np.array(
1028[
10293.1750587329e-01,
10302.1417517159e-02,
10312.3609322038e-02,
10321.2565683360e-03,
10338.3014209421e-07,
1034-3.5073773473e-05,
1035-6.7936409056e-06,
1036]
1037)
1038assert_array_almost_equal(whu, ref)
1039
1040with testing.raises(NotImplementedError):
1041regionprops(SAMPLE, spacing=(2, 1))[0].moments_weighted_hu
1042
1043
1044def test_moments_weighted():
1045wm = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE)[0].moments_weighted
1046ref = np.array(
1047[
1048[7.4000000e01, 6.9900000e02, 7.8630000e03, 9.7317000e04],
1049[4.1000000e02, 3.7850000e03, 4.4063000e04, 5.7256700e05],
1050[2.7500000e03, 2.4855000e04, 2.9347700e05, 3.9007170e06],
1051[1.9778000e04, 1.7500100e05, 2.0810510e06, 2.8078871e07],
1052]
1053)
1054assert_array_almost_equal(wm, ref)
1055
1056# Verify test function
1057Mpq = get_moment_function(INTENSITY_SAMPLE, spacing=(1, 1))
1058assert_almost_equal(Mpq(0, 0), ref[0, 0])
1059assert_almost_equal(Mpq(0, 1), ref[0, 1])
1060assert_almost_equal(Mpq(0, 2), ref[0, 2])
1061assert_almost_equal(Mpq(0, 3), ref[0, 3])
1062assert_almost_equal(Mpq(1, 0), ref[1, 0])
1063assert_almost_equal(Mpq(1, 1), ref[1, 1])
1064assert_almost_equal(Mpq(1, 2), ref[1, 2])
1065assert_almost_equal(Mpq(1, 3), ref[1, 3])
1066assert_almost_equal(Mpq(2, 0), ref[2, 0])
1067assert_almost_equal(Mpq(2, 1), ref[2, 1])
1068assert_almost_equal(Mpq(2, 2), ref[2, 2])
1069assert_almost_equal(Mpq(2, 3), ref[2, 3])
1070assert_almost_equal(Mpq(3, 0), ref[3, 0])
1071assert_almost_equal(Mpq(3, 1), ref[3, 1])
1072assert_almost_equal(Mpq(3, 2), ref[3, 2])
1073assert_almost_equal(Mpq(3, 3), ref[3, 3])
1074
1075
1076def test_moments_weighted_spacing():
1077wm = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE)[0].moments_weighted
1078ref = np.array(
1079[
1080[7.4000000e01, 6.9900000e02, 7.8630000e03, 9.7317000e04],
1081[4.1000000e02, 3.7850000e03, 4.4063000e04, 5.7256700e05],
1082[2.7500000e03, 2.4855000e04, 2.9347700e05, 3.9007170e06],
1083[1.9778000e04, 1.7500100e05, 2.0810510e06, 2.8078871e07],
1084]
1085)
1086assert_array_almost_equal(wm, ref)
1087
1088# Test spacing
1089spacing = (3.2, 1.2)
1090wmu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE, spacing=spacing)[
10910
1092].moments_weighted
1093Mpq = get_moment_function(INTENSITY_SAMPLE, spacing=spacing)
1094assert_almost_equal(wmu[0, 0], Mpq(0, 0))
1095assert_almost_equal(wmu[0, 1], Mpq(0, 1))
1096assert_almost_equal(wmu[0, 2], Mpq(0, 2))
1097assert_almost_equal(wmu[0, 3], Mpq(0, 3))
1098assert_almost_equal(wmu[1, 0], Mpq(1, 0))
1099assert_almost_equal(wmu[1, 1], Mpq(1, 1))
1100assert_almost_equal(wmu[1, 2], Mpq(1, 2))
1101assert_almost_equal(wmu[1, 3], Mpq(1, 3))
1102assert_almost_equal(wmu[2, 0], Mpq(2, 0))
1103assert_almost_equal(wmu[2, 1], Mpq(2, 1))
1104assert_almost_equal(wmu[2, 2], Mpq(2, 2))
1105assert_almost_equal(wmu[2, 3], Mpq(2, 3))
1106assert_almost_equal(wmu[3, 0], Mpq(3, 0))
1107assert_almost_equal(wmu[3, 1], Mpq(3, 1))
1108assert_almost_equal(wmu[3, 2], Mpq(3, 2))
1109assert_almost_equal(wmu[3, 3], Mpq(3, 3), decimal=6)
1110
1111
1112def test_moments_weighted_normalized():
1113wnu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE)[
11140
1115].moments_weighted_normalized
1116ref = np.array(
1117[
1118[np.nan, np.nan, 0.2301467830, -0.0162529732],
1119[np.nan, -0.0160405109, 0.0457932622, -0.0104598869],
1120[0.0873590903, -0.0031421072, 0.0165315478, -0.0028544152],
1121[-0.0161217406, -0.0031376984, 0.0043903193, -0.0011057191],
1122]
1123)
1124assert_array_almost_equal(wnu, ref)
1125
1126
1127def test_moments_weighted_normalized_spacing():
1128spacing = (3, 3)
1129wnu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE, spacing=spacing)[
11300
1131].moments_weighted_normalized
1132
1133np.array(
1134[
1135[np.nan, np.nan, 0.2301467830, -0.0162529732],
1136[np.nan, -0.0160405109, 0.0457932622, -0.0104598869],
1137[0.0873590903, -0.0031421072, 0.0165315478, -0.0028544152],
1138[-0.0161217406, -0.0031376984, 0.0043903193, -0.0011057191],
1139]
1140)
1141
1142# Normalized moments are scale invariant
1143assert_almost_equal(wnu[0, 2], 0.2301467830)
1144assert_almost_equal(wnu[0, 3], -0.0162529732)
1145assert_almost_equal(wnu[1, 1], -0.0160405109)
1146assert_almost_equal(wnu[1, 2], 0.0457932622)
1147assert_almost_equal(wnu[1, 3], -0.0104598869)
1148assert_almost_equal(wnu[2, 0], 0.0873590903)
1149assert_almost_equal(wnu[2, 1], -0.0031421072)
1150assert_almost_equal(wnu[2, 2], 0.0165315478)
1151assert_almost_equal(wnu[2, 3], -0.0028544152)
1152assert_almost_equal(wnu[3, 0], -0.0161217406)
1153assert_almost_equal(wnu[3, 1], -0.0031376984)
1154assert_almost_equal(wnu[3, 2], 0.0043903193)
1155assert_almost_equal(wnu[3, 3], -0.0011057191)
1156
1157
1158def test_offset_features():
1159props = regionprops(SAMPLE)[0]
1160offset = np.array([1024, 2048])
1161props_offset = regionprops(SAMPLE, offset=offset)[0]
1162
1163assert_allclose(props.centroid, props_offset.centroid - offset)
1164
1165
1166def test_label_sequence():
1167a = np.empty((2, 2), dtype=int)
1168a[:, :] = 2
1169ps = regionprops(a)
1170assert len(ps) == 1
1171assert ps[0].label == 2
1172
1173
1174def test_pure_background():
1175a = np.zeros((2, 2), dtype=int)
1176ps = regionprops(a)
1177assert len(ps) == 0
1178
1179
1180def test_invalid():
1181ps = regionprops(SAMPLE)
1182
1183def get_intensity_image():
1184ps[0].image_intensity
1185
1186with pytest.raises(AttributeError):
1187get_intensity_image()
1188
1189
1190def test_invalid_size():
1191wrong_intensity_sample = np.array([[1], [1]])
1192with pytest.raises(ValueError):
1193regionprops(SAMPLE, wrong_intensity_sample)
1194
1195
1196def test_equals():
1197arr = np.zeros((100, 100), dtype=int)
1198arr[0:25, 0:25] = 1
1199arr[50:99, 50:99] = 2
1200
1201regions = regionprops(arr)
1202r1 = regions[0]
1203
1204regions = regionprops(arr)
1205r2 = regions[0]
1206r3 = regions[1]
1207
1208assert_equal(r1 == r2, True, "Same regionprops are not equal")
1209assert_equal(r1 != r3, True, "Different regionprops are equal")
1210
1211
1212def test_iterate_all_props():
1213region = regionprops(SAMPLE)[0]
1214p0 = {p: region[p] for p in region}
1215
1216region = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE)[0]
1217p1 = {p: region[p] for p in region}
1218
1219assert len(p0) < len(p1)
1220
1221
1222def test_cache():
1223SAMPLE_mod = SAMPLE.copy()
1224region = regionprops(SAMPLE_mod)[0]
1225f0 = region.image_filled
1226region._label_image[:10] = 1
1227f1 = region.image_filled
1228
1229# Changed underlying image, but cache keeps result the same
1230assert_array_equal(f0, f1)
1231
1232# Now invalidate cache
1233region._cache_active = False
1234f1 = region.image_filled
1235
1236assert np.any(f0 != f1)
1237
1238
1239def test_disabled_cache_is_empty():
1240SAMPLE_mod = SAMPLE.copy()
1241region = regionprops(SAMPLE_mod, cache=False)[0]
1242# Access one property to trigger cache
1243_ = region.image_filled
1244
1245# Cache should be empty
1246assert region._cache == dict()
1247
1248
1249def test_docstrings_and_props():
1250def foo():
1251"""foo"""
1252
1253has_docstrings = bool(foo.__doc__)
1254
1255region = regionprops(SAMPLE)[0]
1256
1257docs = _parse_docs()
1258props = [m for m in dir(region) if not m.startswith('_')]
1259
1260nr_docs_parsed = len(docs)
1261nr_props = len(props)
1262if has_docstrings:
1263assert_equal(nr_docs_parsed, nr_props)
1264ds = docs['moments_weighted_normalized']
1265assert 'iteration' not in ds
1266assert len(ds.split('\n')) > 3
1267else:
1268assert_equal(nr_docs_parsed, 0)
1269
1270
1271def test_props_to_dict():
1272regions = regionprops(SAMPLE)
1273out = _props_to_dict(regions)
1274assert out == {
1275'label': np.array([1]),
1276'bbox-0': np.array([0]),
1277'bbox-1': np.array([0]),
1278'bbox-2': np.array([10]),
1279'bbox-3': np.array([18]),
1280}
1281
1282regions = regionprops(SAMPLE)
1283out = _props_to_dict(regions, properties=('label', 'area', 'bbox'), separator='+')
1284assert out == {
1285'label': np.array([1]),
1286'area': np.array([72]),
1287'bbox+0': np.array([0]),
1288'bbox+1': np.array([0]),
1289'bbox+2': np.array([10]),
1290'bbox+3': np.array([18]),
1291}
1292
1293
1294def test_regionprops_table():
1295out = regionprops_table(SAMPLE)
1296assert out == {
1297'label': np.array([1]),
1298'bbox-0': np.array([0]),
1299'bbox-1': np.array([0]),
1300'bbox-2': np.array([10]),
1301'bbox-3': np.array([18]),
1302}
1303
1304out = regionprops_table(SAMPLE, properties=('label', 'area', 'bbox'), separator='+')
1305assert out == {
1306'label': np.array([1]),
1307'area': np.array([72]),
1308'bbox+0': np.array([0]),
1309'bbox+1': np.array([0]),
1310'bbox+2': np.array([10]),
1311'bbox+3': np.array([18]),
1312}
1313
1314
1315def test_regionprops_table_deprecated_vector_property():
1316out = regionprops_table(SAMPLE, properties=('local_centroid',))
1317for key in out.keys():
1318# key reflects the deprecated name, not its new (centroid_local) value
1319assert key.startswith('local_centroid')
1320
1321
1322def test_regionprops_table_deprecated_scalar_property():
1323out = regionprops_table(SAMPLE, properties=('bbox_area',))
1324assert list(out.keys()) == ['bbox_area']
1325
1326
1327def test_regionprops_table_equal_to_original():
1328regions = regionprops(SAMPLE, INTENSITY_FLOAT_SAMPLE)
1329out_table = regionprops_table(
1330SAMPLE, INTENSITY_FLOAT_SAMPLE, properties=COL_DTYPES.keys()
1331)
1332
1333for prop, dtype in COL_DTYPES.items():
1334for i, reg in enumerate(regions):
1335rp = reg[prop]
1336if np.isscalar(rp) or prop in OBJECT_COLUMNS or dtype is np.object_:
1337assert_array_equal(rp, out_table[prop][i])
1338else:
1339shape = rp.shape if isinstance(rp, np.ndarray) else (len(rp),)
1340for ind in np.ndindex(shape):
1341modified_prop = "-".join(map(str, (prop,) + ind))
1342loc = ind if len(ind) > 1 else ind[0]
1343assert_equal(rp[loc], out_table[modified_prop][i])
1344
1345
1346def test_regionprops_table_no_regions():
1347out = regionprops_table(
1348np.zeros((2, 2), dtype=int), properties=('label', 'area', 'bbox'), separator='+'
1349)
1350assert len(out) == 6
1351assert len(out['label']) == 0
1352assert len(out['area']) == 0
1353assert len(out['bbox+0']) == 0
1354assert len(out['bbox+1']) == 0
1355assert len(out['bbox+2']) == 0
1356assert len(out['bbox+3']) == 0
1357
1358
1359def test_column_dtypes_correct():
1360msg = 'mismatch with expected type,'
1361region = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE)[0]
1362for col in COL_DTYPES:
1363r = region[col]
1364
1365if col in OBJECT_COLUMNS:
1366assert COL_DTYPES[col] == object
1367continue
1368
1369t = type(np.ravel(r)[0])
1370
1371if np.issubdtype(t, np.floating):
1372assert COL_DTYPES[col] == float, f'{col} dtype {t} {msg} {COL_DTYPES[col]}'
1373elif np.issubdtype(t, np.integer):
1374assert COL_DTYPES[col] == int, f'{col} dtype {t} {msg} {COL_DTYPES[col]}'
1375else:
1376assert False, f'{col} dtype {t} {msg} {COL_DTYPES[col]}'
1377
1378
1379def test_all_documented_items_in_col_dtypes():
1380numpydoc_docscrape = pytest.importorskip("numpydoc.docscrape")
1381docstring = numpydoc_docscrape.FunctionDoc(regionprops)
1382notes_lines = docstring['Notes']
1383property_lines = filter(lambda line: line.startswith('**'), notes_lines)
1384pattern = r'\*\*(?P<property_name>[a-z_]+)\*\*.*'
1385property_names = {
1386re.search(pattern, property_line).group('property_name')
1387for property_line in property_lines
1388}
1389column_keys = set(COL_DTYPES.keys())
1390assert column_keys == property_names
1391
1392
1393def pixelcount(regionmask):
1394"""a short test for an extra property"""
1395return np.sum(regionmask)
1396
1397
1398def intensity_median(regionmask, image_intensity):
1399return np.median(image_intensity[regionmask])
1400
1401
1402def bbox_list(regionmask):
1403"""Extra property whose output shape is dependent on mask shape."""
1404return [1] * regionmask.shape[1]
1405
1406
1407def too_many_args(regionmask, image_intensity, superfluous):
1408return 1
1409
1410
1411def too_few_args():
1412return 1
1413
1414
1415def test_extra_properties():
1416region = regionprops(SAMPLE, extra_properties=(pixelcount,))[0]
1417assert region.pixelcount == np.sum(SAMPLE == 1)
1418
1419
1420def test_extra_properties_intensity():
1421region = regionprops(
1422SAMPLE, intensity_image=INTENSITY_SAMPLE, extra_properties=(intensity_median,)
1423)[0]
1424assert region.intensity_median == np.median(INTENSITY_SAMPLE[SAMPLE == 1])
1425
1426
1427@pytest.mark.parametrize('intensity_prop', _require_intensity_image)
1428def test_intensity_image_required(intensity_prop):
1429region = regionprops(SAMPLE)[0]
1430with pytest.raises(AttributeError) as e:
1431getattr(region, intensity_prop)
1432expected_error = (
1433f"Attribute '{intensity_prop}' unavailable when `intensity_image` has "
1434f"not been specified."
1435)
1436assert expected_error == str(e.value)
1437
1438
1439def test_extra_properties_no_intensity_provided():
1440with pytest.raises(AttributeError):
1441region = regionprops(SAMPLE, extra_properties=(intensity_median,))[0]
1442_ = region.intensity_median
1443
1444
1445def test_extra_properties_nr_args():
1446with pytest.raises(AttributeError):
1447region = regionprops(SAMPLE, extra_properties=(too_few_args,))[0]
1448_ = region.too_few_args
1449with pytest.raises(AttributeError):
1450region = regionprops(SAMPLE, extra_properties=(too_many_args,))[0]
1451_ = region.too_many_args
1452
1453
1454def test_extra_properties_mixed():
1455# mixed properties, with and without intensity
1456region = regionprops(
1457SAMPLE,
1458intensity_image=INTENSITY_SAMPLE,
1459extra_properties=(intensity_median, pixelcount),
1460)[0]
1461assert region.intensity_median == np.median(INTENSITY_SAMPLE[SAMPLE == 1])
1462assert region.pixelcount == np.sum(SAMPLE == 1)
1463
1464
1465def test_extra_properties_table():
1466out = regionprops_table(
1467SAMPLE_MULTIPLE,
1468intensity_image=INTENSITY_SAMPLE_MULTIPLE,
1469properties=('label',),
1470extra_properties=(intensity_median, pixelcount, bbox_list),
1471)
1472assert_array_almost_equal(out['intensity_median'], np.array([2.0, 4.0]))
1473assert_array_equal(out['pixelcount'], np.array([10, 2]))
1474
1475assert out['bbox_list'].dtype == np.object_
1476assert out["bbox_list"][0] == [1] * 10
1477assert out["bbox_list"][1] == [1] * 1
1478
1479
1480def test_multichannel():
1481"""Test that computing multichannel properties works."""
1482astro = data.astronaut()[::4, ::4]
1483astro_green = astro[..., 1]
1484labels = slic(astro.astype(float), start_label=1)
1485
1486segment_idx = np.max(labels) // 2
1487region = regionprops(labels, astro_green, extra_properties=[intensity_median])[
1488segment_idx
1489]
1490region_multi = regionprops(labels, astro, extra_properties=[intensity_median])[
1491segment_idx
1492]
1493
1494for prop in list(PROPS.keys()) + ["intensity_median"]:
1495p = region[prop]
1496p_multi = region_multi[prop]
1497if np.shape(p) == np.shape(p_multi):
1498# property does not depend on multiple channels
1499assert_array_equal(p, p_multi)
1500else:
1501# property uses multiple channels, returns props stacked along
1502# final axis
1503assert_allclose(p, np.asarray(p_multi)[..., 1], rtol=1e-12, atol=1e-12)
1504
1505
1506def test_3d_ellipsoid_axis_lengths():
1507"""Verify that estimated axis lengths are correct.
1508
1509Uses an ellipsoid at an arbitrary position and orientation.
1510"""
1511# generate a centered ellipsoid with non-uniform half-lengths (radii)
1512half_lengths = (20, 10, 50)
1513e = draw.ellipsoid(*half_lengths).astype(int)
1514
1515# Pad by asymmetric amounts so the ellipse isn't centered. Also, pad enough
1516# that the rotated ellipse will still be within the original volume.
1517e = np.pad(e, pad_width=[(30, 18), (30, 12), (40, 20)], mode='constant')
1518
1519# apply rotations to the ellipsoid
1520R = transform.EuclideanTransform(rotation=[0.2, 0.3, 0.4], dimensionality=3)
1521e = ndi.affine_transform(e, R.params)
1522
1523# Compute regionprops
1524rp = regionprops(e)[0]
1525
1526# estimate principal axis lengths via the inertia tensor eigenvalues
1527evs = rp.inertia_tensor_eigvals
1528axis_lengths = _inertia_eigvals_to_axes_lengths_3D(evs)
1529expected_lengths = sorted([2 * h for h in half_lengths], reverse=True)
1530for ax_len_expected, ax_len in zip(expected_lengths, axis_lengths):
1531# verify accuracy to within 1%
1532assert abs(ax_len - ax_len_expected) < 0.01 * ax_len_expected
1533
1534# verify that the axis length regionprops also agree
1535assert abs(rp.axis_major_length - axis_lengths[0]) < 1e-7
1536assert abs(rp.axis_minor_length - axis_lengths[-1]) < 1e-7
1537