From d27deccc35637ed54cc2624dba451712397e6ba7 Mon Sep 17 00:00:00 2001 From: Stephen Aylward Date: Fri, 12 Jun 2026 15:44:35 -0400 Subject: [PATCH 1/3] fix: replace absolute intra-package imports with relative imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All submodules imported `from physiomotion4d.X import Y` (or `from physiomotion4d import Y`), which causes a circular import error at package initialization time — `__init__.py` imports each submodule while the submodules try to reach back into the partially-initialized package. This fails consistently regardless of install method (pip install ., editable install, or PyPI wheel). Replaced all 34 affected files with PEP 328 relative imports (`from .X import Y` for package submodules, `from ..X import Y` for the cli/ sub-package). Docstring examples left unchanged. --- .../fit_statistical_model_to_patient.rst | 6 +- .../3-registration_based_correspondence.py | 30 ++-- .../Heart-Create_Statistical_Model/README.md | 46 +++--- .../heart_model_to_patient.py | 2 +- .../cli/convert_image_4d_to_3d.py | 2 +- .../cli/convert_image_to_usd.py | 2 +- .../cli/convert_image_to_vtk.py | 2 +- src/physiomotion4d/cli/convert_vtk_to_usd.py | 4 +- .../cli/create_statistical_model.py | 2 +- src/physiomotion4d/cli/download_data.py | 2 +- .../cli/fit_statistical_model_to_patient.py | 2 +- .../cli/reconstruct_highres_4d_ct.py | 2 +- src/physiomotion4d/contour_tools.py | 6 +- src/physiomotion4d/convert_image_4d_to_3d.py | 2 +- src/physiomotion4d/image_tools.py | 2 +- src/physiomotion4d/labelmap_tools.py | 2 +- src/physiomotion4d/landmark_tools.py | 2 +- src/physiomotion4d/register_images_ants.py | 6 +- src/physiomotion4d/register_images_base.py | 6 +- src/physiomotion4d/register_images_greedy.py | 12 +- src/physiomotion4d/register_images_icon.py | 4 +- .../register_models_distance_maps.py | 151 ++++++++---------- src/physiomotion4d/register_models_icp.py | 4 +- src/physiomotion4d/register_models_icp_itk.py | 6 +- src/physiomotion4d/register_models_pca.py | 6 +- .../register_time_series_images.py | 8 +- src/physiomotion4d/segment_anatomy_base.py | 4 +- .../segment_chest_total_segmentator.py | 2 +- .../segment_heart_simpleware.py | 2 +- src/physiomotion4d/test_tools.py | 4 +- src/physiomotion4d/transform_tools.py | 6 +- src/physiomotion4d/usd_anatomy_tools.py | 2 +- src/physiomotion4d/usd_tools.py | 4 +- .../workflow_convert_image_to_usd.py | 24 +-- .../workflow_convert_image_to_vtk.py | 12 +- .../workflow_convert_vtk_to_usd.py | 8 +- .../workflow_create_statistical_model.py | 13 +- .../workflow_fine_tune_icon_registration.py | 8 +- ...rkflow_fit_statistical_model_to_patient.py | 36 ++--- .../workflow_reconstruct_highres_4d_ct.py | 4 +- ...ial_04_fit_statistical_model_to_patient.py | 3 +- 41 files changed, 211 insertions(+), 240 deletions(-) diff --git a/docs/cli_scripts/fit_statistical_model_to_patient.rst b/docs/cli_scripts/fit_statistical_model_to_patient.rst index 5ed0a07..03d5204 100644 --- a/docs/cli_scripts/fit_statistical_model_to_patient.rst +++ b/docs/cli_scripts/fit_statistical_model_to_patient.rst @@ -16,7 +16,7 @@ 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**: Deformable registration using distance maps +3. **Mask-to-Mask Registration**: Greedy affine + ICON deformable registration using distance maps 4. **Mask-to-Image Refinement** (optional): Final intensity-based refinement Installation @@ -120,7 +120,9 @@ Registration Configuration ``--template-labelmap`` and template label IDs. Disabled by default. ``--use-ICON-refinement`` - Enable ICON deep learning registration refinement (default: disabled) + Enable ICON deep learning refinement in the mask-to-image stage (Stage 4). + The mask-to-mask stage always uses Greedy affine + ICON deformable. + Default: disabled Output Options -------------- diff --git a/experiments/Heart-Create_Statistical_Model/3-registration_based_correspondence.py b/experiments/Heart-Create_Statistical_Model/3-registration_based_correspondence.py index 59179df..b296505 100644 --- a/experiments/Heart-Create_Statistical_Model/3-registration_based_correspondence.py +++ b/experiments/Heart-Create_Statistical_Model/3-registration_based_correspondence.py @@ -2,19 +2,19 @@ # %% [markdown] # # Registration-Based Correspondence # -# This notebook aligns ICP-aligned models from step 2 to the average surface using **ANTs SyN (Symmetric Normalization)** deformable registration via mask-based registration. +# This notebook aligns ICP-aligned models from step 2 to the average surface using **Greedy affine + ICON deformable** registration via mask-based registration. # # **Workflow:** # 1. Load ICP-aligned models from `kcl-heart-model/surfaces_aligned/` # 2. Load average surface (`average_surface.vtp`) -# 3. Use `RegisterModelsDistanceMaps` to perform ANTs SyN deformable registration +# 3. Use `RegisterModelsDistanceMaps` to perform Greedy affine + ICON deformable registration # 4. Save corresponded models to `kcl-heart-model/surfaces_aligned_corresponded/` # 5. Visualize before/after comparisons # 6. Analyze deformation magnitude and registration statistics # # **Method:** -# - **ANTs SyN** provides diffeomorphic (smooth, invertible) deformation fields -# - Progressive registration stages: rigid → affine → SyN deformable +# - **Greedy** performs fast CPU-based affine pre-alignment +# - **ICON** provides deep learning deformable registration on the affine-pre-aligned masks # - Mask-based approach focuses registration on the anatomical structures # %% @@ -110,11 +110,9 @@ roi_dilation_mm=20.0, # Dilation for ROI mask ) - # Perform ANTs SyN deformable registration - # This performs progressive multi-stage registration: rigid → affine → SyN deformable + # Perform Greedy affine + ICON deformable registration result = registrar.register( - transform_type="Deformable", # Uses ANTs SyN (Symmetric Normalization) - use_ICON=False, # Set to True for additional ICON deep learning refinement + transform_type="Deformable", ) forward_transform = result["forward_transform"] @@ -203,7 +201,7 @@ plotter.show_axes() plotter.camera_position = "iso" - # Right: After distance map registration (ICP + ANTs SyN) + # Right: After distance map registration (Greedy affine + ICON deformable) plotter.subplot(0, 1) plotter.add_mesh( fixed_model, color="lightblue", opacity=1.0, label="Average Surface" @@ -212,7 +210,7 @@ after_mesh, color="green", opacity=1.0, label=f"Case {case_id} (Corresponded)" ) plotter.add_text( - f"After Distance Map Registration (ANTs SyN)\nCase {case_id}", + f"After Distance Map Registration (Greedy + ICON)\nCase {case_id}", position="upper_left", font_size=10, ) @@ -329,7 +327,7 @@ # %% [markdown] # ## Summary # -# This notebook performed mask-based deformable registration using **ANTs SyN (Symmetric Normalization)** to establish correspondence between the ICP-aligned models and the average surface. +# This notebook performed mask-based deformable registration using **Greedy affine + ICON deformable** to establish correspondence between the ICP-aligned models and the average surface. # # **Next Steps:** # - Proceed to step 4: `4-surfaces_aligned_correspond_to_pca_inputs.ipynb` to prepare data for PCA analysis @@ -337,10 +335,8 @@ # - The registration statistics show the deformation applied to each model # # **Registration Details:** -# - The `RegisterModelsDistanceMaps` class uses **ANTs SyN** for progressive registration: -# 1. Rigid alignment -# 2. Affine transformation -# 3. SyN deformable registration (diffeomorphic) -# - Setting `use_ICON=True` in the `register()` call would add ICON deep learning refinement after SyN +# - 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) -# - SyN registration provides smooth, invertible deformation fields for anatomical correspondence +# - Composed Greedy + ICON transforms provide smooth, invertible deformation fields for anatomical correspondence diff --git a/experiments/Heart-Create_Statistical_Model/README.md b/experiments/Heart-Create_Statistical_Model/README.md index b79868b..d4f642a 100644 --- a/experiments/Heart-Create_Statistical_Model/README.md +++ b/experiments/Heart-Create_Statistical_Model/README.md @@ -51,9 +51,9 @@ This experiment follows a fully automated multi-step process. Each step is a - Prepares aligned data for correspondence computation 3. **`3-registration_based_correspondence.py`** - - Establishes point correspondences across the population using ANTs SyN deformable registration + - Establishes point correspondences across the population using Greedy affine + ICON deformable registration - Uses mask-based distance map registration via `RegisterModelsDistanceMaps` - - Performs diffeomorphic (smooth, invertible) deformation to the average surface + - Greedy affine pre-aligns masks; ICON deep learning refines with a deformable field - Critical step for meaningful PCA analysis 4. **`4-surfaces_aligned_correspond_to_pca_inputs.py`** @@ -75,15 +75,15 @@ This experiment uses a fully automated approach combining: Instead of traditional mesh parameterization methods (e.g., SPHARM-PDM), this pipeline uses **deformable image registration** to establish correspondences: -- **ANTs SyN (Symmetric Normalization)** performs diffeomorphic registration +- **Greedy affine** (PICSL Greedy) performs fast CPU-based affine pre-alignment +- **ICON deformable** applies deep learning registration on the affine-pre-aligned masks - Distance maps from surface meshes create continuous fields for registration -- Progressive registration stages: rigid → affine → SyN deformable - Mask-based approach focuses registration on anatomical structures **Advantages:** - Fully automated (no manual parameter tuning) - Handles complex topologies naturally -- Diffeomorphic guarantees smooth, invertible deformations +- Composed Greedy + ICON transforms provide smooth, invertible deformation fields - Integrates seamlessly with medical imaging pipelines ### PCA Computation @@ -108,12 +108,12 @@ cd experiments/Heart-Create_Statistical_Model/ # in VS Code or Cursor via the `# %%` cell markers): python 1-input_meshes_to_input_surfaces.py # Extract surfaces from volumetric meshes python 2-input_surfaces_to_surfaces_aligned.py # Rigid ICP alignment + compute average -python 3-registration_based_correspondence.py # ANTs SyN deformable correspondence +python 3-registration_based_correspondence.py # Greedy affine + ICON deformable correspondence python 4-surfaces_aligned_correspond_to_pca_inputs.py # Prepare PCA input matrices python 5-compute_pca_model.py # Compute PCA and export JSON model ``` -**Total Runtime:** Approximately 2-4 hours depending on hardware (20 heart meshes, ANTs registration is computationally intensive). +**Total Runtime:** Approximately 1-3 hours depending on hardware (20 heart meshes; Greedy affine is fast on CPU, ICON requires a GPU for reasonable speed). ## Outputs @@ -167,7 +167,7 @@ registered_mesh = workflow.run_workflow() - VS Code or Cursor with the Python extension for cell-by-cell execution (optional; scripts also run end-to-end as plain Python) - ITK, VTK, PyVista (included with PhysioMotion4D) -- ANTs (Advanced Normalization Tools) - installed automatically with PhysioMotion4D +- picsl-greedy and ICON (included with PhysioMotion4D) - scikit-learn for PCA computation ### Data @@ -176,10 +176,10 @@ registered_mesh = workflow.run_workflow() - ~2GB for final outputs ### Compute -- CPU: Multi-core processor (8+ cores recommended for ANTs registration) +- CPU: Multi-core processor (4+ cores recommended for Greedy affine registration) - RAM: 16GB minimum (32GB recommended) -- GPU: Not required for this experiment -- Time: ~2-4 hours total (ANTs deformable registration is computationally intensive) +- GPU: Recommended for ICON deformable registration (CUDA-capable GPU) +- Time: ~1-3 hours total (Greedy is fast; ICON speed depends on GPU availability) ## Citation @@ -187,8 +187,8 @@ If you use this experiment or the KCL dataset, please cite: > Rodero et al. (2021), "Linking statistical shape models and simulated function in the healthy adult human heart". *PLOS Computational Biology*. DOI: [10.1371/journal.pcbi.1008851](https://doi.org/10.1371/journal.pcbi.1008851) -For ANTs registration: -> Avants BB, et al. (2011). "A reproducible evaluation of ANTs similarity metric performance in brain image registration". *NeuroImage*. DOI: [10.1016/j.neuroimage.2010.09.025](https://doi.org/10.1016/j.neuroimage.2010.09.025) +For ICON registration: +> Greer et al. (2021). "ICON: Learning Regular Maps Through Inverse Consistency". *ICCV*. DOI: [10.1109/ICCV48922.2021.00129](https://doi.org/10.1109/ICCV48922.2021.00129) ## Related Experiments @@ -199,7 +199,7 @@ For ANTs registration: ## Support and Resources - **KCL Dataset**: [https://zenodo.org/records/4590294](https://zenodo.org/records/4590294) -- **ANTs Documentation**: [https://github.com/ANTsX/ANTs](https://github.com/ANTsX/ANTs) +- **Greedy Documentation**: [https://greedy.readthedocs.io/](https://greedy.readthedocs.io/) - **PhysioMotion4D Documentation**: See main repository README and API documentation - **Issues**: Report bugs or request features on the PhysioMotion4D GitHub repository @@ -210,27 +210,25 @@ For ANTs registration: - Check `data/KCL-Heart-Model/README.md` for download instructions - Verify all 20 heart mesh files (`.vtk` format) are present -### ANTs Registration Taking Too Long -- ANTs SyN registration is computationally intensive (5-15 minutes per subject) -- Total time for 20 subjects: 2-4 hours is normal -- Consider using a machine with more CPU cores -- Progress is saved incrementally - can resume if interrupted +### Registration Taking Too Long +- Greedy affine is fast (< 1 minute per subject on CPU) +- ICON deformable is GPU-accelerated; without a GPU it falls back to CPU and will be significantly slower +- Total time for 20 subjects: 1-3 hours depending on GPU availability ### Memory Issues - Close other applications to free RAM -- ANTs registration can use 4-8GB per process +- ICON can use 4-8GB GPU VRAM; reduce batch size or iterations if needed - Process fewer meshes initially to test pipeline -- Use a machine with more RAM (32GB+ recommended) ### Correspondence Quality Issues - Check alignment quality from step 2 (ICP should produce good initial alignment) - Verify average surface looks reasonable before step 3 -- ANTs parameters are pre-tuned for cardiac anatomy -- If registration fails, check input mesh quality and topology +- If Greedy affine fails, check input mesh quality and topology +- If ICON deformable quality is poor, increase `icon_iterations` in the `register()` call ### Import Errors - Ensure all PhysioMotion4D dependencies are installed -- Check that ANTs is available: `python -c "import ants; print(ants.__version__)"` +- Check Greedy is available: `python -c "from picsl_greedy import Greedy3D; print('ok')"` - Reinstall environment if needed: `pip install -e .` in repository root --- diff --git a/experiments/Heart-Statistical_Model_To_Patient/heart_model_to_patient.py b/experiments/Heart-Statistical_Model_To_Patient/heart_model_to_patient.py index a16b9b3..b90c599 100644 --- a/experiments/Heart-Statistical_Model_To_Patient/heart_model_to_patient.py +++ b/experiments/Heart-Statistical_Model_To_Patient/heart_model_to_patient.py @@ -190,7 +190,7 @@ # Perform deformable registration print("Starting deformable mask-to-mask registration...") - m2m_results = registrar.register_mask_to_mask(use_ICON_refinement=False) + 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"] diff --git a/src/physiomotion4d/cli/convert_image_4d_to_3d.py b/src/physiomotion4d/cli/convert_image_4d_to_3d.py index 1cdfcea..7de5a4f 100644 --- a/src/physiomotion4d/cli/convert_image_4d_to_3d.py +++ b/src/physiomotion4d/cli/convert_image_4d_to_3d.py @@ -70,7 +70,7 @@ def main() -> int: print(f"Error: input image not found: {args.input_image}") return 1 try: - from physiomotion4d import ConvertImage4DTo3D + from .. import ConvertImage4DTo3D converter = ConvertImage4DTo3D() print(f"Loading 4D image: {args.input_image}") diff --git a/src/physiomotion4d/cli/convert_image_to_usd.py b/src/physiomotion4d/cli/convert_image_to_usd.py index 5fff4c7..2b07c98 100644 --- a/src/physiomotion4d/cli/convert_image_to_usd.py +++ b/src/physiomotion4d/cli/convert_image_to_usd.py @@ -110,7 +110,7 @@ def main() -> int: # Initialize processor print("Initializing Image-to-USD processor...") try: - from physiomotion4d import WorkflowConvertImageToUSD + from .. import WorkflowConvertImageToUSD processor = WorkflowConvertImageToUSD( input_filenames=args.input_files, diff --git a/src/physiomotion4d/cli/convert_image_to_vtk.py b/src/physiomotion4d/cli/convert_image_to_vtk.py index fe3d2f4..9f1cc70 100644 --- a/src/physiomotion4d/cli/convert_image_to_vtk.py +++ b/src/physiomotion4d/cli/convert_image_to_vtk.py @@ -160,7 +160,7 @@ def main() -> int: print("=" * 70) try: - from physiomotion4d import WorkflowConvertImageToVTK + from .. import WorkflowConvertImageToVTK workflow = WorkflowConvertImageToVTK( segmentation_method=args.segmentation_method, diff --git a/src/physiomotion4d/cli/convert_vtk_to_usd.py b/src/physiomotion4d/cli/convert_vtk_to_usd.py index 1796627..0ef7bdf 100644 --- a/src/physiomotion4d/cli/convert_vtk_to_usd.py +++ b/src/physiomotion4d/cli/convert_vtk_to_usd.py @@ -11,7 +11,7 @@ import os import sys -from physiomotion4d.usd_anatomy_tools import DEFAULT_RENDER_PARAMS +from ..usd_anatomy_tools import DEFAULT_RENDER_PARAMS # Anatomy types accepted by --anatomy-type, sourced from the renderer's # registered defaults so that new groups/organs registered in @@ -193,7 +193,7 @@ def main() -> int: return 1 try: - from physiomotion4d import WorkflowConvertVTKToUSD + from .. import WorkflowConvertVTKToUSD workflow = WorkflowConvertVTKToUSD( vtk_files=args.vtk_files, diff --git a/src/physiomotion4d/cli/create_statistical_model.py b/src/physiomotion4d/cli/create_statistical_model.py index b16f1ea..5f1b139 100644 --- a/src/physiomotion4d/cli/create_statistical_model.py +++ b/src/physiomotion4d/cli/create_statistical_model.py @@ -138,7 +138,7 @@ def main() -> int: # Run workflow print("\nInitializing create statistical model workflow...") try: - from physiomotion4d import WorkflowCreateStatisticalModel + from .. import WorkflowCreateStatisticalModel workflow = WorkflowCreateStatisticalModel( sample_meshes=sample_meshes, diff --git a/src/physiomotion4d/cli/download_data.py b/src/physiomotion4d/cli/download_data.py index eaf9399..22dd169 100644 --- a/src/physiomotion4d/cli/download_data.py +++ b/src/physiomotion4d/cli/download_data.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import Optional -from physiomotion4d.data_download_tools import DataDownloadTools +from ..data_download_tools import DataDownloadTools SLICER_HEART_CT = "Slicer-Heart-CT" diff --git a/src/physiomotion4d/cli/fit_statistical_model_to_patient.py b/src/physiomotion4d/cli/fit_statistical_model_to_patient.py index 51d6b70..1a8a965 100644 --- a/src/physiomotion4d/cli/fit_statistical_model_to_patient.py +++ b/src/physiomotion4d/cli/fit_statistical_model_to_patient.py @@ -223,7 +223,7 @@ def main() -> int: # Initialize workflow print("\nInitializing heart model to patient registration workflow...") try: - from physiomotion4d import WorkflowFitStatisticalModelToPatient + from .. import WorkflowFitStatisticalModelToPatient workflow = WorkflowFitStatisticalModelToPatient( template_model=template_model, diff --git a/src/physiomotion4d/cli/reconstruct_highres_4d_ct.py b/src/physiomotion4d/cli/reconstruct_highres_4d_ct.py index 13ee832..64e5711 100644 --- a/src/physiomotion4d/cli/reconstruct_highres_4d_ct.py +++ b/src/physiomotion4d/cli/reconstruct_highres_4d_ct.py @@ -269,7 +269,7 @@ def main() -> int: # Initialize workflow print("\nInitializing high-resolution 4D CT reconstruction workflow...") try: - from physiomotion4d import WorkflowReconstructHighres4DCT + from .. import WorkflowReconstructHighres4DCT workflow = WorkflowReconstructHighres4DCT( time_series_images=time_series_images, diff --git a/src/physiomotion4d/contour_tools.py b/src/physiomotion4d/contour_tools.py index c32e076..c697c38 100644 --- a/src/physiomotion4d/contour_tools.py +++ b/src/physiomotion4d/contour_tools.py @@ -12,9 +12,9 @@ import pyvista as pv import trimesh -from physiomotion4d.image_tools import ImageTools -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase -from physiomotion4d.transform_tools import TransformTools +from .image_tools import ImageTools +from .physiomotion4d_base import PhysioMotion4DBase +from .transform_tools import TransformTools class ContourTools(PhysioMotion4DBase): diff --git a/src/physiomotion4d/convert_image_4d_to_3d.py b/src/physiomotion4d/convert_image_4d_to_3d.py index 88b47ba..0dadfa9 100644 --- a/src/physiomotion4d/convert_image_4d_to_3d.py +++ b/src/physiomotion4d/convert_image_4d_to_3d.py @@ -28,7 +28,7 @@ import numpy as np import pydicom -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase +from .physiomotion4d_base import PhysioMotion4DBase class ConvertImage4DTo3D(PhysioMotion4DBase): diff --git a/src/physiomotion4d/image_tools.py b/src/physiomotion4d/image_tools.py index e47882b..42dcd9f 100644 --- a/src/physiomotion4d/image_tools.py +++ b/src/physiomotion4d/image_tools.py @@ -13,7 +13,7 @@ import SimpleITK as sitk from numpy.typing import NDArray -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase +from .physiomotion4d_base import PhysioMotion4DBase class ImageTools(PhysioMotion4DBase): diff --git a/src/physiomotion4d/labelmap_tools.py b/src/physiomotion4d/labelmap_tools.py index 9e28231..3baf22d 100644 --- a/src/physiomotion4d/labelmap_tools.py +++ b/src/physiomotion4d/labelmap_tools.py @@ -13,7 +13,7 @@ import itk import numpy as np -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase +from .physiomotion4d_base import PhysioMotion4DBase class LabelmapTools(PhysioMotion4DBase): diff --git a/src/physiomotion4d/landmark_tools.py b/src/physiomotion4d/landmark_tools.py index 01c3f01..9249cfd 100644 --- a/src/physiomotion4d/landmark_tools.py +++ b/src/physiomotion4d/landmark_tools.py @@ -14,7 +14,7 @@ import logging from pathlib import Path -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase +from .physiomotion4d_base import PhysioMotion4DBase LandmarkDict = dict[str, tuple[float, float, float]] diff --git a/src/physiomotion4d/register_images_ants.py b/src/physiomotion4d/register_images_ants.py index bfda419..3dadea2 100644 --- a/src/physiomotion4d/register_images_ants.py +++ b/src/physiomotion4d/register_images_ants.py @@ -18,8 +18,8 @@ import numpy as np from numpy.typing import NDArray -from physiomotion4d.register_images_base import RegisterImagesBase -from physiomotion4d.transform_tools import TransformTools +from .register_images_base import RegisterImagesBase +from .transform_tools import TransformTools class RegisterImagesANTS(RegisterImagesBase): @@ -296,7 +296,7 @@ def _antsfile_to_itk_displacement_field_transform( # Convert to the correct Image[Vector[D, 3], 3] type for DisplacementFieldTransform # Use ImageTools helper to convert array to vector image with correct type - from physiomotion4d.image_tools import ImageTools + from .image_tools import ImageTools image_tools = ImageTools() diff --git a/src/physiomotion4d/register_images_base.py b/src/physiomotion4d/register_images_base.py index 1c397fb..d037637 100644 --- a/src/physiomotion4d/register_images_base.py +++ b/src/physiomotion4d/register_images_base.py @@ -20,9 +20,9 @@ import itk -from physiomotion4d.labelmap_tools import LabelmapTools -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase -from physiomotion4d.transform_tools import TransformTools +from .labelmap_tools import LabelmapTools +from .physiomotion4d_base import PhysioMotion4DBase +from .transform_tools import TransformTools class RegisterImagesBase(PhysioMotion4DBase): diff --git a/src/physiomotion4d/register_images_greedy.py b/src/physiomotion4d/register_images_greedy.py index c2c41f5..155977f 100644 --- a/src/physiomotion4d/register_images_greedy.py +++ b/src/physiomotion4d/register_images_greedy.py @@ -20,9 +20,9 @@ import numpy as np from numpy.typing import NDArray -from physiomotion4d.image_tools import ImageTools -from physiomotion4d.register_images_base import RegisterImagesBase -from physiomotion4d.transform_tools import TransformTools +from .image_tools import ImageTools +from .register_images_base import RegisterImagesBase +from .transform_tools import TransformTools def _try_import_greedy() -> Any: @@ -262,7 +262,7 @@ def _sitk_warp_to_itk_displacement_transform( ) -> itk.Transform: """Convert SimpleITK displacement field to ITK DisplacementFieldTransform.""" field_itk = self._sitk_to_itk(warp_sitk) - from physiomotion4d.image_tools import ImageTools + from .image_tools import ImageTools image_tools = ImageTools() arr = itk.array_from_image(field_itk) @@ -462,7 +462,7 @@ def registration_method( # integer labelmap is piecewise-constant, so NCC sees zero local # variance and emits NaN gradients (a native crash). Encode each # labelmap as a continuous label-plus-boundary-distance field instead. - from physiomotion4d.labelmap_tools import LabelmapTools + from .labelmap_tools import LabelmapTools labelmap_tools = LabelmapTools() fixed_labelmap_sitk = None @@ -579,7 +579,7 @@ def registration_method( ) else: # Assume numpy displacement field (z,y,x,3) - from physiomotion4d.image_tools import ImageTools + from .image_tools import ImageTools image_tools = ImageTools() warp_arr = np.asarray(warp_sitk, dtype=np.float64) diff --git a/src/physiomotion4d/register_images_icon.py b/src/physiomotion4d/register_images_icon.py index 91a6dda..136397d 100644 --- a/src/physiomotion4d/register_images_icon.py +++ b/src/physiomotion4d/register_images_icon.py @@ -21,8 +21,8 @@ from unigradicon import get_multigradicon, get_unigradicon from unigradicon import preprocess as unigradicon_preprocess -from physiomotion4d.register_images_base import RegisterImagesBase -from physiomotion4d.transform_tools import TransformTools +from .register_images_base import RegisterImagesBase +from .transform_tools import TransformTools DEFAULT_FINETUNE_LEARNING_RATE = 2e-5 diff --git a/src/physiomotion4d/register_models_distance_maps.py b/src/physiomotion4d/register_models_distance_maps.py index c03751f..9dcbd82 100644 --- a/src/physiomotion4d/register_models_distance_maps.py +++ b/src/physiomotion4d/register_models_distance_maps.py @@ -4,19 +4,17 @@ models using mask-based deformable registration. The workflow includes: 1. Generate binary masks from moving and fixed models 2. Generate ROI masks with dilation -4. Progressive registration stages: - - rigid: ANTs rigid registration - - affine: ANTs rigid → affine registration - - deformable: ANTs rigid → affine → deformable (SyN) registration -5. Optional ICON refinement at end +3. Progressive registration stages: + - rigid: Greedy rigid registration + - affine: Greedy affine registration + - deformable: Greedy affine → ICON deformable registration The registration is particularly useful for aligning anatomical models where shape differences require deformable transformations beyond rigid/affine ICP. Key Features: - Automatic mask generation from PyVista models - - Multi-stage ANTs registration (rigid/affine/deformable) - - Optional ICON deep learning refinement + - Multi-stage Greedy/ICON registration (rigid/affine/deformable) - Automatic transform composition - Support for PyVista models @@ -30,14 +28,14 @@ >>> fixed_model = pv.read('patient_surface.stl') >>> reference_image = itk.imread('patient_ct.nii.gz') >>> - >>> # Run deformable registration with ICON refinement + >>> # Run deformable registration (Greedy affine + ICON deformable) >>> registrar = RegisterModelsDistanceMaps( ... moving_model=moving_model, ... fixed_model=fixed_model, ... reference_image=reference_image, ... roi_dilation_mm=20, ... ) - >>> result = registrar.register(mode='deformable', use_ICON=True, icon_iterations=50) + >>> result = registrar.register(transform_type='Deformable', icon_iterations=50) >>> >>> # Access results >>> aligned_model = result['registered_model'] @@ -50,12 +48,12 @@ import itk import pyvista as pv -from physiomotion4d.contour_tools import ContourTools -from physiomotion4d.labelmap_tools import LabelmapTools -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase -from physiomotion4d.register_images_ants import RegisterImagesANTS -from physiomotion4d.register_images_icon import RegisterImagesICON -from physiomotion4d.transform_tools import TransformTools +from .contour_tools import ContourTools +from .labelmap_tools import LabelmapTools +from .physiomotion4d_base import PhysioMotion4DBase +from .register_images_greedy import RegisterImagesGreedy +from .register_images_icon import RegisterImagesICON +from .transform_tools import TransformTools class RegisterModelsDistanceMaps(PhysioMotion4DBase): @@ -63,18 +61,17 @@ class RegisterModelsDistanceMaps(PhysioMotion4DBase): This class provides mask-based alignment of 3D surface models with support for rigid, affine, and deformable transformation modes. The registration pipeline - generates masks from models, applies optional dilation, and uses ANTs for - progressive multi-stage registration with optional ICON refinement. + generates masks from models, applies optional dilation, and uses Greedy for + rigid/affine stages and ICON for deformable registration. **Registration Pipelines:** - - **None mode**: No ANTs registration - - **Rigid mode**: ANTs rigid registration - - **Affine mode**: ANTs rigid → affine registration - - **Deformable mode**: ANTs rigid → affine → deformable (SyN) registration - - **Optional**: ICON deep learning refinement after any mode + - **None mode**: No registration (identity transform) + - **Rigid mode**: Greedy rigid registration + - **Affine mode**: Greedy affine registration + - **Deformable mode**: Greedy affine → ICON deformable registration **Transform Convention:** - These are the underlying image-registration (ANTs/ICON) transforms, so + These are the underlying image-registration (Greedy/ICON) transforms, so they follow the image convention (see docs/developer/transform_conventions): @@ -91,7 +88,7 @@ class RegisterModelsDistanceMaps(PhysioMotion4DBase): roi_dilation_mm (float): Dilation amount in mm for ROI mask transform_tools (TransformTools): Transform utility instance contour_tools (ContourTools): Model utility instance - registrar_ANTS (RegisterImagesANTS): ANTs registration instance + registrar_Greedy (RegisterImagesGreedy): Greedy registration instance registrar_ICON (RegisterImagesICON): ICON registration instance forward_transform (itk.CompositeTransform): Optimized moving→fixed transform inverse_transform (itk.CompositeTransform): Optimized fixed→moving transform @@ -107,15 +104,13 @@ class RegisterModelsDistanceMaps(PhysioMotion4DBase): ... ) >>> >>> # Run rigid registration - >>> result = registrar.register(mode='rigid') + >>> result = registrar.register(transform_type='Rigid') >>> >>> # Or run affine registration - >>> result = registrar.register(mode='affine') + >>> result = registrar.register(transform_type='Affine') >>> - >>> # Or run deformable with ICON refinement - >>> result = registrar.register( - ... mode='deformable', use_ANTS=False, use_ICON=True, icon_iterations=50 - ... ) + >>> # Or run deformable (Greedy affine + ICON) + >>> result = registrar.register(transform_type='Deformable', icon_iterations=50) >>> >>> # Get aligned model and transforms >>> aligned_model = result['registered_model'] @@ -158,7 +153,7 @@ def __init__( self.labelmap_tools = LabelmapTools(log_level=log_level) # Registration instances - self.registrar_ANTS = RegisterImagesANTS(log_level=log_level) + self.registrar_Greedy = RegisterImagesGreedy(log_level=log_level) self.registrar_ICON = RegisterImagesICON(log_level=log_level) self.registrar_ICON.set_modality("ct") self.registrar_ICON.set_multi_modality(False) @@ -230,7 +225,6 @@ def _create_masks_from_models(self) -> None: def register( self, transform_type: str = "Deformable", - use_ICON: bool = False, icon_iterations: int = 50, ) -> dict: """Perform mask-based registration of moving model to fixed model. @@ -238,24 +232,21 @@ def register( This method executes progressive multi-stage registration: **None transform type:** - 1. No ANTs registration + 1. No registration (identity transform) **Rigid transform type:** - 1. ANTs rigid registration + 1. Greedy rigid registration **Affine transform type:** - 1. ANTs affine registration (includes rigid stage) + 1. Greedy affine registration **Deformable transform type:** - 1. ANTs SyN deformable registration (includes rigid + affine + deformable stages) - - **Optional ICON refinement** (all transform type): - 1. ICON deep learning registration for fine-tuning + 1. Greedy affine registration + 2. ICON deformable registration on the affine-pre-aligned masks Args: transform_type: Registration transform type - 'None', 'Rigid', 'Affine', or 'Deformable'. Default: 'Deformable' - use_ICON: Whether to apply ICON registration refinement after ANTs. Default: False - icon_iterations: Number of ICON optimization iterations if use_ICON=True. Default: 50 + icon_iterations: Number of ICON optimization iterations for 'Deformable' mode. Default: 50 Returns: Dictionary containing: @@ -273,10 +264,8 @@ def register( >>> # Affine registration >>> result = registrar.register(transform_type='Affine') >>> - >>> # Deformable registration with ICON refinement - >>> result = registrar.register( - ... transform_type='Deformable', use_ICON=True, icon_iterations=100 - ... ) + >>> # Deformable registration (Greedy affine + ICON) + >>> result = registrar.register(transform_type='Deformable', icon_iterations=100) """ if transform_type not in ["None", "Rigid", "Affine", "Deformable"]: raise ValueError( @@ -288,86 +277,78 @@ def register( # Step 1: Generate masks from models self._create_masks_from_models() - self.log_info( - "Performing ANTs %s registration...", - transform_type, - ) - - inverse_transform_ANTS = None - forward_transform_ANTS = None - if transform_type != "None": - self.registrar_ANTS.set_fixed_image(self.fixed_mask_image) - self.registrar_ANTS.set_fixed_mask(self.fixed_mask_roi_image) + # Step 2: Greedy rigid or affine stage (skipped for None/Deformable uses Affine) + greedy_type = "Affine" if transform_type == "Deformable" else transform_type - self.registrar_ANTS.set_transform_type(transform_type) - self.registrar_ANTS.set_metric("MeanSquares") + forward_transform_Greedy = None + inverse_transform_Greedy = None + if greedy_type != "None": + self.log_info("Performing Greedy %s registration...", greedy_type) + self.registrar_Greedy.set_fixed_image(self.fixed_mask_image) + self.registrar_Greedy.set_fixed_mask(self.fixed_mask_roi_image) + self.registrar_Greedy.set_transform_type(greedy_type) + self.registrar_Greedy.set_metric("MeanSquares") - result_ANTS = self.registrar_ANTS.register( + result_Greedy = self.registrar_Greedy.register( moving_image=self.moving_mask_image, moving_mask=self.moving_mask_roi_image, ) - inverse_transform_ANTS = result_ANTS["inverse_transform"] - forward_transform_ANTS = result_ANTS["forward_transform"] + forward_transform_Greedy = result_Greedy["forward_transform"] + inverse_transform_Greedy = result_Greedy["inverse_transform"] else: identity_transform = itk.AffineTransform[itk.D, 3].New() identity_transform.SetIdentity() - inverse_transform_ANTS = identity_transform - forward_transform_ANTS = identity_transform + forward_transform_Greedy = identity_transform + inverse_transform_Greedy = identity_transform - # Initialize composite transforms - self.forward_transform = forward_transform_ANTS - self.inverse_transform = inverse_transform_ANTS + self.forward_transform = forward_transform_Greedy + self.inverse_transform = inverse_transform_Greedy - # Optional ICON refinement - if use_ICON: + # Step 3: ICON deformable stage (only for Deformable mode) + if transform_type == "Deformable": self.log_info( - "Performing ICON refinement registration (%d iterations)...", + "Performing ICON deformable registration (%d iterations)...", icon_iterations, ) - # Transform masks with ANTs result for ICON input - moving_mask_ANTS_transformed = self.transform_tools.transform_image( + # Pre-align moving mask with the Greedy affine result + moving_mask_affine_transformed = self.transform_tools.transform_image( self.moving_mask_image, - forward_transform_ANTS, + forward_transform_Greedy, self.reference_image, interpolation_method="linear", ) - # Configure ICON + # Configure and run ICON self.registrar_ICON.set_number_of_iterations(icon_iterations) self.registrar_ICON.set_fixed_image(self.fixed_mask_image) self.registrar_ICON.set_fixed_mask(self.fixed_mask_roi_image) - # ICON registration result_ICON = self.registrar_ICON.register( - moving_image=moving_mask_ANTS_transformed, + moving_image=moving_mask_affine_transformed, moving_mask=self.moving_mask_roi_image, ) - inverse_transform_ICON = result_ICON["inverse_transform"] forward_transform_ICON = result_ICON["forward_transform"] + inverse_transform_ICON = result_ICON["inverse_transform"] - # Compose ANTs and ICON transforms - composed_forward = ( + # Compose Greedy affine + ICON deformable + self.forward_transform = ( self.transform_tools.combine_displacement_field_transforms( - forward_transform_ANTS, + forward_transform_Greedy, forward_transform_ICON, reference_image=self.reference_image, mode="compose", ) ) - - composed_inverse = ( + self.inverse_transform = ( self.transform_tools.combine_displacement_field_transforms( inverse_transform_ICON, - inverse_transform_ANTS, + inverse_transform_Greedy, reference_image=self.reference_image, mode="compose", ) ) - self.forward_transform = composed_forward - self.inverse_transform = composed_inverse - # Apply final transform to moving model self.log_info("Transforming moving model...") self.registered_model = self.transform_tools.transform_pvcontour( @@ -376,7 +357,7 @@ def register( with_deformation_magnitude=True, ) - self.log_info("%s mask-based registration complete!", transform_type.upper()) + self.log_info("%s mask-based registration complete.", transform_type.upper()) # Return results as dictionary return { diff --git a/src/physiomotion4d/register_models_icp.py b/src/physiomotion4d/register_models_icp.py index 3236f02..ce2dc95 100644 --- a/src/physiomotion4d/register_models_icp.py +++ b/src/physiomotion4d/register_models_icp.py @@ -45,8 +45,8 @@ import pyvista as pv import vtk -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase -from physiomotion4d.transform_tools import TransformTools +from .physiomotion4d_base import PhysioMotion4DBase +from .transform_tools import TransformTools class RegisterModelsICP(PhysioMotion4DBase): diff --git a/src/physiomotion4d/register_models_icp_itk.py b/src/physiomotion4d/register_models_icp_itk.py index ee1429c..f936d26 100644 --- a/src/physiomotion4d/register_models_icp_itk.py +++ b/src/physiomotion4d/register_models_icp_itk.py @@ -6,9 +6,9 @@ import pyvista as pv from scipy.optimize import minimize -from physiomotion4d.contour_tools import ContourTools -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase -from physiomotion4d.transform_tools import TransformTools +from .contour_tools import ContourTools +from .physiomotion4d_base import PhysioMotion4DBase +from .transform_tools import TransformTools class RegisterModelsICPITK(PhysioMotion4DBase): diff --git a/src/physiomotion4d/register_models_pca.py b/src/physiomotion4d/register_models_pca.py index 339b34a..06645cb 100644 --- a/src/physiomotion4d/register_models_pca.py +++ b/src/physiomotion4d/register_models_pca.py @@ -11,9 +11,9 @@ from scipy.optimize import minimize from typing_extensions import Self -from physiomotion4d.contour_tools import ContourTools -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase -from physiomotion4d.transform_tools import TransformTools +from .contour_tools import ContourTools +from .physiomotion4d_base import PhysioMotion4DBase +from .transform_tools import TransformTools class RegisterModelsPCA(PhysioMotion4DBase): diff --git a/src/physiomotion4d/register_time_series_images.py b/src/physiomotion4d/register_time_series_images.py index 092ec00..277fd0a 100644 --- a/src/physiomotion4d/register_time_series_images.py +++ b/src/physiomotion4d/register_time_series_images.py @@ -13,10 +13,10 @@ import itk -from physiomotion4d.register_images_base import RegisterImagesBase -from physiomotion4d.register_images_greedy import RegisterImagesGreedy -from physiomotion4d.register_images_icon import RegisterImagesICON -from physiomotion4d.transform_tools import TransformTools +from .register_images_base import RegisterImagesBase +from .register_images_greedy import RegisterImagesGreedy +from .register_images_icon import RegisterImagesICON +from .transform_tools import TransformTools REGISTRATION_METHODS: list[str] = [ "Greedy", diff --git a/src/physiomotion4d/segment_anatomy_base.py b/src/physiomotion4d/segment_anatomy_base.py index 8368d6b..38497a1 100644 --- a/src/physiomotion4d/segment_anatomy_base.py +++ b/src/physiomotion4d/segment_anatomy_base.py @@ -12,8 +12,8 @@ import numpy as np from itk import TubeTK as tube -from physiomotion4d.anatomy_taxonomy import AnatomyTaxonomy -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase +from .anatomy_taxonomy import AnatomyTaxonomy +from .physiomotion4d_base import PhysioMotion4DBase class SegmentAnatomyBase(PhysioMotion4DBase): diff --git a/src/physiomotion4d/segment_chest_total_segmentator.py b/src/physiomotion4d/segment_chest_total_segmentator.py index a51c207..c3e0e86 100644 --- a/src/physiomotion4d/segment_chest_total_segmentator.py +++ b/src/physiomotion4d/segment_chest_total_segmentator.py @@ -15,7 +15,7 @@ import numpy as np from totalsegmentator.python_api import totalsegmentator -from physiomotion4d.segment_anatomy_base import SegmentAnatomyBase +from .segment_anatomy_base import SegmentAnatomyBase class SegmentChestTotalSegmentator(SegmentAnatomyBase): diff --git a/src/physiomotion4d/segment_heart_simpleware.py b/src/physiomotion4d/segment_heart_simpleware.py index 63ba60d..a9216ce 100644 --- a/src/physiomotion4d/segment_heart_simpleware.py +++ b/src/physiomotion4d/segment_heart_simpleware.py @@ -17,7 +17,7 @@ import numpy as np from itk import TubeTK as tube -from physiomotion4d.segment_anatomy_base import SegmentAnatomyBase +from .segment_anatomy_base import SegmentAnatomyBase class SegmentHeartSimpleware(SegmentAnatomyBase): diff --git a/src/physiomotion4d/test_tools.py b/src/physiomotion4d/test_tools.py index 653e7e8..083e7da 100644 --- a/src/physiomotion4d/test_tools.py +++ b/src/physiomotion4d/test_tools.py @@ -17,7 +17,7 @@ import itk import numpy as np -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase +from .physiomotion4d_base import PhysioMotion4DBase # Repo root: src/physiomotion4d/test_tools.py -> parent.parent.parent _REPO_ROOT = Path(__file__).resolve().parent.parent.parent @@ -464,7 +464,7 @@ def save_screenshot_openusd( import pyvista as pv - from physiomotion4d.usd_tools import USDTools + from .usd_tools import USDTools # On headless Linux runners VTK needs an X server or off-screen GL # context. If DISPLAY is already provided (e.g. xvfb-run wrapping diff --git a/src/physiomotion4d/transform_tools.py b/src/physiomotion4d/transform_tools.py index 9400825..dd7da81 100644 --- a/src/physiomotion4d/transform_tools.py +++ b/src/physiomotion4d/transform_tools.py @@ -31,9 +31,9 @@ from numpy.typing import NDArray from pxr import Gf, Sdf, Usd, UsdGeom -from physiomotion4d.image_tools import ImageTools -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase -from physiomotion4d.vtk_to_usd import add_framing_camera +from .image_tools import ImageTools +from .physiomotion4d_base import PhysioMotion4DBase +from .vtk_to_usd import add_framing_camera FloatArray: TypeAlias = NDArray[np.float32] | NDArray[np.float64] diff --git a/src/physiomotion4d/usd_anatomy_tools.py b/src/physiomotion4d/usd_anatomy_tools.py index e703eba..9125767 100644 --- a/src/physiomotion4d/usd_anatomy_tools.py +++ b/src/physiomotion4d/usd_anatomy_tools.py @@ -34,7 +34,7 @@ from pxr import Sdf, UsdGeom, UsdShade -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase +from .physiomotion4d_base import PhysioMotion4DBase # Default OmniSurface render parameters keyed by group name (matching # :class:`physiomotion4d.AnatomyTaxonomy.group_names`) and by organ-level diff --git a/src/physiomotion4d/usd_tools.py b/src/physiomotion4d/usd_tools.py index 05b9173..3e2af6a 100644 --- a/src/physiomotion4d/usd_tools.py +++ b/src/physiomotion4d/usd_tools.py @@ -21,8 +21,8 @@ import pyvista as pvtk from pxr import Gf, Sdf, Usd, UsdGeom, UsdShade -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase -from physiomotion4d.vtk_to_usd import add_framing_camera +from .physiomotion4d_base import PhysioMotion4DBase +from .vtk_to_usd import add_framing_camera class USDTools(PhysioMotion4DBase): diff --git a/src/physiomotion4d/workflow_convert_image_to_usd.py b/src/physiomotion4d/workflow_convert_image_to_usd.py index 554eb69..95a681b 100644 --- a/src/physiomotion4d/workflow_convert_image_to_usd.py +++ b/src/physiomotion4d/workflow_convert_image_to_usd.py @@ -15,18 +15,18 @@ import numpy as np import pyvista as pv -from physiomotion4d import ConvertVTKToUSD -from physiomotion4d.contour_tools import ContourTools -from physiomotion4d.convert_image_4d_to_3d import ConvertImage4DTo3D -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase -from physiomotion4d.register_images_base import RegisterImagesBase -from physiomotion4d.register_images_greedy import RegisterImagesGreedy -from physiomotion4d.register_images_icon import RegisterImagesICON -from physiomotion4d.segment_anatomy_base import SegmentAnatomyBase -from physiomotion4d.segment_chest_total_segmentator import SegmentChestTotalSegmentator -from physiomotion4d.segment_heart_simpleware import SegmentHeartSimpleware -from physiomotion4d.transform_tools import TransformTools -from physiomotion4d.usd_anatomy_tools import USDAnatomyTools +from .contour_tools import ContourTools +from .convert_image_4d_to_3d import ConvertImage4DTo3D +from .convert_vtk_to_usd import ConvertVTKToUSD +from .physiomotion4d_base import PhysioMotion4DBase +from .register_images_base import RegisterImagesBase +from .register_images_greedy import RegisterImagesGreedy +from .register_images_icon import RegisterImagesICON +from .segment_anatomy_base import SegmentAnatomyBase +from .segment_chest_total_segmentator import SegmentChestTotalSegmentator +from .segment_heart_simpleware import SegmentHeartSimpleware +from .transform_tools import TransformTools +from .usd_anatomy_tools import USDAnatomyTools #: Supported segmentation backend identifiers. SEGMENTATION_METHODS: tuple[str, ...] = ( diff --git a/src/physiomotion4d/workflow_convert_image_to_vtk.py b/src/physiomotion4d/workflow_convert_image_to_vtk.py index 8617701..b900a36 100644 --- a/src/physiomotion4d/workflow_convert_image_to_vtk.py +++ b/src/physiomotion4d/workflow_convert_image_to_vtk.py @@ -32,10 +32,10 @@ import numpy as np import pyvista as pv -from physiomotion4d.contour_tools import ContourTools -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase -from physiomotion4d.segment_anatomy_base import SegmentAnatomyBase -from physiomotion4d.usd_anatomy_tools import USDAnatomyTools +from .contour_tools import ContourTools +from .physiomotion4d_base import PhysioMotion4DBase +from .segment_anatomy_base import SegmentAnatomyBase +from .usd_anatomy_tools import USDAnatomyTools #: Ordered tuple of anatomy group names matching :meth:`SegmentAnatomyBase.segment` keys. ANATOMY_GROUPS: tuple[str, ...] = ( @@ -148,7 +148,7 @@ def __init__( def _create_segmenter(self) -> SegmentAnatomyBase: """Instantiate the chosen segmentation backend (lazy import).""" if self.segmentation_method_name == "ChestTotalSegmentator": - from physiomotion4d.segment_chest_total_segmentator import ( + from .segment_chest_total_segmentator import ( SegmentChestTotalSegmentator, ) @@ -157,7 +157,7 @@ def _create_segmenter(self) -> SegmentAnatomyBase: "HeartSimpleware", "HeartSimplewareTrimmedBranches", ): - from physiomotion4d.segment_heart_simpleware import SegmentHeartSimpleware + from .segment_heart_simpleware import SegmentHeartSimpleware segmenter = SegmentHeartSimpleware(log_level=self.log_level) segmenter.set_trim_branches( diff --git a/src/physiomotion4d/workflow_convert_vtk_to_usd.py b/src/physiomotion4d/workflow_convert_vtk_to_usd.py index 4eea0f4..e3b8696 100644 --- a/src/physiomotion4d/workflow_convert_vtk_to_usd.py +++ b/src/physiomotion4d/workflow_convert_vtk_to_usd.py @@ -12,10 +12,10 @@ from pathlib import Path from typing import Literal -from physiomotion4d.convert_vtk_to_usd import ConvertVTKToUSD -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase -from physiomotion4d.usd_anatomy_tools import USDAnatomyTools -from physiomotion4d.usd_tools import USDTools +from .convert_vtk_to_usd import ConvertVTKToUSD +from .physiomotion4d_base import PhysioMotion4DBase +from .usd_anatomy_tools import USDAnatomyTools +from .usd_tools import USDTools AppearanceKind = Literal["solid", "anatomy", "colormap"] diff --git a/src/physiomotion4d/workflow_create_statistical_model.py b/src/physiomotion4d/workflow_create_statistical_model.py index f5a8354..ef6014d 100644 --- a/src/physiomotion4d/workflow_create_statistical_model.py +++ b/src/physiomotion4d/workflow_create_statistical_model.py @@ -16,11 +16,11 @@ import pyvista as pv from sklearn.decomposition import PCA -from physiomotion4d.contour_tools import ContourTools -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase -from physiomotion4d.register_models_distance_maps import RegisterModelsDistanceMaps -from physiomotion4d.register_models_icp import RegisterModelsICP -from physiomotion4d.transform_tools import TransformTools +from .contour_tools import ContourTools +from .physiomotion4d_base import PhysioMotion4DBase +from .register_models_distance_maps import RegisterModelsDistanceMaps +from .register_models_icp import RegisterModelsICP +from .transform_tools import TransformTools def _extract_surface(mesh: pv.DataSet) -> pv.PolyData: @@ -40,7 +40,7 @@ class WorkflowCreateStatisticalModel(PhysioMotion4DBase): 1. Extract surfaces from sample and reference meshes, or keep as meshes 2. ICP alignment: align each sample surface to the reference (template) surface. Always extract surfaces for ICP alignment. - 3. Deformable registration: establish dense correspondence via mask-based SyN. Uses + 3. Deformable registration: establish dense correspondence via Greedy affine + ICON deformable registration. Uses either full meshes or surfaces. 4. Correspondence: warp reference model by each transform to get aligned shapes 5. PCA: compute mean and modes from corresponded shapes @@ -191,7 +191,6 @@ def _step3_deformable_correspondence(self) -> None: ) result = registrar.register( transform_type="Deformable", - use_ICON=False, ) new_aligned_models.append(result["registered_model"]) diff --git a/src/physiomotion4d/workflow_fine_tune_icon_registration.py b/src/physiomotion4d/workflow_fine_tune_icon_registration.py index c6ee539..c64bc36 100644 --- a/src/physiomotion4d/workflow_fine_tune_icon_registration.py +++ b/src/physiomotion4d/workflow_fine_tune_icon_registration.py @@ -42,10 +42,10 @@ import numpy as np import yaml -from physiomotion4d.labelmap_tools import LabelmapTools -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase -from physiomotion4d.register_time_series_images import RegisterTimeSeriesImages -from physiomotion4d.transform_tools import TransformTools +from .labelmap_tools import LabelmapTools +from .physiomotion4d_base import PhysioMotion4DBase +from .register_time_series_images import RegisterTimeSeriesImages +from .transform_tools import TransformTools Landmarks = dict[str, tuple[float, float, float]] diff --git a/src/physiomotion4d/workflow_fit_statistical_model_to_patient.py b/src/physiomotion4d/workflow_fit_statistical_model_to_patient.py index 0764c99..bd58f0e 100644 --- a/src/physiomotion4d/workflow_fit_statistical_model_to_patient.py +++ b/src/physiomotion4d/workflow_fit_statistical_model_to_patient.py @@ -29,16 +29,16 @@ import numpy as np import pyvista as pv -from physiomotion4d.contour_tools import ContourTools -from physiomotion4d.labelmap_tools import LabelmapTools -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase -from physiomotion4d.register_images_greedy import RegisterImagesGreedy -from physiomotion4d.register_images_icon import RegisterImagesICON -from physiomotion4d.register_models_distance_maps import RegisterModelsDistanceMaps -from physiomotion4d.register_models_icp import RegisterModelsICP -from physiomotion4d.register_models_pca import RegisterModelsPCA -from physiomotion4d.transform_tools import TransformTools -from physiomotion4d.workflow_convert_image_to_vtk import WorkflowConvertImageToVTK +from .contour_tools import ContourTools +from .labelmap_tools import LabelmapTools +from .physiomotion4d_base import PhysioMotion4DBase +from .register_images_greedy import RegisterImagesGreedy +from .register_images_icon import RegisterImagesICON +from .register_models_distance_maps import RegisterModelsDistanceMaps +from .register_models_icp import RegisterModelsICP +from .register_models_pca import RegisterModelsPCA +from .transform_tools import TransformTools +from .workflow_convert_image_to_vtk import WorkflowConvertImageToVTK def _load_tubetk() -> Any: @@ -680,12 +680,10 @@ def register_model_to_model_pca(self) -> dict: "registered_template_labelmap": self.pca_template_labelmap, } - def register_mask_to_mask( - self, use_ICON_refinement: bool = False - ) -> Optional[dict]: + def register_mask_to_mask(self) -> Optional[dict]: """Perform mask-based deformable registration of model to patient model. - Uses RegisterModelsDistanceMaps class for ANTs deformable registration. + Uses RegisterModelsDistanceMaps with Greedy affine followed by ICON deformable registration. Returns: dict: Dictionary containing: @@ -717,7 +715,6 @@ def register_mask_to_mask( # Run deformable registration mask_result = mask_registrar.register( transform_type="Deformable", - use_ICON=use_ICON_refinement, ) # Store results @@ -948,8 +945,9 @@ def run_workflow( set via set_use_mask_to_image_registration(True, ...). Args: - use_ICON_registration_refinement: Whether to include icon registration - refinement stage. Default: False + use_ICON_registration_refinement: Whether to apply ICON refinement in the + mask-to-image stage (Stage 4). The mask-to-mask stage always uses + Greedy affine + ICON deformable. Default: False Returns: dict with registered_template_model and registered_template_model_surface @@ -968,9 +966,7 @@ def run_workflow( # Stage 3: Optional Mask-to-mask deformable registration if self.use_m2m_registration: - self.register_mask_to_mask( - use_ICON_refinement=use_ICON_registration_refinement - ) + self.register_mask_to_mask() # Stage 4: Optional mask-to-image refinement if self.use_m2i_registration: diff --git a/src/physiomotion4d/workflow_reconstruct_highres_4d_ct.py b/src/physiomotion4d/workflow_reconstruct_highres_4d_ct.py index 8b12136..9c63d03 100644 --- a/src/physiomotion4d/workflow_reconstruct_highres_4d_ct.py +++ b/src/physiomotion4d/workflow_reconstruct_highres_4d_ct.py @@ -28,8 +28,8 @@ import itk -from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase -from physiomotion4d.register_time_series_images import RegisterTimeSeriesImages +from .physiomotion4d_base import PhysioMotion4DBase +from .register_time_series_images import RegisterTimeSeriesImages class WorkflowReconstructHighres4DCT(PhysioMotion4DBase): diff --git a/tutorials/tutorial_04_fit_statistical_model_to_patient.py b/tutorials/tutorial_04_fit_statistical_model_to_patient.py index d9d4c3a..810ce11 100644 --- a/tutorials/tutorial_04_fit_statistical_model_to_patient.py +++ b/tutorials/tutorial_04_fit_statistical_model_to_patient.py @@ -33,8 +33,7 @@ # multiprocessing.Pool. On Windows the spawn start method re-imports this # script in each child; without the __name__ == "__main__" guard around # top-level work, that re-import fires the segmenter again and Python's -# spawn-cascade detector raises RuntimeError. Wrapping consistently across -# tutorials also matches the style of tutorial_01. +# spawn-cascade detector raises RuntimeError. if __name__ == "__main__": # %% # Data directory specification From e85710dd4c3b5f7107b9962cce99f885a03b4479 Mon Sep 17 00:00:00 2001 From: Stephen Aylward Date: Fri, 12 Jun 2026 17:25:51 -0400 Subject: [PATCH 2/3] BUG: Affine transform mask when pre-transforming moving image --- src/physiomotion4d/register_models_distance_maps.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/physiomotion4d/register_models_distance_maps.py b/src/physiomotion4d/register_models_distance_maps.py index 9dcbd82..63312a9 100644 --- a/src/physiomotion4d/register_models_distance_maps.py +++ b/src/physiomotion4d/register_models_distance_maps.py @@ -311,13 +311,19 @@ def register( icon_iterations, ) - # Pre-align moving mask with the Greedy affine result + # Pre-align moving image and ROI mask into the fixed grid using the Greedy affine result moving_mask_affine_transformed = self.transform_tools.transform_image( self.moving_mask_image, forward_transform_Greedy, self.reference_image, interpolation_method="linear", ) + moving_mask_roi_affine_transformed = self.transform_tools.transform_image( + self.moving_mask_roi_image, + forward_transform_Greedy, + self.reference_image, + interpolation_method="nearest_neighbor", + ) # Configure and run ICON self.registrar_ICON.set_number_of_iterations(icon_iterations) @@ -326,7 +332,7 @@ def register( result_ICON = self.registrar_ICON.register( moving_image=moving_mask_affine_transformed, - moving_mask=self.moving_mask_roi_image, + moving_mask=moving_mask_roi_affine_transformed, ) forward_transform_ICON = result_ICON["forward_transform"] inverse_transform_ICON = result_ICON["inverse_transform"] From da6ad141b04644af308d8e851b3885d698dc76e8 Mon Sep 17 00:00:00 2001 From: Stephen Aylward Date: Fri, 12 Jun 2026 21:49:36 -0400 Subject: [PATCH 3/3] BUG: Fixed enum name --- src/physiomotion4d/register_models_distance_maps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/physiomotion4d/register_models_distance_maps.py b/src/physiomotion4d/register_models_distance_maps.py index 63312a9..b93af17 100644 --- a/src/physiomotion4d/register_models_distance_maps.py +++ b/src/physiomotion4d/register_models_distance_maps.py @@ -322,7 +322,7 @@ def register( self.moving_mask_roi_image, forward_transform_Greedy, self.reference_image, - interpolation_method="nearest_neighbor", + interpolation_method="nearest", ) # Configure and run ICON