Skip to content

ENH: Standardize forward/inverse transform conventions in registration#63

Open
aylward wants to merge 2 commits into
Project-MONAI:mainfrom
aylward:registration_convention_fix
Open

ENH: Standardize forward/inverse transform conventions in registration#63
aylward wants to merge 2 commits into
Project-MONAI:mainfrom
aylward:registration_convention_fix

Conversation

@aylward
Copy link
Copy Markdown
Collaborator

@aylward aylward commented May 30, 2026

Transform conventions

  • Add docs/developer/transform_conventions.rst documenting the rule that forward_transform warps the moving IMAGE onto the fixed grid, while warping moving points/landmarks into fixed space uses inverse_transform (image and point warps need opposite transforms), and that model (PCA) registration returns transforms in the opposite orientation from image registration.
  • Clarify docstrings/comments to this convention across register_images_{base,ants,greedy,icon}, register_models_{distance_maps, icp,pca}, register_time_series_images, and the workflow_* modules; link the new page from registration_images.rst, registration_models.rst, index.rst.
  • register_images_base: rework initial-transform handling to pre-warp the moving image onto the fixed grid before registration (matching the ICON backend) instead of composing an itk transform.

Longitudinal registration experiment

  • Add 1-preregistration.py: register every gated frame to its patient reference with ANTS and Greedy, recording per-label Dice and landmark squared error, and writing the warped image, labelmap, mask, transforms, and deformation-grid visualization per frame.
  • 1-finetune_icon.py: merge each patient's ANTS- and Greedy-warped frames into the ICON fine-tuning group and consume the warped loss masks; use Optional[X] type hints.
  • Update 2-recon_4d_icon_eval.py and 3-run_registration_method_comparison.py for the convention; add tests/test_register_images_ants.py and registration_test.py; drop the obsolete experiments/Heart-GatedCT_To_USD/test_compare_registration_speed.py.

Code style

  • Correct the documented string-quote standard to double quotes (the actual codebase standard) in pyproject ruff quote-style/inline-quotes, CLAUDE.md, AGENTS.md, and docs/contributing.rst.

Summary by CodeRabbit

  • New Features

    • Added a LabelmapTools utility and made it publicly available; optional fixed-image ROI labelmap can now be set for time-series registration.
    • Cohort-wide pre-registration and fine-tuning workflows with improved handling of warped frames and masks.
  • Bug Fixes

    • Fixed transform-direction issues in mesh propagation and landmark warping; clarified image vs point transform semantics.
  • Documentation

    • Added a comprehensive transform conventions guide and updated docs to reflect mask/transform conventions and double-quote style.
  • Tests

    • Expanded ANTs registration tests and added tests for labelmap-to-mask behavior.
  • Chores

    • Removed obsolete registration speed comparison script.

Transform conventions
- Add docs/developer/transform_conventions.rst documenting the rule that
  forward_transform warps the moving IMAGE onto the fixed grid, while warping
  moving points/landmarks into fixed space uses inverse_transform (image and
  point warps need opposite transforms), and that model (PCA) registration
  returns transforms in the opposite orientation from image registration.
- Clarify docstrings/comments to this convention across
  register_images_{base,ants,greedy,icon}, register_models_{distance_maps,
  icp,pca}, register_time_series_images, and the workflow_* modules; link the
  new page from registration_images.rst, registration_models.rst, index.rst.
- register_images_base: rework initial-transform handling to pre-warp the
  moving image onto the fixed grid before registration (matching the ICON
  backend) instead of composing an itk transform.

Longitudinal registration experiment
- Add 1-preregistration.py: register every gated frame to its patient
  reference with ANTS and Greedy, recording per-label Dice and landmark
  squared error, and writing the warped image, labelmap, mask, transforms,
  and deformation-grid visualization per frame.
- 1-finetune_icon.py: merge each patient's ANTS- and Greedy-warped frames
  into the ICON fine-tuning group and consume the warped loss masks; use
  Optional[X] type hints.
- Update 2-recon_4d_icon_eval.py and 3-run_registration_method_comparison.py
  for the convention; add tests/test_register_images_ants.py and
  registration_test.py; drop the obsolete
  experiments/Heart-GatedCT_To_USD/test_compare_registration_speed.py.

Code style
- Correct the documented string-quote standard to double quotes (the actual
  codebase standard) in pyproject ruff quote-style/inline-quotes, CLAUDE.md,
  AGENTS.md, and docs/contributing.rst.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 30, 2026 12:15
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

Walkthrough

This PR standardizes quoting to double quotes, adds a transform-conventions developer guide, introduces LabelmapTools and tests, migrates mask/dilation to LabelmapTools across workflows, changes ANTs pre-warp composition and Greedy affine handling, updates ICON weights resolution, and adds preregistration experiments and related script/test updates.

Changes

Transform convention documentation and API clarity

Layer / File(s) Summary
Transform conventions reference documentation
docs/developer/transform_conventions.rst, docs/index.rst
New comprehensive developer guide explaining image-registration forward/inverse transform orientation and PCA point-transform conventions, with comparison tables and rule-of-thumb guidance.
Registration API docstring clarifications
src/physiomotion4d/register_images_base.py, src/physiomotion4d/register_images_ants.py, src/physiomotion4d/register_images_icon.py
Base and backend registration classes now document what forward/inverse transforms warp, inverse consistency, and point-versus-image warp direction conventions.
Model registration documentation
src/physiomotion4d/register_models_pca.py, src/physiomotion4d/register_models_icp.py, src/physiomotion4d/register_models_distance_maps.py
Point transform documentation clarified to state that point transforms map opposite direction from image transforms (template↔target mappings).
Time-series and workflow documentation
src/physiomotion4d/register_time_series_images.py, src/physiomotion4d/workflow_reconstruct_highres_4d_ct.py
Updated return-value and method docstrings to clarify transform warping directions; added set_fixed_labelmap() method.
Developer guide cross-references
docs/developer/registration_images.rst, docs/developer/registration_models.rst
Added transform_conventions references in developer guides.

Code style standardization

Layer / File(s) Summary
Double-quote style enforcement
AGENTS.md, CLAUDE.md, pyproject.toml, docs/contributing.rst
Updated contributor guidance and linting configuration to enforce double quotes for all strings and docstrings.

Registration backend implementation improvements

Layer / File(s) Summary
ANTs pre-warp and composition logic
src/physiomotion4d/register_images_ants.py, tests/test_register_images_ants.py
Changed ANTs registration to pre-warp moving image using initial transform before registration, then compose with refinement output. Tests added to assert composition and transform-type behavior.
Greedy temporary file-based affine handling
src/physiomotion4d/register_images_greedy.py
Replaces in-memory affine matrix passing with temporary .mat file workflow to avoid native heap corruption. New _write_affine_matrix_file helper writes 4x4 affines; both affine/rigid and deformable paths use file paths for initial alignment.
ICON script-relative weights path resolution
src/physiomotion4d/register_images_icon.py
Resolves default checkpoint paths relative to the running script with fallback to cwd; removes class create_mask helper and moves mask derivation to LabelmapTools.

LabelmapTools and mask migration

Layer / File(s) Summary
LabelmapTools utility and API docs
src/physiomotion4d/labelmap_tools.py, docs/api/utilities/labelmap_tools.rst, src/physiomotion4d/__init__.py
Adds LabelmapTools.convert_labelmap_to_mask for thresholding, label exclusion, and physical-radius dilation; exports in package and documents module.
Migrate mask creation to LabelmapTools
src/physiomotion4d/register_images_base.py, src/physiomotion4d/workflow_fine_tune_icon_registration.py, src/physiomotion4d/workflow_fit_statistical_model_to_patient.py, src/physiomotion4d/register_models_distance_maps.py
Replaces ad-hoc NumPy/TubeTK mask/dilation with LabelmapTools calls; updates ctor/fields and docstrings accordingly.
Tests for LabelmapTools and related updates
tests/test_labelmap_tools.py, tests/test_image_tools.py
New tests covering thresholding, dilation, anisotropic spacing, label exclusion, and metadata preservation; update tests to use itk.array_from_matrix conversions.

Pre-registration experiment workflow and experiments integration

Layer / File(s) Summary
Pre-registration comparison analysis script
experiments/LongitudinalRegistration/1-preregistration.py
New script performs cohort-wide registration comparing ANTS and Greedy backends, writes per-frame CSVs (dice, landmarks) and a summary CSV with aggregated metrics.
Fine-tuning augmentation with pre-registered frames
experiments/LongitudinalRegistration/1-finetune_icon.py
Gathers pre-registered warped images/labelmaps as augmented training samples; adds derive_mask_for() and gather_warped_frames() helpers; increases training epochs from 100 to 500.
Reconstruction mask and labelmap handling
experiments/LongitudinalRegistration/2-recon_4d_icon_eval.py
Computes and caches label-derived masks per timepoint, passes moving_labelmaps to registration, switches to script-relative outputs, and preloads landmarks.
New registration test script
experiments/LongitudinalRegistration/registration_test.py
End-to-end test script registering gated CT timepoint to later timepoint using selectable backend (ANTS, ICON, Greedy), measuring runtime and loss.
Landmark warp transform direction correction
experiments/LongitudinalRegistration/3-run_registration_method_comparison.py
Corrects direct landmark warping to use forward_transform when warping reference landmarks into timepoint space and adds clarifying comments.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

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 directly summarizes the main objective of the PR: standardizing forward/inverse transform conventions across the registration framework and its documentation.
Docstring Coverage ✅ Passed Docstring coverage is 97.10% 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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Standardizes documentation and code around the forward/inverse transform direction conventions across image- and model-registration backends, adds a developer doc page on the rules, reworks ANTs initial-transform handling to pre-warp the moving image (matching the ICON backend), and adds new longitudinal-registration experiment scripts plus ANTs regression tests. Also corrects the documented string-quote standard to double quotes.

Changes:

  • New docs/developer/transform_conventions.rst plus docstring/comment clarifications throughout register_images_*, register_models_*, register_time_series_images, and workflow modules; usage sites in tutorials and experiments updated to use the matching transform.
  • register_images_ants.py initial transform now pre-warps the moving image instead of passing an ANTs initial_transform; register_images_greedy.py writes affine init to a temp file to avoid a heap-corruption crash; register_images_icon.py adds a default weights-path fallback.
  • Experiments: new 1-preregistration.py and registration_test.py, updated 2-recon_4d_icon_eval.py and 3-run_registration_method_comparison.py, removed obsolete test_compare_registration_speed.py; new ANTs regression tests; quote-style updated to double in pyproject.toml, CLAUDE.md, AGENTS.md, and docs/contributing.rst.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
