Skip to content

Commit 2cfc985

Browse files
Update tests
1 parent 2b5693e commit 2cfc985

1 file changed

Lines changed: 99 additions & 73 deletions

File tree

tests/test_roi_size_filter.py

Lines changed: 99 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,166 +1,192 @@
11
import numpy as np
22
import pytest
33
from scipy.ndimage import generate_binary_structure
4+
from scipy.ndimage import label as nd_label
45

56
from eitprocessing.roi import PixelMask
67
from eitprocessing.roi.roi_size_filter import FilterROIBySize
78

89

9-
def make_pixel_mask(array: np.ndarray):
10-
"""Helper to wrap numpy array into PixelMask with NaNs for False."""
11-
return PixelMask(array, keep_zeros=True)
12-
13-
1410
def test_basic_region_selection():
1511
arr = np.full((5, 5), np.nan)
1612
arr[1:4, 1:4] = True
17-
mask = make_pixel_mask(arr)
18-
selector = FilterROIBySize(min_region_size=5, connectivity="4-connectivity")
13+
mask = PixelMask(arr, keep_zeros=True)
14+
selector = FilterROIBySize(min_region_size=5, connectivity="1-connectivity")
1915
result = selector.apply(mask)
2016
expected_mask = np.full(arr.shape, np.nan)
2117
expected_mask[1:4, 1:4] = 1
22-
np.testing.assert_array_equal(result.mask, expected_mask)
18+
assert np.array_equal(result.mask, expected_mask, equal_nan=True)
2319

2420

2521
def test_region_smaller_than_threshold_is_excluded():
2622
arr = np.full((5, 5), np.nan)
2723
arr[0, 0:2] = True
2824
arr[2:4, 2:5] = True
29-
mask = make_pixel_mask(arr)
30-
selector = FilterROIBySize(min_region_size=5, connectivity="4-connectivity")
25+
mask = PixelMask(arr, keep_zeros=True)
26+
selector = FilterROIBySize(min_region_size=5, connectivity="1-connectivity")
3127
result = selector.apply(mask)
3228
expected = np.zeros_like(arr, dtype=float)
3329
expected[2:4, 2:5] = 1
3430
expected[expected == 0] = np.nan
35-
np.testing.assert_array_equal(result.mask, expected)
31+
assert np.array_equal(result.mask, expected, equal_nan=True)
3632

3733

38-
def test_no_regions_above_threshold_warns_and_returns_empty():
34+
def test_no_regions_above_threshold_raises():
3935
arr = np.full((4, 4), np.nan)
4036
arr[0, 0] = True
41-
mask = make_pixel_mask(arr)
42-
selector = FilterROIBySize(min_region_size=5, connectivity="4-connectivity")
43-
with pytest.warns(UserWarning, match="No regions found above min_pixels threshold."):
44-
result = selector.apply(mask)
45-
assert isinstance(result, PixelMask)
46-
assert np.all(np.isnan(result.mask))
37+
mask = PixelMask(arr, keep_zeros=True)
38+
selector = FilterROIBySize(min_region_size=5, connectivity="1-connectivity")
39+
with pytest.raises(RuntimeError, match="No regions found above min_region_size threshold"):
40+
selector.apply(mask)
4741

4842

4943
def test_custom_connectivity():
5044
arr = np.full((3, 3), np.nan)
5145
arr[0, 0] = True
5246
arr[1, 1] = True
53-
mask = make_pixel_mask(arr)
54-
# Default (4-connectivity) — should warn and return empty
55-
selector_default = FilterROIBySize(min_region_size=2, connectivity="4-connectivity")
56-
with pytest.warns(UserWarning, match="No regions found above min_pixels threshold."):
57-
result_default = selector_default.apply(mask)
58-
assert np.all(np.isnan(result_default.mask))
59-
# 8-connectivity — diagonal pixels connected
60-
selector_diag = FilterROIBySize(min_region_size=2, connectivity="8-connectivity")
47+
mask = PixelMask(arr, keep_zeros=True)
48+
49+
# Default (1-connectivity) — should raise
50+
selector_default = FilterROIBySize(min_region_size=2, connectivity="1-connectivity")
51+
with pytest.raises(RuntimeError, match="No regions found above min_region_size threshold"):
52+
selector_default.apply(mask)
53+
54+
# 2-connectivity — diagonal pixels connected
55+
selector_diag = FilterROIBySize(min_region_size=2, connectivity="2-connectivity")
6156
result_diag = selector_diag.apply(mask)
6257
assert not np.all(np.isnan(result_diag.mask))
58+
6359
# Custom structure
6460
custom_structure = generate_binary_structure(2, 2)
6561
selector_custom = FilterROIBySize(min_region_size=2, connectivity=custom_structure)
6662
result_custom = selector_custom.apply(mask)
6763
assert not np.all(np.isnan(result_custom.mask))
6864

