Skip to content

Commit e218193

Browse files
committed
adding in default pipeline configuration
1 parent baaac7c commit e218193

3 files changed

Lines changed: 156 additions & 118 deletions

File tree

release/__init__.py

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,115 @@
1-
from .skeletonize import Skeletonize
1+
from .skeletonize import Skeletonize, ImageSource
22
from .segment import Segment
33
from .graph import StrokeGraph
44
from .vectorize.low_geometry import Vectorize as LowGeometryVectorize
55
from .vectorize.high_geometry import Vectorize as HighGeometryVectorize
6+
7+
import numpy as np
8+
9+
10+
def default_pipeline(source: ImageSource):
11+
skeleton = Skeletonize(
12+
source,
13+
Skeletonize.Config.Binarize(threshold=0.5),
14+
Skeletonize.Config.Skeletonize(method="zhang"),
15+
Skeletonize.Config.Collapse(
16+
skeletonize_method="lee",
17+
max_hole_area=10,
18+
max_thin_thickness=3.0,
19+
reskeletonize=True,
20+
),
21+
detect_config={
22+
"local_tau_radius": 40,
23+
"fat_ratio": 1.3,
24+
"min_fat_area": 8,
25+
"group_dilate": 15,
26+
"skel_ring_dilate": 5,
27+
"pairing_tangent_steps": 8,
28+
"pairing_threshold": 1.2,
29+
"min_chromosome_skel_length": 15,
30+
},
31+
)
32+
33+
junction_tol = 2.5
34+
tangent_sample = 10
35+
36+
segment = Segment(
37+
skeleton.uncrossed,
38+
skeleton.binary,
39+
Segment.Config.Segment(min_length=10.0),
40+
Segment.Config.Fuse(
41+
max_path_length=20,
42+
lookback=10,
43+
min_tangent_score=0.5,
44+
gap_penalty=0.05,
45+
curvature_penalty=3.0,
46+
),
47+
Segment.Config.Repair(
48+
junction_tol=junction_tol,
49+
stable_skip=2,
50+
stable_sample=6,
51+
max_junction_region_length=20,
52+
min_output_polyline_length=2,
53+
min_tangent_spread_deg=15.0,
54+
interp_max_spacing=1.0,
55+
min_curvature_spike_ratio=2.0,
56+
curvature_context_window=8,
57+
),
58+
Segment.Config.PostRepairFuse(
59+
junction_tol=junction_tol,
60+
tangent_skip=2,
61+
tangent_sample=tangent_sample,
62+
min_tangent_score=0.6,
63+
curvature_penalty=1.0,
64+
),
65+
)
66+
67+
graph = StrokeGraph(
68+
segment.fused_post_repair,
69+
StrokeGraph.Config.Build(
70+
junction_tol=junction_tol, # match Repair.junction_tol
71+
terminal_tangent_window=10, # should match fuse.lookback
72+
crossing_tangent_skip=2, # baseline; dynamic walk handles arbitrary bridges
73+
crossing_tangent_half_window=6,
74+
cusp_angle_threshold_deg=50.0, # raise to ~50 to handle bikelove's cusp-like junction
75+
cluster_merge_centroid_distance=10.0,
76+
cluster_merge_index_gap=10,
77+
),
78+
)
79+
80+
start_pos = np.array([0.0, 0.0])
81+
start_heading = 0.0
82+
low_geometry = LowGeometryVectorize(
83+
graph,
84+
start_pos=start_pos,
85+
start_heading=start_heading,
86+
)
87+
high_geometry = HighGeometryVectorize(
88+
segment.fused_post_repair,
89+
start_pos=start_pos,
90+
start_heading=start_heading,
91+
commands=HighGeometryVectorize.Config.ToCommands(
92+
sigma=2.0,
93+
corner_threshold=0.25,
94+
max_fit_residual=5.0,
95+
),
96+
consolidate=HighGeometryVectorize.Config.Consolidate(
97+
center_tol_rel=0.25,
98+
radius_tol_rel=0.25,
99+
center_tol_abs=3.0,
100+
radius_tol_abs=3.0,
101+
max_endpoint_snap_rel=0.15,
102+
max_endpoint_snap_abs=6.0,
103+
proximity_min_radius_ratio=0.4,
104+
line_angle_tol_deg=6.0,
105+
line_offset_tol_abs=5.0,
106+
min_line_length=5.0,
107+
max_line_endpoint_snap_abs=5.0,
108+
junction_epsilon=3.0,
109+
merge_arcs=True,
110+
merge_lines=True,
111+
return_report=False,
112+
),
113+
)
114+
115+
return skeleton, segment, graph, low_geometry, high_geometry

