stentfit.core.skeleton_2d

Functions

open_stent_to_plane(→ dict)

Unroll a cylindrical stent point cloud onto a flat (arc, z) plane.

compute_skeleton_2d(→ dict)

Rasterise, dilate, and thin the flat surface points into a 2D skeleton.

_two_core_mask(→ numpy.ndarray)

Find a graph's 2-core: every node with degree >= 2 once dead-ends are

check_skeleton_quality(→ dict)

Score a 2D skeleton for three defect types and report where they are.

tune_skeleton_params(, verbose)

Search for the pixels_per_strut / dilate_px pair that gives the

_grid_adjacency(→ tuple[numpy.ndarray, list[list[int]]])

Rebuild the 8-neighbour pixel-grid graph for a set of 2D skeleton points.

_interp_2d(→ numpy.ndarray)

Generate evenly-spaced points strictly between two 2D points.

fix_ring_loop_2d(→ tuple[numpy.ndarray, numpy.ndarray, ...)

Collapse a loop (bubble) in the 2D skeleton into a single straight path.

fix_ring_connection_2d(→ tuple[numpy.ndarray, ...)

Delete a whole wrong bridge between two points on the 2D skeleton.

auto_clean_bad_connections_2d(→ tuple[numpy.ndarray, ...)

Remove the skeleton bridges flagged as bad connections by

_downsample_surface_pair(→ tuple[numpy.ndarray, ...)

Randomly subsample two matching coordinate arrays down to n points.

skeletonize_rings_2d(→ dict)

2D-skeletonise every ring of the stent, one ring at a time.

save_ring_2d_checkpoint(→ str)

Save the assembled per-ring 2D skeletons to disk as a resume checkpoint.

load_ring_2d_checkpoint(→ dict)

Reload the per-ring 2D skeletons and surface point cloud from disk.

_parse_two_ids(→ tuple[int, int])

Parse a "a, b" / "a b" string into exactly two int point indices.

edit_rings_2d_interactive(→ dict)

Prompt the user to manually fix defects in any ring's 2D skeleton.

assemble_2d_skeleton(→ dict)

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 theta to an arc-length coordinate (r_mid * theta), leaving z unchanged. This flat representation is what compute_skeleton_2d() rasterises and thins.

Parameters:
  • stent_df – Stent point cloud with theta and z_cylindrical columns.

  • 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 full circumference at r_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_strut pixels 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 by dilate_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_thickness is 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), the pixel_size used, 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, True for 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_2d to check: (1) bad connections — an edge joins two regions that are not actually adjacent in 3D (per region_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 under loop_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 function tune_skeleton_params() minimises over.

Parameters:
  • df_skeleton_2d – Skeleton points with arc and z columns, from compute_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 region column.

  • r_mid – Mid-wall radius, used to unroll theta to arc length.

  • region_allowed – Region-adjacency matrix from segment_stent().

  • strut_thickness – Strut thickness, used to size the small-loop cutoff. None treats 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). None builds it from stent_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_px pair that gives the cleanest 2D skeleton in the least time.

Each step runs compute_skeleton_2d() at the current (pps, dil) and scores it with check_skeleton_quality(). The score is a defect_error (bad connections, loops, empty regions — zero for a clean skeleton) plus a quality_error residual 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 for res_no_improve_max steps after going clean, or if the same (pps, dil) state repeats max_repeats times.

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 region column.

  • 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 pps rises per unit of bad-connection error.

  • s_pps_empty – How much pps rises per unit of empty-region error.

  • s_pps_loop – How much pps falls per unit of loop error, once dilate_px is already capped.

  • s_pps_explore – Step size pps is raised by once the skeleton is clean.

  • s_dil_loop – How much dilate_px rises per unit of loop error.

  • s_dil_conn – How much dilate_px falls 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_error so total_error never 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_penaltydefect_error at 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_px found (best_pps, best_dilate_px), their errors (best_defect_error, best_quality_error, best_total_error), the full step history (history), and the winning skeleton_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 between a and b (excluding both endpoints); empty if they’re already closer than spacing.

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_a and anchor_b must both sit on the same loop (found via the grid graph’s 2-core, same as check_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 empty changed_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_a and point_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_a and point_b should 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_xy is 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, with n_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 n points.

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 if len(a) <= n, otherwise the same random n indices 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 with open_stent_to_plane(), skeletonises it (either a single pass with compute_skeleton_2d(), or an auto-tuned search over pixels_per_strut / dilate_px via tune_skeleton_params()), then trims the result back down to the ring’s own z-band. Detected bad connections are auto-cleaned with auto_clean_bad_connections_2d(). Writes per-ring plots and a CSV under output_dir/skeleton_plots/.

Parameters:
  • stent_df – Stent point cloud with a ring_id column (from detect_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_plots subfolder).

  • auto_tune – Search for the best skeletonisation parameters per ring, instead of using pixels_per_strut / dilate_px directly.

  • 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 by ring_XX label) 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_2d plus the scalars needed to rebuild it, into ring_2d.pkl. Together with the already-saved ring_points.csv, this lets load_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.pkl is 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 (from save_ring_2d_checkpoint()) and ring_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.pkl and ring_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 s does 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 (loop or connection), give the two point indices at the defect, and the matching fixer runs (fix_ring_loop_2d() or fix_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’s pixel_size, since different rings may have been skeletonised at different resolutions.

Parameters:

ring_2d – Per-ring 2D skeletons, from skeletonize_rings_2d() (optionally edited by edit_rings_2d_interactive()).

Returns:

Dict with the concatenated coordinates (skel_arc, skel_z), the per-point pixel size (skel_px), and the median pixel_size across all rings.