6965

70-
def test_empty_mask_returns_empty():
66+
def test_empty_mask_raises():
7167
arr = np.full((4, 4), np.nan)
72-
mask = make_pixel_mask(arr)
73-
selector = FilterROIBySize(min_region_size=1, connectivity="4-connectivity")
74-
result = selector.apply(mask)
75-
assert np.all(np.isnan(result.mask))
76-
77-
78-
def test_all_nan_mask_returns_empty():
79-
arr = np.full((4, 4), np.nan)
80-
mask = PixelMask(arr)
81-
selector = FilterROIBySize(min_region_size=1, connectivity="4-connectivity")
82-
result = selector.apply(mask)
83-
assert np.all(np.isnan(result.mask))
68+
mask = PixelMask(arr, keep_zeros=True)
69+
selector = FilterROIBySize(min_region_size=1, connectivity="1-connectivity")
70+
with pytest.raises(RuntimeError, match="No regions found above min_region_size threshold"):
71+
selector.apply(mask)
8472

8573

8674
def test_zeros_are_regions_nans_are_excluded():
8775
arr = np.array([[0, 1, np.nan], [0, 0.5, np.nan], [np.nan, np.nan, 0]], dtype=float)
88-
mask = make_pixel_mask(arr)
89-
selector = FilterROIBySize(min_region_size=3, connectivity="4-connectivity")
76+
mask = PixelMask(arr, keep_zeros=True)
77+
selector = FilterROIBySize(min_region_size=3, connectivity="1-connectivity")
9078
result_mask = selector.apply(mask).mask
91-
result_binary = ~np.isnan(result_mask)
92-
expected_binary = np.zeros_like(arr, dtype=bool)
93-
expected_binary[0:2, 0:2] = True
94-
np.testing.assert_array_equal(result_binary, expected_binary)
79+
expected_mask = np.array([[1, 1, np.nan], [1, 1, np.nan], [np.nan, np.nan, np.nan]], dtype=float)
80+
assert np.array_equal(result_mask, expected_mask, equal_nan=True)
9581

9682

9783
def test_multiple_large_regions_are_all_included():
9884
arr = np.full((6, 6), np.nan)
9985
arr[0:2, 0:2] = True
10086
arr[4:6, 4:6] = True
101-
mask = make_pixel_mask(arr)
102-
selector = FilterROIBySize(min_region_size=4, connectivity="4-connectivity")
87+
mask = PixelMask(arr, keep_zeros=True)
88+
selector = FilterROIBySize(min_region_size=4, connectivity="1-connectivity")
10389
result = selector.apply(mask)
10490
assert np.sum(~np.isnan(result.mask)) == 8
10591

10692

10793
def test_all_true_mask_returns_full_mask():
10894
arr = np.ones((3, 3), dtype=bool)
109-
mask = make_pixel_mask(arr)
110-
selector = FilterROIBySize(min_region_size=1, connectivity="4-connectivity")
95+
mask = PixelMask(arr, keep_zeros=True)
96+
selector = FilterROIBySize(min_region_size=1, connectivity="1-connectivity")
11197
result = selector.apply(mask)
11298
assert np.all(result.mask == 1)
11399

114100

115101
def test_edge_connected_region():
102+
"""Test that a region around the edges of the mask is correctly identified.
103+
104+
This checks that all edge pixels (top, bottom, left, right borders) are included as a single connected region.
105+
"""
116106
arr = np.full((5, 5), np.nan)
117-
arr[0, :] = True
118-
mask = make_pixel_mask(arr)
119-
selector = FilterROIBySize(min_region_size=5, connectivity="4-connectivity")
107+
arr[0, :] = True # top edge
108+
arr[-1, :] = True # bottom edge
109+
arr[:, 0] = True # left edge
110+
arr[:, -1] = True # right edge
111+
mask = PixelMask(arr, keep_zeros=True)
112+
selector = FilterROIBySize(min_region_size=16, connectivity="1-connectivity")
120113
result = selector.apply(mask)
121-
assert np.sum(~np.isnan(result.mask)) == 5
114+
# There are 16 edge pixels in a 5x5 array (corners counted only once)
115+
assert np.sum(~np.isnan(result.mask)) == 16
122116

123117

124118
def test_min_pixels_threshold_variation():
125119
arr = np.full((5, 5), np.nan)
126120
arr[1:3, 1:3] = True
127-
mask = make_pixel_mask(arr)
128-
selector = FilterROIBySize(min_region_size=4, connectivity="4-connectivity")
121+
mask = PixelMask(arr, keep_zeros=True)
122+
selector = FilterROIBySize(min_region_size=4, connectivity="1-connectivity")
129123
result = selector.apply(mask)
130124
assert np.sum(~np.isnan(result.mask)) == 4
131-
selector2 = FilterROIBySize(min_region_size=5, connectivity="4-connectivity")
132-
result2 = selector2.apply(mask)
133-
assert np.all(np.isnan(result2.mask))
125+
selector2 = FilterROIBySize(min_region_size=5, connectivity="1-connectivity")
126+
with pytest.raises(RuntimeError, match="No regions found above min_region_size threshold"):
127+
selector2.apply(mask)
134128