docs/developer/transform_conventions.rst New developer doc capturing the forward/inverse rule.
docs/developer/registration_images.rst, registration_models.rst, index.rst Link the new convention page.
docs/contributing.rst, CLAUDE.md, AGENTS.md, pyproject.toml Switch documented quote style to double quotes.
src/physiomotion4d/register_images_base.py Clarify forward/inverse semantics in docstrings.
src/physiomotion4d/register_images_ants.py Apply initial transform via pre-warp; switch to "Affine"/"Rigid" presets.
src/physiomotion4d/register_images_greedy.py Pass affine init via temp .mat file; add docstring/log updates.
src/physiomotion4d/register_images_icon.py Docstring updates; add default weights-path fallback.
src/physiomotion4d/register_models_pca.py, register_models_icp.py, register_models_distance_maps.py Document point-vs-image transform orientation.
src/physiomotion4d/register_time_series_images.py Add set_fixed_labelmap; clarify return docstrings.
src/physiomotion4d/workflow_reconstruct_highres_4d_ct.py Clarify transform docstrings.
src/physiomotion4d/workflow_fine_tune_icon_registration.py Change lncc_sigma default 5→1 and hard-code use_label=False.
tutorials/tutorial_08_dirlab_pca_time_series.py Use phase_to_reference for warping reference mesh into phase space.
experiments/LongitudinalRegistration/1-preregistration.py New cohort-wide ANTs vs Greedy pre-registration script.
experiments/LongitudinalRegistration/1-finetune_icon.py Merge ANTs/Greedy warped frames into ICON fine-tune groups.
experiments/LongitudinalRegistration/2-recon_4d_icon_eval.py Wire labelmaps/masks through; reuse warped masks.
experiments/LongitudinalRegistration/3-run_registration_method_comparison.py Use forward transform for landmark warps.
experiments/LongitudinalRegistration/registration_test.py New single-pair sanity script.
experiments/Heart-GatedCT_To_USD/test_compare_registration_speed.py Obsolete script removed.
tests/test_register_images_ants.py Add NCC-based regression tests for initial-transform composition and Affine/Rigid presets.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"lncc_sigma": self.lncc_sigma,
"loss_function_masking": self.uses_masks,
"use_label": self.uses_segmentations,
"use_label": False,
lambda_value: float = 1.5,
dice_loss_weight: float = 0.5,
lncc_sigma: int = 5,
lncc_sigma: int = 1,

# Debug knob: when non-empty, only these patient IDs are processed.
# Set to ``[]`` (or ``None``) to run the full cohort.
debug_subjects: list[str] = ["pm0002"]
Comment on lines +306 to +328
main_module = sys.modules.get("__main__")
main_file = getattr(main_module, "__file__", None)
top_dir = pathlib.Path.cwd()
if main_file is not None:
top_dir = pathlib.Path(main_file).resolve().parent
if self.use_multi_modality:
if self.weights_path is None:
self.weights_path = str(
top_dir
/ "network_weights"
/ "multigradicon1.0"
/ "Step_2_final.trch"
)
self.net = get_multigradicon(
loss_fn=icon.LNCC(sigma=5),
apply_intensity_conservation_loss=self.use_mass_preservation,
weights_location=self.weights_path,
)
else:
if self.weights_path is None:
self.weights_path = str(
top_dir / "network_weights" / "unigradicon1.0" / "Step_2_final.trch"
)
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (2)
experiments/LongitudinalRegistration/2-recon_4d_icon_eval.py (1)

165-176: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pass the fixed labelmap into the registrar too.

fixed_labelmap is loaded on Line 136, but this setup only calls set_fixed_mask(). RegisterTimeSeriesImages forwards fixed-side labelmap logic through set_fixed_labelmap(...), so the new moving_labelmaps= argument is currently one-sided and won't activate any fixed/moving labelmap path.

Suggested fix
         registrar = RegisterTimeSeriesImages(registration_method="ICON")
         registrar.set_modality("ct")
         registrar.set_fixed_image(fixed_image)
         registrar.set_fixed_mask(fixed_mask)
+        registrar.set_fixed_labelmap(fixed_labelmap)
         registrar.set_number_of_iterations_ICON(icon_iterations)
         if weights_path is not None:
             registrar.registrar_ICON.set_weights_path(str(weights_path))
🤖 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 `@experiments/LongitudinalRegistration/2-recon_4d_icon_eval.py` around lines
165 - 176, The registrar setup omits the fixed-side labelmap, so when you call
RegisterTimeSeriesImages and later register_time_series with moving_labelmaps
the fixed/moving labelmap path isn't activated; call
registrar.set_fixed_labelmap(fixed_labelmap) after set_fixed_mask (using the
fixed_labelmap variable loaded earlier) so the registrar has both fixed and
moving labelmaps before register_time_series is invoked, ensuring
RegisterTimeSeriesImages's fixed/moving labelmap logic runs.
src/physiomotion4d/workflow_fine_tune_icon_registration.py (1)

520-533: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't disable label supervision in the shared workflow.

Hard-coding training.use_label to False means callers that pass segmentations still build segmentation entries and may skip frames missing segs in prepare_dataset(), but the generated YAML will never enable label loss. That breaks this class's data-driven contract and the existing expectation in tests/test_workflow_fine_tune_icon_registration.py.

Suggested fix
-                "use_label": False,
+                "use_label": self.uses_segmentations,
🤖 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/physiomotion4d/workflow_fine_tune_icon_registration.py` around lines 520
- 533, The YAML generator hard-codes "use_label": False which disables label
supervision; change it to respect the instance setting by replacing the False
with self.use_label (or use getattr(self, "use_label", True) if the attribute
may not exist) so the generated config reflects whether labels are provided;
update the code around where "training": {... "use_label": ... } is assembled
(refer to use_label / uses_masks fields and prepare_dataset() in this class) to
ensure tests and data-driven behavior remain correct.
🧹 Nitpick comments (2)
AGENTS.md (1)

127-128: ⚡ Quick win

Sync downstream agent rule docs with this new quote policy.

This change is good, but .agents/agents/implementation.md still says “single quotes for strings,” which can cause conflicting agent behavior. Please update that rule to match this source-of-truth guidance.

🤖 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 `@AGENTS.md` around lines 127 - 128, Update the downstream agent rule that
currently reads "single quotes for strings" in implementation.md to match
AGENTS.md's policy "Double quotes for strings and docstrings. Keep lines at or
below 88 characters." Replace the rule text and any examples in
implementation.md so they use double quotes for strings and docstrings and
enforce the 88-character line limit; search for the exact phrase "single quotes
for strings" to locate all occurrences and adjust examples and explanatory text
accordingly.
src/physiomotion4d/register_images_base.py (1)

62-63: ⚡ Quick win

Use double quotes in docstring examples for string literals.

The updated examples still use single-quoted keys; switch to double quotes to match the repository Python style rule.

Proposed docstring fix
-        ...             'forward_transform': tfm_forward,  # warps moving image -> fixed grid
-        ...             'inverse_transform': tfm_inverse,  # warps fixed image -> moving grid
+        ...             "forward_transform": tfm_forward,  # warps moving image -> fixed grid
+        ...             "inverse_transform": tfm_inverse,  # warps fixed image -> moving grid
@@
-        >>> forward_tfm = result['forward_transform']  # warps moving image -> fixed grid
-        >>> inverse_tfm = result['inverse_transform']  # warps fixed image -> moving grid
+        >>> forward_tfm = result["forward_transform"]  # warps moving image -> fixed grid
+        >>> inverse_tfm = result["inverse_transform"]  # warps fixed image -> moving grid

As per coding guidelines, "Use double quotes for strings and docstrings".

Also applies to: 71-72

🤖 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/physiomotion4d/register_images_base.py` around lines 62 - 63, Update the
docstring examples to use double quotes for all string literals: replace
occurrences of single-quoted keys like 'forward_transform' and
'inverse_transform' (and the similar single-quoted examples around lines 71-72)
with double-quoted versions ("forward_transform", "inverse_transform") so they
follow the repository style rule for strings/docstrings.
🤖 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 `@experiments/LongitudinalRegistration/1-preregistration.py`:
- Around line 89-91: The debug_subjects default currently limits processing to a
single patient ("pm0002"), so change the debug_subjects variable in
1-preregistration.py to an empty list or None (e.g., debug_subjects: list[str] =
[] or debug_subjects = None) so the script runs the full cohort by default;
ensure any downstream checks that test debug_subjects handle both [] and None
(or adjust them) to avoid silently filtering the cohort.

In `@experiments/LongitudinalRegistration/2-recon_4d_icon_eval.py`:
- Around line 41-46: The hard-coded finetuned_weights_path uses
"network_weights_final.trch" but the fine-tune workflow emits
"Finetune_multi_final.trch" (see
WorkflowFineTuneICONRegistration.expected_weights_path()); update
finetuned_weights_path to reference the actual checkpoint name produced by the
fine-tune workflow—either replace the filename with "Finetune_multi_final.trch"
or, better, compute the path by calling
WorkflowFineTuneICONRegistration.expected_weights_path()/equivalent helper so
the eval script points to the exact checkpoint the finetune workflow produces
(refer to finetuned_weights_path and
WorkflowFineTuneICONRegistration.expected_weights_path).

In `@src/physiomotion4d/register_images_ants.py`:
- Around line 597-611: When an initial_forward_transform is applied you pre-warp
the moving image (using TransformTools.transform_image into
self.moving_image_pre) but forget to pre-warp the corresponding moving mask,
leaving self.moving_mask in the original moving grid and causing masked ANTs
runs to use inconsistent coordinate systems; update the same pre-warp block to
also transform self.moving_mask with TransformTools.transform_image (into the
fixed grid) so both self.moving_image_pre and self.moving_mask are aligned
before the masked registration, and apply the same fix for the other pre-warp
occurrence around the later registration path that mirrors this logic.
- Around line 597-611: The code pre-warps the moving image using
TransformTools.transform_image but resamples it onto self.fixed_image while
ants.registration later uses self.fixed_image_pre as the fixed image; change the
resampling target to self.fixed_image_pre so the pre-warped moving image and the
subsequent registration share the exact same grid (update the call in the block
that checks initial_forward_transform inside register_images_ants.py to pass
self.fixed_image_pre instead of self.fixed_image, referencing
self.moving_image_pre, initial_forward_transform, and
TransformTools.transform_image).

