Add optional rib-anchored vertebral identity stage (vertebrae_identity) - #11
Open
brijeshm2026-hash wants to merge 1 commit into
Open
Conversation
Vertebra predictions can delineate bone well while assigning level names
that are displaced, so that a run of vertebrae carries the identity of a
neighbouring level. This adds an optional stage that adjudicates level
identity from costovertebral rib attachment geometry before the configured
vertebrae_engine performs its usual shape cleanup.
The cue is external: rib attachment position is a physical measurement of a
structure outside the vertebral column, rather than an inference from the
vertebral predictions themselves, their ordering, spacing, connected
components or internal consistency.
Architecture:
- new config key vertebrae_identity (default 'none'), independent of
vertebrae_engine rather than a value of it, so the selected engine still
runs afterwards
- utils/vertebrae_rib_identity_engine.py holds the scientific core
- utils/vertebrae_rib_identity.py is the ShapeKit adapter: input
discovery, grid validation, label-space conversion, logging, fallbacks
Inputs are the case CT (via the existing ct_file_name / ct_root keys) and a
precomputed TotalSegmentator 'total' volume (via new rib_file_name /
rib_root keys). Only the geometry of the rib masks is read; TotalSegmentator
vertebral labels are not consulted. TotalSegmentator is not invoked and is
not added as a dependency.
Conservative by construction: a missing CT, a missing rib volume, an
incompatible voxel grid, fewer than four usable rib pairs, or no level
exceeding the correction threshold all leave the masks unchanged, log the
reason, and let the batch continue. The stage never raises into a worker.
vertebrae_identity: rib_anchor requires vertebrae_engine: shapekit_pro. The
combination with the default engine is rejected at startup, before any case
is processed, because that engine may reassign contiguous cranio-caudal
identities during cleanup and undo the correction. No existing ShapeKit
algorithm was modified.
Adds tests/test_vertebrae_rib_identity.py (64 tests, synthetic phantoms, no
imaging data required) and docs/vertebrae_rib_identity.md. No new runtime
dependencies.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an optional, default-off stage that adjudicates which vertebral level is which before the configured
vertebrae_engineruns. It is not a new engine and performs no shape cleanup of its own; the selected engine still runs afterwards.Enabled with a new config key:
Problem
Vertebra segmentation can delineate bone well while assigning level names that are displaced, so that a run of vertebrae carries the identity of a neighbouring level. The foreground voxels are present and locally plausible, but a block of levels is named one position off. Per-class Dice then collapses across that block while the anchored ends stay high.
This error can evade checks that operate only on the vertebral prediction. A column where every level is named one position low is internally self-consistent: ordering is monotonic, spacing is regular, components are clean, and voxel counts are unremarkable. Resolving it needs evidence from outside the vertebral labelling task.
Why ribs
Ribs supply that evidence. The thoracic spine typically corresponds to twelve numbered rib pairs, the lowest of which is borne by T12, and where a rib meets the spine is a physical measurement of a structure that is not part of the vertebral column.
The distinguishing property of this approach is the cue it uses: it reads an external anatomical structure rather than inferring vertebral identity solely from the vertebral predictions, their ordering, spacing, connected components, or internal anatomical consistency. This is a statement about the input it consumes, not a claim about the effectiveness of any other approach.
Costovertebral anatomy, as encoded:
Left and right ribs are measured independently and a pair whose sides disagree by more than 15 mm is discarded rather than trusted. The remaining pairs form a weighted least-squares system with a second-difference smoothness term, a soft tie to the model's own centroids weighted by how clean each level looks, and a strongly weighted lumbar anchor. The anchor is a heavily weighted row in the least-squares system rather than an exact equality constraint, so the lumbar end is held firmly but not fixed absolutely. Only levels whose solved position differs by more than 12 mm are relabelled, at boundaries placed on CT-detected disc troughs. A scan whose geometry already satisfies the rib constraints is left untouched.
Architecture
vertebrae_identityis deliberately a separate key rather than anothervertebrae_enginevalue. The stage does no shape cleanup, so selecting it as the engine would disable the cleanup ShapeKit already performs. Keeping the two orthogonal means the engine dispatch is untouched and the stage composes with whichever engine is configured.Two new modules, following the existing adapter/engine split:
utils/vertebrae_rib_identity_engine.py— the scientific core.utils/vertebrae_rib_identity.py— the ShapeKit adapter: input discovery, grid validation, label-space conversion, logging, QA record, and fallback handling. It contains no anatomical reasoning.Configuration
The rib volume is looked up the same way the CT already is:
<input_case>/<rib_file_name>, then<rib_root>/<case_id>/<rib_file_name>, then<rib_root>/<case_id>.nii.gz.Required inputs
segmentations/folderct_file_name/ct_rootrib_file_name/rib_roottotalmultilabel, same gridBoth extra inputs are precomputed. ShapeKit does not run TotalSegmentator, and TotalSegmentator is not added to
requirements.txt. Keeping it external avoids introducing a GPU inference dependency into a CPU post-processor, and pins the rib label convention to a file the operator controls rather than to an installed version.TotalSegmentator's vertebral labels are not used. Only the geometry of the 24 rib masks (ids 92–115) is read. The vertebral labels in that volume are never consulted and play no part in any decision.
Fallback behaviour
Every condition below leaves the masks exactly as they arrived, logs the reason, and lets processing continue. A case that cannot be adjudicated is not degraded, and a batch is not halted by one.
vertebrae_identity: noneThe stage never raises into a
Poolworker.Compatibility requirement
vertebrae_identity: rib_anchorrequiresvertebrae_engine: shapekit_pro. Combining it with the defaultshapekitengine is rejected at startup, before any case is processed, with a non-zero exit and an explanatory message.The reason is measurable. The default engine may reassign contiguous cranio-caudal identities during its own cleanup. When the identity stage has deliberately changed which level a body belongs to, that reassignment can restore the original labelling. On a synthetic shifted phantom where the identity stage relabels 38,400 voxels:
shapekit(default)shapekit_proA run that appeared to succeed while discarding the correction on every case would be worse than not running the stage at all, so the combination is refused rather than warned about. No existing ShapeKit algorithm was modified to accommodate this —
spine_adjacent_pairsand the rest of the default engine are untouched.This is a configuration check and is separate from the per-case conditions above. Missing inputs remain safe no-ops that let the batch continue; only a combination of settings that would discard the correction for every case stops the run.
Testing
tests/test_vertebrae_rib_identity.py— 64 tests, all passing. Everything runs on synthetic phantoms; no imaging data is required. The phantom is a column of uniform blocks at a known pitch with rib slabs at their exact costovertebral attachments, so the expected answer is known in closed form and faults can be injected deliberately.Coverage:
k_of_T) and the complete 24-level label mapping, cross-checked against bothvertebrae_postprocessing.all_labelsandconfig.yaml;shapekit_pro.Batch behaviour was exercised through
main.pywithmultiprocessing.Poolover six cases covering every input combination. All six completed; the four with deliberately absent or mismatched inputs logged a reason and were left unchanged.Regression against the reference implementation
The scientific core is vendored unchanged from the implementation the method was developed with: the module body is a byte-for-byte copy of that source, verifiable with a single
diff, so the refactor into ShapeKit provably did not alter the algorithm.Equivalence was then checked end to end. The reference implementation and the ShapeKit path were run on identical inputs, and the identity-corrected labels were compared voxel by voxel before any engine touched them:
Per-case reports match as well: the same ribs used, the same ribs excluded with the same left/right disagreements, the same levels corrected, and the same voxel counts. Comparison is on the anatomical arrays rather than the NIfTI files, since headers and gzip framing legitimately differ between writers.
Per-level displacements were also compared as raw IEEE-754 bit patterns between default and single-threaded BLAS execution and were bit-identical, so the correction set does not depend on threading configuration on the tested platform.
Deployment notes
requirements.txtis unmodified.--cpu_count.Known limitations
shapekit_pro. Any future engine is rejected until it has been verified to preserve corrected identities and added to the list.totalclass map (92–115); a future renumbering would require the table to be updated.Files
Added
utils/vertebrae_rib_identity_engine.py— scientific core, vendored unchangedutils/vertebrae_rib_identity.py— ShapeKit adaptertests/test_vertebrae_rib_identity.py— 64 testsdocs/vertebrae_rib_identity.md— method, inputs, configuration, fallbacks, engine requirementModified
main.py— import the stage and its validator, read four config keys, resolve the rib path beside the existing CT path, run the stage ahead of the engine branch, validate the configuration at startup. Additive only; no existing line changed.config.yaml— the new keys with an explanatory comment blockdocs/config.md— entries for the new keysNo other file is touched.