Skip to content

Commit 17656db

Browse files
committed
test: binary closing and hole filling
1 parent ab60322 commit 17656db

1 file changed

Lines changed: 164 additions & 0 deletions

File tree

tests/test_preprocessing.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import numpy as np
2+
3+
from vesskel.config import ExtractionConfig, OutputConfig, PipelineConfig
4+
from vesskel.pipeline import analyze_binary_image
5+
6+
7+
class TestClosing:
8+
def test_closing_bridges_1px_gap_in_thick_line(self):
9+
gap = np.zeros((16, 16), dtype=np.uint8)
10+
gap[7:10, 4:7] = 1
11+
gap[7:10, 8:12] = 1
12+
13+
config = PipelineConfig(
14+
extraction=ExtractionConfig(closing_iterations=1),
15+
output=OutputConfig(),
16+
)
17+
result = analyze_binary_image(gap, "gap", config)
18+
# closing bridges the 1px gap at column 7 in the 3px-thick line
19+
assert result.skeleton[7:10, 7].any()
20+
21+
def test_closing_bridges_gap_in_thick_region(self):
22+
block = np.zeros((16, 16), dtype=np.uint8)
23+
block[6:10, 4:7] = 1
24+
block[6:10, 8:12] = 1
25+
26+
config = PipelineConfig(
27+
extraction=ExtractionConfig(closing_iterations=1),
28+
output=OutputConfig(),
29+
)
30+
result = analyze_binary_image(block, "block", config)
31+
# the 1px gap at column 7 should be bridged before thinning
32+
assert result.skeleton[6:10, 7].any()
33+
34+
def test_closing_off_does_not_modify(self):
35+
gap = np.zeros((16, 16), dtype=np.uint8)
36+
gap[7:10, 4:7] = 1
37+
gap[7:10, 8:12] = 1
38+
39+
config = PipelineConfig(
40+
extraction=ExtractionConfig(closing_iterations=0),
41+
output=OutputConfig(),
42+
)
43+
result = analyze_binary_image(gap, "gap", config)
44+
# without closing the gap remains
45+
assert not result.skeleton[7:10, 7].any()
46+
47+
48+
class TestFillHoles:
49+
def test_fill_holes_fills_enclosed_void(self):
50+
ring = np.ones((16, 16), dtype=np.uint8)
51+
ring[6:10, 6:10] = 0
52+
ring[6:10, 6] = 1
53+
ring[6:10, 9] = 1
54+
ring[6, 6:10] = 1
55+
ring[9, 6:10] = 1
56+
57+
config = PipelineConfig(
58+
extraction=ExtractionConfig(fill_holes=True),
59+
output=OutputConfig(),
60+
)
61+
result = analyze_binary_image(ring, "ring", config)
62+
# preprocessed_binary should have the hole filled
63+
assert result.preprocessed_binary is not None
64+
assert result.preprocessed_binary[7:9, 7:9].all()
65+
66+
67+
class TestMaxHoleSize:
68+
def test_max_hole_size_skips_large_holes(self):
69+
ring = np.ones((32, 32), dtype=np.uint8)
70+
ring[8:24, 8:24] = 0
71+
ring[8:24, 8] = 1
72+
ring[8:24, 23] = 1
73+
ring[8, 8:24] = 1
74+
ring[23, 8:24] = 1
75+
76+
config = PipelineConfig(
77+
extraction=ExtractionConfig(fill_holes=True, max_hole_size=50),
78+
output=OutputConfig(),
79+
)
80+
result = analyze_binary_image(ring, "ring", config)
81+
# 15x15 = 225 pixels, above threshold, so preprocessed binary keeps it as a hole
82+
assert result.preprocessed_binary is not None
83+
assert not result.preprocessed_binary[9:23, 9:23].any()
84+
85+
def test_max_hole_size_fills_small_holes(self):
86+
ring = np.ones((16, 16), dtype=np.uint8)
87+
ring[7:9, 7:9] = 0
88+
ring[7:9, 7] = 1
89+
ring[7:9, 8] = 1
90+
ring[7, 7:9] = 1
91+
ring[8, 7:9] = 1
92+
93+
config = PipelineConfig(
94+
extraction=ExtractionConfig(fill_holes=True, max_hole_size=50),
95+
output=OutputConfig(),
96+
)
97+
result = analyze_binary_image(ring, "ring", config)
98+
# 2x2 = 4 pixels, under threshold, so filled in preprocessed
99+
assert result.preprocessed_binary is not None
100+
assert result.preprocessed_binary[7:9, 7:9].all()
101+
102+
def test_max_hole_size_zero_fills_all(self):
103+
ring = np.ones((16, 16), dtype=np.uint8)
104+
ring[7:11, 7:11] = 0
105+
ring[7:11, 7] = 1
106+
ring[7:11, 10] = 1
107+
ring[7, 7:11] = 1
108+
ring[10, 7:11] = 1
109+
110+
config = PipelineConfig(
111+
extraction=ExtractionConfig(fill_holes=True, max_hole_size=0),
112+
output=OutputConfig(),
113+
)
114+
result = analyze_binary_image(ring, "ring", config)
115+
# zero = unlimited, so 4x4 hole gets filled
116+
assert result.preprocessed_binary is not None
117+
assert result.preprocessed_binary[8:10, 8:10].all()
118+
119+
120+
class TestPreprocessedBinary:
121+
def test_set_when_closing_active(self):
122+
img = np.zeros((16, 16), dtype=np.uint8)
123+
img[8, 4:12] = 1
124+
config = PipelineConfig(
125+
extraction=ExtractionConfig(closing_iterations=1),
126+
output=OutputConfig(),
127+
)
128+
result = analyze_binary_image(img, "test", config)
129+
assert result.preprocessed_binary is not None
130+
assert result.preprocessed_binary.shape == img.shape
131+
132+
def test_set_when_fill_holes_active(self):
133+
ring = np.ones((16, 16), dtype=np.uint8)
134+
ring[7:9, 7:9] = 0
135+
ring[7:9, 7] = 1
136+
ring[7:9, 8] = 1
137+
ring[7, 7:9] = 1
138+
ring[8, 7:9] = 1
139+
config = PipelineConfig(
140+
extraction=ExtractionConfig(fill_holes=True),
141+
output=OutputConfig(),
142+
)
143+
result = analyze_binary_image(ring, "test", config)
144+
assert result.preprocessed_binary is not None
145+
146+
def test_none_when_preprocessing_disabled(self):
147+
img = np.zeros((16, 16), dtype=np.uint8)
148+
img[8, 4:12] = 1
149+
config = PipelineConfig(
150+
extraction=ExtractionConfig(),
151+
output=OutputConfig(),
152+
)
153+
result = analyze_binary_image(img, "test", config)
154+
assert result.preprocessed_binary is None
155+
156+
def test_none_when_show_preprocessed_checked_but_no_preprocessing(self):
157+
img = np.zeros((16, 16), dtype=np.uint8)
158+
img[8, 4:12] = 1
159+
config = PipelineConfig(
160+
extraction=ExtractionConfig(show_preprocessed=True),
161+
output=OutputConfig(),
162+
)
163+
result = analyze_binary_image(img, "test", config)
164+
assert result.preprocessed_binary is None

0 commit comments

Comments
 (0)