release/skeletonize/__init__.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import numpy as np
22
from numpy.typing import NDArray
33
from skimage import io
4+
from skimage.color import rgb2gray, rgba2rgb
45
from skimage.morphology import skeletonize as _skeletonize
56

67
from .cleanup import collapse_small_holes, CollapseConfig
@@ -11,17 +12,45 @@
1112
resolve_crossings,
1213
)
1314

14-
from typing import Literal, TypedDict, NamedTuple
15+
from typing import Literal, TypedDict, NamedTuple, Union
1516

1617

1718
class BinarizeConfig(TypedDict):
1819
threshold: float # 0.0 to 1.0
1920

2021

21-
def to_binary(path: str, config: BinarizeConfig) -> NDArray[np.bool_]:
22-
img: np.ndarray = io.imread(path, as_gray=True)
23-
if img.dtype != np.float64 and img.dtype != np.float32:
24-
img = img / 255.0
22+
ImageSource = Union[str, np.ndarray]
23+
24+
25+
def _to_grayscale_float(img: np.ndarray) -> np.ndarray:
26+
"""Normalize an arbitrary image array to a float grayscale in [0, 1]."""
27+
if img.ndim == 3:
28+
if img.shape[-1] == 4:
29+
img = rgba2rgb(img) # also yields float in [0, 1]
30+
if img.shape[-1] == 3:
31+
return rgb2gray(img) # float in [0, 1]
32+
raise ValueError(
33+
f"Unsupported channel count {img.shape[-1]} for 3D image input; "
34+
"expected 3 (RGB) or 4 (RGBA)."
35+
)
36+
if img.ndim != 2:
37+
raise ValueError(
38+
f"Unsupported image ndim {img.ndim}; expected 2 (grayscale) or 3."
39+
)
40+
if img.dtype == np.bool_:
41+
return img.astype(np.float64)
42+
if np.issubdtype(img.dtype, np.integer):
43+
return img / np.float64(np.iinfo(img.dtype).max)
44+
return img.astype(np.float64, copy=False)
45+
46+
47+
def to_binary(source: ImageSource, config: BinarizeConfig) -> NDArray[np.bool_]:
48+
if isinstance(source, np.ndarray):
49+
img = _to_grayscale_float(source)
50+
else:
51+
img = io.imread(source, as_gray=True)
52+
if img.dtype != np.float64 and img.dtype != np.float32:
53+
img = img / 255.0
2554
return img < config["threshold"]
2655

2756

@@ -80,13 +109,13 @@ class Output(NamedTuple):
80109

81110
def __init__(
82111
self,
83-
path: str,
112+
source: ImageSource,
84113
binarize_config: Config.Binarize,
85114
skeletonize_config: Config.Skeletonize,
86115
collapse_config: Config.Collapse,
87116
detect_config: Config.Detect,
88117
):
89-
self.binary = to_binary(path, binarize_config)
118+
self.binary = to_binary(source, binarize_config)
90119
self.skeletonized = skeletonize(self.binary, skeletonize_config)
91120
self.collapsed = collapse_small_holes(self.skeletonized, collapse_config)
92121
# Detection uses the BINARY for its distance-transform analysis

tests/__init__.py

Lines changed: 9 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import os
22

33
from release import (
4+
default_pipeline,
45
Skeletonize,
56
Segment,
6-
StrokeGraph,
77
LowGeometryVectorize,
88
HighGeometryVectorize,
99
)
@@ -141,123 +141,22 @@ def process():
141141
for example in examples:
142142
if only and len(only) > 0 and example not in only:
143143
continue
144-
skeleton = Skeletonize(
145-
f"examples/{example}.png",
146-
Skeletonize.Config.Binarize(threshold=0.5),
147-
Skeletonize.Config.Skeletonize(method="zhang"),
148-
Skeletonize.Config.Collapse(
149-
skeletonize_method="lee",
150-
max_hole_area=10,
151-
max_thin_thickness=3.0,
152-
reskeletonize=True,
153-
),
154-
detect_config={
155-
"local_tau_radius": 40,
156-
"fat_ratio": 1.3,
157-
"min_fat_area": 8,
158-
"group_dilate": 15,
159-
"skel_ring_dilate": 5,
160-
"pairing_tangent_steps": 8,
161-
"pairing_threshold": 1.2,
162-
"min_chromosome_skel_length": 15,
163-
},
164-
)
165-
Visualize.skeleton(skeleton, example)
166144