135129

136130
def test_touching_regions_are_separated():
137131
arr = np.full((5, 5), np.nan)
138132
arr[1:3, 1:3] = True
139-
arr[3, 1:3] = True
140-
mask = make_pixel_mask(arr)
141-
selector = FilterROIBySize(min_region_size=2, connectivity="4-connectivity")
133+
arr[3:4, 3:5] = True
134+
mask = PixelMask(arr, keep_zeros=True)
135+
136+
# Step 1: Check intermediate labeling for 1-connectivity
137+
binary_array = ~np.isnan(mask.mask)
138+
structure1 = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]) # 1-connectivity
139+
labeled_array1, num_features1 = nd_label(binary_array, structure=structure1)
140+
assert num_features1 == 2, f"Expected 2 regions, got {num_features1}"
141+
sizes1 = [np.sum(labeled_array1 == i) for i in range(1, num_features1 + 1)]
142+
assert sorted(sizes1) == [2, 4], f"Unexpected sizes for 1-connectivity: {sizes1}"
143+
144+
selector = FilterROIBySize(min_region_size=2, connectivity="1-connectivity")
142145
result = selector.apply(mask)
143-
assert np.sum(~np.isnan(result.mask)) == 6
144-
selector8 = FilterROIBySize(min_region_size=6, connectivity="8-connectivity")
146+
assert np.sum(~np.isnan(result.mask)) == 6 # total combined size
147+
148+
# Step 2: Check intermediate labeling for 2-connectivity
149+
structure2 = np.ones((3, 3), dtype=int) # 2-connectivity
150+
labeled_array2, num_features2 = nd_label(binary_array, structure=structure2)
151+
assert num_features2 == 1, f"Expected 1 region for 2-connectivity, got {num_features2}"
152+
153+
selector8 = FilterROIBySize(min_region_size=6, connectivity="2-connectivity")
145154
result8 = selector8.apply(mask)
146155
assert np.sum(~np.isnan(result8.mask)) == 6
147156

148157

149158
def test_structure_input_variants():
150-
"""Test all possible structure inputs: None, string, array."""
159+
"""Test that selector.structure is the same for '1-connectivity' and the equivalent custom array."""
151160
arr = np.full((4, 4), np.nan)
152161
arr[1:3, 1:3] = True
153-
mask = make_pixel_mask(arr)
154-
# None (should default to 4-connectivity)
155-
selector_none = FilterROIBySize(min_region_size=4, connectivity=None)
156-
result_none = selector_none.apply(mask)
157-
assert np.sum(~np.isnan(result_none.mask)) == 4
162+
mask = PixelMask(arr, keep_zeros=True)
163+
158164
# String
159-
selector_str = FilterROIBySize(min_region_size=4, connectivity="4-connectivity")
160-
result_str = selector_str.apply(mask)
161-
assert np.sum(~np.isnan(result_str.mask)) == 4
165+
selector_str = FilterROIBySize(min_region_size=4, connectivity="1-connectivity")
162166
# Array
163167
structure_arr = generate_binary_structure(2, 1)
164168
selector_arr = FilterROIBySize(min_region_size=4, connectivity=structure_arr)
169+
170+
# Check that the internal structure is the same
171+
assert np.array_equal(selector_str.connectivity, selector_arr.connectivity)
172+
173+
# Also check that the results are the same
174+
result_str = selector_str.apply(mask)
165175
result_arr = selector_arr.apply(mask)
166-
assert np.sum(~np.isnan(result_arr.mask)) == 4
176+
assert np.array_equal(result_str.mask, result_arr.mask, equal_nan=True)
177+
178+
179+
def test_invalid_connectivity_none_raises():
180+
arr = np.full((4, 4), np.nan)
181+
arr[1:3, 1:3] = True
182+
with pytest.raises(
183+
ValueError, match="Unsupported connectivity type: <class 'NoneType'>. Must be a string or numpy array."
184+
):
185+
FilterROIBySize(min_region_size=4, connectivity=None)
186+
187+
188+
def test_invalid_connectivity_string_raises():
189+
arr = np.full((4, 4), np.nan)
190+
arr[1:3, 1:3] = True
191+
with pytest.raises(ValueError, match="Unsupported connectivity string: bad-connectivity."):
192+
FilterROIBySize(min_region_size=4, connectivity="bad-connectivity")

0 commit comments

Comments
 (0)