Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/architecture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ Primary Workflows

``WorkflowFitStatisticalModelToPatient``
Fits a template/statistical model to patient-specific surfaces with ICP,
optional PCA fitting, mask-to-mask registration, and optional image
refinement.
optional PCA fitting, labelmap-to-labelmap registration, and optional
labelmap-to-image refinement.

``WorkflowReconstructHighres4DCT``
Reconstructs higher-resolution 4D CT frames from a time series and a fixed
Expand Down
20 changes: 10 additions & 10 deletions docs/cli_scripts/fit_statistical_model_to_patient.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ The registration pipeline consists of four stages:

1. **ICP Alignment**: Rigid/affine alignment using surface matching
2. **PCA Registration** (optional): Statistical shape model fitting
3. **Mask-to-Mask Registration**: Greedy affine + ICON deformable registration using distance maps
4. **Mask-to-Image Refinement** (optional): Final intensity-based refinement
3. **Labelmap-to-Labelmap Registration**: Greedy affine + ICON deformable registration using distance maps
4. **Labelmap-to-Image Refinement** (optional): Final intensity-based refinement

Installation
============
Expand Down Expand Up @@ -84,7 +84,7 @@ Optional inputs:

``--template-labelmap PATH``
Path to template labelmap image (.nii.gz, .nrrd, .mha). Required only when
``--mask-to-image`` is set.
``--labelmap-to-image`` is set.

See :class:`physiomotion4d.WorkflowFitStatisticalModelToPatient` for API documentation.

Expand Down Expand Up @@ -112,16 +112,16 @@ PCA Registration Options
Registration Configuration
---------------------------

``--no-mask-to-mask``
Disable mask-to-mask deformable registration (default: enabled)
``--no-labelmap-to-labelmap``
Disable labelmap-to-labelmap deformable registration (default: enabled)

``--mask-to-image``
Enable mask-to-image refinement registration. Requires
``--labelmap-to-image``
Enable labelmap-to-image refinement registration. Requires
``--template-labelmap`` and template label IDs. Disabled by default.

``--use-ICON-refinement``
Enable ICON deep learning refinement in the mask-to-image stage (Stage 4).
The mask-to-mask stage always uses Greedy affine + ICON deformable.
Enable ICON deep learning refinement in the labelmap-to-image stage (Stage 4).
The labelmap-to-labelmap stage always uses Greedy affine + ICON deformable.
Default: disabled

Output Options
Expand Down Expand Up @@ -180,7 +180,7 @@ Intermediate Results

* ``{prefix}_icp_surface.vtp`` - Result after ICP alignment
* ``{prefix}_pca_surface.vtp`` - Result after PCA fitting (if used)
* ``{prefix}_m2m_surface.vtp`` - Result after mask-to-mask registration
* ``{prefix}_l2l_surface.vtp`` - Result after labelmap-to-labelmap registration

See Also
========
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
moving_model=moving_model,
fixed_model=fixed_model,
reference_image=reference_image,
roi_dilation_mm=20.0, # Dilation for ROI mask
mask_dilation_mm=20.0, # Dilation for binary registration mask
)

# Perform Greedy affine + ICON deformable registration
Expand Down Expand Up @@ -338,5 +338,5 @@
# - The `RegisterModelsDistanceMaps` class uses a two-stage pipeline:
# 1. **Greedy affine** registration (fast CPU-based alignment)
# 2. **ICON deformable** registration on the affine-pre-aligned masks (deep learning)
# - The `roi_dilation_mm` parameter controls the dilation of the ROI mask (default 20mm)
# - The `mask_dilation_mm` parameter controls the dilation of the binary registration mask (default 20mm)
# - Composed Greedy + ICON transforms provide smooth, invertible deformation fields for anatomical correspondence
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
True, pca_model=model_pca_data, pca_number_of_modes=model_pca_n_modes
)

registrar.set_use_mask_to_mask_registration(True)
registrar.set_use_labelmap_to_labelmap_registration(True)

