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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ repos:

# Type checking with mypy
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1
rev: v2.1.0
hooks:
- id: mypy
# With pass_filenames: false, we must provide an explicit target for mypy.
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,11 @@ major workflow. They are the recommended starting point for new users.
| 4 | `tutorials/tutorial_04_fit_statistical_model_to_patient.py` | Fit statistical model to patient | KCL-Heart-Model plus Tutorial 3 output |
| 5 | `tutorials/tutorial_05_vtk_to_usd.py` | VTK surfaces to animated USD | output of tutorial 2 |
| 6 | `tutorials/tutorial_06_reconstruct_highres_4d_ct.py` | Reconstruct high-res 4D CT | DirLab-4DCT (manual) |
| 7 | `tutorials/tutorial_07_dirlab_pca_model.py` | Build a surface PCA lung-lobe model and fit all cases | DirLab-4DCT (manual) |
| 8 | `tutorials/tutorial_08_dirlab_pca_time_series.py` | Propagate PCA-fitted lung-lobe meshes through DirLab time series | DirLab-4DCT plus Tutorial 7 output |
| 9 | `tutorials/tutorial_09_physicsnemo_mesh_stage_model.py` | Train a PhysicsNeMo mesh stage model (requires `[physicsnemo]` extra; Python >= 3.11) | Tutorial 8 output |
| 8 | `tutorials/tutorial_08_cardiac_fit_model.py` | Fit the cardiac PCA model and propagate it through gated phases | Bring your own (`D:/PhysioMotion4D/`) |
| 9a | `tutorials/tutorial_09a_cardiac_train_physicsnemo_mgn.py` | Train a PhysicsNeMo MeshGraphNet cardiac stage model (requires `[physicsnemo]` + `torch-geometric`; Python >= 3.11) | Tutorial 8 output |
| 9b | `tutorials/tutorial_09b_cardiac_train_physicsnemo_mlp.py` | Train a PhysicsNeMo MLP cardiac stage model (requires `[physicsnemo]` extra; Python >= 3.11) | Tutorial 8 output |
| 10a | `tutorials/tutorial_10a_cardiac_eval_physicsnemo_mgn.py` | Predict / score cardiac surfaces from a MeshGraphNet checkpoint | Tutorial 9a checkpoint |
| 10b | `tutorials/tutorial_10b_cardiac_eval_physicsnemo_mlp.py` | Predict / score cardiac surfaces from an MLP checkpoint | Tutorial 9b checkpoint |

Each tutorial is a `# %%` percent-cell Python script. Paths are defined near
the top of the script; edit those constants for custom data/output locations,
Expand Down
114 changes: 68 additions & 46 deletions docs/tutorials.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,24 +55,24 @@ Tutorials
<p>Register respiratory CT phases and reconstruct a higher-resolution 4D volume series.</p>
<span class="pm4d-card__meta">DirLab-4DCT</span>
</a>
<a class="pm4d-card" href="#tutorial-7-dirlab-lung-lobe-pca-model">
<span class="pm4d-card__number">07</span>
<h2>DirLab Lung-Lobe PCA Model</h2>
<p>Build a surface PCA model from five lung lobes and fit it to all cases.</p>
<span class="pm4d-card__meta">DirLab-4DCT</span>
</a>
<a class="pm4d-card" href="#tutorial-8-dirlab-pca-time-series-propagation">
<a class="pm4d-card" href="#tutorial-8-fit-the-cardiac-ssm-and-propagate-through-gated-phases">
<span class="pm4d-card__number">08</span>
<h2>DirLab PCA Time-Series Propagation</h2>
<p>Register respiratory phases with ANTs+ICON and propagate fitted meshes.</p>
<span class="pm4d-card__meta">Tutorial 7 output</span>
<h2>Fit the Cardiac SSM and Propagate Through Gated Phases</h2>
<p>Fit a PCA heart model to the reference phase and propagate it to every gated phase with ICON registration.</p>
<span class="pm4d-card__meta">Bring your own cardiac data</span>
</a>
<a class="pm4d-card" href="#tutorial-9-physicsnemo-mesh-stage-model">
<a class="pm4d-card" href="#tutorial-9a-9b-train-a-physicsnemo-cardiac-stage-model">
<span class="pm4d-card__number">09</span>
<h2>PhysicsNeMo Mesh Stage Model</h2>
<p>Train a PhysicsNeMo MLP to predict lung-lobe meshes at requested stages.</p>
<h2>Train a PhysicsNeMo Cardiac Stage Model</h2>
<p>Train a PhysicsNeMo MeshGraphNet (9a) or MLP (9b) to predict cardiac meshes at requested stages.</p>
<span class="pm4d-card__meta">Tutorial 8 output</span>
</a>
<a class="pm4d-card" href="#tutorial-10a-10b-predict-and-evaluate-cardiac-surfaces">
<span class="pm4d-card__number">10</span>
<h2>Predict and Evaluate Cardiac Surfaces</h2>
<p>Load a Tutorial 9 checkpoint and predict cardiac surfaces at gated phases or caller-specified stages.</p>
<span class="pm4d-card__meta">Tutorial 9a / 9b output</span>
</a>
</section>

Recommended Run Order
Expand All @@ -89,9 +89,12 @@ arguments.
3. Run Tutorial 3 after downloading KCL-Heart-Model.
4. Run Tutorial 4 after Tutorial 3 because it can consume the PCA model output.
5. Run Tutorial 6 after downloading DirLab-4DCT.
6. Run Tutorial 7 after downloading DirLab-4DCT.
7. Run Tutorial 8 after Tutorial 7 because it consumes fitted PCA meshes.
8. Run Tutorial 9 after Tutorial 8 because it trains from propagated meshes.
6. Run Tutorial 8 after preparing your own cardiac gated CT, labelmaps, KCL
volume PCA model, and ICON weights (bring-your-own-data; see the note below).
7. Run Tutorial 9a and/or 9b after Tutorial 8 because they train from its
fitted meshes.
8. Run Tutorial 10a and/or 10b after Tutorial 9a / 9b because they evaluate
the trained checkpoints.

Tutorial 1: Heart-Gated CT to Animated USD
==========================================
Expand Down Expand Up @@ -215,74 +218,93 @@ Outputs
Registered respiratory phases, reconstructed high-resolution CT volumes,
and preview screenshots.

Tutorial 7: DirLab Lung-Lobe PCA Model
======================================
.. note::

Tutorials 8-10 form the cardiac mesh stage-prediction pipeline and are
**bring-your-own-data**: unlike Tutorials 1-6 they do not use the repository
``data/`` directory or a downloadable sample. Their path constants point at a
local ``D:/PhysioMotion4D/`` cardiac layout (gated CT, labelmaps, the KCL
volume PCA model, and ICON weights); edit those constants to match your own
data. The former DirLab lung-lobe PCA tutorial (number 7) has been removed;
numbering continues at 8.

Comment thread
aylward marked this conversation as resolved.
Tutorial 8: Fit the Cardiac SSM and Propagate Through Gated Phases
==================================================================

Script
``tutorials/tutorial_07_dirlab_pca_model.py``
``tutorials/tutorial_08_cardiac_fit_model.py``

Workflow
``WorkflowConvertImageToVTK``, ``WorkflowCreateStatisticalModel``, and
``WorkflowFitStatisticalModelToPatient``
``WorkflowFitStatisticalModelToPatient`` (PCA registration) and
``WorkflowReconstructHighres4DCT`` (ICON time-series registration)

Dataset
DirLab-4DCT, downloaded manually.
Bring your own cardiac gated CT, labelmaps, KCL volume PCA model, and ICON
weights under ``D:/PhysioMotion4D/``.

Run
.. code-block:: bash

python tutorials/tutorial_07_dirlab_pca_model.py
python tutorials/tutorial_08_cardiac_fit_model.py

Outputs
Five-lobe lung surface meshes, a surface PCA model, and PCA-fitted surfaces
for every available case.
Per-patient fitted SSM mesh/surface, PCA coefficients, and the SSM warped to
every gated phase, all written under ``OUTPUT_DIR``.

Tutorial 8: DirLab PCA Time-Series Propagation
==============================================
Tutorial 9a / 9b: Train a PhysicsNeMo Cardiac Stage Model
=========================================================

Script
``tutorials/tutorial_08_dirlab_pca_time_series.py``
``tutorials/tutorial_09a_cardiac_train_physicsnemo_mgn.py`` (MeshGraphNet) and
``tutorials/tutorial_09b_cardiac_train_physicsnemo_mlp.py`` (MLP)

Workflow
``RegisterTimeSeriesImages`` with ``registration_method='Greedy_ICON'`` and
``TransformTools``
``physicsnemo.models.meshgraphnet.MeshGraphNet`` (9a) and
``physicsnemo.models.mlp.FullyConnected`` (9b), trained on Tutorial 8 meshes.

Dataset
DirLab-4DCT plus Tutorial 7 fitted mesh outputs.
Tutorial 8 fitted-mesh outputs.

Extra install
PhysicsNeMo is an optional dependency. Install with
``pip install "physiomotion4d[physicsnemo]"`` (requires Python >= 3.11). The
MeshGraphNet variant also requires ``torch-geometric``.

Run
.. code-block:: bash

python tutorials/tutorial_08_dirlab_pca_time_series.py
python tutorials/tutorial_09a_cardiac_train_physicsnemo_mgn.py
python tutorials/tutorial_09b_cardiac_train_physicsnemo_mlp.py

Outputs
Per-case ANTs+ICON transforms and one PCA-fitted lung-lobe surface for each
DirLab respiratory phase.
Shared PhysicsNeMo checkpoints, training metadata, loss / RMSE histories, and
held-out predictions written under each trainer's ``OUTPUT_DIR``.

Tutorial 9: PhysicsNeMo Mesh Stage Model
========================================
Tutorial 10a / 10b: Predict and Evaluate Cardiac Surfaces
=========================================================

Script
``tutorials/tutorial_09_physicsnemo_mesh_stage_model.py``
``tutorials/tutorial_10a_cardiac_eval_physicsnemo_mgn.py`` (MeshGraphNet) and
``tutorials/tutorial_10b_cardiac_eval_physicsnemo_mlp.py`` (MLP)

Workflow
``physicsnemo.models.mlp.FullyConnected`` trained on Tutorial 8 meshes.
Load a Tutorial 9 checkpoint and predict cardiac surfaces for one subject at
each gated phase (with error statistics) or at caller-specified stages.

Dataset
Tutorial 8 propagated PCA mesh outputs.

Extra install
PhysicsNeMo is an optional dependency. Install with
``pip install "physiomotion4d[physicsnemo]"`` (requires Python >= 3.11).
Tutorial 9a / 9b trained checkpoints plus the Tutorial 8 fitted meshes.

Run
.. code-block:: bash

python tutorials/tutorial_09_physicsnemo_mesh_stage_model.py
python tutorials/tutorial_10b_cardiac_eval_physicsnemo_mlp.py pm0002 --epoch 5000 --out results/pm0002

Run with no arguments to use the ``run_tutorial`` entry point and its
``DEFAULT_SUBJECT`` / ``DEFAULT_EPOCH`` constants.

Outputs
Per-case PhysicsNeMo checkpoints, training metadata, loss histories, and a
predicted PCA-fitted mesh at the requested normalized respiratory stage.
Predicted ``.vtp`` surfaces per phase (with per-point error arrays when
ground truth exists) and a ``statistics.csv`` error summary.

Dataset Notes
=============
Expand Down
1 change: 1 addition & 0 deletions experiments/LongitudinalRegistration/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
uniGradICON
fixed
*.txt
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ dev = [
"pre-commit>=3.0.0",
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"mypy>=1.0.0",
"mypy>=2.1.0",
"ruff>=0.1.0",
"scipy-stubs>=1.14.0.0,<1.16.0.0",
"types-requests>=2.32.0.0",
Expand Down Expand Up @@ -268,7 +268,11 @@ module = [
"physiomotion4d.cli.visualize_pca_modes",
"physiomotion4d.vtk_to_usd.mesh_utils",
"physiomotion4d.vtk_to_usd.vtk_reader",
"tutorial_09_physicsnemo_mesh_stage_model",
"tutorial_08_cardiac_fit_model",
"tutorial_09a_cardiac_train_physicsnemo_mgn",
"tutorial_09b_cardiac_train_physicsnemo_mlp",
"tutorial_10a_cardiac_eval_physicsnemo_mgn",
"tutorial_10b_cardiac_eval_physicsnemo_mlp",
]
disable_error_code = ["import-not-found", "import-untyped"]

Expand Down
10 changes: 7 additions & 3 deletions src/physiomotion4d/workflow_reconstruct_highres_4d_ct.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def __init__(

# Set default number of iterations based on registration method
self.number_of_iterations_Greedy: list[int] = [30, 15, 7, 3]
self.number_of_iterations_ICON: int = 20
self.number_of_iterations_ICON: Optional[int] = 20

# Initialize registrar
self.registrar = RegisterTimeSeriesImages(
Expand All @@ -184,11 +184,15 @@ def set_number_of_iterations_Greedy(
"""
self.number_of_iterations_Greedy = number_of_iterations_Greedy

def set_number_of_iterations_ICON(self, number_of_iterations_ICON: int) -> None:
def set_number_of_iterations_ICON(
self, number_of_iterations_ICON: Optional[int]
) -> None:
"""Set the number of iterations for ICON registration.

Args:
number_of_iterations_ICON: Number of fine-tuning steps for ICON
number_of_iterations_ICON: Number of fine-tuning steps for ICON.
If None, ICON fine-tuning is disabled (the pretrained network
is used as-is).
"""
self.number_of_iterations_ICON = number_of_iterations_ICON

Expand Down
127 changes: 127 additions & 0 deletions tests/test_tutorials.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@

from __future__ import annotations

import importlib.util
import runpy
import sys
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -231,6 +233,131 @@ def test_run(self, test_directories: dict[str, Path]) -> None:
_compare_screenshots(results["screenshots"], tt)


# -----------------------------------------------------------------------------
# Tutorials 8-10 - Cardiac mesh stage-prediction pipeline (bring-your-own-data)
#
# These tutorials use a local ``D:/PhysioMotion4D/`` cardiac dataset and (for
# Tutorials 9 and 10) the optional PhysicsNeMo dependency, so they are skipped
# automatically unless that data / those checkpoints are present. They produce
# no screenshots; the tests assert the tutorial ran and populated
# ``tutorial_results``.
# -----------------------------------------------------------------------------

_CARDIAC_DATA_ROOT = Path("D:/PhysioMotion4D")
_CARDIAC_FITTED_MESHES_DIR = _CARDIAC_DATA_ROOT / "duke_data" / "fitted_kcl_meshes"
_TUTORIALS_DIR = _REPO_ROOT / "tutorials"


def _physicsnemo_available() -> bool:
"""True if the optional PhysicsNeMo dependency is importable."""
return importlib.util.find_spec("physicsnemo") is not None


def _torch_geometric_available() -> bool:
"""True if the optional PyTorch Geometric dependency is importable."""
return importlib.util.find_spec("torch_geometric") is not None


def _run_eval_tutorial(script_name: str) -> dict[str, Any]:
"""Run a Tutorial 10 eval script through its no-argument ``run_tutorial`` path.

``runpy`` does not reset ``sys.argv``, so without this the eval scripts would
see pytest's arguments and try to parse them as CLI options. Force a single
argv entry so the ``len(sys.argv) > 1`` dispatch selects ``run_tutorial``.
"""
saved_argv = sys.argv
sys.argv = [script_name]
try:
return _run_tutorial_script(script_name)
finally:
sys.argv = saved_argv


@pytest.mark.tutorial
@pytest.mark.slow
class TestTutorial08CardiacFitModel:
"""End-to-end test for tutorial_08_cardiac_fit_model.py (bring-your-own-data)."""

def test_run(self) -> None:
if not (_CARDIAC_DATA_ROOT / "duke_data" / "gated_nii").exists():
pytest.skip(
"Cardiac dataset not present at D:/PhysioMotion4D/. Tutorial 8 is "
"bring-your-own-data; see tutorials/README.md."
)
results = _run_tutorial_script("tutorial_08_cardiac_fit_model.py")
assert "patients" in results, "Tutorial 8 should report processed patients"


@pytest.mark.tutorial
@pytest.mark.slow
@pytest.mark.requires_gpu
class TestTutorial09aCardiacTrainMGN:
"""End-to-end test for tutorial_09a_cardiac_train_physicsnemo_mgn.py."""

def test_run(self) -> None:
if not _physicsnemo_available():
pytest.skip("PhysicsNeMo not installed (optional [physicsnemo] extra).")
if not _torch_geometric_available():
pytest.skip("torch-geometric not installed (required for MeshGraphNet).")
if not _CARDIAC_FITTED_MESHES_DIR.exists():
pytest.skip("Tutorial 8 cardiac output not present; run Tutorial 8 first.")
results = _run_tutorial_script("tutorial_09a_cardiac_train_physicsnemo_mgn.py")
assert isinstance(results, dict)


@pytest.mark.tutorial
@pytest.mark.slow
@pytest.mark.requires_gpu
class TestTutorial09bCardiacTrainMLP:
"""End-to-end test for tutorial_09b_cardiac_train_physicsnemo_mlp.py."""

def test_run(self) -> None:
if not _physicsnemo_available():
pytest.skip("PhysicsNeMo not installed (optional [physicsnemo] extra).")
if not _CARDIAC_FITTED_MESHES_DIR.exists():
pytest.skip("Tutorial 8 cardiac output not present; run Tutorial 8 first.")
results = _run_tutorial_script("tutorial_09b_cardiac_train_physicsnemo_mlp.py")
assert isinstance(results, dict)


@pytest.mark.tutorial
@pytest.mark.slow
class TestTutorial10aCardiacEvalMGN:
"""End-to-end test for tutorial_10a_cardiac_eval_physicsnemo_mgn.py."""

def test_run(self) -> None:
if not _physicsnemo_available():
pytest.skip("PhysicsNeMo not installed (optional [physicsnemo] extra).")
if not _torch_geometric_available():
pytest.skip("torch-geometric not installed (required for MeshGraphNet).")
checkpoint = _TUTORIALS_DIR / "output_mgn" / "mgn_stage_model.pt"
if not checkpoint.exists() or not _CARDIAC_FITTED_MESHES_DIR.exists():
pytest.skip(
"Tutorial 9a checkpoint or cardiac data not present; "
"run Tutorials 8 and 9a first."
)
results = _run_eval_tutorial("tutorial_10a_cardiac_eval_physicsnemo_mgn.py")
assert "predicted_files" in results


@pytest.mark.tutorial
@pytest.mark.slow
class TestTutorial10bCardiacEvalMLP:
"""End-to-end test for tutorial_10b_cardiac_eval_physicsnemo_mlp.py."""

def test_run(self) -> None:
if not _physicsnemo_available():
pytest.skip("PhysicsNeMo not installed (optional [physicsnemo] extra).")
checkpoint = _TUTORIALS_DIR / "output" / "physicsnemo_stage_model.pt"
if not checkpoint.exists() or not _CARDIAC_FITTED_MESHES_DIR.exists():
pytest.skip(
"Tutorial 9b checkpoint or cardiac data not present; "
"run Tutorials 8 and 9b first."
)
results = _run_eval_tutorial("tutorial_10b_cardiac_eval_physicsnemo_mlp.py")
assert "predicted_files" in results


# -----------------------------------------------------------------------------
# Tutorial 6 - Reconstruct High-Resolution 4D CT
# -----------------------------------------------------------------------------
Expand Down
Loading
Loading