Skip to content
Merged
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 @@ -21,21 +21,21 @@

# %%
# Patient CT image (defines coordinate frame)
patient_data_dir = Path.cwd().parent.parent / "data" / "CHOP-Valve4D" / "CT"
patient_data_dir = Path(__file__).parent.parent.parent / "data" / "CHOP-Valve4D" / "CT"
patient_ct_path = patient_data_dir / "RVOT28-Dias.mha"

# Template model (moving)
model_data_dir = Path.cwd().parent.parent / "data" / "KCL-Heart-Model"
model_data_dir = Path(__file__).parent.parent.parent / "data" / "KCL-Heart-Model"
model_labelmap_path = model_data_dir / "labelmap" / "average_labelmap_with_bkg.mha"
model_pca_data_dir = (
Path.cwd().parent / "Heart-Create_Statistical_Model" / "kcl-heart-model"
Path(__file__).parent.parent / "Heart-Create_Statistical_Model" / "kcl-heart-model"
)
model_pca_json_path = model_pca_data_dir / "pca_model.json"
model_mesh_path = model_pca_data_dir / "pca_mean.vtp"
model_pca_n_modes = 10

# Output directory
output_dir = Path.cwd() / "results-chop"
output_dir = Path(__file__).parent / "results-chop"

# %%
patient_image = itk.imread(str(patient_ct_path))
Expand All @@ -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 All @@ -75,10 +75,6 @@

registered_model.save(str(output_dir / "registered_model.vtp"))
registered_model_surface.save(str(output_dir / "registered_model_surface.vtp"))
registered_labelmap = results["registered_template_labelmap"]
itk.imwrite(
registered_labelmap, str(output_dir / "registered_labelmap.mha"), compression=True
)

# %%
pca_model = registrar.pca_template_model
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
10 changes: 10 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ show_error_codes = true
module = [
"ants",
"cupy",
"cupy_backends",
"cupy_backends.*",
"icon_registration",
"icon_registration.*",
"itk",
Expand Down Expand Up @@ -270,6 +272,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 +275 to +279

Comment on lines +275 to +280

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 +406,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
Loading
Loading