# %%
patient_image = registrar.patient_image
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,16 @@
registrar.set_use_pca_registration(
True, pca_model=pca_model, pca_number_of_modes=pca_n_modes
)
registrar.set_use_mask_to_image_registration(
registrar.set_use_labelmap_to_image_registration(
True,
template_labelmap=template_labelmap,
template_labelmap_organ_mesh_ids=[1],
template_labelmap_organ_extra_ids=[2, 3, 4, 5],
template_labelmap_background_ids=[6],
)

registrar.set_mask_dilation_mm(0)
registrar.set_roi_dilation_mm(25)
registrar.set_labelmap_dilation_mm(0)
registrar.set_mask_dilation_mm(25)

patient_image = registrar.patient_image
itk.imwrite(
Expand Down Expand Up @@ -184,37 +184,37 @@
itk.imwrite(pca_labelmap, str(output_dir / "pca_labelmap.mha"), compression=True)

# %% [markdown]
# ## Mask Alignment
# ## Labelmap Alignment

# %%
# Perform deformable registration
print("Starting deformable mask-to-mask registration...")
print("Starting deformable labelmap-to-labelmap registration...")

m2m_results = registrar.register_mask_to_mask()
m2m_inverse_transform = m2m_results["inverse_transform"]
m2m_forward_transform = m2m_results["forward_transform"]
m2m_model_surface = m2m_results["registered_template_model_surface"]
m2m_labelmap = m2m_results["registered_template_labelmap"]
l2l_results = registrar.register_labelmap_to_labelmap()
l2l_inverse_transform = l2l_results["inverse_transform"]
l2l_forward_transform = l2l_results["forward_transform"]
l2l_model_surface = l2l_results["registered_template_model_surface"]
l2l_labelmap = l2l_results["registered_template_labelmap"]

print("Registration complete!")

m2m_model_surface.save(str(output_dir / "m2m_model_surface.vtp"))
itk.imwrite(m2m_labelmap, str(output_dir / "m2m_labelmap.mha"), compression=True)
l2l_model_surface.save(str(output_dir / "l2l_model_surface.vtp"))
itk.imwrite(l2l_labelmap, str(output_dir / "l2l_labelmap.mha"), compression=True)

# %%
print("Starting deformable registration...")
print("This may take several minutes depending on GPU availability.")

m2i_results = registrar.register_labelmap_to_image()
m2i_inverse_transform = m2i_results["inverse_transform"]
m2i_forward_transform = m2i_results["forward_transform"]
m2i_surface = m2i_results["registered_template_model_surface"]
m2i_labelmap = m2i_results["registered_template_labelmap"]
l2i_results = registrar.register_labelmap_to_image()
l2i_inverse_transform = l2i_results["inverse_transform"]
l2i_forward_transform = l2i_results["forward_transform"]
l2i_surface = l2i_results["registered_template_model_surface"]
l2i_labelmap = l2i_results["registered_template_labelmap"]
print("\nRegistration complete!")

# Save registration results to output folder
m2i_surface.save(str(output_dir / "m2i_model_surface.vtp"))
itk.imwrite(m2i_labelmap, str(output_dir / "m2i_labelmap.mha"), compression=True)
l2i_surface.save(str(output_dir / "l2i_model_surface.vtp"))
itk.imwrite(l2i_labelmap, str(output_dir / "l2i_labelmap.mha"), compression=True)

# %%
tmp_p = itk.Point[itk.D, 3]()
Expand All @@ -241,16 +241,16 @@
print(f"PCA transform time: {time.time() - start_time} seconds", flush=True)

start_time = time.time()
tmp_p = registrar.m2m_inverse_transform.TransformPoint(tmp_p)
print(f"M2M inverse transform time: {time.time() - start_time} seconds", flush=True)
tmp_p = registrar.l2l_inverse_transform.TransformPoint(tmp_p)
print(f"L2L inverse transform time: {time.time() - start_time} seconds", flush=True)

start_time = time.time()
tmp_p = registrar.m2i_inverse_transform.TransformPoint(tmp_p)
print(f"M2I inverse transform time: {time.time() - start_time} seconds", flush=True)
tmp_p = registrar.l2i_inverse_transform.TransformPoint(tmp_p)
print(f"L2I inverse transform time: {time.time() - start_time} seconds", flush=True)

# %%
# Verify registration using the transform member function
surface_transformed = registrar.m2i_template_model_surface
surface_transformed = registrar.l2i_template_model_surface
surface_transformed.save(str(output_dir / "registered_template_surface.vtp"))

model_transformed = registrar.transform_model()
Expand All @@ -265,8 +265,8 @@
registered_surface = registrar.registered_template_model_surface
icp_surface = registrar.icp_template_model_surface
pca_surface = registrar.pca_template_model_surface
m2m_surface = registrar.m2m_template_model_surface
m2i_surface = registrar.m2i_template_model_surface
l2l_surface = registrar.l2l_template_model_surface
l2i_surface = registrar.l2i_template_model_surface

# Create side-by-side comparison
plotter = pv.Plotter(shape=(1, 2))
Expand All @@ -280,7 +280,7 @@
# After deformable registration
plotter.subplot(0, 1)
plotter.add_mesh(patient_surface, color="red", opacity=0.5, label="Patient")
plotter.add_mesh(m2i_surface, color="blue", opacity=1.0, label="Registered")
plotter.add_mesh(l2i_surface, color="blue", opacity=1.0, label="Registered")
plotter.add_title("Final Registration")

plotter.link_views()
Expand Down
4 changes: 2 additions & 2 deletions experiments/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,9 @@ to handle diverse cases.

**Technologies:**
- ICP (Iterative Closest Point) registration for initial alignment
- Mask-based deformable registration for anatomical fitting
- Labelmap-based deformable registration for anatomical fitting
- PCA (Principal Component Analysis) shape modeling for shape constraints
- Three-stage registration pipeline (ICP → Mask-to-MaskMask-to-Image)
- Three-stage registration pipeline (ICP → Labelmap-to-LabelmapLabelmap-to-Image)
Comment on lines +152 to +154

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

Synchronize the remaining registration terminology in this README.

You updated this section to labelmap-based wording, but Line 250 still describes the same workflow as “mask-based,” which leaves conflicting guidance in one document.

🤖 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/README.md` around lines 152 - 154, The README has inconsistent
terminology for describing the registration workflow. The section around lines
152-154 now uses "labelmap-based" terminology, but a later section describing
the same workflow still refers to it as "mask-based." Search through the README
for all references to "mask-based" that describe the registration pipeline
workflow and update them to use "labelmap-based" terminology instead to maintain
consistency with the updated section and provide clear, non-conflicting guidance
throughout the document.

- Computationally intensive (>1 hour on typical PC)

**Prerequisites:**
Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,12 @@ module = [
]
disable_error_code = ["import-not-found", "import-untyped"]

[[tool.mypy.overrides]]
# torch/icon_registration/unigradicon are lazy-loaded at runtime; string
# forward-reference annotations like "torch.Size" are intentional.
module = ["physiomotion4d.register_images_icon"]
ignore_errors = true

Comment on lines +273 to +278

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

Avoid blanket type/lint suppression for register_images_icon.

Lines 273-278 (ignore_errors = true) disable all mypy checks for this module, and Lines 407-408 suppress F821 file-wide. Together, this can hide real regressions in a critical registration path. Prefer targeted suppressions (disable_error_code scoped to specific diagnostics) and TYPE_CHECKING imports for torch symbols.

As per coding guidelines, "Use full type hints with mypy strict mode (disallow_untyped_defs = true)."

Also applies to: 407-408

🤖 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 `@pyproject.toml` around lines 273 - 278, The tool.mypy.overrides section for
the physiomotion4d.register_images_icon module uses blanket suppression with
ignore_errors = true which can hide real regressions. Replace ignore_errors =
true with targeted disable_error_code entries for only the specific mypy
diagnostics that are actually needed (such as string forward-reference
annotation issues). Additionally, use TYPE_CHECKING imports for torch symbols in
the register_images_icon module itself rather than relying on file-wide
suppression, and apply the same targeted approach to the F821 suppression
mentioned at lines 407-408.

Source: Coding guidelines

[tool.pyright]
# Third-party packages (e.g. pyvista) are in dependencies but may have no stubs;
# do not report import-not-found so analysis matches mypy overrides above.
Expand Down Expand Up @@ -398,6 +404,8 @@ ignore = [
"test_*.py" = ["S101", "PLR2004", "ARG001", "ARG002"]
"tests/*.py" = ["S101", "PLR2004", "ARG001", "ARG002"]
"experiments/**/*.py" = ["T201", "S101"]
# torch/icon_registration are lazy-loaded at runtime; forward-reference strings are intentional
"src/physiomotion4d/register_images_icon.py" = ["F821"]

[tool.ruff.lint.isort]
known-first-party = ["physiomotion4d"]
Expand Down
5 changes: 2 additions & 3 deletions src/physiomotion4d/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@

__version__ = "2026.05.9"

import importlib.util as _importlib_util
import warnings as _warnings

try:
import cupy as _cupy # noqa: F401
except ImportError:
if _importlib_util.find_spec("cupy") is None:
_warnings.warn(
"CuPy is not installed — GPU acceleration is unavailable and processing "
"will be slow. Re-install with uv to get CuPy and CUDA-enabled PyTorch "
Expand Down
52 changes: 28 additions & 24 deletions src/physiomotion4d/cli/fit_statistical_model_to_patient.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ def main() -> int:
--pca-number-of-modes 10 \\
--output-dir ./results

# Enable mask-to-image refinement (requires template labelmap and label IDs)
# Enable labelmap-to-image refinement (requires template labelmap and label IDs)
%(prog)s \\
--template-model heart_model.vtu \\
--patient-models lv.vtp rv.vtp myo.vtp \\
--patient-image patient_ct.nii.gz \\
--mask-to-image \\
--labelmap-to-image \\
--template-labelmap heart_labelmap.nii.gz \\
--template-labelmap-muscle-ids 1 2 3 \\
--template-labelmap-chamber-ids 4 5 6 \\
Expand Down Expand Up @@ -76,7 +76,7 @@ def main() -> int:
)
parser.add_argument(
"--template-labelmap",
help="Path to template labelmap image (.nii.gz, .nrrd, .mha). Required when --mask-to-image is set.",
help="Path to template labelmap image (.nii.gz, .nrrd, .mha). Required when --labelmap-to-image is set.",
)
parser.add_argument(
"--output-dir", required=True, help="Output directory for results"
Expand Down Expand Up @@ -119,18 +119,18 @@ def main() -> int:

# Registration configuration
parser.add_argument(
"--no-mask-to-mask",
dest="use_mask_to_mask",
"--no-labelmap-to-labelmap",
dest="use_labelmap_to_labelmap",
action="store_false",
default=True,
help="Disable mask-to-mask deformable registration",
help="Disable labelmap-to-labelmap deformable registration",
)
parser.add_argument(
"--mask-to-image",
dest="use_mask_to_image",
"--labelmap-to-image",
dest="use_labelmap_to_image",
action="store_true",
default=False,
help="Enable mask-to-image refinement (requires --template-labelmap and label IDs)",
help="Enable labelmap-to-image refinement (requires --template-labelmap and label IDs)",
)
parser.add_argument(
"--use-ICON-refinement",
Expand Down Expand Up @@ -163,9 +163,11 @@ def main() -> int:
print(f"Error: Patient image not found: {args.patient_image}")
return 1

if args.use_mask_to_image:
if args.use_labelmap_to_image:
if args.template_labelmap is None:
print("Error: --template-labelmap is required when --mask-to-image is set.")
print(
"Error: --template-labelmap is required when --labelmap-to-image is set."
)
return 1
if not os.path.exists(args.template_labelmap):
print(f"Error: Template labelmap not found: {args.template_labelmap}")
Expand Down Expand Up @@ -238,10 +240,12 @@ def main() -> int:
pca_model=pca_model,
pca_number_of_modes=args.pca_number_of_modes,
)
if args.use_mask_to_mask:
workflow.set_use_mask_to_mask_registration(args.use_mask_to_mask)
if args.use_mask_to_image:
workflow.set_use_mask_to_image_registration(
if args.use_labelmap_to_labelmap:
workflow.set_use_labelmap_to_labelmap_registration(
args.use_labelmap_to_labelmap
)
Comment on lines +243 to +246

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

Always wire the L2L flag, including the disabled case.

When --no-labelmap-to-labelmap is passed, args.use_labelmap_to_labelmap is False, so this if block is skipped and the workflow keeps its default True. The CLI flag therefore has no effect.

Suggested fix
-        if args.use_labelmap_to_labelmap:
-            workflow.set_use_labelmap_to_labelmap_registration(
-                args.use_labelmap_to_labelmap
-            )
+        workflow.set_use_labelmap_to_labelmap_registration(
+            args.use_labelmap_to_labelmap
+        )
🤖 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/cli/fit_statistical_model_to_patient.py` around lines 243
- 246, The if condition around the set_use_labelmap_to_labelmap_registration
call only executes when args.use_labelmap_to_labelmap is True, which means when
the flag is False (from --no-labelmap-to-labelmap), the method is never called
and the workflow retains its default value. Remove the if statement and
unconditionally call workflow.set_use_labelmap_to_labelmap_registration with
args.use_labelmap_to_labelmap so that both True and False values from the CLI
flag are properly wired to the workflow.

if args.use_labelmap_to_image:
workflow.set_use_labelmap_to_image_registration(
True,
template_labelmap=template_labelmap,
template_labelmap_organ_mesh_ids=args.template_labelmap_muscle_ids,
Expand Down Expand Up @@ -282,17 +286,17 @@ def main() -> int:
print(f" Registered surface: {output_surface_file}")

# Save registered labelmap if available
if workflow.m2i_template_labelmap is not None:
if workflow.l2i_template_labelmap is not None:
output_labelmap_file = os.path.join(
args.output_dir, f"{args.output_prefix}_labelmap.nii.gz"
)
itk.imwrite(workflow.m2i_template_labelmap, output_labelmap_file)
itk.imwrite(workflow.l2i_template_labelmap, output_labelmap_file)
print(f" Registered labelmap: {output_labelmap_file}")
elif workflow.m2m_template_labelmap is not None:
elif workflow.l2l_template_labelmap is not None:
output_labelmap_file = os.path.join(
args.output_dir, f"{args.output_prefix}_labelmap.nii.gz"
)
itk.imwrite(workflow.m2m_template_labelmap, output_labelmap_file)
itk.imwrite(workflow.l2l_template_labelmap, output_labelmap_file)
Comment on lines +289 to +299

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

Write labelmaps with ITK compression enabled.

The new registered labelmap exports omit compression=True. As per coding guidelines, images must be “stored using itk.imwrite with compression=True.”

Suggested fix
-            itk.imwrite(workflow.l2i_template_labelmap, output_labelmap_file)
+            itk.imwrite(
+                workflow.l2i_template_labelmap,
+                output_labelmap_file,
+                compression=True,
+            )
@@
-            itk.imwrite(workflow.l2l_template_labelmap, output_labelmap_file)
+            itk.imwrite(
+                workflow.l2l_template_labelmap,
+                output_labelmap_file,
+                compression=True,
+            )
🤖 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/cli/fit_statistical_model_to_patient.py` around lines 289
- 299, The itk.imwrite calls used to save the labelmap files (both for
workflow.l2i_template_labelmap and workflow.l2l_template_labelmap) are missing
the compression parameter. Add compression=True as a keyword argument to both
itk.imwrite calls to enable compression when writing the output labelmap files,
following the coding guidelines for image storage.

Source: Coding guidelines

print(f" Registered labelmap: {output_labelmap_file}")

# Save intermediate results if available
Expand All @@ -310,12 +314,12 @@ def main() -> int:
workflow.pca_template_model_surface.save(output_pca_file)
print(f" PCA result: {output_pca_file}")

if workflow.m2m_template_model_surface is not None:
output_m2m_file = os.path.join(
args.output_dir, f"{args.output_prefix}_m2m_surface.vtp"
if workflow.l2l_template_model_surface is not None:
output_l2l_file = os.path.join(
args.output_dir, f"{args.output_prefix}_l2l_surface.vtp"
)
workflow.m2m_template_model_surface.save(output_m2m_file)
print(f" Mask-to-mask result: {output_m2m_file}")
workflow.l2l_template_model_surface.save(output_l2l_file)
print(f" Labelmap-to-labelmap result: {output_l2l_file}")

print("\n" + "=" * 70)
print("Registration completed successfully!")
Expand Down
Loading
Loading