Atlas label transfer: labelling a scan from a labelled atlas¶
Reference scripts: atlas_label_transfer.py.
Atlas label transfer: from a labelled atlas to a segmented scan¶
The pipeline of PardesLine use case 01 (3-D ultrasound heart, 60 lines), made
general and runnable locally with scripts/atlas_label_transfer.py.
atlas (points + per-point label) scan (points, no labels, unknown pose)
| |
+---- RANSAC + ICP (atlas -> scan) --+ registration.ransac_icp_pipeline
|
aligned atlas, SAME point order -> labels still valid
|
per-structure KD-tree distances; each scan point takes the closest structure
|
per-label Poisson surface (density_quantile=0 -> closed) -> volume in mL
|
evaluation: pose error, label agreement, per-structure Dice, volume vs atlas
Step 1: registration (the scan is the fixed reference)¶
The atlas moves onto the scan so that the scan's coordinates (and its frame,
units, provenance) stay untouched. fixed.align(moving) keeps the moving
cloud's point order in the output, so the atlas labels index the aligned
points directly. Settings: voxel_size 2-5x the point spacing (auto when the
units are right), point-to-plane ICP, ransac_with_scaling=False unless the
atlas is in different units (then fix the units instead). See
point-cloud-registration for every parameter and failure mode. Expect
fitness 0.6-0.95 on partial, noisy ultrasound; inlier RMSE near the noise
floor (0.5-1 mm).
Step 2: nearest-structure label transfer¶
For each structure k, compute the distance from every scan point to the
aligned atlas points of structure k (one KD-tree per structure); assign
argmin_k. This differs from "label of the single nearest atlas point":
per-structure minima are robust to a few mislabelled atlas points and to
density differences. Reject scan points whose minimum distance exceeds a
tolerance (2-3x the noise, or the atlas point spacing) as unlabelled rather
than forcing a label; report their fraction.
Refinements when needed: majority vote in a k-neighbourhood of the scan
(smooths salt-and-pepper labels at boundaries); a non-rigid CPD step
(deformable_registration.cpd_deformable) after the rigid alignment when the
patient's anatomy differs in shape, with the folding check from
evaluation-metrics-3d.
Step 3: surfaces and volumes¶
Poisson reconstruction expects one closed surface: reconstruct each
structure separately (PointCloud.to_mesh(depth=8, density_quantile=0.0,
normal_radius=...)), never the nested chambers together (that produces a
tangle). density_quantile=0 keeps every vertex so the mesh stays closed and
its volume is meaningful; normal_radius must be in the data's units (mm:
3-6; the catalog default 0.1 is for metre-scale scenes). Volume = PyVista
mass_properties on a mesh with n_open_edges == 0; mm^3 / 1000 = mL.
Step 4: evaluation (what to report)¶
| Quantity | How | Meaning |
|---|---|---|
| Pose error | rotation (deg) and translation at the object centre against the true transform (synthetic or phantom) | the registration itself |
| Label agreement | fraction of scan points whose transferred label equals the label obtained with the true transform | the transfer, independent of the registration quality |
| Per-structure surface Dice / HD95 | between transferred and reference structures | where it fails (small chambers) |
| Volume | per structure, vs the atlas or a reference measurement | the clinically read number |
| Unlabelled fraction | points beyond the tolerance | honesty about partial scans |
"100 % label accuracy" on a scan generated from the atlas itself is a pipeline sanity check; on a real patient, expect 85-95 % at chamber boundaries and report where the 5-15 % sits.
Building an atlas with pardesline¶
A labelled atlas is a point cloud with a label per point. From a label map:
import pardesline as pl, numpy as np
s = pl.load("totalsegmentator_ct")[0] # 117 organ masks
atlas_points, atlas_labels = [], []
for k, organ in enumerate(["heart", "aorta", "lung_upper_lobe_left", "lung_upper_lobe_right"], start=1):
pts = s.get(organ).to_mesh(label=1).to_points(num_points=4000).to_numpy() # LPS mm
atlas_points.append(pts)
atlas_labels.append(np.full(len(pts), k))
atlas = np.vstack(atlas_points)
labels = np.concatenate(atlas_labels)
Or from msd_task02_heart (left atrium), amos22 (15 organs), MedShapeNet
STL meshes (medshapenet, one structure per file). Keep the atlas frame and
units with the arrays (write a .pardesline.json sidecar or a manifest role
with labels).
Pitfalls¶
- Units: an atlas in mm and a scan in m never register (fitness ~0). Check
frame.describe()on both. - Point order: any operation that reorders the aligned atlas (voxel downsampling, outlier removal) breaks the label mapping; transfer labels from the aligned output of the registration, which keeps the order, or carry labels as a colour attribute.
- Partial scans: fitness caps at the overlap; the unlabelled fraction tells the truth.
- Symmetric structures (two ventricles, paired kidneys) can swap: verify with a landmark or the volume ordering.
- Noise: outlier removal on the scan before registration
(
point_cloud_processing.statistical_outlier_removal) improves both stages.
Datasets to practise on¶
open3d_bunny (synthetic atlas, the script), medshapenet (pairs of the same
organ from different patients, ds.filter(structure="liver")),
totalsegmentator_ct / amos22 (build atlases), c3vd_endoscopy_splats
(points role: scene-to-scene transfer).
Source: skills/atlas-label-transfer/SKILL.md, one of the twenty skills served by pardesline mcp; published on 2026-10-14.