In `@src/physiomotion4d/register_images_greedy.py`:
- Around line 158-160: The command-building currently injects the temp affine
filepath (created via tempfile.mkstemp and saved with np.savetxt into variable
initial_affine_file) directly into the Greedy command string (see uses in cmd
and cmd_def), which breaks if the temp path contains spaces; change to pass the
init-transform path via the picsl-greedy wrapper’s placeholder/kwargs
substitution (i.e., use the wrapper’s parameter for "-ia"/"-it" instead of
f-string interpolation), or if substitution isn’t available create the temp file
in a guaranteed space-free directory like /tmp (use
tempfile.NamedTemporaryFile(dir="/tmp", delete=False) or equivalent) and
reference that variable without string concatenation so the wrapper receives it
as a single argument; update usages around where cmd += f" -ia
{initial_affine_file}" and the "-it {initial_affine_file}" insertion in cmd_def
accordingly.
- Around line 140-160: The function _write_affine_matrix_file writes a 4x4 RAS
matrix but callers (e.g., registration_method building initial_affine from
initial_forward_transform.GetMatrix()/GetTranslation()/GetCenter) supply ITK/LPS
matrices, so the .mat is in the wrong convention; fix by converting the incoming
LPS affine into Greedy/RAS before saving: construct S = np.diag([-1, -1, 1, 1])
and replace the write value with mat_ras = S @ mat_4x4 @ S (then save mat_ras),
and update the _write_affine_matrix_file docstring to state it accepts an
ITK/LPS matrix and writes a Greedy RAS .mat (or explicitly note the conversion).

In `@src/physiomotion4d/register_images_icon.py`:
- Around line 93-95: The docstring for the setter set_weights_path in
register_images_icon.py is contradictory: it currently says the target file must
not exist (for downloading) but the setter is also used to point to an existing
custom checkpoint; update the docstring to clearly state both behaviors — if the
path points to a non-existent file the weights will be downloaded there (typical
suffix ".trch"), and if it points to an existing file it will be used as the
custom checkpoint to load — and remove the absolute "must not exist" wording
while giving both cases and expected suffix/format.
- Around line 306-328: The code currently sets top_dir to the parent of
__main__.__file__ without validating that the script lives inside the project,
which can point to non-writable launcher dirs; change the top_dir selection
logic in the block initializing top_dir/main_file so that you: (1) start with
pathlib.Path.cwd() as default, (2) if main_file is not None, resolve it and only
adopt its parent when that parent is a descendant of the project cwd or contains
a project marker (e.g., .git, pyproject.toml, setup.py), otherwise keep cwd; (3)
additionally verify the chosen top_dir is writable and if not fallback to a
writable directory (e.g., user home or cwd); update the code around
main_module/main_file/top_dir and use this validated top_dir when building
default self.weights_path for both use_multi_modality branches so
get_multigradicon and the unigradicon path logic use the safe, writable
fallback.

In `@src/physiomotion4d/register_time_series_images.py`:
- Around line 78-79: Update the example string literals to use double quotes:
replace single-quoted strings in the example assignments referencing
result['forward_transforms'] and result['inverse_transforms'] (symbols:
forward_tfms, inverse_tfms, result) with double-quoted equivalents, and apply
the same change to the other occurrences noted (the similar examples around the
occurrences of those variables later in the file).
- Around line 214-215: The docstring for the parameter fixed_labelmap currently
documents it as required itk.Image but the function signature uses
Optional[itk.Image]; update the Args section to reflect Optional[itk.Image] (or
itk.Image | None) and indicate it may be None/default omitted, and adjust the
description to say it is an optional labelmap defining the ROI; locate the
parameter entry for fixed_labelmap in the docstring of the
register_time_series_images function and change its type and wording
accordingly.

In `@tests/test_register_images_ants.py`:
- Around line 333-543: Add coverage that validates the inverse composition path
by warping the fixed image using the returned inverse transform and scoring
against the original moving image grid: after each registration call that
currently uses result["forward_transform"] (e.g., baseline, identity_result,
prior_result, and the matrix/transform_type loops), also retrieve
result["inverse_transform"] and call
TransformTools().transform_image(fixed_image, inverse_transform, moving_image,
interpolation_method="linear") then compute _foreground_ncc between
itk.array_from_image(moving_image) and the warped-fixed array (using the same
foreground mask) and assert it meets the same improvement/equality thresholds as
the forward checks; ensure you reference result["inverse_transform"] and
TransformTools.transform_image when adding these assertions.

---

Outside diff comments:
In `@experiments/LongitudinalRegistration/2-recon_4d_icon_eval.py`:
- Around line 165-176: The registrar setup omits the fixed-side labelmap, so
when you call RegisterTimeSeriesImages and later register_time_series with
moving_labelmaps the fixed/moving labelmap path isn't activated; call
registrar.set_fixed_labelmap(fixed_labelmap) after set_fixed_mask (using the
fixed_labelmap variable loaded earlier) so the registrar has both fixed and
moving labelmaps before register_time_series is invoked, ensuring
RegisterTimeSeriesImages's fixed/moving labelmap logic runs.

In `@src/physiomotion4d/workflow_fine_tune_icon_registration.py`:
- Around line 520-533: The YAML generator hard-codes "use_label": False which
disables label supervision; change it to respect the instance setting by
replacing the False with self.use_label (or use getattr(self, "use_label", True)
if the attribute may not exist) so the generated config reflects whether labels
are provided; update the code around where "training": {... "use_label": ... }
is assembled (refer to use_label / uses_masks fields and prepare_dataset() in
this class) to ensure tests and data-driven behavior remain correct.

---

Nitpick comments:
In `@AGENTS.md`:
- Around line 127-128: Update the downstream agent rule that currently reads
"single quotes for strings" in implementation.md to match AGENTS.md's policy
"Double quotes for strings and docstrings. Keep lines at or below 88
characters." Replace the rule text and any examples in implementation.md so they
use double quotes for strings and docstrings and enforce the 88-character line
limit; search for the exact phrase "single quotes for strings" to locate all
occurrences and adjust examples and explanatory text accordingly.

In `@src/physiomotion4d/register_images_base.py`:
- Around line 62-63: Update the docstring examples to use double quotes for all
string literals: replace occurrences of single-quoted keys like
'forward_transform' and 'inverse_transform' (and the similar single-quoted
examples around lines 71-72) with double-quoted versions ("forward_transform",
"inverse_transform") so they follow the repository style rule for
strings/docstrings.
🪄 Autofix (Beta)

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

Run ID: 39d7ea3b-8dba-43f9-b50d-891b3e1d2918

📥 Commits

Reviewing files that changed from the base of the PR and between d1c4c95 and e6a8b02.

📒 Files selected for processing (26)
  • AGENTS.md
  • CLAUDE.md
  • docs/contributing.rst
  • docs/developer/registration_images.rst
  • docs/developer/registration_models.rst
  • docs/developer/transform_conventions.rst
  • docs/index.rst
  • experiments/Heart-GatedCT_To_USD/test_compare_registration_speed.py
  • experiments/LongitudinalRegistration/1-finetune_icon.py
  • experiments/LongitudinalRegistration/1-preregistration.py
  • experiments/LongitudinalRegistration/2-recon_4d_icon_eval.py
  • experiments/LongitudinalRegistration/3-run_registration_method_comparison.py
  • experiments/LongitudinalRegistration/registration_test.py
  • pyproject.toml
  • src/physiomotion4d/register_images_ants.py
  • src/physiomotion4d/register_images_base.py
  • src/physiomotion4d/register_images_greedy.py
  • src/physiomotion4d/register_images_icon.py
  • src/physiomotion4d/register_models_distance_maps.py
  • src/physiomotion4d/register_models_icp.py
  • src/physiomotion4d/register_models_pca.py
  • src/physiomotion4d/register_time_series_images.py
  • src/physiomotion4d/workflow_fine_tune_icon_registration.py
  • src/physiomotion4d/workflow_reconstruct_highres_4d_ct.py
  • tests/test_register_images_ants.py
  • tutorials/tutorial_08_dirlab_pca_time_series.py
💤 Files with no reviewable changes (1)
  • experiments/Heart-GatedCT_To_USD/test_compare_registration_speed.py

