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
12 changes: 9 additions & 3 deletions docs/API_MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -779,8 +779,8 @@ _Re-run `py utils/generate_api_map.py` whenever public APIs change._
## src/physiomotion4d/workflow_convert_image_to_usd.py

- **class WorkflowConvertImageToUSD** (line 42): Complete workflow for converting 4D CT images to dynamic USD models.
- `def __init__(self, input_filenames, contrast_enhanced, output_directory, project_name, reference_image_filename=None, number_of_registration_iterations=1, segmentation_method='ChestTotalSegmentator', registration_method='ICON', log_level=logging.INFO, save_registered_images=True, save_registration_transforms=True, save_labelmaps=True)` (line 61): Initialize the image-to-USD workflow.
- `def process(self)` (line 235): Execute the complete workflow from 4D CT to dynamic USD models.
- `def __init__(self, input_filenames, contrast_enhanced, output_directory, project_name, reference_image_filename=None, number_of_registration_iterations=1, segmentation_method='ChestTotalSegmentator', registration_method='ICON', times_per_second=24.0, log_level=logging.INFO, save_registered_images=True, save_registration_transforms=True, save_labelmaps=True)` (line 61): Initialize the image-to-USD workflow.
- `def process(self)` (line 239): Execute the complete workflow from 4D CT to dynamic USD models.

## src/physiomotion4d/workflow_convert_image_to_vtk.py

Expand Down Expand Up @@ -874,7 +874,9 @@ _Re-run `py utils/generate_api_map.py` whenever public APIs change._

## tests/test_cli_smoke.py

- `def test_cli_help(module_name, monkeypatch, capsys)` (line 25): Each CLI module exits successfully for --help.
- `def test_cli_help(module_name, monkeypatch, capsys)` (line 27): Each CLI module exits successfully for --help.
- `def test_convert_image_to_usd_help_includes_fps(monkeypatch, capsys)` (line 44): Image-to-USD CLI exposes playback FPS for animated USD output.
- `def test_convert_image_to_usd_cli_passes_fps(monkeypatch, tmp_path)` (line 60): Image-to-USD CLI forwards --fps as times_per_second.

## tests/test_contour_tools.py

Expand Down Expand Up @@ -1168,6 +1170,10 @@ _Re-run `py utils/generate_api_map.py` whenever public APIs change._
- `def test_normals_remain_unit_length(self, tmp_path)` (line 384): Normal vectors must not be scaled.
- `def test_stage_meters_per_unit(self, tmp_path)` (line 404): Stage metersPerUnit metadata must be 1.0.

## tests/test_workflow_convert_image_to_usd.py

- `def test_create_usd_files_passes_times_per_second(monkeypatch, tmp_path)` (line 14): Workflow forwards FPS to VTK-to-USD for shape (X, Y, Z, T) outputs.

## tests/test_workflow_fit_statistical_model_to_patient.py

- `def test_auto_generate_mask_accumulates_multilabel_models(monkeypatch)` (line 19): Multi-model masks accumulate label IDs instead of overwriting prior labels.
Expand Down
10 changes: 6 additions & 4 deletions docs/cli_scripts/byod_tutorials.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ inside ``--output-dir``.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Pass a 4D DICOM directory, a single 4D image file, or an explicit list of 3D
image files to produce an animated USD scene. The image-to-USD CLI does not
provide an ``--fps`` option. Use ``--reference-image`` only when you need to
provide a separate fixed image for registration; otherwise the workflow selects
its default reference frame internally.
image files to produce an animated USD scene. Use ``--fps`` when you need to
set the animated USD playback rate. Use ``--reference-image`` only when you
need to provide a separate fixed image for registration; otherwise the workflow
selects its default reference frame internally.

**CLI:**

Expand All @@ -128,6 +128,7 @@ its default reference frame internally.
physiomotion4d-convert-image-to-usd \
phase_000.mha phase_001.mha phase_002.mha \
--output-dir ./results \
--fps 30 \
--project-name heart_animated

**Python API:**
Expand All @@ -141,6 +142,7 @@ its default reference frame internally.
contrast_enhanced=False,
output_directory="./results",
project_name="heart_animated",
times_per_second=30.0,
)
workflow.process()

Expand Down
11 changes: 11 additions & 0 deletions src/physiomotion4d/cli/convert_image_to_usd.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ def main() -> int:

# Use the cardiac-only Simpleware segmentation backend
%(prog)s input.nrrd --segmentation-method HeartSimpleware