167-
junction_tol = 2.5
168-
tangent_sample = 10
169-
170-
segment = Segment(
171-
skeleton.uncrossed,
172-
skeleton.binary,
173-
Segment.Config.Segment(min_length=10.0),
174-
Segment.Config.Fuse(
175-
max_path_length=20,
176-
lookback=10,
177-
min_tangent_score=0.5,
178-
gap_penalty=0.05,
179-
curvature_penalty=3.0,
180-
),
181-
Segment.Config.Repair(
182-
junction_tol=junction_tol,
183-
stable_skip=2,
184-
stable_sample=6,
185-
max_junction_region_length=20,
186-
min_output_polyline_length=2,
187-
min_tangent_spread_deg=15.0,
188-
interp_max_spacing=1.0,
189-
min_curvature_spike_ratio=2.0,
190-
curvature_context_window=8,
191-
),
192-
Segment.Config.PostRepairFuse(
193-
junction_tol=junction_tol,
194-
tangent_skip=2,
195-
tangent_sample=tangent_sample,
196-
min_tangent_score=0.6,
197-
curvature_penalty=1.0,
198-
),
145+
skeleton, segment, graph, low_geometry, high_geometry = default_pipeline(
146+
f"examples/{example}.png"
199147
)
200-
Visualize.segments(skeleton, segment, example)
201148

202-
graph = StrokeGraph(
203-
segment.fused_post_repair,
204-
StrokeGraph.Config.Build(
205-
junction_tol=junction_tol, # match Repair.junction_tol
206-
terminal_tangent_window=10, # should match fuse.lookback
207-
crossing_tangent_skip=2, # baseline; dynamic walk handles arbitrary bridges
208-
crossing_tangent_half_window=6,
209-
cusp_angle_threshold_deg=50.0, # raise to ~50 to handle bikelove's cusp-like junction
210-
cluster_merge_centroid_distance=10.0,
211-
cluster_merge_index_gap=10,
212-
),
213-
)
149+
Visualize.skeleton(skeleton, example)
150+
Visualize.segments(skeleton, segment, example)
214151
visualize_graph(
215152
skeleton.binary, graph, scale=1, output_path=f"examples/{example}.graph.png"
216153
)
217-
218-
start_pos = np.array([0.0, 0.0])
219-
start_heading = 0.0
220-
vectorized = {
221-
"low_geometry": LowGeometryVectorize(
222-
graph,
223-
start_pos=start_pos,
224-
start_heading=start_heading,
225-
),
226-
"high_geometry": HighGeometryVectorize(
227-
segment.fused_post_repair,
228-
start_pos=start_pos,
229-
start_heading=start_heading,
230-
commands=HighGeometryVectorize.Config.ToCommands(
231-
sigma=2.0,
232-
corner_threshold=0.25,
233-
max_fit_residual=5.0,
234-
),
235-
consolidate=HighGeometryVectorize.Config.Consolidate(
236-
center_tol_rel=0.25,
237-
radius_tol_rel=0.25,
238-
center_tol_abs=3.0,
239-
radius_tol_abs=3.0,
240-
max_endpoint_snap_rel=0.15,
241-
max_endpoint_snap_abs=6.0,
242-
proximity_min_radius_ratio=0.4,
243-
line_angle_tol_deg=6.0,
244-
line_offset_tol_abs=5.0,
245-
min_line_length=5.0,
246-
max_line_endpoint_snap_abs=5.0,
247-
junction_epsilon=3.0,
248-
merge_arcs=True,
249-
merge_lines=True,
250-
return_report=False,
251-
),
252-
),
253-
}
254-
255154
Visualize.vectorized(
256-
vectorized["low_geometry"],
257-
vectorized["high_geometry"],
155+
low_geometry,
156+
high_geometry,
258157
example,
259-
start_pos=start_pos,
260-
start_heading=start_heading,
158+
start_pos=np.array([0.0, 0.0]),
159+
start_heading=0.0,
261160
)
262161

263162

0 commit comments

Comments
 (0)