Comment on lines +89 to +91
# Debug knob: when non-empty, only these patient IDs are processed.
# Set to ``[]`` (or ``None``) to run the full cohort.
debug_subjects: list[str] = ["pm0002"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove the default single-subject filter.

Line 91 ships with ["pm0002"], so this script never performs the cohort-wide comparison promised by its header/comments unless the user edits the file first. That silently skews the summary CSV/table and leaves downstream preregistration augmentation incomplete by default.

Suggested fix
- debug_subjects: list[str] = ["pm0002"]
+ debug_subjects: list[str] = []
🤖 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 `@experiments/LongitudinalRegistration/1-preregistration.py` around lines 89 -
91, The debug_subjects default currently limits processing to a single patient
("pm0002"), so change the debug_subjects variable in 1-preregistration.py to an
empty list or None (e.g., debug_subjects: list[str] = [] or debug_subjects =
None) so the script runs the full cohort by default; ensure any downstream
checks that test debug_subjects handle both [] and None (or adjust them) to
avoid silently filtering the cohort.

Comment on lines +41 to +46
finetuned_weights_path = (
output_dir
/ "icon_finetuned"
/ "icon_finetuned_model"
/ "checkpoints"
/ "network_weights_final.trch"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Point this at the checkpoint 1-finetune_icon.py actually produces.

This hard-coded filename doesn't match WorkflowFineTuneICONRegistration.expected_weights_path(), which still resolves to .../checkpoints/Finetune_multi_final.trch. As written, the eval script won't find the checkpoint emitted by the companion fine-tuning workflow.

Suggested fix
 finetuned_weights_path = (
     output_dir
     / "icon_finetuned"
     / "icon_finetuned_model"
     / "checkpoints"
-    / "network_weights_final.trch"
+    / "Finetune_multi_final.trch"
 )
🤖 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 `@experiments/LongitudinalRegistration/2-recon_4d_icon_eval.py` around lines 41
- 46, The hard-coded finetuned_weights_path uses "network_weights_final.trch"
but the fine-tune workflow emits "Finetune_multi_final.trch" (see
WorkflowFineTuneICONRegistration.expected_weights_path()); update
finetuned_weights_path to reference the actual checkpoint name produced by the
fine-tune workflow—either replace the filename with "Finetune_multi_final.trch"
or, better, compute the path by calling
WorkflowFineTuneICONRegistration.expected_weights_path()/equivalent helper so
the eval script points to the exact checkpoint the finetune workflow produces
(refer to finetuned_weights_path and
WorkflowFineTuneICONRegistration.expected_weights_path).

Comment on lines +597 to 611
# Apply any initial transform by pre-warping the moving image onto the
# fixed grid (the same approach RegisterImagesICON uses), instead of
# passing it to ants.registration as an initial_transform. ANTS
# mishandles matrix (affine/translation) initial transforms, badly
# corrupting the result; pre-warping keeps the composition below
# self-consistent for any initial transform type. The registration then
# solves the residual and the composition recovers the full transform.
if initial_forward_transform is not None:
self.log_info("Converting initial ITK transform to ANTs format...")
initial_transform = self.itk_transform_to_ANTSfile(
itk_tfm=initial_forward_transform,
reference_image=self.fixed_image,
output_filename="initial_transform_temp.mat",
self.log_info("Pre-warping moving image with initial transform...")
transform_tools = TransformTools()
self.moving_image_pre = transform_tools.transform_image(
self.moving_image_pre,
initial_forward_transform,
self.fixed_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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pre-warp the moving mask when an initial transform is supplied.

The new path moves self.moving_image_pre onto the fixed grid, but self.moving_mask stays on the original moving grid and is still passed into the masked ANTs call. That gives ANTs a moving image and moving mask in different coordinate systems, so masked runs can optimize the wrong ROI.

Also applies to: 668-675

🤖 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/physiomotion4d/register_images_ants.py` around lines 597 - 611, When an
initial_forward_transform is applied you pre-warp the moving image (using
TransformTools.transform_image into self.moving_image_pre) but forget to
pre-warp the corresponding moving mask, leaving self.moving_mask in the original
moving grid and causing masked ANTs runs to use inconsistent coordinate systems;
update the same pre-warp block to also transform self.moving_mask with
TransformTools.transform_image (into the fixed grid) so both
self.moving_image_pre and self.moving_mask are aligned before the masked
registration, and apply the same fix for the other pre-warp occurrence around
the later registration path that mirrors this logic.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pre-warp onto the same fixed grid ANTs actually registers against.

self.moving_image_pre is resampled against self.fixed_image, but the subsequent ants.registration() call uses self.fixed_image_pre as fixed. If preprocess() changes spacing/origin/size, the residual registration is solving on a different domain than the one used for the initial alignment.

Suggested fix
         if initial_forward_transform is not None:
             self.log_info("Pre-warping moving image with initial transform...")
             transform_tools = TransformTools()
             self.moving_image_pre = transform_tools.transform_image(
                 self.moving_image_pre,
                 initial_forward_transform,
-                self.fixed_image,
+                self.fixed_image_pre,
             )
🤖 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/physiomotion4d/register_images_ants.py` around lines 597 - 611, The code
pre-warps the moving image using TransformTools.transform_image but resamples it
onto self.fixed_image while ants.registration later uses self.fixed_image_pre as
the fixed image; change the resampling target to self.fixed_image_pre so the
pre-warped moving image and the subsequent registration share the exact same
grid (update the call in the block that checks initial_forward_transform inside
register_images_ants.py to pass self.fixed_image_pre instead of
self.fixed_image, referencing self.moving_image_pre, initial_forward_transform,
and TransformTools.transform_image).

Comment on lines +140 to +160
def _write_affine_matrix_file(self, mat_4x4: NDArray[np.float64]) -> str:
"""Write a 4x4 RAS affine matrix to a temporary Greedy ``.mat`` file.

Greedy's in-memory interface corrupts the heap when a numpy affine
matrix is supplied as an initial transform (``-ia``/``-it``); passing a
file path instead avoids that native crash. Greedy reads a plain-text
4x4 RAS matrix, which is what ``numpy.savetxt`` writes here.

Args:
mat_4x4: 4x4 affine matrix in RAS (Greedy) convention.

Returns:
Path to the written temporary ``.mat`` file. The caller is
responsible for deleting it.
"""
mat_4x4 = np.asarray(mat_4x4, dtype=np.float64)
if mat_4x4.shape != (4, 4):
raise ValueError(f"Expected 4x4 matrix, got shape {mat_4x4.shape}")
fd, path = tempfile.mkstemp(suffix=".mat", prefix="greedy_aff_")
os.close(fd)
np.savetxt(path, mat_4x4, fmt="%.10f")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/physiomotion4d/register_images_greedy.py"
echo "== File: $FILE =="
wc -l "$FILE"
echo

echo "== Extract: _write_affine_matrix_file() (lines ~140-200) =="
nl -ba "$FILE" | sed -n '120,210p'
echo

echo "== Search for _write_affine_matrix_file( calls =="
rg -n "_write_affine_matrix_file\s*\(" "$FILE"
echo

echo "== Extract: call sites around those matches =="
python3 - <<'PY'
import re, subprocess, os, sys, textwrap
FILE="src/physiomotion4d/register_images_greedy.py"
out=subprocess.check_output(["rg","-n",r"_write_affine_matrix_file\s*\(",FILE],text=True)
lines=[]
for line in out.strip().splitlines():
    m=re.match(r".+:(\d+):",line)
    if m: lines.append(int(m.group(1)))
for ln in sorted(lines):
    start=max(1,ln-25); end=ln+45
    print(f"\n--- Context for call at line {ln} ({start}-{end}) ---")
    subprocess.run(["bash","-lc",f"nl -ba {FILE} | sed -n '{start},{end}p'"],check=True)
PY

echo
echo "== Extract: referenced lines 228-229 =="
nl -ba "$FILE" | sed -n '215,245p'
echo
echo "== Extract: referenced lines 275-278 =="
nl -ba "$FILE" | sed -n '255,305p'
echo

echo "== Check for print() usage and itk.imwrite usage in this file =="
rg -n "print\(" "$FILE" || true
rg -n "itk\.imwrite|imwrite\(" "$FILE" || true
echo

echo "== Check for string-double-quotes and line length violations (rough) =="
# Quick heuristic: find long lines > 120 just to catch obvious issues
python3 - <<'PY'
import pathlib
FILE=pathlib.Path("src/physiomotion4d/register_images_greedy.py")
long=[]
for i,line in enumerate(FILE.read_text().splitlines(),start=1):
    if len(line)>120:
        long.append((i,len(line),line[:140]))
print("Long lines >120:",len(long))
for i,l,txt in long[:30]:
    print(i,l,txt.replace("\t"," "))
PY

Repository: Project-MONAI/physiomotion4d

Length of output: 301


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/physiomotion4d/register_images_greedy.py"

echo "== File: $FILE =="
wc -l "$FILE"
echo

echo "== Extract: _write_affine_matrix_file() (lines 120-220) =="
python3 - <<'PY'
import itertools
path="src/physiomotion4d/register_images_greedy.py"
start, end = 120, 220
with open(path, "r", encoding="utf-8") as f:
    for i, line in enumerate(f, start=1):
        if start <= i <= end:
            print(f"{i:4d}: {line.rstrip()}")
        if i > end:
            break
PY

echo
echo "== Search for _write_affine_matrix_file( calls =="
rg -n "_write_affine_matrix_file\s*\(" "$FILE" || true
echo

echo
echo "== Extract: context around each call =="
python3 - <<'PY'
import re, subprocess
FILE="src/physiomotion4d/register_images_greedy.py"
text=subprocess.check_output(["rg","-n",r"_write_affine_matrix_file\s*\(",FILE],text=True)
lines=[]
for m in re.finditer(r"^.+:(\d+):",text,flags=re.M):
    lines.append(int(m.group(1)))
for ln in sorted(set(lines)):
    start=max(1,ln-25); end=ln+45
    print(f"\n--- call at line {ln} (show {start}-{end}) ---")
    with open(FILE,"r",encoding="utf-8") as f:
        for i,line in enumerate(f,start=1):
            if start<=i<=end:
                print(f"{i:4d}: {line.rstrip()}")
            if i>end:
                break
PY

echo
echo "== Extract: referenced lines 228-240 =="
python3 - <<'PY'
path="src/physiomotion4d/register_images_greedy.py"
start,end=228,240
with open(path,"r",encoding="utf-8") as f:
    for i,line in enumerate(f,start=1):
        if start<=i<=end:
            print(f"{i:4d}: {line.rstrip()}")
        if i>end: break
PY

echo
echo "== Extract: referenced lines 275-290 =="
python3 - <<'PY'
path="src/physiomotion4d/register_images_greedy.py"
start,end=275,290
with open(path,"r",encoding="utf-8") as f:
    for i,line in enumerate(f,start=1):
        if start<=i<=end:
            print(f"{i:4d}: {line.rstrip()}")
        if i>end: break
PY

echo
echo "== Check for print() usage and itk.imwrite usage in this file =="
rg -n "print\(" "$FILE" || true
rg -n "itk\.imwrite|imwrite\(" "$FILE" || true
echo

echo
echo "== Check for long lines > 120 chars =="
python3 - <<'PY'
import pathlib
FILE=pathlib.Path("src/physiomotion4d/register_images_greedy.py")
long=[]
for i,line in enumerate(FILE.read_text(encoding="utf-8").splitlines(),start=1):
    if len(line)>120:
        long.append((i,len(line),line))
print("Count:",len(long))
for i,l,txt in long[:20]:
    print(i,l,txt[:160].replace("\t"," "))
PY

Repository: Project-MONAI/physiomotion4d

Length of output: 6864


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/physiomotion4d/register_images_greedy.py"

echo "== Search for initial_affine derivation and related symbols =="
rg -n "initial_affine|initial_forward_transform|LPS|RAS|diag\\(\\-1|lps_to_ras|ras_to_lps" "$FILE"

echo
echo "== Extract: lines around initial_affine assignments (broad) =="
python3 - <<'PY'
import re
path="src/physiomotion4d/register_images_greedy.py"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()
hits=[]
for i,l in enumerate(lines, start=1):
    if re.search(r"\binitial_affine\b", l):
        hits.append(i)
for ln in sorted(set(hits)):
    start=max(1,ln-35); end=min(len(lines), ln+35)
    print(f"\n--- around line {ln} ({start}-{end}) ---")
    for j in range(start,end+1):
        print(f"{j:4d}: {lines[j-1].rstrip()}")
PY

echo
echo "== Search for where transforms are converted to arrays (matrix extraction) =="
rg -n "to.*array|GetMatrix|Transform|matrix|affine.*matrix|array_from" "$FILE"

echo
echo "== Check for any coordinate-convention conversions in this module =="
rg -n "ras|lps|world|convention|orientation" "$FILE"

Repository: Project-MONAI/physiomotion4d

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/physiomotion4d/register_images_greedy.py"

echo "== print() usage =="
rg -n "print\(" "$FILE" || true

echo
echo "== Long lines >88 chars (count + samples) =="
python3 - <<'PY'
import pathlib
path=pathlib.Path("src/physiomotion4d/register_images_greedy.py")
long=[]
for i,line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
    if len(line)>88:
        long.append((i,len(line),line))
print("Count >",88,":",len(long))
for i,l,txt in long[:25]:
    print(i,l,txt.replace("\t"," ")[:140])
PY

echo
echo "== itk.imwrite usage (if any) =="
rg -n "itk\.imwrite|itk\.ImageFileWriter|imwrite\(" "$FILE" || true

Repository: Project-MONAI/physiomotion4d

Length of output: 726


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for LPS/RAS convention docs and any conversion helpers.
rg -n "LPS|RAS|lps_to_ras|ras_to_lps|diag\\(\\[-1\\.|transform_conventions" . -S

echo
echo "== Find Greedy/greedy_aff usage =="
rg -n "greedy_aff_|Greedy3D|picsl_greedy|Greedy" src/physiomotion4d -S

echo
echo "== Inspect _try_import_greedy definition =="
rg -n "def _try_import_greedy|_try_import_greedy\\(" src/physiomotion4d/register_images_greedy.py src/physiomotion4d -S

# Print the Greedy-related imports / helper defs at top of the file
echo
echo "== Top of register_images_greedy.py (imports/utilities) =="
python3 - <<'PY'
path="src/physiomotion4d/register_images_greedy.py"
with open(path,'r',encoding='utf-8') as f:
    for i,line in enumerate(f,start=1):
        if i>120: break
        print(f"{i:4d}: {line.rstrip()}")
PY

Repository: Project-MONAI/physiomotion4d

Length of output: 24510


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect transform conventions doc for LPS/RAS/Greedy notes
for f in docs/developer/transform_conventions.* docs/developer/transform_conventions/*; do
  if [ -e "$f" ]; then
    echo "== Found: $f =="
    ls -la "$f" | head -n 20
  fi
done

# Greedy/picsl-specific mentions in docs
rg -n "Greedy|picsl-greedy|picsl_greedy|-ia|-it|\\.mat|RAS|LPS|affine" docs -S || true

# Inspect how ANTs backend serializes initial transforms (for comparison)
FILE_ANTS="src/physiomotion4d/register_images_ants.py"
if [ -e "$FILE_ANTS" ]; then
  rg -n "initial_forward_transform|initial_affine|-ia|-it|\\.mat|RAS|LPS" "$FILE_ANTS" -S || true
fi

# Inspect RegisterImagesBase conventions handling
FILE_BASE="src/physiomotion4d/register_images_base.py"
if [ -e "$FILE_BASE" ]; then
  rg -n "transform_conventions|RAS|LPS|matrix|affine" "$FILE_BASE" -S || true
fi

Repository: Project-MONAI/physiomotion4d

Length of output: 4154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

DOC="docs/developer/transform_conventions.rst"
echo "== File: $DOC =="
wc -l "$DOC"

echo
echo "== Extract: Greedy / RAS / LPS sections =="
# show lines containing Greedy or RAS/LPS mentions in the doc
rg -n "Greedy|picsl-greedy|picsl_greedy|RAS|LPS|\\-ia|\\-it|\\.mat|affine init|matrix" "$DOC" -n -S || true

# Print the whole doc around those matches (limit output)
python3 - <<'PY'
import re
path="docs/developer/transform_conventions.rst"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()
need=[]
for i,l in enumerate(lines, start=1):
    if re.search(r"Greedy|picsl-greedy|picsl_greedy|RAS|LPS|\-ia|\-it|\.mat|affine init|matrix", l):
        need.append(i)
# de-dup sorted
need=sorted(set(need))
print("Matches:",len(need))
# print up to first 8 match contexts
shown=0
for ln in need:
    start=max(1,ln-18); end=min(len(lines),ln+25)
    if shown>=8: break
    print(f"\n--- context around {ln} ({start}-{end}) ---")
    for j in range(start,end+1):
        print(f"{j:4d}: {lines[j-1].rstrip()}")
    shown+=1
PY

Repository: Project-MONAI/physiomotion4d

Length of output: 5168


🏁 Script executed:

#!/bin/bash
set -euo pipefail
DOC="docs/api/registration/greedy.rst"
echo "== File: $DOC =="
wc -l "$DOC"

echo
echo "== Extract: initial_forward_transform / -ia / -it / .mat =="
rg -n "initial_forward_transform|-ia|-it|\\.mat|RAS|LPS|matrix" "$DOC" -S || true

echo
echo "== Context around those matches (limited) =="
python3 - <<'PY'
import re
path="docs/api/registration/greedy.rst"
lines=open(path,'r',encoding='utf-8').read().splitlines()
patterns=[r"initial_forward_transform", r"-ia", r"-it", r"\.mat", r"RAS", r"LPS", r"matrix"]
idx=[i+1 for i,l in enumerate(lines) if any(re.search(p,l) for p in patterns)]
idx=sorted(set(idx))
print("Matches:",len(idx))
for ln in idx[:12]:
    start=max(1,ln-12); end=min(len(lines),ln+20)
    print(f"\n--- context around line {ln} ({start}-{end}) ---")
    for j in range(start,end+1):
        print(f"{j:4d}: {lines[j-1]}")
PY

Repository: Project-MONAI/physiomotion4d

Length of output: 271


🌐 Web query:

picsl-greedy -ia -it mat coordinate system RAS LPS

💡 Result:

picsl-greedy (Greedy) uses the standard medical-image anatomical conventions, and the -it / -o affine matrix it writes is expressed in RAS voxel-coordinate ordering (not LPS). 1) What -it means - In Greedy, -it supplies a chain of transforms (affine matrices and/or warps) that map the moving image into a reference space (i.e., it is applied to the moving image before registration). It is described as equivalent to reslicing the moving image into the fixed image space using the same chain of transformations. [1] 2) What coordinate system the written affine matrix uses (RAS) - Greedy’s documentation states that the -o output affine matrix maps voxels in fixed image space to voxels in moving image space, and gives the explicit relationship using ras(·): - [ras(B); 1] = Matrix * [ras(B); 1] [1] - This indicates the affine matrix representation is in “ras” coordinates (RAS), not “lps”. [1] 3) How this relates to RAS vs LPS in general - Many medical-imaging conventions use LPS (common in DICOM), while RAS is used internally by some toolkits; the difference between RAS and LPS is that the sign of the first two coordinate axes is inverted. [2] - Therefore, if you are interoperating with DICOM/LPS-based systems, you may need to flip the first two axes when converting to/from the Greedy (RAS) convention. [2] Answer summary - For the specific Greedy/picsl-greedy question “mat coordinate system RAS LPS”: Greedy’s -o affine “mat” is expressed using ras(·) coordinates (RAS). [1] If you tell me which software you’re converting to/from (e.g., SimpleITK, 3D Slicer, elastix, ITK), I can outline the exact axis-flip/convention mapping you need.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
BASE="src/physiomotion4d/register_images_base.py"

echo "== File: $BASE =="
wc -l "$BASE"

echo
echo "== Search for RAS/LPS and any conversion helpers in base =="
rg -n "RAS|LPS|ras_to|lps_to|diag\\(\\-1|transform_conventions|Greedy|picsl-greedy|\\.mat" "$BASE" -S || true

echo
echo "== Extract: likely backend boundary/convention code (lines 240-380) =="
python3 - <<'PY'
path="src/physiomotion4d/register_images_base.py"
start,end=240,420
with open(path,'r',encoding='utf-8') as f:
    for i,line in enumerate(f,start=1):
        if start<=i<=end:
            print(f"{i:4d}: {line.rstrip()}")
        if i>end: break
PY

echo
echo "== Extract: around compose/initial_forward_transform handling (search in file) =="
rg -n "initial_forward_transform|compose|forward_transform|inverse_transform|transform_image|transform_dataset" "$BASE" -S || true

Repository: Project-MONAI/physiomotion4d

Length of output: 9965


Fix Greedy temp-file affine convention (LPS→RAS) for -ia/-it

_write_affine_matrix_file() writes a “4x4 RAS” matrix, but registration_method() builds initial_affine directly from initial_forward_transform.GetMatrix()/GetTranslation()/GetCenter (ITK/LPS convention) and passes it through unchanged (lines ~140-162, ~342-356). With PICSL Greedy, the -ia/-it .mat is interpreted in RAS voxel-coordinate ordering, so the initial transform can be serialized in the wrong basis.

Convert from ITK/LPS to Greedy/RAS at the Greedy boundary (or change the helper contract/docstring to accept ITK/LPS and convert before writing).

🤖 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/physiomotion4d/register_images_greedy.py` around lines 140 - 160, The
function _write_affine_matrix_file writes a 4x4 RAS matrix but callers (e.g.,
registration_method building initial_affine from
initial_forward_transform.GetMatrix()/GetTranslation()/GetCenter) supply ITK/LPS
matrices, so the .mat is in the wrong convention; fix by converting the incoming
LPS affine into Greedy/RAS before saving: construct S = np.diag([-1, -1, 1, 1])
and replace the write value with mat_ras = S @ mat_4x4 @ S (then save mat_ras),
and update the _write_affine_matrix_file docstring to state it accepts an
ITK/LPS matrix and writes a Greedy RAS .mat (or explicitly note the conversion).

Comment on lines +158 to +160
fd, path = tempfile.mkstemp(suffix=".mat", prefix="greedy_aff_")
os.close(fd)
np.savetxt(path, mat_4x4, fmt="%.10f")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the relevant sections in the file
FILE="src/physiomotion4d/register_images_greedy.py"
ls -l "$FILE" 2>/dev/null || true

# Show around the originally referenced lines (158-160)
nl -ba "$FILE" | sed -n '130,260p'

# Show any other nearby occurrences of "-ia" / "-it" and temp paths
rg -n --hidden --no-ignore-vcs "(-ia|-it|mkstemp|tempfile|savetxt|greedy_aff_)" "$FILE" || true

Repository: Project-MONAI/physiomotion4d

Length of output: 228


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/physiomotion4d/register_images_greedy.py"

# Print around the referenced line ranges using awk (no nl dependency)
echo "=== Lines 140-190 ==="
awk 'NR>=140 && NR<=190 {printf "%5d  %s\n", NR, $0}' "$FILE"

echo "=== Lines 215-245 ==="
awk 'NR>=215 && NR<=245 {printf "%5d  %s\n", NR, $0}' "$FILE"

echo "=== Lines 255-310 ==="
awk 'NR>=255 && NR<=310 {printf "%5d  %s\n", NR, $0}' "$FILE"

echo "=== Search for Greedy -ia/-it and temp path usage ==="
rg -n --hidden --no-ignore-vcs "(-ia|-it|mkstemp|tempfile|savetxt|greedy_aff_|cmd\\s*=|Greedy|greedy)" "$FILE" || true

Repository: Project-MONAI/physiomotion4d

Length of output: 11260


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search within repo for Greedy3D / execute implementation or wrappers
rg -n --hidden --no-ignore-vcs "picsl_greedy|Greedy3D|def execute|execute\\(" src/physiomotion4d || true

# Search for any quoting/shlex usage related to greedy commands
rg -n --hidden --no-ignore-vcs "shlex|quote\\(|mkdtemp\\(|tempdir|tempfile\\.gettempdir" src/physiomotion4d/register_images_greedy.py src/physiomotion4d || true

Repository: Project-MONAI/physiomotion4d

Length of output: 948


🌐 Web query:

picsl-greedy Greedy3D execute cmd kwargs placeholders -ia -it

💡 Result:

In the context of the picsl-greedy Python package, the Greedy3D.execute method is used to run greedy registration commands [1][2][3]. The greedy tool itself is a fast deformable registration software for 2D and 3D medical images [4][5][6]. Regarding your query about execute cmd kwargs placeholders like -ia and -it: 1. Placeholders and Keyword Arguments: The picsl-greedy Python wrapper allows you to substitute file paths in your greedy command string with arbitrary placeholders (e.g., my_fixed, my_moving) [2][3]. You then pass these placeholders as keyword arguments to the execute method, mapping them to objects in your Python environment such as SimpleITK images or NumPy arrays [2][3]. For example: g.execute('-i my_fixed my_moving -o my_output', my_fixed=img_obj, my_moving=img_obj) [2][3] 2. Standard Greedy Flags (-ia, -it): These are standard command-line flags for the underlying greedy registration tool, not specific to the Python wrapper's placeholder functionality [4]. * -ia: Often used in combination with other flags to specify input image affine/initialization parameters [4]. * -it: Typically stands for "Initial Transformation," used to provide a starting transformation matrix or field for the registration process [4]. The picsl-greedy wrapper simply passes these strings directly to the greedy binary [2][3]. You can use the standard greedy documentation to understand how these specific flags function within your registration pipeline [4].

Citations:


Avoid interpolating temporary affine file paths into Greedy command strings

src/physiomotion4d/register_images_greedy.py builds the Greedy commands by directly embedding the absolute temp path returned from tempfile.mkstemp():

  • cmd += f" -ia {initial_affine_file}" (around 229)
  • -it {initial_affine_file} in cmd_def (around 277-278)

If the temp directory path can contain spaces, whitespace tokenization can break the -ia/-it arguments. Use picsl-greedy’s placeholder/kwargs substitution for the init-transform argument instead of splicing the path into the command string; otherwise, ensure the temp file is created under a known space-free directory (e.g., /tmp) / escape in a way the wrapper will keep it as a single argument.

🤖 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/physiomotion4d/register_images_greedy.py` around lines 158 - 160, The
command-building currently injects the temp affine filepath (created via
tempfile.mkstemp and saved with np.savetxt into variable initial_affine_file)
directly into the Greedy command string (see uses in cmd and cmd_def), which
breaks if the temp path contains spaces; change to pass the init-transform path
via the picsl-greedy wrapper’s placeholder/kwargs substitution (i.e., use the
wrapper’s parameter for "-ia"/"-it" instead of f-string interpolation), or if
substitution isn’t available create the temp file in a guaranteed space-free
directory like /tmp (use tempfile.NamedTemporaryFile(dir="/tmp", delete=False)
or equivalent) and reference that variable without string concatenation so the
wrapper receives it as a single argument; update usages around where cmd += f"
-ia {initial_affine_file}" and the "-it {initial_affine_file}" insertion in
cmd_def accordingly.

Comment on lines +93 to +95
Also, use this to specify the path to store the downloaded weights. The
file must not exist for the weights to be downloaded correctly. Typical
suffix is ".trch".
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix contradictory set_weights_path contract text.

At Line 93, the docstring says the target file must not exist, but this setter is also documented/used for loading an existing custom checkpoint. That contradiction will mislead callers.

Proposed docstring fix
-        Also, use this to specify the path to store the downloaded weights.  The
-        file must not exist for the weights to be downloaded correctly.  Typical
-        suffix is ".trch".
+        Also, this path is passed to the UniGradICON loader as ``weights_location``:
+        it can point to an existing checkpoint to load, or to a target path used
+        by the loader when downloading default weights. Typical suffix is ".trch".
📝 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
Also, use this to specify the path to store the downloaded weights. The
file must not exist for the weights to be downloaded correctly. Typical
suffix is ".trch".
Also, this path is passed to the UniGradICON loader as ``weights_location``:
it can point to an existing checkpoint to load, or to a target path used
by the loader when downloading default weights. Typical suffix is ".trch".
🤖 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/physiomotion4d/register_images_icon.py` around lines 93 - 95, The
docstring for the setter set_weights_path in register_images_icon.py is
contradictory: it currently says the target file must not exist (for
downloading) but the setter is also used to point to an existing custom
checkpoint; update the docstring to clearly state both behaviors — if the path
points to a non-existent file the weights will be downloaded there (typical
suffix ".trch"), and if it points to an existing file it will be used as the
custom checkpoint to load — and remove the absolute "must not exist" wording
while giving both cases and expected suffix/format.

Comment on lines +306 to +328
main_module = sys.modules.get("__main__")
main_file = getattr(main_module, "__file__", None)
top_dir = pathlib.Path.cwd()
if main_file is not None:
top_dir = pathlib.Path(main_file).resolve().parent
if self.use_multi_modality:
if self.weights_path is None:
self.weights_path = str(
top_dir
/ "network_weights"
/ "multigradicon1.0"
/ "Step_2_final.trch"
)
self.net = get_multigradicon(
loss_fn=icon.LNCC(sigma=5),
apply_intensity_conservation_loss=self.use_mass_preservation,
weights_location=self.weights_path,
)
else:
if self.weights_path is None:
self.weights_path = str(
top_dir / "network_weights" / "unigradicon1.0" / "Step_2_final.trch"
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden default weights root fallback when __main__.__file__ points outside the project.

At Line 309-Line 310, choosing script-parent unconditionally can resolve to non-writable launcher dirs (test runners, console entrypoints), which breaks default weight loading for callers that don’t set weights_path.

Proposed resilience fix
         main_module = sys.modules.get("__main__")
         main_file = getattr(main_module, "__file__", None)
         top_dir = pathlib.Path.cwd()
         if main_file is not None:
-            top_dir = pathlib.Path(main_file).resolve().parent
+            candidate = pathlib.Path(main_file).resolve().parent
+            # Prefer script-relative directory only when it looks usable;
+            # otherwise keep cwd fallback.
+            if (candidate / "network_weights").exists() or candidate.is_dir():
+                top_dir = candidate
🤖 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/physiomotion4d/register_images_icon.py` around lines 306 - 328, The code
currently sets top_dir to the parent of __main__.__file__ without validating
that the script lives inside the project, which can point to non-writable
launcher dirs; change the top_dir selection logic in the block initializing
top_dir/main_file so that you: (1) start with pathlib.Path.cwd() as default, (2)
if main_file is not None, resolve it and only adopt its parent when that parent
is a descendant of the project cwd or contains a project marker (e.g., .git,
pyproject.toml, setup.py), otherwise keep cwd; (3) additionally verify the
chosen top_dir is writable and if not fallback to a writable directory (e.g.,
user home or cwd); update the code around main_module/main_file/top_dir and use
this validated top_dir when building default self.weights_path for both
use_multi_modality branches so get_multigradicon and the unigradicon path logic
use the safe, writable fallback.

Comment on lines +78 to +79
>>> forward_tfms = result['forward_transforms'] # warp moving images -> fixed grid
>>> inverse_tfms = result['inverse_transforms'] # warp fixed image -> moving grids
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use double quotes in changed Python examples.

Changed example snippets still use single quotes for string literals.

Proposed style fix
-        >>> forward_tfms = result['forward_transforms']  # warp moving images -> fixed grid
-        >>> inverse_tfms = result['inverse_transforms']  # warp fixed image -> moving grids
+        >>> forward_tfms = result["forward_transforms"]  # warp moving images -> fixed grid
+        >>> inverse_tfms = result["inverse_transforms"]  # warp fixed image -> moving grids
...
-            ...     moving_labelmaps=labelmap_list,  # Optional
+            ...     moving_labelmaps=labelmap_list,  # Optional
...
-            ...     zip(result['forward_transforms'], result['losses'])
+            ...     zip(result["forward_transforms"], result["losses"])

As per coding guidelines: "Use double quotes for strings and docstrings".

Also applies to: 294-295

🤖 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/physiomotion4d/register_time_series_images.py` around lines 78 - 79,
Update the example string literals to use double quotes: replace single-quoted
strings in the example assignments referencing result['forward_transforms'] and
result['inverse_transforms'] (symbols: forward_tfms, inverse_tfms, result) with
double-quoted equivalents, and apply the same change to the other occurrences
noted (the similar examples around the occurrences of those variables later in
the file).

Comment on lines +214 to +215
fixed_labelmap (itk.Image): Labelmap defining ROI
"""
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Docstring type should match Optional[itk.Image].

fixed_labelmap is typed as optional in the signature, but the Args section documents it as required.

Proposed docstring fix
-            fixed_labelmap (itk.Image): Labelmap defining ROI
+            fixed_labelmap (Optional[itk.Image]): Labelmap defining ROI, or None

As per coding guidelines: "Use full type hints with mypy strict mode; use Optional[X] not X | None".

📝 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
fixed_labelmap (itk.Image): Labelmap defining ROI
"""
fixed_labelmap (Optional[itk.Image]): Labelmap defining ROI, or None
"""
🤖 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/physiomotion4d/register_time_series_images.py` around lines 214 - 215,
The docstring for the parameter fixed_labelmap currently documents it as
required itk.Image but the function signature uses Optional[itk.Image]; update
the Args section to reflect Optional[itk.Image] (or itk.Image | None) and
indicate it may be None/default omitted, and adjust the description to say it is
an optional labelmap defining the ROI; locate the parameter entry for
fixed_labelmap in the docstring of the register_time_series_images function and
change its type and wording accordingly.

Comment on lines +333 to +543
def test_initial_transform_composition_metrics(
self,
registrar_ANTS: RegisterImagesANTS,
test_images: list[Any],
test_directories: dict[str, Path],
) -> None:
"""Verify the initial_forward_transform composition path with metrics.

Exercises the two initial-transform inputs the platform actually uses
(identity and a prior deformable forward_transform, as in prior-based
time-series registration) and confirms the composed forward_transform
warps the moving image onto the fixed grid. Scored with foreground NCC
over the brightest 30% of the fixed image (tissue/blood pool). See
docs/developer/transform_conventions.

Asserted facts:
* a plain registration improves on the unregistered pair,
* an identity initial reproduces the baseline exactly (the
composition machinery is a structurally correct no-op; note an
identity AffineTransform is itself a matrix initial),
* a prior-deformable initial reaches the no-initial baseline quality
(the composition recovers the full transform).

The initial transform is applied by pre-warping the moving image (as in
RegisterImagesICON), which keeps the composition self-consistent for any
initial transform type.
"""
output_dir = test_directories["output"]
reg_output_dir = output_dir / "registration_ANTS"
reg_output_dir.mkdir(exist_ok=True)

# Pick two phases that are far apart in the cycle so there is real motion.
fixed_image = test_images[0]
moving_image = test_images[min(10, len(test_images) - 1)]

fixed_arr = itk.array_from_image(fixed_image)
# Moving and fixed share the acquisition grid (split from one 4D image),
# so the moving array is directly comparable for the unregistered score.
moving_arr = itk.array_from_image(moving_image)
threshold = float(np.percentile(fixed_arr, 70.0))
foreground = fixed_arr > threshold

transform_tools = TransformTools()

def warp_score(forward_transform: Any) -> float:
warped = transform_tools.transform_image(
moving_image,
forward_transform,
fixed_image,
interpolation_method="linear",
)
return _foreground_ncc(fixed_arr, itk.array_from_image(warped), foreground)

ncc_unregistered = _foreground_ncc(fixed_arr, moving_arr, foreground)

# Baseline: no initial transform.
registrar_ANTS.set_modality("ct")
registrar_ANTS.set_fixed_image(fixed_image)
baseline = registrar_ANTS.register(moving_image=moving_image)
ncc_baseline = warp_score(baseline["forward_transform"])

# Identity initial: the composition machinery must be a no-op.
identity = itk.AffineTransform[itk.D, 3].New()
identity.SetIdentity()
registrar_identity = RegisterImagesANTS()
registrar_identity.set_modality("ct")
registrar_identity.set_fixed_image(fixed_image)
identity_result = registrar_identity.register(
moving_image=moving_image, initial_forward_transform=identity
)
ncc_identity = warp_score(identity_result["forward_transform"])

# Prior deformable initial: the realistic time-series prior use case.
registrar_prior = RegisterImagesANTS()
registrar_prior.set_modality("ct")
registrar_prior.set_fixed_image(fixed_image)
prior_result = registrar_prior.register(
moving_image=moving_image,
initial_forward_transform=baseline["forward_transform"],
)
ncc_prior = warp_score(prior_result["forward_transform"])

print("\nANTS initial-transform composition metrics (foreground NCC):")
print(f" unregistered: {ncc_unregistered:.4f}")
print(f" baseline (no initial): {ncc_baseline:.4f}")
print(f" identity initial: {ncc_identity:.4f}")
print(f" prior-deformable init: {ncc_prior:.4f}")

warped_prior = transform_tools.transform_image(
moving_image,
prior_result["forward_transform"],
fixed_image,
interpolation_method="linear",
)
itk.imwrite(
warped_prior,
str(reg_output_dir / "ants_warped_prior_initial.mha"),
compression=True,
)

# Registration must improve alignment over the unregistered pair.
assert ncc_baseline > ncc_unregistered, (
f"Baseline registration did not improve alignment: "
f"{ncc_baseline:.4f} <= {ncc_unregistered:.4f}"
)
# Identity initial must reproduce the baseline (composition is a no-op).
assert abs(ncc_identity - ncc_baseline) < 0.03, (
f"Identity initial transform changed the result: "
f"identity={ncc_identity:.4f} vs baseline={ncc_baseline:.4f}"
)
# A prior-deformable initial must reach the no-initial baseline quality
# (the composition recovers the full transform).
assert ncc_prior >= ncc_baseline - 0.03, (
f"Prior-initial composition did not reach baseline quality: "
f"{ncc_prior:.4f} < {ncc_baseline:.4f} - 0.03"
)

def test_initial_transform_matrix_composition(
self,
registrar_ANTS: RegisterImagesANTS,
test_images: list[Any],
) -> None:
"""A matrix (translation/affine) initial composes without corruption.

Regression guard for the previously-broken matrix initial_transform
path: feeding a translation initial used to corrupt the composition
(foreground NCC far below the unregistered pair). With the moving image
pre-warped by the initial, the composed forward_transform must align the
moving image onto the fixed grid at least as well as the unregistered
pair.
"""
fixed_image = test_images[0]
moving_image = test_images[min(10, len(test_images) - 1)]

fixed_arr = itk.array_from_image(fixed_image)
threshold = float(np.percentile(fixed_arr, 70.0))
foreground = fixed_arr > threshold
ncc_unregistered = _foreground_ncc(
fixed_arr, itk.array_from_image(moving_image), foreground
)

translation = itk.TranslationTransform[itk.D, 3].New()
translation.SetOffset([-5.0, -5.0, -5.0])

registrar_ANTS.set_modality("ct")
registrar_ANTS.set_fixed_image(fixed_image)
result = registrar_ANTS.register(
moving_image=moving_image, initial_forward_transform=translation
)

transform_tools = TransformTools()
warped = transform_tools.transform_image(
moving_image,
result["forward_transform"],
fixed_image,
interpolation_method="linear",
)
ncc = _foreground_ncc(fixed_arr, itk.array_from_image(warped), foreground)
print(
f"\nMatrix-initial composed NCC={ncc:.4f} (unregistered={ncc_unregistered:.4f})"
)
assert ncc > ncc_unregistered, (
f"Matrix-initial composition worse than unregistered: "
f"{ncc:.4f} <= {ncc_unregistered:.4f}"
)

def test_affine_and_rigid_transform_types(
self,
registrar_ANTS: RegisterImagesANTS,
test_images: list[Any],
) -> None:
"""Affine and Rigid transform types run and improve alignment.

Regression guard for the ANTS preset names: ``set_transform_type``
previously mapped Affine/Rigid to ``antsRegistration{Affine,Rigid}Quick``
preset strings that do not exist in antspy, raising ValueError. Each
type must now run and warp the moving image onto the fixed grid at least
as well as the unregistered pair.
"""
fixed_image = test_images[0]
moving_image = test_images[min(10, len(test_images) - 1)]

fixed_arr = itk.array_from_image(fixed_image)
threshold = float(np.percentile(fixed_arr, 70.0))
foreground = fixed_arr > threshold
ncc_unregistered = _foreground_ncc(
fixed_arr, itk.array_from_image(moving_image), foreground
)

transform_tools = TransformTools()
for transform_type in ("Rigid", "Affine"):
registrar = RegisterImagesANTS()
registrar.set_modality("ct")
registrar.set_transform_type(transform_type)
registrar.set_fixed_image(fixed_image)
result = registrar.register(moving_image=moving_image)
warped = transform_tools.transform_image(
moving_image,
result["forward_transform"],
fixed_image,
interpolation_method="linear",
)
ncc = _foreground_ncc(fixed_arr, itk.array_from_image(warped), foreground)
print(
f"\n{transform_type} transform NCC={ncc:.4f} "
f"(unregistered={ncc_unregistered:.4f})"
)
assert ncc > ncc_unregistered, (
f"{transform_type} registration did not improve alignment: "
f"{ncc:.4f} <= {ncc_unregistered:.4f}"
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Please add inverse-composition coverage too.

These regressions only score forward_transform, but the implementation also changed how inverse_transform is built when initial_forward_transform is present. A broken fixed→moving path would still pass this suite, so I'd add one check that warps the fixed image with inverse_transform and compares it against the expected moving-grid result.

As per coding guidelines, "Update docstrings and tests for every changed public method".

🤖 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_register_images_ants.py` around lines 333 - 543, Add coverage that
validates the inverse composition path by warping the fixed image using the
returned inverse transform and scoring against the original moving image grid:
after each registration call that currently uses result["forward_transform"]
(e.g., baseline, identity_result, prior_result, and the matrix/transform_type
loops), also retrieve result["inverse_transform"] and call
TransformTools().transform_image(fixed_image, inverse_transform, moving_image,
interpolation_method="linear") then compute _foreground_ncc between
itk.array_from_image(moving_image) and the warped-fixed array (using the same
foreground mask) and assert it meets the same improvement/equality thresholds as
the forward checks; ensure you reference result["inverse_transform"] and
TransformTools.transform_image when adding these assertions.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 30, 2026

Codecov Report

❌ Patch coverage is 49.50495% with 51 lines in your changes missing coverage. Please review.
✅ Project coverage is 32.80%. Comparing base (d1c4c95) to head (cd1d3e9).

Files with missing lines Patch % Lines
src/physiomotion4d/register_images_greedy.py 10.71% 25 Missing ⚠️
src/physiomotion4d/register_images_icon.py 18.18% 9 Missing ⚠️
src/physiomotion4d/register_images_ants.py 0.00% 6 Missing ⚠️
...rc/physiomotion4d/register_models_distance_maps.py 25.00% 3 Missing ⚠️
src/physiomotion4d/register_images_base.py 50.00% 2 Missing ⚠️
src/physiomotion4d/segment_heart_simpleware.py 0.00% 2 Missing ⚠️
...ion4d/workflow_fit_statistical_model_to_patient.py 50.00% 2 Missing ⚠️
src/physiomotion4d/contour_tools.py 0.00% 1 Missing ⚠️
src/physiomotion4d/register_time_series_images.py 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #63      +/-   ##
==========================================
+ Coverage   32.58%   32.80%   +0.22%     
==========================================
  Files          52       53       +1     
  Lines        7156     7181      +25     
==========================================
+ Hits         2332     2356      +24     
- Misses       4824     4825       +1     
Flag Coverage Δ
integration-tests 32.69% <49.50%> (?)
unittests 32.79% <49.50%> (+0.20%) ⬆️

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

☔ View full report in Codecov by Sentry.
📢 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
src/physiomotion4d/workflow_fine_tune_icon_registration.py (1)

749-767: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid double-dilating the registration masks.

apply_registration() derives dilated binary masks here, but RegisterImagesBase.set_fixed_mask() and RegisterImagesBase.register() convert and dilate incoming masks again. That turns one mask_dilation_mm into two successive dilations and widens the ROI more than the caller requested.

Proposed fix
-        fixed_mask = (
-            self.labelmap_tools.convert_labelmap_to_mask(
-                reference_segmentation, dilation_in_mm=self.mask_dilation_mm
-            )
-            if reference_segmentation is not None
-            else None
-        )
-        moving_masks: Optional[list[Optional[itk.Image]]] = None
-        if moving_segmentations is not None:
-            moving_masks = [
-                (
-                    self.labelmap_tools.convert_labelmap_to_mask(
-                        seg, dilation_in_mm=self.mask_dilation_mm
-                    )
-                    if seg is not None
-                    else None
-                )
-                for seg in moving_segmentations
-            ]
+        # RegisterImagesBase converts aligned binary masks/labelmaps to the
+        # actual registration masks, including dilation.
+        fixed_mask = reference_segmentation
+        moving_masks = moving_segmentations
🤖 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/physiomotion4d/workflow_fine_tune_icon_registration.py` around lines 749
- 767, The code currently pre-converts/dilates segmentations into masks using
self.labelmap_tools.convert_labelmap_to_mask (producing fixed_mask and
moving_masks), which causes RegisterImagesBase.set_fixed_mask() /
RegisterImagesBase.register() to convert/dilate them again and double-dilate; to
fix, stop pre-dilating here in apply_registration: instead of calling
self.labelmap_tools.convert_labelmap_to_mask use the raw reference_segmentation
and moving_segmentations (or None) so fixed_mask and moving_masks hold the
original labelmaps, and let RegisterImagesBase.set_fixed_mask() / register()
perform the single conversion/dilation using mask_dilation_mm; update the
variables fixed_mask and moving_masks accordingly and remove the dilation_in_mm
argument usage in this function.
♻️ Duplicate comments (1)
src/physiomotion4d/register_images_ants.py (1)

602-609: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Pre-warp onto the registration grid and pre-warp the moving mask.

Two critical coordinate-system mismatches:

  1. Grid mismatch: Line 608 resamples moving_image_pre onto self.fixed_image, but the subsequent ants.registration() calls (lines 668, 684) use self.fixed_image_pre as fixed. If preprocess() changes spacing/origin/size, the pre-warped image and the registration optimize on different grids, causing alignment errors.

  2. Mask mismatch: When initial_forward_transform is supplied, self.moving_image_pre is pre-warped onto the fixed grid (line 605-608) but self.moving_mask (set at line 590) remains on the original moving grid. The masked registration (line 672) then passes an image and mask in different coordinate systems to ANTs, so the mask constrains the wrong ROI.

🐛 Proposed fix
         if initial_forward_transform is not None:
             self.log_info("Pre-warping moving image with initial transform...")
             transform_tools = TransformTools()
             self.moving_image_pre = transform_tools.transform_image(
                 self.moving_image_pre,
                 initial_forward_transform,
-                self.fixed_image,
+                self.fixed_image_pre,
             )
+            if self.moving_mask is not None:
+                self.moving_mask = transform_tools.transform_image(
+                    self.moving_mask,
+                    initial_forward_transform,
+                    self.fixed_image_pre,
+                    interpolation_method="nearest",
+                )
🤖 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/physiomotion4d/register_images_ants.py` around lines 602 - 609, The code
pre-warps self.moving_image_pre with initial_forward_transform onto
self.fixed_image but later registration uses self.fixed_image_pre and expects
both image and mask on the same preprocessed grid; also self.moving_mask is left
on the original moving grid, so ants.registration receives misaligned inputs.
Fix by pre-warping both the moving image and the moving mask with
initial_forward_transform onto the same grid used for registration (the
preprocess() output referenced by self.fixed_image_pre), i.e., call
TransformTools.transform_image/transform_mask (or the equivalent method) to
resample self.moving_image_pre and self.moving_mask into the fixed_image_pre
coordinate system whenever initial_forward_transform is provided, so that
ants.registration(...) always receives image, mask and fixed that share the same
spacing/origin/size.
🧹 Nitpick comments (2)
tests/test_labelmap_tools.py (2)

98-109: ⚡ Quick win

Actually verify direction metadata here.

This test currently proves spacing and origin preservation only. If convert_labelmap_to_mask() dropped direction cosines, it would still pass. Setting a non-identity direction on labelmap and asserting it on mask would cover the behavior the docstring promises.

🤖 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_labelmap_tools.py` around lines 98 - 109, The test
test_preserves_image_information currently only checks spacing and origin;
update it to also set a non-identity direction on the input labelmap (use
labelmap.SetDirection with a 3x3 direction cosine tuple) before calling
labelmap_tools.convert_labelmap_to_mask and then assert that mask.GetDirection()
matches the same direction values; reference the test function name, the
labelmap.SetDirection call, and the labelmap_tools.convert_labelmap_to_mask /
mask.GetDirection methods to locate where to add the additional setup and
assertion.

28-28: ⚡ Quick win

Add shape and axis-order details to these test docstrings.

These tests build synthetic itk.Image volumes from NumPy arrays, but the docstrings do not spell out the array shape or the NumPy-vs-ITK axis order. In this module that distinction is part of the behavior under test.

As per coding guidelines "tests/**/*.py: State image shape and axis order in every test docstring" and "**/*.py: State axis order and shape explicitly in every docstring and comment that touches arrays".

Also applies to: 42-42, 58-58, 81-81, 99-99

🤖 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_labelmap_tools.py` at line 28, Update the test docstrings in
test_labelmap_tools.py for the tests that construct synthetic itk.Image volumes
so each docstring explicitly states the NumPy array shape and the axis-order
mapping between NumPy and ITK (e.g., "NumPy array shape (Z, Y, X); ITK image
axis order (X, Y, Z)"), applying the same change to the other similar tests in
the file; locate the docstrings in the tests that build itk.Image from NumPy
arrays and add a one-line note with the exact array shape and axis order there.
🤖 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.

Outside diff comments:
In `@src/physiomotion4d/workflow_fine_tune_icon_registration.py`:
- Around line 749-767: The code currently pre-converts/dilates segmentations
into masks using self.labelmap_tools.convert_labelmap_to_mask (producing
fixed_mask and moving_masks), which causes RegisterImagesBase.set_fixed_mask() /
RegisterImagesBase.register() to convert/dilate them again and double-dilate; to
fix, stop pre-dilating here in apply_registration: instead of calling
self.labelmap_tools.convert_labelmap_to_mask use the raw reference_segmentation
and moving_segmentations (or None) so fixed_mask and moving_masks hold the
original labelmaps, and let RegisterImagesBase.set_fixed_mask() / register()
perform the single conversion/dilation using mask_dilation_mm; update the
variables fixed_mask and moving_masks accordingly and remove the dilation_in_mm
argument usage in this function.

---

Duplicate comments:
In `@src/physiomotion4d/register_images_ants.py`:
- Around line 602-609: The code pre-warps self.moving_image_pre with
initial_forward_transform onto self.fixed_image but later registration uses
self.fixed_image_pre and expects both image and mask on the same preprocessed
grid; also self.moving_mask is left on the original moving grid, so
ants.registration receives misaligned inputs. Fix by pre-warping both the moving
image and the moving mask with initial_forward_transform onto the same grid used
for registration (the preprocess() output referenced by self.fixed_image_pre),
i.e., call TransformTools.transform_image/transform_mask (or the equivalent
method) to resample self.moving_image_pre and self.moving_mask into the
fixed_image_pre coordinate system whenever initial_forward_transform is
provided, so that ants.registration(...) always receives image, mask and fixed
that share the same spacing/origin/size.

---

Nitpick comments:
In `@tests/test_labelmap_tools.py`:
- Around line 98-109: The test test_preserves_image_information currently only
checks spacing and origin; update it to also set a non-identity direction on the
input labelmap (use labelmap.SetDirection with a 3x3 direction cosine tuple)
before calling labelmap_tools.convert_labelmap_to_mask and then assert that
mask.GetDirection() matches the same direction values; reference the test
function name, the labelmap.SetDirection call, and the
labelmap_tools.convert_labelmap_to_mask / mask.GetDirection methods to locate
where to add the additional setup and assertion.
- Line 28: Update the test docstrings in test_labelmap_tools.py for the tests
that construct synthetic itk.Image volumes so each docstring explicitly states
the NumPy array shape and the axis-order mapping between NumPy and ITK (e.g.,
"NumPy array shape (Z, Y, X); ITK image axis order (X, Y, Z)"), applying the
same change to the other similar tests in the file; locate the docstrings in the
tests that build itk.Image from NumPy arrays and add a one-line note with the
exact array shape and axis order there.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 11651f73-7a7e-439e-b8c0-535dfccca177

📥 Commits

Reviewing files that changed from the base of the PR and between e6a8b02 and cd1d3e9.

📒 Files selected for processing (21)
  • docs/api/utilities/index.rst
  • docs/api/utilities/labelmap_tools.rst
  • experiments/LongitudinalRegistration/1-finetune_icon.py
  • experiments/LongitudinalRegistration/1-preregistration.py
  • experiments/LongitudinalRegistration/2-recon_4d_icon_eval.py
  • pyproject.toml
  • src/physiomotion4d/__init__.py
  • src/physiomotion4d/contour_tools.py
  • src/physiomotion4d/image_tools.py
  • src/physiomotion4d/labelmap_tools.py
  • src/physiomotion4d/register_images_ants.py
  • src/physiomotion4d/register_images_base.py
  • src/physiomotion4d/register_images_icon.py
  • src/physiomotion4d/register_models_distance_maps.py
  • src/physiomotion4d/segment_anatomy_base.py
  • src/physiomotion4d/segment_heart_simpleware.py
  • src/physiomotion4d/workflow_fine_tune_icon_registration.py
  • src/physiomotion4d/workflow_fit_statistical_model_to_patient.py
  • tests/test_image_tools.py
  • tests/test_labelmap_tools.py
  • tests/test_workflow_fine_tune_icon_registration.py
💤 Files with no reviewable changes (2)
  • src/physiomotion4d/register_images_icon.py
  • src/physiomotion4d/segment_anatomy_base.py
✅ Files skipped from review due to trivial changes (4)
  • src/physiomotion4d/init.py
  • tests/test_image_tools.py
  • docs/api/utilities/index.rst
  • docs/api/utilities/labelmap_tools.rst
🚧 Files skipped from review as they are similar to previous changes (2)
  • experiments/LongitudinalRegistration/1-preregistration.py
  • experiments/LongitudinalRegistration/2-recon_4d_icon_eval.py

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.

2 participants