# Set animated USD playback to 30 frames per second
%(prog)s input.nrrd --fps 30
""",
)

Expand Down Expand Up @@ -88,6 +91,13 @@ def main() -> int:
default="ICON",
help="Registration method to use: ANTS or ICON (default: ICON)",
)
parser.add_argument(
"--fps",
type=float,
default=24.0,
dest="times_per_second",
help="Frames per second for animated USD time series (default: 24)",
)

args = parser.parse_args()

Expand All @@ -111,6 +121,7 @@ def main() -> int:
number_of_registration_iterations=args.registration_iterations,
segmentation_method=args.segmentation_method,
registration_method=args.registration_method,
times_per_second=args.times_per_second,
)
except Exception as e:
print(f"Error initializing workflow: {e}")
Expand Down
5 changes: 5 additions & 0 deletions src/physiomotion4d/workflow_convert_image_to_usd.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def __init__(
number_of_registration_iterations: Optional[int] = 1,
segmentation_method: str = "ChestTotalSegmentator",
registration_method: str = "ICON",
times_per_second: float = 24.0,
log_level: int | str = logging.INFO,
save_registered_images: bool = True,
save_registration_transforms: bool = True,
Expand Down Expand Up @@ -95,6 +96,8 @@ def __init__(
pulmonary/great-vessel branches trimmed to the cardiac region).
registration_method (str): Registration method to use:
``'ANTS'`` or ``'ICON'`` (default: ``'ICON'``).
times_per_second: Frames per second for animated USD time series.
Defaults to 24.0, matching the underlying VTK-to-USD converter.
log_level: Logging level (default: logging.INFO)
save_registered_images: Write registered image intermediates to
output_directory when True
Expand All @@ -114,6 +117,7 @@ def __init__(
self.save_registered_images = save_registered_images
self.save_registration_transforms = save_registration_transforms
self.save_labelmaps = save_labelmaps
self.times_per_second = times_per_second

# Validate segmentation method
if segmentation_method not in SEGMENTATION_METHODS:
Expand Down Expand Up @@ -578,6 +582,7 @@ def _create_usd_files(self) -> None:
self._transformed_contours[anatomy_type],
self.segmenter.taxonomy.all_labels(),
segmenter=self.segmenter,
times_per_second=self.times_per_second,
log_level=self.log_level,
)
usd_file = os.path.join(
Expand Down
59 changes: 59 additions & 0 deletions tests/test_cli_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from __future__ import annotations

import importlib
from pathlib import Path
import sys
from typing import Any

import pytest

Expand Down Expand Up @@ -37,3 +39,60 @@ def test_cli_help(
captured = capsys.readouterr()
assert exc_info.value.code == 0
assert "usage:" in captured.out.lower()


def test_convert_image_to_usd_help_includes_fps(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
"""Image-to-USD CLI exposes playback FPS for animated USD output."""
module = importlib.import_module("physiomotion4d.cli.convert_image_to_usd")
monkeypatch.setattr(sys, "argv", ["convert_image_to_usd", "--help"])

with pytest.raises(SystemExit) as exc_info:
module.main()

captured = capsys.readouterr()
assert exc_info.value.code == 0
assert "--fps" in captured.out


def test_convert_image_to_usd_cli_passes_fps(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
"""Image-to-USD CLI forwards --fps as times_per_second."""
import physiomotion4d

module = importlib.import_module("physiomotion4d.cli.convert_image_to_usd")
input_file = tmp_path / "input.mha"
input_file.write_text("placeholder")
captured_kwargs: dict[str, Any] = {}

class FakeWorkflowConvertImageToUSD:
def __init__(self, **kwargs: Any) -> None:
captured_kwargs.update(kwargs)

def process(self) -> str:
return "output.usd"

monkeypatch.setattr(
physiomotion4d,
"WorkflowConvertImageToUSD",
FakeWorkflowConvertImageToUSD,
)
monkeypatch.setattr(
sys,
"argv",
[
"convert_image_to_usd",
str(input_file),
"--output-dir",
str(tmp_path),
"--fps",
"30",
],
)

assert module.main() == 0
assert captured_kwargs["times_per_second"] == 30.0
75 changes: 75 additions & 0 deletions tests/test_workflow_convert_image_to_usd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Tests for the image-to-USD workflow."""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Any

from physiomotion4d.physiomotion4d_base import PhysioMotion4DBase
from physiomotion4d.workflow_convert_image_to_usd import WorkflowConvertImageToUSD
import physiomotion4d.workflow_convert_image_to_usd as workflow_module


def test_create_usd_files_passes_times_per_second(
monkeypatch: Any,
tmp_path: Path,
) -> None:
"""Workflow forwards FPS to VTK-to-USD for shape (X, Y, Z, T) outputs."""
times_per_second_values: list[float] = []

class FakeStage:
def Export(self, _output_filename: str) -> None:
return None

class FakeConvertVTKToUSD:
def __init__(
self,
_project_name: str,
_input_polydata: list[Any],
_mask_ids: dict[int, str],
*,
times_per_second: float,
**_kwargs: Any,
) -> None:
times_per_second_values.append(times_per_second)

def convert(self, _usd_file: str) -> FakeStage:
return FakeStage()

class FakeUSDAnatomyTools:
def __init__(self, _stage: FakeStage) -> None:
return None

def enhance_meshes(self, _segmenter: Any) -> None:
return None

class FakeTaxonomy:
def all_labels(self) -> dict[int, str]:
return {1: "heart"}

class FakeSegmenter:
taxonomy = FakeTaxonomy()

monkeypatch.setattr(workflow_module, "ConvertVTKToUSD", FakeConvertVTKToUSD)
monkeypatch.setattr(workflow_module, "USDAnatomyTools", FakeUSDAnatomyTools)

workflow = WorkflowConvertImageToUSD.__new__(WorkflowConvertImageToUSD)
PhysioMotion4DBase.__init__(
workflow,
class_name=WorkflowConvertImageToUSD.__name__,
log_level=logging.CRITICAL,
)
workflow.project_name = "patient"
workflow.output_directory = str(tmp_path)
workflow.segmenter = FakeSegmenter()
workflow.times_per_second = 12.5
workflow._transformed_contours = {
"all": [],
"dynamic": [],
"static": [],
}

workflow._create_usd_files()

assert times_per_second_values == [12.5, 12.5, 12.5]
Loading