Skip to content

ENH: Per-organ tutorial parameters and distance-map registration fixes - #119

Open
aylward wants to merge 1 commit into
Project-MONAI:mainfrom
aylward:params_and_distance_maps
Open

ENH: Per-organ tutorial parameters and distance-map registration fixes#119
aylward wants to merge 1 commit into
Project-MONAI:mainfrom
aylward:params_and_distance_maps

Conversation

@aylward

@aylward aylward commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Introduce tutorials/parameters_lung_ct_dirlab.py and tutorials/parameters_heart_ct_kcl.py as the single source for each use case's mask dilation, distance-map saturation radius, PCA component counts, Greedy iteration schedule, segmenter class, and (heart only) the interior chamber label ids. Every tutorial that rasterizes or registers a distance map now reads the same values, so the maps a network is finetuned on match the maps it later infers over. No paths live in these modules; each tutorial keeps its own inputs and outputs.

Add tutorials/tutorial_02_heart_distancemap_finetune_icon.py, which finetunes uniGradICON on heart distance maps built from the Duke-Heart-4DLabelmaps labelmaps with the chambers excluded. The heart needs its own run rather than reusing the lung weights: its registration mask is much tighter, so its distance maps saturate over a shorter radius and do not share an intensity distribution with the lung ones.

Library fixes:

  • transform_tools.transform_image gains an explicit background_value. Resampling previously fell back to ITK's default of 0, which for CT is water, not air, so pre-warped moving images carried a false soft-tissue shell wherever they had no data. register_images_base now fills with -1000 HU for CT (exactly uniGradICON's window floor) on the image warp only; masks and labelmaps keep 0.
  • register_models_distance_maps composed the Greedy and ICON transforms in the wrong order. ITK's CompositeTransform applies back to front, so the residual must be added last in the forward and first in the inverse. Also drops two unconditional debug_*.nii.gz writes that crashed when mask_dilation_mm was 0.
  • register_from restores moving_image and clears moving_image_registered, so a registrar can be reused after an initialized run.
  • workflow_fit_statistical_model_to_patient grids the PCA field on a template-frame reference image rather than the patient image, and pads physically from spacing.
  • Default registrar for intensity registration switches from RegisterImagesGreedyICON/ICON to RegisterImagesGreedy in register_time_series_images and workflow_convert_image_to_usd. Distance-map registration keeps ICON, now with the finetuned weights.
  • convert_vtk_to_usd validates object names as USD identifiers and rejects duplicates; workflow_convert_vtk_to_usd wraps raw vtkDataSet.

Rename number_of_components / number_of_modes to
number_of_pca_components throughout the workflows and tutorials.

tutorial_02_lung_finetune_icon now writes difference images (fixed minus registered) instead of the resampled volumes, and reports the chain's Greedy-stage-only score as its own row. The chain remains unconditional: on DIR-Lab, ICON's 175^3 residual grid is about 1.4 mm over the FOV, coarser than the 1.10 mm Greedy already achieves, so it cannot refine and the tutorial reports that honestly.

tutorial_02_lung_distancemap_finetune_icon restricts its cached labelmaps to the lung labels. They previously held all 97 whole-body classes, and uniGradICON's Dice loss one-hots every shared class at 175^3 by batch 4, which saturated GPU memory.

Baselines for the slow and GPU buckets will need refreshing: the composition-order fix and the PCA field grid change alter registration output.

Summary by CodeRabbit

  • New Features

    • Added a heart distance-map fine-tuning tutorial with evaluation metrics and saved results.
    • Added configurable distance-map limits and image background handling for improved CT processing.
    • Added shared lung and heart tutorial configurations.
  • Documentation

    • Expanded the tutorial index to 17 runnable scripts, including lung and heart variants.
    • Documented the Duke Heart dataset’s unavailable status and expected layout.
  • Updates

    • Registration defaults now use CPU-based Greedy registration in applicable workflows and tutorials.
    • Clarified multi-stage registration results, distance-map normalization, and fallback behavior.

Introduce tutorials/parameters_lung_ct_dirlab.py and
tutorials/parameters_heart_ct_kcl.py as the single source for each use
case's mask dilation, distance-map saturation radius, PCA component
counts, Greedy iteration schedule, segmenter class, and (heart only) the
interior chamber label ids. Every tutorial that rasterizes or registers a
distance map now reads the same values, so the maps a network is
finetuned on match the maps it later infers over. No paths live in these
modules; each tutorial keeps its own inputs and outputs.

Add tutorials/tutorial_02_heart_distancemap_finetune_icon.py, which
finetunes uniGradICON on heart distance maps built from the
Duke-Heart-4DLabelmaps labelmaps with the chambers excluded. The heart
needs its own run rather than reusing the lung weights: its registration
mask is much tighter, so its distance maps saturate over a shorter radius
and do not share an intensity distribution with the lung ones.

Library fixes:

- transform_tools.transform_image gains an explicit background_value.
  Resampling previously fell back to ITK's default of 0, which for CT is
  water, not air, so pre-warped moving images carried a false soft-tissue
  shell wherever they had no data. register_images_base now fills with
  -1000 HU for CT (exactly uniGradICON's window floor) on the image warp
  only; masks and labelmaps keep 0.
- register_models_distance_maps composed the Greedy and ICON transforms
  in the wrong order. ITK's CompositeTransform applies back to front, so
  the residual must be added last in the forward and first in the
  inverse. Also drops two unconditional debug_*.nii.gz writes that
  crashed when mask_dilation_mm was 0.
- register_from restores moving_image and clears moving_image_registered,
  so a registrar can be reused after an initialized run.
- workflow_fit_statistical_model_to_patient grids the PCA field on a
  template-frame reference image rather than the patient image, and pads
  physically from spacing.
- Default registrar for intensity registration switches from
  RegisterImagesGreedyICON/ICON to RegisterImagesGreedy in
  register_time_series_images and workflow_convert_image_to_usd.
  Distance-map registration keeps ICON, now with the finetuned weights.
- convert_vtk_to_usd validates object names as USD identifiers and
  rejects duplicates; workflow_convert_vtk_to_usd wraps raw vtkDataSet.

Rename number_of_components / number_of_modes to
number_of_pca_components throughout the workflows and tutorials.

tutorial_02_lung_finetune_icon now writes difference images
(fixed minus registered) instead of the resampled volumes, and reports
the chain's Greedy-stage-only score as its own row. The chain remains
unconditional: on DIR-Lab, ICON's 175^3 residual grid is about 1.4 mm
over the FOV, coarser than the 1.10 mm Greedy already achieves, so it
cannot refine and the tutorial reports that honestly.

tutorial_02_lung_distancemap_finetune_icon restricts its cached
labelmaps to the lung labels. They previously held all 97
whole-body classes, and uniGradICON's Dice loss one-hots every shared
class at 175^3 by batch 4, which saturated GPU memory.

Baselines for the slow and GPU buckets will need refreshing: the
composition-order fix and the PCA field grid change alter registration
output.
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Walkthrough

The PR changes selected workflows and tutorials from ICON-based registration to Greedy registration, adds configurable image and distance-map handling, centralizes heart and lung parameters, and introduces a Duke heart distance-map finetuning tutorial.

Changes

Registration and tutorial workflows

Layer / File(s) Summary
Registration defaults and resampling
src/physiotwin4d/register_images_base.py, src/physiotwin4d/transform_tools.py, src/physiotwin4d/register_time_series_images.py, src/physiotwin4d/workflow_convert_image_to_usd.py, src/physiotwin4d/workflow_reconstruct_highres_4d_ct.py, src/physiotwin4d/register_images_chain.py, tests/*
Greedy registration becomes the default in selected workflows. Resampling supports configurable background values and preserves zero fill for masks and labelmaps.
Distance-map configuration
src/physiotwin4d/workflow_fit_statistical_model_to_patient.py, src/physiotwin4d/register_models_distance_maps.py, tutorials/parameters_heart_ct_kcl.py, tutorials/parameters_lung_ct_dirlab.py, pyproject.toml
Statistical-model fitting accepts configurable distance-map saturation. Shared immutable heart and lung parameter objects provide PCA, dilation, normalization, and iteration settings.
Heart distance-map finetuning
tutorials/tutorial_02_duke_heart_distancemap_finetune_icon.py, data/Duke-Heart-4DLabelmaps/*, tutorials/README.md, docs/tutorials.rst, tutorials/tutorial_07_heart_fit_statistical_model_to_patient.py
The heart tutorial creates distance maps, trains or loads ICON weights, evaluates registration metrics, and saves outputs. Dataset availability and fallback behavior are documented.
Tutorial registration migration
tutorials/tutorial_01_*, tutorials/tutorial_03_*, tutorials/tutorial_06_*, tutorials/tutorial_07_*, tutorials/tutorial_08_*, docs/tutorials.rst
Tutorials use direct Greedy registration where specified. PCA, distance-map, and weight settings use shared configuration.
Lung finetuning and diagnostics
tutorials/tutorial_02_lung_distancemap_finetune_icon.py, tutorials/tutorial_02_lung_finetune_icon.py
Lung labelmaps are filtered before caching. Registration evaluation adds iteration sweeps, chain diagnostics, coverage metrics, and shared image/labelmap warping.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Tutorial02Heart
  participant DukeHeartLabelmaps
  participant RegisterModelsDistanceMaps
  participant ICONWeights
  participant EvaluationOutputs
  Tutorial02Heart->>DukeHeartLabelmaps: discover labelmaps and landmarks
  DukeHeartLabelmaps-->>Tutorial02Heart: provide cases and evaluation inputs
  Tutorial02Heart->>RegisterModelsDistanceMaps: train or load distance-map model
  RegisterModelsDistanceMaps->>ICONWeights: save or validate checkpoint
  Tutorial02Heart->>EvaluationOutputs: write transformed labelmaps and metrics
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: per-organ tutorial parameters and distance-map registration fixes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.07692% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.20%. Comparing base (743938d) to head (a34fc1d).

Files with missing lines Patch % Lines
...win4d/workflow_fit_statistical_model_to_patient.py 42.85% 4 Missing ⚠️
src/physiotwin4d/register_images_base.py 75.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #119      +/-   ##
==========================================
+ Coverage   42.18%   42.20%   +0.01%     
==========================================
  Files          72       72              
  Lines        8742     8760      +18     
==========================================
+ Hits         3688     3697       +9     
- Misses       5054     5063       +9     
Flag Coverage Δ
integration-tests 42.00% <73.07%> (?)
unittests 42.20% <73.07%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/physiotwin4d/transform_tools.py (1)

452-475: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use modality-aware fill values for off-grid intensity resampling.

Keep background_value=0.0 for masks and labelmaps. Use _prewarp_background_value() where intensity data is resampled to avoid zero-filling CT data as water. Update RegisterImagesBase.get_registered_image() and the time-series reconstruction path, then add a regression test for off-grid CT fill values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiotwin4d/transform_tools.py` around lines 452 - 475, Update
RegisterImagesBase.get_registered_image() and the time-series reconstruction
path to pass _prewarp_background_value() for intensity-image resampling, while
preserving background_value=0.0 for masks and labelmaps. Add a regression test
verifying off-grid CT voxels receive the modality-appropriate fill value rather
than zero.

Source: Coding guidelines

🧹 Nitpick comments (3)
tutorials/parameters_heart_ct_kcl.py (1)

64-69: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Make the two interior-id fields use the same type.

interior_object_ids_totalsegmentator is Optional[list[int]] and interior_object_ids_simpleware is list[int]. Both always hold a list. The consumer parameter labelmap_interior_object_ids accepts Optional[list], so list[int] works for both. Drop the Optional for consistency, and then the Optional import becomes unused.

♻️ Proposed change
-    interior_object_ids_totalsegmentator: Optional[list[int]] = field(
+    interior_object_ids_totalsegmentator: list[int] = field(
         default_factory=lambda: [141, 142, 143, 144]
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tutorials/parameters_heart_ct_kcl.py` around lines 64 - 69, Update the
interior_object_ids_totalsegmentator field to use list[int], matching
interior_object_ids_simpleware and the labelmap_interior_object_ids consumer,
and remove the now-unused Optional import.
src/physiotwin4d/workflow_fit_statistical_model_to_patient.py (1)

248-248: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add distancemap_squared_max to the class Attributes docstring.

The class docstring at lines 68-110 lists the configurable state, including mask_dilation_mm. The new attribute is missing there. Add one line so readers find the knob and its default derivation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiotwin4d/workflow_fit_statistical_model_to_patient.py` at line 248,
Update the Attributes class docstring to document distancemap_squared_max
alongside the existing configurable state, including that its default is derived
as appropriate from the implementation. Leave the attribute declaration and
surrounding behavior unchanged.
tutorials/tutorial_02_duke_heart_distancemap_finetune_icon.py (1)

95-97: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider defaulting run_finetuning to False.

run_finetuning = True combined with lines 225-230 deletes experiment_dir on every run. tutorials/tutorial_07_heart_fit_statistical_model_to_patient.py reads the checkpoint from that tree, so a re-run of this tutorial discards the weights Tutorial 7 depends on before it retrains them. The sibling tutorial tutorials/tutorial_02_lung_finetune_icon.py documents the opposite default: finetuning is off so runs reuse the checkpoint. Align the two defaults, or state in the module docstring why the heart run always retrains.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tutorials/tutorial_02_duke_heart_distancemap_finetune_icon.py` around lines
95 - 97, Default the run_finetuning setting in the tutorial to False, matching
tutorials/tutorial_02_lung_finetune_icon.py so existing checkpoints are reused
and experiment_dir is not deleted on reruns; retain the True option for
explicitly requested retraining.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/api/registration/chained.rst`:
- Around line 12-13: Update the documentation for RegisterImagesGreedyICON to
remove the incorrect claim that Tutorial 2 uses it. Either name a verified
consumer of the chained registrar or describe it only as the Greedy-then-ICON
pairing, while preserving the accurate statistical-model fit reference.

In `@docs/tutorials.rst`:
- Line 14: Update the tutorial summary near “Ten numbered stages across 17
runnable Python scripts” to avoid claiming all scripts are runnable: use “17
Python scripts” or explicitly distinguish the 16 runnable scripts from the
dataset-gated tutorial_02_duke_heart_distancemap_finetune_icon.py.

In `@src/physiotwin4d/register_images_base.py`:
- Around line 137-169: Add regression tests covering all branches of
_prewarp_background_value: an explicit override, CT returning -1000.0, and
non-CT using the moving image minimum. Add nearest-neighbor mask and labelmap
cases that verify zero fill, and assert the actual off-grid voxel value rather
than only output shape.

In `@src/physiotwin4d/transform_tools.py`:
- Around line 520-535: Update the background-value conversion in the resampling
flow before itk.resample_image_filter: for integer and discrete types, round to
an integer, validate it against np.iinfo(dtype), and reject out-of-range values
before invoking ITK; include boolean types via np.issubdtype(dtype, np.bool_).
Preserve floating-point conversion for non-discrete pixel types.

In `@src/physiotwin4d/workflow_reconstruct_highres_4d_ct.py`:
- Line 58: Update the class and constructor docstrings in
src/physiotwin4d/workflow_reconstruct_highres_4d_ct.py to consistently document
the standalone RegisterImagesGreedy backend and remove references to the retired
combined backend. In docs/tutorials.rst lines 313-321, remove Tutorial 3 from
Tutorial 2’s weight dependency; in lines 658-660, state that Tutorial 2 is
optional when stock weights are acceptable.

In `@tests/test_workflow_convert_image_to_usd.py`:
- Around line 121-122: Extend the migration test around workflow.registrar after
setting iterations to validate registration semantics, not just the
RegisterImagesGreedy type and artifact creation. Compare the registered image or
contours against an appropriate TestTools baseline, or assert a near-identity
result for the same-frame input, ensuring incorrect transform direction or
composition fails the test.

In `@tutorials/tutorial_02_duke_heart_distancemap_finetune_icon.py`:
- Around line 276-289: Update read_landmarks to validate the loaded markups
metadata before using control points: assert that the file’s coordinateSystem
field is exactly “LPS”, and fail clearly otherwise. Preserve the existing point
extraction only after this validation.
- Around line 198-217: After the training-case loop, validate that the collected
cohort is non-empty before the existing “Finetuning cohort” log and before
constructing WorkflowFinetuneICONRegistration. Mirror the held-out validation
behavior and fail immediately when subject_distance_map_files (and corresponding
subject IDs/labels) contains no surviving cases.
- Around line 139-151: The distance-map cache in distance_map_for currently keys
files only by frame stem, causing collisions across case directories. Include
the case directory name when constructing distance_map_file, while preserving
the existing derived_dir location and cache lookup behavior.

In `@tutorials/tutorial_07_heart_fit_statistical_model_to_patient.py`:
- Around line 164-169: Update the set_use_pca_registration call in the pca_model
block to pass the configured HEART_CT_KCL.pca_components(test_mode) value as
number_of_pca_components, matching the heart PCA builder and lung fit tutorial
instead of relying on the default.

---

Outside diff comments:
In `@src/physiotwin4d/transform_tools.py`:
- Around line 452-475: Update RegisterImagesBase.get_registered_image() and the
time-series reconstruction path to pass _prewarp_background_value() for
intensity-image resampling, while preserving background_value=0.0 for masks and
labelmaps. Add a regression test verifying off-grid CT voxels receive the
modality-appropriate fill value rather than zero.

---

Nitpick comments:
In `@src/physiotwin4d/workflow_fit_statistical_model_to_patient.py`:
- Line 248: Update the Attributes class docstring to document
distancemap_squared_max alongside the existing configurable state, including
that its default is derived as appropriate from the implementation. Leave the
attribute declaration and surrounding behavior unchanged.

In `@tutorials/parameters_heart_ct_kcl.py`:
- Around line 64-69: Update the interior_object_ids_totalsegmentator field to
use list[int], matching interior_object_ids_simpleware and the
labelmap_interior_object_ids consumer, and remove the now-unused Optional
import.

In `@tutorials/tutorial_02_duke_heart_distancemap_finetune_icon.py`:
- Around line 95-97: Default the run_finetuning setting in the tutorial to
False, matching tutorials/tutorial_02_lung_finetune_icon.py so existing
checkpoints are reused and experiment_dir is not deleted on reruns; retain the
True option for explicitly requested retraining.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e3e3d426-db8d-4bc2-a2e9-01e6df24641a

📥 Commits

Reviewing files that changed from the base of the PR and between 743938d and a34fc1d.

📒 Files selected for processing (30)
  • data/Duke-Heart-4DLabelmaps/.gitignore
  • data/Duke-Heart-4DLabelmaps/README.md
  • docs/api/registration/chained.rst
  • docs/tutorials.rst
  • pyproject.toml
  • src/physiotwin4d/register_images_base.py
  • src/physiotwin4d/register_images_chain.py
  • src/physiotwin4d/register_models_distance_maps.py
  • src/physiotwin4d/register_time_series_images.py
  • src/physiotwin4d/transform_tools.py
  • src/physiotwin4d/workflow_convert_image_to_usd.py
  • src/physiotwin4d/workflow_fit_statistical_model_to_patient.py
  • src/physiotwin4d/workflow_reconstruct_highres_4d_ct.py
  • tests/test_workflow_convert_image_to_usd.py
  • tests/test_workflow_reconstruct_highres_4d_ct.py
  • tutorials/README.md
  • tutorials/parameters_heart_ct_kcl.py
  • tutorials/parameters_lung_ct_dirlab.py
  • tutorials/tutorial_01_heart_gated_ct_to_usd.py
  • tutorials/tutorial_01_lung_gated_ct_to_usd.py
  • tutorials/tutorial_02_duke_heart_distancemap_finetune_icon.py
  • tutorials/tutorial_02_lung_distancemap_finetune_icon.py
  • tutorials/tutorial_02_lung_finetune_icon.py
  • tutorials/tutorial_03_heart_reconstruct_highres_4d_ct.py
  • tutorials/tutorial_03_lung_reconstruct_highres_4d_ct.py
  • tutorials/tutorial_06_heart_create_statistical_model.py
  • tutorials/tutorial_06_lung_create_statistical_model.py
  • tutorials/tutorial_07_heart_fit_statistical_model_to_patient.py
  • tutorials/tutorial_07_lung_fit_statistical_model_to_patient.py
  • tutorials/tutorial_08_lung_fit_model_to_4d_patients.py

Comment on lines +12 to +13
``RegisterImagesGreedyICON`` is the Greedy-then-ICON pairing, used by Tutorial 2
and by the distance-map stage of the statistical-model fit.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching chained.rst:"
fd -a 'chained\.rst$' . || true

echo
echo "Target file excerpt:"
if [ -f docs/api/registration/chained.rst ]; then
  nl -ba docs/api/registration/chained.rst | sed -n '1,80p'
fi

echo
echo "Search for RegisterImagesGreedyICON/RegisterModelsDistanceMaps/Tutorial 2:"
rg -n "RegisterImagesGreedyICON|RegisterModelsDistanceMaps|RegisterImagesGreedy|RegisterImagesICON|Tutorial 2|Tutorial.*2|distance-map|distance map" . || true

Repository: Project-MONAI/physiotwin4d

Length of output: 308


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files relevant to registration:"
git ls-files | rg 'registration|tutorial|models|fit|distance' || true

echo
echo "All definitions/usages with some context:"
rg -n "class RegisterImagesGreedyICON|RegisterImagesGreedyICON\s*=|class RegisterModelsDistanceMaps|RegisterModelsDistanceMaps\s*=|RegisterImagesGreedy|RegisterImagesICON" . || true

echo
echo "Candidate files by name:"
git ls-files | rg 'registration|tutorial|statistical|model|distance' | head -200 || true

Repository: Project-MONAI/physiotwin4d

Length of output: 40451


Correct the documented consumers of RegisterImagesGreedyICON.

Tutorial 2 distance-map stages use separate RegisterImagesGreedy and RegisterImagesICON instances; they do not use RegisterImagesGreedyICON. Replace the Tutorial 2 claim with a consumer that actually uses the chained registrar, or keep the wording as the documented Greedy-then-ICON pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/api/registration/chained.rst` around lines 12 - 13, Update the
documentation for RegisterImagesGreedyICON to remove the incorrect claim that
Tutorial 2 uses it. Either name a verified consumer of the chained registrar or
describe it only as the Greedy-then-ICON pairing, while preserving the accurate
statistical-model fit reference.

Comment thread docs/tutorials.rst
Comment on lines +137 to +169
def set_prewarp_background_value(self, background_value: float) -> None:
"""Override the value a seeded registration's pre-warp writes off-grid.

Args:
background_value: Intensity written where the fixed grid samples
outside the moving image. Leave unset to derive it from the
modality; see :meth:`_prewarp_background_value`.
"""
self.prewarp_background_value = background_value

def _prewarp_background_value(self, moving_image: itk.Image) -> float:
"""Return the intensity that means "no tissue" for the moving image.

Pre-warping onto the fixed grid samples outside the moving image
wherever the two extents disagree. ITK's default fill of 0 is wrong for
an intensity image: in CT it is water, so the filled region reads as
soft tissue rather than air and any downstream similarity metric treats
it as structure to match. -1000 HU is also uniGradICON's ``ct_window``
lower bound, so it normalizes to exactly the same value as true air.

Args:
moving_image: Image being pre-warped.

Returns:
The explicit override when set, -1000.0 for CT, otherwise the
moving image's own minimum intensity.
"""
if self.prewarp_background_value is not None:
return self.prewarp_background_value
if self.modality == "ct":
return -1000.0
return float(np.min(itk.GetArrayViewFromImage(moving_image)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add regression tests for all pre-warp fill branches.

Cover the explicit override, CT -1000.0, and non-CT minimum branches. Also verify zero fill for nearest-neighbor masks and labelmaps. Assert an off-grid voxel value, not only the output shape.

As per coding guidelines: “For behavior changes and bug fixes, define verifiable success criteria and add or update tests that reproduce invalid inputs or regressions before implementation.”

Also applies to: 499-499

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiotwin4d/register_images_base.py` around lines 137 - 169, Add
regression tests covering all branches of _prewarp_background_value: an explicit
override, CT returning -1000.0, and non-CT using the moving image minimum. Add
nearest-neighbor mask and labelmap cases that verify zero fill, and assert the
actual off-grid voxel value rather than only output shape.

Source: Coding guidelines

Comment on lines +520 to +535
# ITK's wrapping types DefaultPixelValue to the image's pixel type, and
# rejects a Python float for an integer image.
dtype = itk.GetArrayViewFromImage(img).dtype
default_pixel_value: Union[int, float] = (
int(round(background_value))
if np.issubdtype(dtype, np.integer)
else float(background_value)
)

img_reg = itk.resample_image_filter(
Input=img,
Transform=dftfm,
Interpolator=interpolator,
ReferenceImage=reference_image,
UseReferenceImage=True,
DefaultPixelValue=default_pixel_value,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | grep -F 'src/physiotwin4d/transform_tools.py' || true

echo "== relevant source =="
sed -n '480,555p' src/physiotwin4d/transform_tools.py | cat -n

echo "== function outline around class/method =="
ast-grep outline src/physiotwin4d/transform_tools.py --match transform_tools --view compact || true
rg -n "def .*|class .*|DefaultPixelValue|background_value|resample_image_filter|Transform" src/physiotwin4d/transform_tools.py

echo "== package itk version / dependency hints =="
for f in pyproject.toml requirements.txt requirements/*.txt setup.py setup.cfg; do
  [ -f "$f" ] && echo "--- $f ---" && sed -n '1,220p' "$f"
done

Repository: Project-MONAI/physiotwin4d

Length of output: 17284


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== all background_value call sites =="
rg -n "transform_image\(|background_value\s*=" -S .

echo "== transform_image call sites with context =="
while IFS= read -r line; do
  file=$(echo "$line" | cut -d: -f1)
  n=$(echo "$line" | cut -d: -f2)
  echo "--- $file:$n ---"
  sed -n "$((n-8)),$((n+8))p" "$file" 2>/dev/null | cat -n
done < <(rg -n "background_value\s*=" -S .)

echo "== transform_image signature/docs =="
sed -n '440,492p' src/physiotwin4d/transform_tools.py | cat -n

echo "== ITK availability/version if present =="
python3 - <<'PY'
import importlib.util
spec = importlib.util.find_spec("itk")
print("itk_available", bool(spec), "itk_location", getattr(spec, "origin", None))
if spec:
    import itk
    print("itk_version", getattr(itk, "__version__", None))
PY

echo "== read-only probe of integer conversion and numpy iinfo behavior =="
python3 - <<'PY'
import numpy as np

for dtype_str, value in [("uint8", -1000.0), ("uint8", 350.0), ("uint16", "value"), ("uint8", True)]:
    dtype = np.dtype(dtype_str)
    rounded = int(round(value))
    limits = np.iinfo(dtype)
    print(f"{dtype_str} value={value!r} rounded={rounded} in_range={limits.min <= rounded <= limits.max}")
PY

echo "== inspect resample filter implementation docs via itk package if present =="
python3 - <<'PY'
import importlib.util
spec = importlib.util.find_spec("itk")
if not spec:
    raise SystemExit
import itk
for name in ["_ImageIOHelper", "resample_image_filter"]:
    obj = getattr(itk, name, None)
    print(name, obj, type(obj))
    print("has __doc__", bool(getattr(obj, "__doc__")))
    doc = getattr(obj, "__doc__", "") or ""
    lines = doc.splitlines()[:40]
    for line in lines:
        if line.strip().lower().startswith(("default", "pixel", "value", "arg")):
            print(line)
PY

Repository: Project-MONAI/physiotwin4d

Length of output: 8434


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== focused call sites =="
for f in \
  src/physiotwin4d/register_images_base.py \
  src/physiotwin4d/workflow_fit_statistical_model_to_patient.py \
  tutorials/tutorial_02_lung_finetune_icon.py \
  tests/test_transform_tools.py
do
  [ -f "$f" ] || continue
  echo "--- $f ---"
  grep -n "transform_image\s*(\|background_value\s*=" "$f" || true
done

echo "== register prewarp/distance-map call sites =="
sed -n '470,505p' src/physiotwin4d/register_images_base.py | cat -n
sed -n '730,765p' src/physiotwin4d/workflow_fit_statistical_model_to_patient.py | cat -n

echo "== tests around background and integer images =="
sed -n '40,190p' tests/test_transform_tools.py | cat -n
sed -n '685,735p' tests/test_transform_tools.py | cat -n
rg -n "uint8|uint16|int8|neighbour|nearest|distance|labelmap|background_value" tests src -S | head -n 200

echo "== python probe conversion and numpy iinfo =="
python3 - <<'PY'
import numpy as np

for dtype_str, value in [("uint8", -1000.0), ("uint8", 350.0), ("uint8", True), ("uint16", -999999.0)]:
    dtype = np.dtype(dtype_str)
    rounded = int(round(value))
    limits = np.iinfo(dtype)
    print(f"{dtype_str} value={value!r} rounded={rounded} in_range={limits.min <= rounded <= limits.max}")
PY

echo "== locate and inspect transform method implementation =="
rg -n "    def transform_image\(" src/physiotwin4d/transform_tools.py
sed -n '446,540p' src/physiotwin4d/transform_tools.py | cat -n

Repository: Project-MONAI/physiotwin4d

Length of output: 35765


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== prewarp background resolver =="
sed -n '147,166p' src/physiotwin4d/register_images_base.py | cat -n

echo "== transform_image implementation =="
sed -n '446,540p' src/physiotwin4d/transform_tools.py | cat -n

echo "== focused examples with padded CT and transformed labelmap =="
sed -n '730,765p' src/physiotwin4d/workflow_fit_statistical_model_to_patient.py | cat -n
sed -n '358,370p' tutorials/tutorial_02_lung_finetune_icon.py | cat -n

echo "== exact Python behavior of background_value conversion =="
python3 - <<'PY'
import numpy as np

for dtype_str, value in [("uint8", -1000.0), ("uint8", 350.0), ("uint8", True), ("uint16", -999999.0), ("uint16", 65535.9)]:
    dtype = np.dtype(dtype_str)
    rounded = int(round(value))
    limits = np.iinfo(dtype)
    print(f"{dtype_str} value={value!r} rounded={rounded!r} dtype(rounded)={type(rounded).__name__} in_range={limits.min <= rounded <= limits.max}")
PY

Repository: Project-MONAI/physiotwin4d

Length of output: 8925


🌐 Web query:

ITK Python itk image default_pixel_value unsigned integer overflow resample_image_filter DefaultPixelValue

💡 Result:

In ITK and SimpleITK, the ResampleImageFilter uses the DefaultPixelValue to fill regions sampled outside the input image domain [1][2][3]. While the filter interface often accepts a generic type (such as double in SimpleITK [3] or the specific PixelType in C++ ITK [4]), this value is internally cast to the pixel type of the output image [5][6]. Regarding unsigned integer overflow, ITK does not perform automatic bounds checking or clamping of the DefaultPixelValue to the range of the output pixel type by default [5][6]. If you provide a DefaultPixelValue that is outside the representable range of the output image's pixel type (e.g., a negative number or a value exceeding the maximum of an unsigned integer), the value will undergo standard C++ static_cast behavior, which can result in integer overflow or wrapping [5][6]. Key technical considerations: 1. Casting Behavior: The filter casts the DefaultPixelValue to the output PixelType [5][6]. For example, if your output image is unsigned 8-bit integer (sitkUInt8), and you set a DefaultPixelValue of -1, the resulting value will typically wrap around to 255. 2. Best Practices: To avoid unexpected overflow, always ensure the DefaultPixelValue is within the valid range of your target image pixel type (e.g., for 8-bit unsigned) [7]. 3. SimpleITK vs. ITK: SimpleITK provides a simplified interface where DefaultPixelValue is handled as a double [3], but the underlying casting to the target pixel type remains subject to the same potential for overflow if the value is incompatible with that type. If you require specific behavior (such as clamping), you must manually ensure the DefaultPixelValue is within the appropriate bounds before passing it to the filter [7].

Citations:


🌐 Web query:

ITK 5.3.0 ResampleImageFilter Python DefaultPixelValue cast output pixel type static_cast

💡 Result:

In ITK 5.3.0, the ResampleImageFilter uses the DefaultPixelValue to fill pixels that fall outside the input image domain during resampling [1][2]. Regarding your technical query: 1. Pixel Type and Casting: The DefaultPixelValue is stored as the PixelType of the output image [1][3]. When you set this value, you are providing a value that must be compatible with the output image's pixel type. In the C++ implementation, this is managed by the template parameter TOutputImage [1][4]. Because the filter is templated, the value assigned to m_DefaultPixelValue is inherently constrained by the OutputImageType's PixelType [3][2]. 2. Python Context: When using the ITK Python wrapping, you generally do not need to perform manual static_cast calls yourself. The Python wrapping layer handles the conversion of native Python types (like int or float) into the appropriate C++ ITK PixelType automatically when you call SetDefaultPixelValue [5][6]. 3. Internal Mechanics: Internally, ITK does handle pixel conversion. For example, historical ITK versions have included a CastPixelWithBoundsChecking method [2][7][8] to handle the conversion of interpolated values to the output pixel type, ensuring the data is properly cast for the output image. If the value you provide to DefaultPixelValue is incompatible or requires specific casting, the ITK wrapping system or the underlying C++ template instantiation will typically handle the necessary assignment based on the output pixel type defined in your filter instantiation. If you encounter issues where the DefaultPixelValue is not being interpreted as expected, ensure that your output image type is explicitly defined (e.g., as unsigned char, float, etc.) and that the value passed to the filter is compatible with that specific type [5][4]. In Python, you can explicitly cast the value before setting it if you are concerned about precision or truncation, but this is usually handled by the ITK wrapper's type conversion system [5].

Citations:


🌐 Web query:

ITK source itkResampleImageFilter.hxx SetDefaultPixelValue static_cast or bounds checking

💡 Result:

In the ITK (Insight Segmentation and Registration Toolkit) ResampleImageFilter class, the SetDefaultPixelValue method is implemented via the standard ITK macro system [1][2]. Specifically, it uses itkSetMacro(DefaultPixelValue, PixelType), which generates a standard setter that assigns the provided argument directly to the m_DefaultPixelValue member variable [1][3][2]. The static_cast and bounds checking functionality you are inquiring about is not part of the SetDefaultPixelValue method itself [1][2]. Instead, that logic is located in the CastPixelWithBoundsChecking method (and related static helper functions like CastComponentWithBoundsChecking) within the ResampleImageFilter class [2][4][5]. This specialized mechanism is used internally during the image resampling process—specifically when interpolating pixel values and ensuring they are valid for the output pixel type—rather than for setting the default pixel value [2][5]. In summary: 1. SetDefaultPixelValue simply updates the member variable used when a transformed pixel falls outside the input image domain [6][7]. 2. CastPixelWithBoundsChecking is a separate internal utility method used to handle value conversions and clamping during the actual resampling execution [2][5].

Citations:


🌐 Web query:

site:discourse.itk.org ITK ResampleAnImage DefaultPixelValue unsigned char overflow

💡 Result:

In ITK and SimpleITK, the ResampleImageFilter relies on the DefaultPixelValue parameter to fill regions where the resampling grid maps to an area outside the input image domain [1][2]. When your output image pixel type is unsigned char (which has a range of 0–255), setting a DefaultPixelValue that falls outside this range will cause numerical overflow [3][4]. If you pass a value that is negative (e.g., -1000) or greater than 255 to an unsigned char image, the value will wrap around according to standard C++ integer overflow rules (often resulting in unexpected, low-intensity pixels) [3][4]. To resolve or prevent this behavior: 1. Ensure the DefaultPixelValue is within the valid range of your output image pixel type (e.g., for unsigned char). 2. If your data requires values outside the 0–255 range, change your output pixel type to a wider format such as float, int, or short (depending on your data requirements) before running the resampler [3][5]. 3. If you are resampling labels or mask images, ensure the DefaultPixelValue is set to a valid label value (e.g., 0 for background) [6][7]. 4. For pipelines involving floating-point calculations during interpolation, consider using a floating-point output type for the resampler, followed by a casting or rescaling filter to map the values back to unsigned char if necessary [3][8]. In SimpleITK, you can explicitly set the output pixel type when calling Execute or by using the SetOutputPixelType method to match your intended data range, which helps avoid implicit overflow issues during the resampling process [5][2].

Citations:


Reject out-of-range integer background fills before resampling.

background_value=-1000.0 is valid for CT, but it becomes -1000 when converted for an unsigned integer image. ITK treats DefaultPixelValue as a member assignment then casts to the filter output pixel type, so unsigned fills can wrap and miss the intended air/background value. Validate the rounded integer against np.iinfo(dtype) before itk.resample_image_filter, and extend this to discrete pixel types such as np.issubdtype(dtype, np.bool_).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiotwin4d/transform_tools.py` around lines 520 - 535, Update the
background-value conversion in the resampling flow before
itk.resample_image_filter: for integer and discrete types, round to an integer,
validate it against np.iinfo(dtype), and reject out-of-range values before
invoking ITK; include boolean types via np.issubdtype(dtype, np.bool_). Preserve
floating-point conversion for non-discrete pixel types.

parameters (iteration counts, etc.) on the instance before passing it
in. Defaults to a new :class:`RegisterImagesGreedyICON` (Greedy followed
by ICON refinement) when omitted.
in. Defaults to a new :class:`RegisterImagesGreedy` when omitted.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Synchronize public documentation after the registration migration.

The implementation now uses standalone RegisterImagesGreedy, and Tutorial 8 treats Tutorial 2 distance-map weights as optional. The remaining documentation still advertises the removed combined backend and obsolete prerequisites.

  • src/physiotwin4d/workflow_reconstruct_highres_4d_ct.py#L58-L58: update the class and constructor docstrings to describe RegisterImagesGreedy consistently.
  • docs/tutorials.rst#L313-L321: remove Tutorial 3 from the Tutorial 2 weight dependency.
  • docs/tutorials.rst#L658-L660: update the Tutorial 8 run order so Tutorial 2 is optional when stock weights are acceptable.

As per coding guidelines, changed public docstrings must remain factual.

📍 Affects 2 files
  • src/physiotwin4d/workflow_reconstruct_highres_4d_ct.py#L58-L58 (this comment)
  • docs/tutorials.rst#L313-L321
  • docs/tutorials.rst#L658-L660
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiotwin4d/workflow_reconstruct_highres_4d_ct.py` at line 58, Update
the class and constructor docstrings in
src/physiotwin4d/workflow_reconstruct_highres_4d_ct.py to consistently document
the standalone RegisterImagesGreedy backend and remove references to the retired
combined backend. In docs/tutorials.rst lines 313-321, remove Tutorial 3 from
Tutorial 2’s weight dependency; in lines 658-660, state that Tutorial 2 is
optional when stock weights are acceptable.

Source: Coding guidelines

Comment on lines +121 to +122
assert isinstance(workflow.registrar, RegisterImagesGreedy)
workflow.registrar.set_number_of_iterations([2])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Add a semantic registration assertion to this migration test.

The test verifies the Greedy type and artifact creation, but it does not verify registration correctness. Because the backend and transform composition changed, an incorrect transform direction can still pass. Compare the registered image or contours with a TestTools baseline, or assert a near-identity result for the same-frame input.

As per coding guidelines: “When a test produces an image or surface, compare it with a baseline using utilities such as TestTools.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_workflow_convert_image_to_usd.py` around lines 121 - 122, Extend
the migration test around workflow.registrar after setting iterations to
validate registration semantics, not just the RegisterImagesGreedy type and
artifact creation. Compare the registered image or contours against an
appropriate TestTools baseline, or assert a near-identity result for the
same-frame input, ensuring incorrect transform direction or composition fails
the test.

Source: Coding guidelines

Comment on lines +139 to +151
def distance_map_for(labelmap_file: Path) -> Path:
"""Rasterize one frame's heart distance map, caching it under derived_dir.

Mirrors ``RegisterModelsDistanceMaps._create_masks_from_models`` so the
finetuning inputs match what that class feeds ICON at inference: a
signed squared distance to the heart surface, normalized to [-1, 1] by
``distance_squared_max``, then multiplied by 1000 to fill the
[-1000, 1000] window uniGradICON's CT preprocessing expects.
"""
stem = labelmap_file.name[: -len("_labelmap.nii.gz")]
distance_map_file = derived_dir / f"{stem}_distance_map.mha"
if distance_map_file.exists():
return distance_map_file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Look for Duke-Heart-4DLabelmaps frame naming conventions in the repo.
rg -n --hidden -C3 'Duke-Heart-4DLabelmaps|_labelmap\.nii\.gz|_landmark\.mrk\.json'

Repository: Project-MONAI/physiotwin4d

Length of output: 164


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the referenced tutorial around the distance-map cache code.
file="$(git ls-files | grep -F 'tutorials/tutorial_02_duke_heart_distancemap_finetune_icon.py' || true)"
if [ -z "$file" ]; then
  echo "Referenced tutorial file not found"
  exit 0
fi

echo "== file =="
echo "$file"
echo "== outline =="
ast-grep outline "$file" --view compact || true
echo "== relevant lines 90-190 =="
sed -n '90,190p' "$file" | nl -ba -v90

# Search repository for related identifiers/datasets without assuming matches.
echo "== repo references =="
rg -n --hidden -C2 'duke|Duke|heart|distance_map|_labelmap\.nii\.gz|_landmark\.mrk\.json|4DLabelmaps|derived_dir|case_dirs|frames_for_case' . || true

# Inspect referenced class code if present in repository.
echo "== class/model references =="
rg -n --hidden -C3 'class RegisterModelsDistanceMaps|def _create_masks_from_models|distance_squared_max|itk\.GetArrayViewFromImage\(distance_map\)' . || true

Repository: Project-MONAI/physiotwin4d

Length of output: 471


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="tutorials/tutorial_02_duke_heart_distancemap_finetune_icon.py"

echo "== tutorial lines 90-190 =="
python3 - <<'PY'
from pathlib import Path
p = Path("tutorials/tutorial_02_duke_heart_distancemap_finetune_icon.py")
text = p.read_text()
lines = text.splitlines()
for i, line in enumerate(lines, start=1):
    if 90 <= i <= 190:
        print(f"{i:04d}: {line}")
PY

echo "== cache path/source references =="
rg -n --hidden -C2 'distance_map_for|derived_dir|labelmap_file|_labelmap\.nii\.gz|case_dirs|frames_for_case|companion|labelmap' "$file" . || true

echo "== related tutorial references =="
rg -n --hidden -C2 'distance_map|DistanceMap|distance_squared_max|itk\.GetArrayViewFromImage\(distance_map\)|_labelmap\.nii\.gz' tutorials . || true

Repository: Project-MONAI/physiotwin4d

Length of output: 50383


Key the distance-map cache by case, not by frame stem only.

derived_dir is flat, but each case directory uses the same frame labels (frame_*.nii.gz). A shared stem for a gated frame collides across cases and writes into the same *_distance_map.mha file. Include the case directory name in distance_map_file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tutorials/tutorial_02_duke_heart_distancemap_finetune_icon.py` around lines
139 - 151, The distance-map cache in distance_map_for currently keys files only
by frame stem, causing collisions across case directories. Include the case
directory name when constructing distance_map_file, while preserving the
existing derived_dir location and cache lookup behavior.

Comment on lines +198 to +217
for case_dir in training_dirs:
frames = frames_for_case(case_dir)
if len(frames) < 2:
reporter.log_warning(
"Case %s has %d frame(s); skipping (paired training needs 2+)",
case_dir.name,
len(frames),
)
continue
subject_ids.append(case_dir.name)
subject_distance_map_files.append(
[str(distance_map_for(frame)) for frame in frames]
)
subject_labelmap_files.append([str(frame) for frame in frames])
reporter.log_info(
"Finetuning cohort: %d cases, %d frames (held out %s)",
len(subject_ids),
sum(len(files) for files in subject_distance_map_files),
held_out_dir.name,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Fail early when no training case survives the frame filter.

The guard at line 124 only requires two case directories. If every training case has fewer than two frames, the loop skips them all and subject_distance_map_files is empty. WorkflowFinetuneICONRegistration then receives an empty cohort and fails later, inside the finetuning subprocess. Add a check after the loop that mirrors the held-out check at lines 185-189.

🛡️ Proposed fix
+    if not subject_ids:
+        raise FileNotFoundError(
+            f"No training case under {data_dir} has the 2+ frames paired "
+            "finetuning needs."
+        )
     reporter.log_info(
         "Finetuning cohort: %d cases, %d frames (held out %s)",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for case_dir in training_dirs:
frames = frames_for_case(case_dir)
if len(frames) < 2:
reporter.log_warning(
"Case %s has %d frame(s); skipping (paired training needs 2+)",
case_dir.name,
len(frames),
)
continue
subject_ids.append(case_dir.name)
subject_distance_map_files.append(
[str(distance_map_for(frame)) for frame in frames]
)
subject_labelmap_files.append([str(frame) for frame in frames])
reporter.log_info(
"Finetuning cohort: %d cases, %d frames (held out %s)",
len(subject_ids),
sum(len(files) for files in subject_distance_map_files),
held_out_dir.name,
)
for case_dir in training_dirs:
frames = frames_for_case(case_dir)
if len(frames) < 2:
reporter.log_warning(
"Case %s has %d frame(s); skipping (paired training needs 2+)",
case_dir.name,
len(frames),
)
continue
subject_ids.append(case_dir.name)
subject_distance_map_files.append(
[str(distance_map_for(frame)) for frame in frames]
)
subject_labelmap_files.append([str(frame) for frame in frames])
if not subject_ids:
raise FileNotFoundError(
f"No training case under {data_dir} has the 2+ frames paired "
"finetuning needs."
)
reporter.log_info(
"Finetuning cohort: %d cases, %d frames (held out %s)",
len(subject_ids),
sum(len(files) for files in subject_distance_map_files),
held_out_dir.name,
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tutorials/tutorial_02_duke_heart_distancemap_finetune_icon.py` around lines
198 - 217, After the training-case loop, validate that the collected cohort is
non-empty before the existing “Finetuning cohort” log and before constructing
WorkflowFinetuneICONRegistration. Mirror the held-out validation behavior and
fail immediately when subject_distance_map_files (and corresponding subject
IDs/labels) contains no surviving cases.

Comment on lines +276 to +289
def read_landmarks(labelmap_file: Path) -> dict[str, np.ndarray]:
"""Read a frame's Slicer markups file as ``{label: LPS point}``.

The markups files declare ``coordinateSystem: LPS``, the frame this
project works in, so the control points are used as written.
"""
landmark_file = companion(labelmap_file, "_landmark.mrk.json")
with landmark_file.open(encoding="utf-8") as f:
markups = json.load(f)["markups"]
return {
point["label"]: np.asarray(point["position"], dtype=np.float64)
for markup in markups
for point in markup["controlPoints"]
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Enforce the LPS assumption instead of documenting it.

The docstring states that the markups files declare coordinateSystem: LPS, and the code uses the control points as written. The code never reads that field. 3D Slicer writes RAS in its default configuration. If one frame's file is RAS, the X and Y signs flip and every TRE number in the summary is wrong, with no error. Assert the field.

🛡️ Proposed fix
         landmark_file = companion(labelmap_file, "_landmark.mrk.json")
         with landmark_file.open(encoding="utf-8") as f:
             markups = json.load(f)["markups"]
+        for markup in markups:
+            if markup.get("coordinateSystem") != "LPS":
+                raise ValueError(
+                    f"{landmark_file} declares coordinateSystem "
+                    f"{markup.get('coordinateSystem')!r}; this project works "
+                    "in LPS."
+                )
         return {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def read_landmarks(labelmap_file: Path) -> dict[str, np.ndarray]:
"""Read a frame's Slicer markups file as ``{label: LPS point}``.
The markups files declare ``coordinateSystem: LPS``, the frame this
project works in, so the control points are used as written.
"""
landmark_file = companion(labelmap_file, "_landmark.mrk.json")
with landmark_file.open(encoding="utf-8") as f:
markups = json.load(f)["markups"]
return {
point["label"]: np.asarray(point["position"], dtype=np.float64)
for markup in markups
for point in markup["controlPoints"]
}
def read_landmarks(labelmap_file: Path) -> dict[str, np.ndarray]:
"""Read a frame's Slicer markups file as ``{label: LPS point}``.
The markups files declare ``coordinateSystem: LPS``, the frame this
project works in, so the control points are used as written.
"""
landmark_file = companion(labelmap_file, "_landmark.mrk.json")
with landmark_file.open(encoding="utf-8") as f:
markups = json.load(f)["markups"]
for markup in markups:
if markup.get("coordinateSystem") != "LPS":
raise ValueError(
f"{landmark_file} declares coordinateSystem "
f"{markup.get('coordinateSystem')!r}; this project works "
"in LPS."
)
return {
point["label"]: np.asarray(point["position"], dtype=np.float64)
for markup in markups
for point in markup["controlPoints"]
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tutorials/tutorial_02_duke_heart_distancemap_finetune_icon.py` around lines
276 - 289, Update read_landmarks to validate the loaded markups metadata before
using control points: assert that the file’s coordinateSystem field is exactly
“LPS”, and fail clearly otherwise. Preserve the existing point extraction only
after this validation.

Comment on lines 164 to 169
if pca_model is not None:
workflow.set_use_pca_registration(
use_pca_registration=True,
pca_model=pca_model,
use_surface=False,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Compare PCA component wiring across the fit tutorials.
rg -n -C6 'set_use_pca_registration|pca_components\(|running_as_test' tutorials/

Repository: Project-MONAI/physiotwin4d

Length of output: 28363


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file imports and relevant sections =="
sed -n '1,80p' tutorials/tutorial_07_heart_fit_statistical_model_to_patient.py
sed -n '120,180p' tutorials/tutorial_07_heart_fit_statistical_model_to_patient.py

echo
echo "== search WorkflowFitStatisticalModelToPatient and set_use_pca_registration signatures =="
rg -n -C8 'class WorkflowFitStatisticalModelToPatient|def set_use_pca_registration|number_of_pca_components.*use_pca' examples src tests tutorials 2>/dev/null || true

echo
echo "== compare tutorial 07 heart with 07 lung top imports =="
sed -n '1,35p' tutorials/tutorial_07_heart_fit_statistical_model_to_patient.py
sed -n '1,35p' tutorials/tutorial_07_lung_fit_statistical_model_to_patient.py

Repository: Project-MONAI/physiotwin4d

Length of output: 11158


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== WorkflowFitStatisticalModelToPatient __init__ and set_use_pca_registration doc =="
sed -n '50,130p' src/physiotwin4d/workflow_fit_statistical_model_to_patient.py
sed -n '345,385p' src/physiotwin4d/workflow_fit_statistical_model_to_patient.py

echo
echo "== static Python check: absent argument gets default zero =="
python3 - <<'PY'
import ast
from pathlib import Path

node = ast.parse(Path("tutorials/tutorial_07_heart_fit_statistical_model_to_patient.py").read_text())
for func in [n for n in node.body if isinstance(n, ast.FunctionDef) and n.name == "__main__"]:
    if isinstance(node.body[node.body.index(func)-1], ast.ImportFrom):
        continue
print({
    "imports_HEART_CT_KCL": any(isinstance(im, ast.ImportFrom) for im in node.body if isinstance(im, ast.ImportFrom) and im.module == "parameters_heart_ct_kcl"),
    "imports_WorkflowFitStatisticalModelToPatient": any(isinstance(im, ast.ImportFrom) for im in node.body if isinstance(im, ast.ImportFrom) and any(t.name == "WorkflowFitStatisticalModelToPatient" for t in im.names)),
})

for node_ast in ast.walk(node):
    if getattr(node_ast, "attr", None) == "set_use_pca_registration":
        print("arguments_named_number_of_pca_components:", any(kw.arg == "number_of_pca_components" for kw in node_ast.keywords))
        print("docstring_uses_all_default_zero:", "use all components" in (ast.get_docstring(getattr(ast, "find_keyword", lambda **kw: None)(node_ast)) or ""))
PY

Repository: Project-MONAI/physiotwin4d

Length of output: 7116


Pass the configured PCA component count.

set_use_pca_registration() defaults number_of_pca_components to 0, which is documented as "use all components", so this heart fit ignores the HEART_CT_KCL.pca_components(test_mode) value used by the heart PCA builder and the lung fit tutorial.

🐛 Proposed fix
     if pca_model is not None:
         workflow.set_use_pca_registration(
             use_pca_registration=True,
             pca_model=pca_model,
+            number_of_pca_components=HEART_CT_KCL.pca_components(test_mode),
             use_surface=False,
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if pca_model is not None:
workflow.set_use_pca_registration(
use_pca_registration=True,
pca_model=pca_model,
use_surface=False,
)
if pca_model is not None:
workflow.set_use_pca_registration(
use_pca_registration=True,
pca_model=pca_model,
number_of_pca_components=HEART_CT_KCL.pca_components(test_mode),
use_surface=False,
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tutorials/tutorial_07_heart_fit_statistical_model_to_patient.py` around lines
164 - 169, Update the set_use_pca_registration call in the pca_model block to
pass the configured HEART_CT_KCL.pca_components(test_mode) value as
number_of_pca_components, matching the heart PCA builder and lung fit tutorial
instead of relying on the default.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant