stentfit.core.skeleton_2d
Functions
|
Unroll a cylindrical stent point cloud onto a flat (arc, z) plane. |
|
Rasterise, dilate, and thin the flat surface points into a 2D skeleton. |
|
Find a graph's 2-core: every node with degree >= 2 once dead-ends are |
|
Score a 2D skeleton for three defect types and report where they are. |
|
Search for the |
|
Rebuild the 8-neighbour pixel-grid graph for a set of 2D skeleton points. |
|
Generate evenly-spaced points strictly between two 2D points. |
|
Collapse a loop (bubble) in the 2D skeleton into a single straight path. |
|
Delete a whole wrong bridge between two points on the 2D skeleton. |
|
Remove the skeleton bridges flagged as bad connections by |
|
Randomly subsample two matching coordinate arrays down to |
|
2D-skeletonise every ring of the stent, one ring at a time. |
|
Save the assembled per-ring 2D skeletons to disk as a resume checkpoint. |
|
Reload the per-ring 2D skeletons and surface point cloud from disk. |
|
Parse a "a, b" / "a b" string into exactly two int point indices. |
|
Prompt the user to manually fix defects in any ring's 2D skeleton. |
|
Concatenate every ring's 2D skeleton into one flat skeleton. |
Module Contents
- stentfit.core.skeleton_2d.open_stent_to_plane(stent_df: pandas.DataFrame, r_mid: float) dict[source]
Unroll a cylindrical stent point cloud onto a flat (arc, z) plane.
Converts each point’s
thetato an arc-length coordinate (r_mid * theta), leavingzunchanged. This flat representation is whatcompute_skeleton_2d()rasterises and thins.- Parameters:
stent_df – Stent point cloud with
thetaandz_cylindricalcolumns.r_mid – Mid-wall radius, used to convert angle to arc length.
- Returns:
Dict with the unrolled coordinates (
arc_flat,z_flat), the minimum arc value (arc_min_flat), and the fullcircumferenceatr_mid.
- stentfit.core.skeleton_2d.compute_skeleton_2d(arc_flat: numpy.ndarray, z_flat: numpy.ndarray, arc_min_flat: float, circumference: float, stent_df: pandas.DataFrame, stent_geometry: dict, pixels_per_strut: int, dilate_px: int, pad_fraction: float) dict[source]
Rasterise, dilate, and thin the flat surface points into a 2D skeleton.
The (arc, z) points are rasterised onto a pixel grid sized so one strut is
pixels_per_strutpixels wide, with the arc axis wrapped (the last column borders the first) so a strut sitting on the seam stays one continuous line. The raster is dilated bydilate_px, closed to fill tiny gaps, then thinned to a 1-pixel-wide skeleton (skimage.morphology.skeletonize). A padded copy of the wrap is used only during thinning so the seam sees its true neighbours; the padding is cropped back off before returning.- Parameters:
arc_flat – Flat arc-coordinates of the ring’s surface points, from
open_stent_to_plane().z_flat – Flat z-coordinates of the ring’s surface points.
arc_min_flat – Minimum arc value, used as the raster’s column origin.
circumference – Full circumference at the ring’s mid-wall radius, sets the raster width.
stent_df – Ring’s surface point cloud (unused directly here, kept for a consistent call signature with the quality check).
stent_geometry – Stent features dict; only
strut_thicknessis used.pixels_per_strut – Raster resolution, in pixels across one strut width.
dilate_px – Dilation radius, in pixels, before thinning.
pad_fraction – Seam padding, as a fraction of the raster width, so thinning sees the wrap correctly.
- Returns:
Dict with the skeleton’s flat coordinates (
skel_arc,skel_z), thepixel_sizeused, and the same points as a DataFrame (df_skeleton_2d).
- stentfit.core.skeleton_2d._two_core_mask(V: int, a: numpy.ndarray, b: numpy.ndarray) numpy.ndarray[source]
Find a graph’s 2-core: every node with degree >= 2 once dead-ends are peeled away.
Repeatedly removes degree-<=1 nodes (leaves), updating their neighbours’ degree as they go, until only nodes that are part of a cycle remain. Used to distinguish a real loop from a simple dead-end branch: a node survives here only if it’s actually on a cycle, not just reachable from one.
- Parameters:
V – Total number of nodes in the graph.
a – Edge endpoints, first side (parallel to
b).b – Edge endpoints, second side (parallel to
a).
- Returns:
Boolean mask,
Truefor every node in the 2-core.
- stentfit.core.skeleton_2d.check_skeleton_quality(df_skeleton_2d: pandas.DataFrame, pixel_size: float, stent_df: pandas.DataFrame, r_mid: float, region_allowed: numpy.ndarray, strut_thickness: float | None = None, loop_size_factor: float = 2.0, surf_tree: scipy.spatial.cKDTree | None = None, surf_reg: numpy.ndarray | None = None, verbose: bool = True) dict[source]
Score a 2D skeleton for three defect types and report where they are.
Every skeleton point is assigned to its nearest surface region, and the skeleton’s pixel-grid graph is rebuilt from
df_skeleton_2dto check: (1) bad connections — an edge joins two regions that are not actually adjacent in 3D (perregion_allowed), meaning the skeleton bridged two unrelated struts; (2) loops — a small closed cycle inside a region or straddling a border, found via the graph’s 2-core, and only flagged if its bounding diagonal is underloop_size_factor * strut_thickness(a larger loop is a real design cell, not a defect); (3) empty regions — a region with no skeleton point at all. This is the scoring functiontune_skeleton_params()minimises over.- Parameters:
df_skeleton_2d – Skeleton points with
arcandzcolumns, fromcompute_skeleton_2d().pixel_size – Pixel size the skeleton was rasterised at, used to recover its integer grid coordinates.
stent_df – Ring’s surface point cloud with a
regioncolumn.r_mid – Mid-wall radius, used to unroll
thetato arc length.region_allowed – Region-adjacency matrix from
segment_stent().strut_thickness – Strut thickness, used to size the small-loop cutoff.
Nonetreats every loop as small (no size filtering).loop_size_factor – A loop wider than this many strut thicknesses is a design cell, not a defect.
surf_tree – Prebuilt KD-tree of the surface points, to skip rebuilding it across repeated calls (e.g. inside the tuning loop).
Nonebuilds it fromstent_df.surf_reg – Region labels matching
surf_tree, required together with it.verbose – Print a pass/fail summary for each defect type.
- Returns:
Dict with the region and skeleton-point counts (
n_regions,n_skel_points), the defects found (bad_connections,region_loops,border_loops,empty_regions), the per-point region labels (skel_region), and (arc, z) markers for plotting each defect (bad_edge_xy,loop_points_xy,empty_xy).
- stentfit.core.skeleton_2d.tune_skeleton_params(arc: numpy.ndarray, z: numpy.ndarray, stent_df: pandas.DataFrame, stent_features: dict, region_allowed: numpy.ndarray, pps0: float = 10.0, dil0: int = 3, pps_min: float = 5.0, pps_max: float = 120.0, dil_min: int = 1, dil_max: int = 30, s_pps_conn: float = 25.0, s_pps_empty: float = 20.0, s_pps_loop: float = 15.0, s_pps_explore: float = 5.0, s_dil_loop: float = 20.0, s_dil_conn: float = 8.0, w_conn: float = 2.0, w_loop: float = 10.0, w_empty: float = 1.0, loop_size_factor: float = 2.0, q_eps: float = 0.001, quality_gamma: float = 2.0, res_no_improve_max: int = 5, target_penalty: float = 1.0, max_repeats: int = 2, pad_fraction: float = 0.2, time_limit: float = 100.0, predictive_stop: bool = True, max_iters: int = int(1000.0), verbose: bool = True) dict[source]
Search for the
pixels_per_strut/dilate_pxpair that gives the cleanest 2D skeleton in the least time.Each step runs
compute_skeleton_2d()at the current (pps, dil) and scores it withcheck_skeleton_quality(). The score is adefect_error(bad connections, loops, empty regions — zero for a clean skeleton) plus aquality_errorresidual that only shrinks as pps/dil gets finer. While defects remain, pps and dil are nudged in proportion to which defect dominates (sharpen for bad connections or empty regions, thicken for loops, thin back down once dilation hits its cap). Once clean, pps is raised step by step to shrink the residual. The search stops on the time limit, on no improvement forres_no_improve_maxsteps after going clean, or if the same (pps, dil) state repeatsmax_repeatstimes.- Parameters:
arc – Flat arc-coordinates of the ring’s surface points.
z – Flat z-coordinates of the ring’s surface points.
stent_df – Ring’s surface point cloud with a
regioncolumn.stent_features – Stent features dict (
r_mid,strut_thickness).region_allowed – Region-adjacency matrix from
segment_stent().pps0 – Starting
pixels_per_strut.dil0 – Starting
dilate_px.pps_min – Lower bound on
pixels_per_strut.pps_max – Upper bound on
pixels_per_strut, keeps runtime bounded.dil_min – Lower bound on
dilate_px.dil_max – Upper bound on
dilate_px.s_pps_conn – How much
ppsrises per unit of bad-connection error.s_pps_empty – How much
ppsrises per unit of empty-region error.s_pps_loop – How much
ppsfalls per unit of loop error, oncedilate_pxis already capped.s_pps_explore – Step size
ppsis raised by once the skeleton is clean.s_dil_loop – How much
dilate_pxrises per unit of loop error.s_dil_conn – How much
dilate_pxfalls per unit of bad-connection error.w_conn – Weight of bad connections in
defect_error.w_loop – Weight of loops in
defect_error.w_empty – Weight of empty regions in
defect_error.loop_size_factor – A loop wider than this many strut thicknesses is a real design cell, not a defect.
q_eps – Floor on
quality_errorsototal_errornever hits zero.quality_gamma – Convexity of
quality_error; 1 is linear, higher gives diminishing returns as it approaches zero.res_no_improve_max – Steps without improvement, after first going clean, before the search stops.
target_penalty –
defect_errorat or below this counts as clean.max_repeats – Times the same (pps, dil) state may repeat before the search stops as a cycle.
pad_fraction – Seam padding passed through to
compute_skeleton_2d().time_limit – Time budget, in seconds, for the whole search.
predictive_stop – Stop early if the next step is projected to blow the time budget, instead of only checking after it runs.
max_iters – Hard cap on the number of steps, regardless of time.
verbose – Print the per-step error table.
- Returns:
Dict with the best
pps/dilate_pxfound (best_pps,best_dilate_px), their errors (best_defect_error,best_quality_error,best_total_error), the full step history (history), and the winningskeleton_2d/quality_report.
- stentfit.core.skeleton_2d._grid_adjacency(arc: numpy.ndarray, z: numpy.ndarray, pixel_size: float) tuple[numpy.ndarray, list[list[int]]][source]
Rebuild the 8-neighbour pixel-grid graph for a set of 2D skeleton points.
Recovers each point’s integer grid coordinates from its (arc, z) position and connects it to the neighbours sharing an edge or a non-corner-cutting diagonal — the same connectivity rule used in
check_skeleton_quality(). Used by the manual/automatic bad-edge fixers (fix_ring_loop_2d(),fix_ring_connection_2d(),auto_clean_bad_connections_2d()) to walk the skeleton as a graph.- Parameters:
arc – Flat arc-coordinates of the ring’s 2D skeleton.
z – Flat z-coordinates of the ring’s 2D skeleton.
pixel_size – Pixel size the skeleton was rasterised at.
- Returns:
(edges, adj)— an(E, 2)array of point-index pairs, and a per-point adjacency list of neighbour indices.
- stentfit.core.skeleton_2d._interp_2d(a: numpy.ndarray, b: numpy.ndarray, spacing: float) numpy.ndarray[source]
Generate evenly-spaced points strictly between two 2D points.
Used by
fix_ring_loop_2d()to fill a collapsed loop back in as a single straight line between its two kept anchor points, at roughly the skeleton’s own pixel spacing.- Parameters:
a – Start point, as
(arc, z).b – End point, as
(arc, z).spacing – Target distance between consecutive inserted points.
- Returns:
(n, 2)array of points strictly betweenaandb(excluding both endpoints); empty if they’re already closer thanspacing.
- stentfit.core.skeleton_2d.fix_ring_loop_2d(arc: numpy.ndarray, z: numpy.ndarray, pixel_size: float, anchor_a: int, anchor_b: int, verbose: bool = True) tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray][source]
Collapse a loop (bubble) in the 2D skeleton into a single straight path.
anchor_aandanchor_bmust both sit on the same loop (found via the grid graph’s 2-core, same ascheck_skeleton_quality()’s loop check). Every other point on that loop is deleted, and an evenly-spaced straight line is inserted between the two anchors instead. Everything outside the loop is left untouched. If the anchors aren’t both on the same loop, nothing changes.- Parameters:
arc – Flat arc-coordinates of the ring’s 2D skeleton.
z – Flat z-coordinates of the ring’s 2D skeleton.
pixel_size – Pixel size the skeleton was rasterised at, used to rebuild its grid adjacency and space the inserted points.
anchor_a – Point index at one end of the loop to keep.
anchor_b – Point index at the other end of the loop to keep.
verbose – Print what was changed, or why nothing was.
- Returns:
(arc, z, changed_idx)— the edited coordinates and the indices of the newly inserted points. Unchanged, with an emptychanged_idx, if the anchors aren’t both on the same loop.
- stentfit.core.skeleton_2d.fix_ring_connection_2d(arc: numpy.ndarray, z: numpy.ndarray, pixel_size: float, point_a: int, point_b: int, verbose: bool = True) tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray][source]
Delete a whole wrong bridge between two points on the 2D skeleton.
Walks the shortest grid-graph path between
point_aandpoint_b, then extends outward from each end along the thin (degree-2) chain until it hits a junction or endpoint (degree != 2) — that junction is kept as the boundary, everything strictly inside is removed.point_aandpoint_bshould sit on the bridge itself (e.g. its two ends), not on the junctions bounding it. If there’s no path between them, or nothing thin to remove, nothing changes.- Parameters:
arc – Flat arc-coordinates of the ring’s 2D skeleton.
z – Flat z-coordinates of the ring’s 2D skeleton.
pixel_size – Pixel size the skeleton was rasterised at, used to rebuild its grid adjacency.
point_a – Point index on one end of the wrong bridge.
point_b – Point index on the other end of the wrong bridge.
verbose – Print what was removed, or why nothing was.
- Returns:
(arc, z, changed_idx)— the edited coordinates and the indices of any newly inserted points (always empty here — this fix only deletes). Unchanged if there was no path or nothing removable.
- stentfit.core.skeleton_2d.auto_clean_bad_connections_2d(arc: numpy.ndarray, z: numpy.ndarray, pixel_size: float, bad_edge_xy: numpy.ndarray, verbose: bool = True) tuple[numpy.ndarray, numpy.ndarray, int][source]
Remove the skeleton bridges flagged as bad connections by
check_skeleton_quality().Each entry in
bad_edge_xyis the (arc, z) midpoint of one flagged edge. For each, the nearest skeleton point is used as a seed, and the whole degree-2 chain it belongs to (the thin bridge between two junctions) is removed. A seed that sits on a junction (degree != 2) has no removable chain and is left alone, for the manual-edit step to fix by hand.- Parameters:
arc – Flat arc-coordinates of the ring’s 2D skeleton.
z – Flat z-coordinates of the ring’s 2D skeleton.
pixel_size – Pixel size the skeleton was rasterised at, used to rebuild its grid adjacency.
bad_edge_xy – (K, 2) array of (arc, z) midpoints, from
quality_report['bad_edge_xy'].verbose – Print how many bridges (and points) were removed.
- Returns:
(arc, z, n_bridges)— the cleaned coordinates and the number of bridges removed. Unchanged, withn_bridges=0, if nothing was removable.
- stentfit.core.skeleton_2d._downsample_surface_pair(a: numpy.ndarray, b: numpy.ndarray, n: int = 40000, seed: int = 0) tuple[numpy.ndarray, numpy.ndarray][source]
Randomly subsample two matching coordinate arrays down to
npoints.Used to shrink a ring’s surface points before storing them for the 2D skeleton plot, so
plot_ring_skeleton_2d_html()stays responsive.- Parameters:
a – First coordinate array (e.g. arc).
b – Second coordinate array (e.g. z), same length as
a.n – Maximum number of points to keep.
seed – Seed for the row sampling, for repeatable plots.
- Returns:
(a, b)unchanged iflen(a) <= n, otherwise the same randomnindices taken from both.
- stentfit.core.skeleton_2d.skeletonize_rings_2d(stent_df: pandas.DataFrame, stent_features: dict, ring_edges: numpy.ndarray, conn_radius_3d: float, output_dir: str, auto_tune: bool = True, pixels_per_strut: int = 10, dilate_px: int = 3, pad_fraction: float = 0.2, tune_time_limit: int = 120, quality_gamma: float = 2.0, ring_halo_frac: float = 0.4) dict[source]
2D-skeletonise every ring of the stent, one ring at a time.
Runs
segment_stent()once on the whole stent to get a shared region map, then for each ring: unrolls its points (plus a z-halo, so struts still reconnect across ring boundaries) to a flat (arc, z) plane withopen_stent_to_plane(), skeletonises it (either a single pass withcompute_skeleton_2d(), or an auto-tuned search overpixels_per_strut/dilate_pxviatune_skeleton_params()), then trims the result back down to the ring’s own z-band. Detected bad connections are auto-cleaned withauto_clean_bad_connections_2d(). Writes per-ring plots and a CSV underoutput_dir/skeleton_plots/.- Parameters:
stent_df – Stent point cloud with a
ring_idcolumn (fromdetect_rings()).stent_features – Stent features dict (
r_mid,strut_thickness, …).ring_edges – Z-boundaries between rings, used to size each ring’s halo.
conn_radius_3d – 3D connectivity radius used for the region segmentation.
output_dir – Folder the per-ring plots and CSVs are written into (under a
skeleton_plotssubfolder).auto_tune – Search for the best skeletonisation parameters per ring, instead of using
pixels_per_strut/dilate_pxdirectly.pixels_per_strut – Raster resolution, in pixels across one strut width.
dilate_px – Dilation radius, in pixels, before thinning.
pad_fraction – Seam padding added when unrolling a ring to 2D.
tune_time_limit – Time budget, in seconds, for the auto-tune search.
quality_gamma – Weight of the skeleton quality score during tuning.
ring_halo_frac – Z-halo, as a fraction of ring height, so struts reconnect across neighbouring rings.
- Returns:
Dict with the per-ring 2D skeletons (
ring_2d, keyed byring_XXlabel) and the ring order along the stent axis (ring_order).
- stentfit.core.skeleton_2d.save_ring_2d_checkpoint(ring_2d: dict, stent_features: dict, stent_centerline_direction: numpy.ndarray, r_mid: float, strut_thickness: float, circumference: float, ring_edges: numpy.ndarray, output_dir: str, verbose: bool = True) str[source]
Save the assembled per-ring 2D skeletons to disk as a resume checkpoint.
Pickles
ring_2dplus the scalars needed to rebuild it, intoring_2d.pkl. Together with the already-savedring_points.csv, this letsload_ring_2d_checkpoint()restore the pipeline state after a kernel restart, without rerunning sampling, ring detection, or 2D skeletonisation.- Parameters:
ring_2d – Per-ring 2D skeletons, from
skeletonize_rings_2d().stent_features – Stent features dict.
stent_centerline_direction – Stent centreline unit vector.
r_mid – Mid-wall radius.
strut_thickness – Strut thickness.
circumference – Full circumference at
r_mid.ring_edges – Z-boundaries between rings.
output_dir – Folder
ring_2d.pklis written into.verbose – Print the saved path and ring count.
- Returns:
Path to the written
ring_2d.pkl.
- stentfit.core.skeleton_2d.load_ring_2d_checkpoint(output_dir: str) dict[source]
Reload the per-ring 2D skeletons and surface point cloud from disk.
Reads back
ring_2d.pkl(fromsave_ring_2d_checkpoint()) andring_points.csv, so the pipeline can resume the manual-edit step after a kernel restart without rerunning sampling, ring detection, or 2D skeletonisation.- Parameters:
output_dir – Folder containing
ring_2d.pklandring_points.csv.- Raises:
FileNotFoundError – If either file is missing.
- Returns:
State dict with the per-ring 2D skeletons (
ring_2d), the stent features and geometry (stent_features,stent_centerline_direction,r_mid,strut_thickness,circumference,ring_edges), and the surface point cloud (stent_df).
- stentfit.core.skeleton_2d._parse_two_ids(s: str) tuple[int, int][source]
Parse a “a, b” / “a b” string into exactly two int point indices.
- Parameters:
s – User input like
"12, 40"or"12 40".- Raises:
ValueError – If
sdoes not contain exactly two integers.- Returns:
The two parsed indices, as
(a, b).
- stentfit.core.skeleton_2d.edit_rings_2d_interactive(ring_2d: dict, stent_features: dict, stent_centerline_direction: numpy.ndarray, r_mid: float, strut_thickness: float, circumference: float, ring_edges: numpy.ndarray, output_dir: str) dict[source]
Prompt the user to manually fix defects in any ring’s 2D skeleton.
Asks once whether to edit any ring at all. If yes, loops: pick a ring by label, describe the problem (
looporconnection), give the two point indices at the defect, and the matching fixer runs (fix_ring_loop_2d()orfix_ring_connection_2d()). Each edit is applied tentatively, rendered to a preview PNG/HTML via_render_ring_2d(), and only kept (and checkpointed to disk) if the user confirms it looks right — otherwise the ring is reverted to its state before that edit.- Parameters:
ring_2d – Per-ring 2D skeletons, from
skeletonize_rings_2d(). Edited in place and also returned.stent_features – Stent features dict, passed through to the checkpoint save on each confirmed edit.
stent_centerline_direction – Stent centreline unit vector, passed through to the checkpoint save.
r_mid – Mid-wall radius, passed through to the checkpoint save.
strut_thickness – Strut thickness, passed through to the checkpoint save.
circumference – Full circumference at
r_mid, passed through to the checkpoint save.ring_edges – Z-boundaries between rings, passed through to the checkpoint save.
output_dir – Folder edit previews are rendered into and the checkpoint is saved to.
- Returns:
ring_2d, with any confirmed edits applied.
- stentfit.core.skeleton_2d.assemble_2d_skeleton(ring_2d: dict) dict[source]
Concatenate every ring’s 2D skeleton into one flat skeleton.
Rings are ordered by
ring_id(bottom to top along the stent axis) and their (arc, z) points are stacked into single arrays. Each point also carries its ring’spixel_size, since different rings may have been skeletonised at different resolutions.- Parameters:
ring_2d – Per-ring 2D skeletons, from
skeletonize_rings_2d()(optionally edited byedit_rings_2d_interactive()).- Returns:
Dict with the concatenated coordinates (
skel_arc,skel_z), the per-point pixel size (skel_px), and the medianpixel_sizeacross all rings.