@@ -2339,6 +2339,74 @@ Important differences:
23392339 and ` number_of_threads ` parallelizes the pairwise evaluation. Still threshold
23402340 the distance map first to keep the candidate count modest.
23412341
2342+ ### Discrete Grid Dijkstra
2343+
2344+ ` bioimage-cpp ` exposes its exact masked-grid shortest-path primitive directly.
2345+ This is useful when a predecessor chain is required, when edge costs are
2346+ naturally attached to voxels, and for independently benchmarking the
2347+ shortest-path phase of algorithms such as TEASAR.
2348+
2349+ ``` python
2350+ import bioimage_cpp as bic
2351+
2352+ # Multi-source field on an 8-connected 2D or 26-connected 3D voxel graph.
2353+ field = bic.distance.dijkstra_distance_field(
2354+ mask,
2355+ sources, # (N, mask.ndim) integer coordinates
2356+ connectivity = None , # full connectivity by default
2357+ spacing = (2.0 , 1.0 , 1.0 ),
2358+ number_of_threads = 4 ,
2359+ )
2360+
2361+ # Flat C-order predecessor indices can be requested with the same solve.
2362+ field, predecessors = bic.distance.dijkstra_distance_field(
2363+ mask, sources, return_predecessors = True
2364+ )
2365+
2366+ # Stop as soon as the cheapest of several targets is settled.
2367+ path = bic.distance.dijkstra_path(mask, source, targets)
2368+ ```
2369+
2370+ Three edge-cost conventions are available:
2371+
2372+ - ` cost_mode="physical" ` assigns every edge the Euclidean length of its
2373+ neighbour offset under ` spacing ` ; ` costs ` must be omitted.
2374+ - ` cost_mode="node" ` assigns directed edge ` u -> v ` the value ` costs[v] ` ;
2375+ ` spacing ` must be ` None ` .
2376+ - ` cost_mode="node_times_physical" ` assigns ` costs[v] ` times the physical
2377+ neighbour-step length.
2378+
2379+ All costs must be finite and non-negative. Distances are ` float64 ` ; background
2380+ and unreachable voxels are ` +inf ` . Predecessors are ` int64 ` with the mask shape:
2381+ sources point to their own flat C-order index and background/unreachable voxels
2382+ contain ` -1 ` . Paths are ` int64 ` NumPy-axis-order coordinates from the source to
2383+ the selected target. Target ties are deterministic and use flat C-order indices.
2384+ ` number_of_threads=1 ` retains the specialized sequential heaps; ` 0 ` uses
2385+ hardware concurrency. Broad multi-source physical fields can use exact FP64
2386+ delta stepping: distance fields are bitwise-identical to the sequential result,
2387+ while an equal-cost predecessor may differ from the sequential heap but remains
2388+ deterministic. Paths, weighted modes, small workloads, and narrow source sets
2389+ retain the faster specialized heaps.
2390+
2391+ This Dijkstra field is deliberately distinct from
2392+ ` geodesic_distance_field ` below. Dijkstra is exact for the chosen 4/8- or
2393+ 6/18/26-neighbour voxel graph, supports zero node costs, and provides an exact
2394+ predecessor chain. Its physical metric is still a discrete chamfer metric: a
2395+ continuous direction must be approximated by the available neighbour steps.
2396+ The geodesic implementation instead solves the continuous Eikonal equation on
2397+ the sampled grid with a first-order Godunov fast-marching scheme. It is smoother
2398+ and less tied to the finite neighbour directions, and its ` speed ` argument has
2399+ travel-time semantics, but it does not retain discrete predecessors. The two
2400+ are close on well-sampled, uniform domains but are not numerically equivalent;
2401+ TEASAR therefore uses Dijkstra where it must reconstruct and join paths.
2402+
2403+ See ` development/distance/benchmark_dijkstra.py ` for a standalone benchmark of
2404+ the Dijkstra field, predecessor field, weighted field, and early-stopping path;
2405+ pass ` --broad-multisource ` to exercise the delta-stepping dispatch.
2406+ The implementation uses reusable fixed neighbor metadata and specialized heap
2407+ strategies internally, but these do not change dtype, tie-breaking, or public
2408+ results.
2409+
23422410## scikit-fmm
23432411
23442412` scikit-fmm ` computes geodesic distances on regular grids with the fast
@@ -2431,6 +2499,120 @@ Important differences:
24312499 solver it slightly overestimates. See ` development/distance/ ` for the
24322500 reference oracles and ` benchmark_geodesic.py ` for timings.
24332501
2502+ ## kimimaro / TEASAR skeletonization
2503+
2504+ Kimimaro skeletonizes densely labelled volumes and splits labels into connected
2505+ components internally. ` bioimage-cpp ` exposes the same two useful input
2506+ semantics explicitly: ` teasar ` treats all nonzero values as one binary class,
2507+ while ` teasar_labels ` preserves integer semantic-label identities.
2508+
2509+ Kimimaro:
2510+
2511+ ``` python
2512+ import kimimaro
2513+
2514+ skeletons = kimimaro.skeletonize(
2515+ labels,
2516+ teasar_params = {
2517+ " scale" : 1.5 ,
2518+ " const" : 0 ,
2519+ " pdrf_scale" : 100000 ,
2520+ " pdrf_exponent" : 4 ,
2521+ },
2522+ anisotropy = (2.0 , 1.0 , 1.0 ),
2523+ )
2524+ ```
2525+
2526+ bioimage-cpp:
2527+
2528+ ``` python
2529+ import bioimage_cpp as bic
2530+
2531+ vertices, edges, radii = bic.skeleton.teasar(
2532+ mask,
2533+ spacing = (2.0 , 1.0 , 1.0 ),
2534+ scale = 1.5 ,
2535+ constant = 0 ,
2536+ pdrf_scale = 100000 ,
2537+ pdrf_exponent = 4 ,
2538+ number_of_threads = 4 ,
2539+ )
2540+
2541+ skeletons = bic.skeleton.teasar_labels(
2542+ labels,
2543+ background = 0 ,
2544+ spacing = (2.0 , 1.0 , 1.0 ),
2545+ scale = 1.5 ,
2546+ constant = 0 ,
2547+ pdrf_scale = 100000 ,
2548+ pdrf_exponent = 4 ,
2549+ number_of_threads = 4 ,
2550+ )
2551+ # {original_label: (vertices, edges, radii), ...}
2552+ ```
2553+
2554+ Convert the topology of either result to the native undirected graph while
2555+ keeping coordinates and radii in their NumPy arrays:
2556+
2557+ ``` python
2558+ import numpy as np
2559+
2560+ graph = bic.skeleton.skeleton_to_graph(vertices, edges)
2561+
2562+ # Node ids index vertices and radii directly.
2563+ degrees = np.fromiter(
2564+ (len (graph.node_adjacency(node)) for node in range (graph.number_of_nodes)),
2565+ dtype = np.uint64,
2566+ count = graph.number_of_nodes,
2567+ )
2568+ endpoint_vertices = vertices[degrees <= 1 ]
2569+ branch_vertices = vertices[degrees > 2 ]
2570+ ```
2571+
2572+ The conversion also preserves disconnected forests and isolated vertices. The
2573+ graph stores topology only; it does not copy or own `` vertices `` or `` radii `` .
2574+
2575+ Important differences and current scope:
2576+
2577+ - ` teasar(mask) ` treats every nonzero value as one foreground class. It
2578+ skeletonizes every 26-connected component independently and returns their
2579+ deterministic concatenation as one forest tuple.
2580+ - ` teasar_labels(labels) ` accepts native-endian ` uint8 ` , ` uint16 ` , ` uint32 ` ,
2581+ ` uint64 ` , ` int32 ` , or ` int64 ` arrays. It returns one dictionary entry per
2582+ original non-background label. Disconnected occurrences of one label form
2583+ one forest, while touching distinct labels remain separate.
2584+ - Coordinates follow NumPy axis order. ` vertices ` is ` float64 ` physical
2585+ ` (z, y, x) ` coordinates, ` edges ` is ` (E, 2) ` ` uint64 ` , and ` radii ` is
2586+ per-vertex physical distance-to-boundary in ` float32 ` . Empty binary masks
2587+ return typed empty arrays; all-background labeled inputs return ` {} ` .
2588+ - The implementation follows the core TEASAR loop: a padded exact Euclidean
2589+ distance-to-boundary field, a deterministic two-sweep root, a physical
2590+ Dijkstra distance-from-root field, penalized repeated Dijkstra paths, and
2591+ rolling invalidation with radius ` scale * radius + constant ` .
2592+ - This first correctness-oriented version uses a physical axis-aligned
2593+ invalidation cube. It does not implement kimimaro's soma handling, hole
2594+ filling, border stitching, manual targets, component dust filtering,
2595+ cross-section metadata, or postprocessing heuristics. These differences can
2596+ change branch positions and vertex counts, so output is not expected to be
2597+ vertex-for-vertex identical to kimimaro.
2598+ - The C++ core remains dependency-free. Component discovery uses x-runs and a
2599+ union-find rather than dense component-label images. ` number_of_threads=1 `
2600+ is the default and ` 0 ` uses hardware concurrency; one shared budget covers
2601+ component fan-out and each component's exact distance transform without
2602+ nested oversubscription.
2603+ - Compact-foreground root sweeps and rails remain ordered on the optimized
2604+ heap. Compact IDs avoid full-volume shortest-path fields; the public Dijkstra
2605+ functions remain dense and exact FP64.
2606+
2607+ Correctness tests are under ` tests/skeleton/test_teasar.py ` and
2608+ ` tests/skeleton/test_teasar_labels.py ` . The independent
2609+ Dijkstra benchmark is ` development/distance/benchmark_dijkstra.py ` ; the
2610+ end-to-end benchmark is ` development/skeleton/benchmark_teasar.py ` . Use
2611+ ` --suite all --kimimaro ` to compare paired binary and multi-label scenarios,
2612+ ` --memory ` for fresh-process peak-RSS probes, or ` --sequential-backends ` for
2613+ the dense/compact FP64 binary design matrix. Extended density, spacing, and
2614+ PDRF regimes are in ` development/skeleton/benchmark_teasar_sequential.py ` .
2615+
24342616## I/O and Build Dependencies
24352617
24362618` bioimage-cpp ` intentionally does not replace nifty or affogato I/O helpers.
0 commit comments