1010--------
1111python development/skeleton/benchmark_teasar.py --small --repeats 3
1212python development/skeleton/benchmark_teasar.py --repeats 5
13+ python development/skeleton/benchmark_teasar.py --large --sequential-backends
1314python development/skeleton/benchmark_teasar.py --large --kimimaro --repeats 3 \
1415 --json /tmp/teasar.json
1516"""
2728import numpy as np
2829
2930import bioimage_cpp as bic
31+ from bioimage_cpp import _core
3032
3133
3234def draw_ball (mask : np .ndarray , center : np .ndarray , radius : int ) -> None :
@@ -85,6 +87,43 @@ def count_bic(result) -> tuple[int, int]:
8587 return len (vertices ), len (edges )
8688
8789
90+ def compare_skeletons (reference , candidate , spacing ):
91+ """Return the FP32 acceptance metrics used by OPTIM_DIJKSTRA.md."""
92+ ref_vertices , ref_edges , _ = reference
93+ got_vertices , got_edges , _ = candidate
94+
95+ def degree_signature (edges , n_vertices ):
96+ degrees = np .bincount (edges .ravel ().astype (np .int64 ), minlength = n_vertices )
97+ return tuple (sorted (int (degree ) for degree in degrees if degree != 2 ))
98+
99+ ref_voxels = ref_vertices / np .asarray (spacing , dtype = np .float64 )
100+ got_voxels = got_vertices / np .asarray (spacing , dtype = np .float64 )
101+ pairwise = np .linalg .norm (
102+ ref_voxels [:, None , :] - got_voxels [None , :, :], axis = 2
103+ )
104+ ref_to_got = pairwise .min (axis = 1 )
105+ got_to_ref = pairwise .min (axis = 0 )
106+
107+ def total_length (vertices , edges ):
108+ if len (edges ) == 0 :
109+ return 0.0
110+ return float (
111+ np .linalg .norm (vertices [edges [:, 0 ]] - vertices [edges [:, 1 ]], axis = 1 ).sum ()
112+ )
113+
114+ ref_length = total_length (ref_vertices , ref_edges )
115+ got_length = total_length (got_vertices , got_edges )
116+ return {
117+ "topology_match" : degree_signature (ref_edges , len (ref_vertices ))
118+ == degree_signature (got_edges , len (got_vertices )),
119+ "bidirectional_p95_voxels" : float (
120+ max (np .percentile (ref_to_got , 95 ), np .percentile (got_to_ref , 95 ))
121+ ),
122+ "hausdorff_voxels" : float (max (ref_to_got .max (), got_to_ref .max ())),
123+ "relative_length_error" : abs (got_length - ref_length ) / max (ref_length , 1e-300 ),
124+ }
125+
126+
88127def kimimaro_call (mask , spacing , scale , constant , pdrf_scale , pdrf_exponent ):
89128 import kimimaro
90129
@@ -112,6 +151,19 @@ def kimimaro_call(mask, spacing, scale, constant, pdrf_scale, pdrf_exponent):
112151 return skeletons [1 ]
113152
114153
154+ def bic_backend_call (mask , spacing , parameters , backend ):
155+ """Call a development-only C++ backend without changing the public API."""
156+ return _core ._teasar_uint8_backend (
157+ mask ,
158+ spacing ,
159+ parameters ["scale" ],
160+ parameters ["constant" ],
161+ parameters ["pdrf_scale" ],
162+ parameters ["pdrf_exponent" ],
163+ backend ,
164+ )
165+
166+
115167def main () -> int :
116168 parser = argparse .ArgumentParser (description = __doc__ )
117169 sizes = parser .add_mutually_exclusive_group ()
@@ -120,6 +172,11 @@ def main() -> int:
120172 parser .add_argument ("--repeats" , type = int , default = 5 )
121173 parser .add_argument ("--warmup" , type = int , default = 1 )
122174 parser .add_argument ("--kimimaro" , action = "store_true" )
175+ parser .add_argument (
176+ "--sequential-backends" ,
177+ action = "store_true" ,
178+ help = "compare dense FP64, compact on-the-fly/CSR FP64, and CSR FP32" ,
179+ )
123180 parser .add_argument ("--json" , default = "" , help = "optional JSON result path" )
124181 args = parser .parse_args ()
125182 if args .repeats < 1 or args .warmup < 0 :
@@ -142,13 +199,30 @@ def main() -> int:
142199 "pdrf_scale" : 100000.0 ,
143200 "pdrf_exponent" : 4.0 ,
144201 }
145- backends = [
146- (
147- "bioimage-cpp" ,
148- lambda mask : bic .skeleton .teasar (mask , spacing = spacing , ** parameters ),
149- count_bic ,
150- )
151- ]
202+ if args .sequential_backends :
203+ backends = [
204+ (
205+ backend ,
206+ lambda mask , backend = backend : bic_backend_call (
207+ mask , spacing , parameters , backend
208+ ),
209+ count_bic ,
210+ )
211+ for backend in (
212+ "dense-fp64" ,
213+ "compact-on-the-fly-fp64" ,
214+ "compact-csr-fp64" ,
215+ "compact-csr-fp32" ,
216+ )
217+ ]
218+ else :
219+ backends = [
220+ (
221+ "bioimage-cpp" ,
222+ lambda mask : bic .skeleton .teasar (mask , spacing = spacing , ** parameters ),
223+ count_bic ,
224+ )
225+ ]
152226 if args .kimimaro :
153227 backends .append (
154228 (
@@ -162,6 +236,7 @@ def main() -> int:
162236 )
163237
164238 rows = []
239+ quality_rows = []
165240 header = f"{ 'backend' :>14} { 'shape' :>14} { 'foreground' :>11} { 'vertices' :>9} { 'median ms' :>11} { 'min ms' :>10} "
166241 print (header )
167242 print ("-" * len (header ))
@@ -193,7 +268,34 @@ def main() -> int:
193268 f"{ name :>14} { str (mask .shape ):>14} { foreground :11d} { vertices :9d} "
194269 f"{ median_s * 1e3 :11.2f} { min (samples ) * 1e3 :10.2f} "
195270 )
196- if len (case_rows ) == 2 :
271+ if args .sequential_backends :
272+ dense_result = results_by_backend ["dense-fp64" ]
273+ for exact_backend in (
274+ "compact-on-the-fly-fp64" , "compact-csr-fp64"
275+ ):
276+ exact = all (
277+ np .array_equal (got , expected )
278+ for got , expected in zip (
279+ results_by_backend [exact_backend ], dense_result
280+ )
281+ )
282+ print (f" { exact_backend } exact dense parity: { exact } " )
283+ quality = compare_skeletons (
284+ results_by_backend ["compact-csr-fp64" ],
285+ results_by_backend ["compact-csr-fp32" ],
286+ spacing ,
287+ )
288+ quality_rows .append ({"shape" : list (mask .shape ), ** quality })
289+ print (
290+ " FP32 quality: "
291+ f"topology={ quality ['topology_match' ]} "
292+ f"p95={ quality ['bidirectional_p95_voxels' ]:.3f} vox "
293+ f"hausdorff={ quality ['hausdorff_voxels' ]:.3f} vox "
294+ f"length_error={ quality ['relative_length_error' ]:.3%} "
295+ )
296+ if len (case_rows ) == 2 and {row ["backend" ] for row in case_rows } == {
297+ "bioimage-cpp" , "kimimaro"
298+ }:
197299 by_name = {row ["backend" ]: row for row in case_rows }
198300 ratio = (
199301 by_name ["bioimage-cpp" ]["median_s" ]
@@ -203,7 +305,11 @@ def main() -> int:
203305
204306 if args .json :
205307 with open (args .json , "w" , encoding = "utf-8" ) as file :
206- json .dump ({"repeats" : args .repeats , "results" : rows }, file , indent = 2 )
308+ json .dump (
309+ {"repeats" : args .repeats , "results" : rows , "quality" : quality_rows },
310+ file ,
311+ indent = 2 ,
312+ )
207313 print (f"wrote { args .json } " , file = sys .stderr )
208314 return 0
209315
0 commit comments