Skip to content

ENH: Update fit-to-patient for non-contrast heart models#87

Open
aylward wants to merge 2 commits into
Project-MONAI:mainfrom
aylward:fit_to_patient
Open

ENH: Update fit-to-patient for non-contrast heart models#87
aylward wants to merge 2 commits into
Project-MONAI:mainfrom
aylward:fit_to_patient

Conversation

@aylward

@aylward aylward commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Non-contrast heart models from TotalSegmentator are incomplete, but this update makes registration to a statistical model possible.

Summary by CodeRabbit

  • New Features

    • Added high-resolution heart segmentation support, including optional licensed post-processing.
    • Segmentation outputs now preserve anatomy label IDs in grouped labelmaps.
    • Tutorial 6 now saves forward and inverse transforms alongside reconstructed frames.
    • Image-to-USD conversion now defaults to 1 frame per second.
  • Bug Fixes

    • Corrected contrast-region labeling and updated related segmentation behavior.
  • Documentation

    • Updated workflows, tutorials, and developer guides to use the process() entry point and current segmentation terminology.

Non-contrast heart models from TotalSegmentator are incomplete,
but this update makes registration to a statistical model
possible.
Copilot AI review requested due to automatic review settings July 14, 2026 03:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@aylward, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b0cd6fc2-3eac-43fb-b173-2ec03aeca268

📥 Commits

Reviewing files that changed from the base of the PR and between 5e27522 and 5ce2265.

📒 Files selected for processing (1)
  • src/physiotwin4d/segment_chest_total_segmentator.py

Walkthrough

The changes rename workflow execution methods to process, revise PCA configuration, replace anatomy group masks with labelmaps, expand TotalSegmentator heart processing, update workflow examples, and adjust contour and USD conversion utilities.

Changes

Segmentation pipeline

Layer / File(s) Summary
Group labelmap contract
src/physiotwin4d/segment_anatomy_base.py, src/physiotwin4d/segment_chest_total_segmentator_with_contrast.py, tests/*, docs/developer/segmentation.rst, experiments/Heart-GatedCT_To_USD/*
Anatomy groups now preserve original label IDs, include other, and are validated as labelmaps rather than binary masks.
High-resolution TotalSegmentator pipeline
src/physiotwin4d/segment_chest_total_segmentator.py
Taxonomy mappings, task execution, optional licensed heart expansion, label remapping, and final ITK image construction are updated.

Workflow API migration

Layer / File(s) Summary
Workflow entrypoints and PCA configuration
src/physiotwin4d/workflow_fit_statistical_model_to_patient.py, src/physiotwin4d/workflow_reconstruct_highres_4d_ct.py
Public workflow entrypoints use process, and PCA surface selection uses use_surface.
Workflow callers and documentation
src/physiotwin4d/cli/*, docs/*, experiments/*
Examples, CLI commands, and experiment scripts use the renamed workflow entrypoint.
Tutorial workflow execution
tutorials/tutorial_04_fit_statistical_model_to_patient.py, tutorials/tutorial_06_reconstruct_highres_4d_ct.py, tutorials/tutorial_08_cardiac_fit_model.py
Tutorial workflows use updated inputs and APIs; Tutorial 6 also saves forward and inverse transforms.

Workflow utility updates

Layer / File(s) Summary
Static contour helpers
src/physiotwin4d/contour_tools.py
Five ContourTools methods are declared as static methods.
USD segmentation result handling
src/physiotwin4d/workflow_convert_image_to_usd.py
Segmentation result storage names and labelmap extraction are updated, and the default frame rate changes to 1.0.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant WorkflowClient
  participant WorkflowFitStatisticalModelToPatient
  participant WorkflowReconstructHighres4DCT
  participant ResultWriters
  WorkflowClient->>WorkflowFitStatisticalModelToPatient: Configure PCA and call process()
  WorkflowFitStatisticalModelToPatient-->>WorkflowClient: Return registration result
  WorkflowClient->>WorkflowReconstructHighres4DCT: Call process()
  WorkflowReconstructHighres4DCT-->>WorkflowClient: Return reconstructions and transforms
  WorkflowClient->>ResultWriters: Save models, images, and transforms
Loading

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: improving fit-to-patient registration for non-contrast heart models.
Docstring Coverage ✅ Passed Docstring coverage is 94.74% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI review requested due to automatic review settings July 14, 2026 03:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/physiotwin4d/workflow_convert_image_to_usd.py (1)

83-84: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the docstring to reflect the new default value.

The default value for frames_per_second was changed to 1.0 (at line 57), but the docstring still states it defaults to 24.0. As per coding guidelines, docstrings must be updated and kept factual for changed public methods.

📝 Proposed fix
-            frames_per_second: Frames per second for animated USD time series.
-                Defaults to 24.0, matching the underlying VTK-to-USD converter.
+            frames_per_second: Frames per second for animated USD time series.
+                Defaults to 1.0.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiotwin4d/workflow_convert_image_to_usd.py` around lines 83 - 84,
Update the frames_per_second parameter documentation in the workflow conversion
function’s docstring to state that the default is 1.0 instead of 24.0, matching
the value defined in the function signature.

Source: Coding guidelines

docs/developer/segmentation.rst (1)

40-44: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Doc references stale contrast id 135.

segment_chest_total_segmentator_with_contrast.py now registers "contrast" at id 155 (moved from 135 to avoid colliding with the new body_extremities=135 label), but this line still documents "contrast at id 135".

🤖 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 `@docs/developer/segmentation.rst` around lines 40 - 44, Update the taxonomy
documentation for the default “contrast” placeholder to use id 155 instead of
135, while retaining the “soft_tissue” id 133 reference and the surrounding
description.
src/physiotwin4d/segment_chest_total_segmentator.py (1)

46-47: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore target_spacing to 1.5 or update the contract around resampling. self.target_spacing = 0.0 disables SegmentAnatomyBase.preprocess_input(), contradicts the class docstring, and breaks the current initialization test that expects 1.5. If the new behavior is intentional, update the docstring and test to match.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiotwin4d/segment_chest_total_segmentator.py` around lines 46 - 47,
Restore self.target_spacing to 1.5 in the TotalSegmentator class so it matches
the documented contract and existing initialization test, preserving the
expected resampling behavior through SegmentAnatomyBase.preprocess_input().
🧹 Nitpick comments (3)
src/physiotwin4d/contour_tools.py (1)

34-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update docstrings for the changed public methods.

The static-method conversion is compatible with the supplied callers, but the public documentation is incomplete: transform_contours omits several parameters, create_reference_image lacks argument and return documentation, and create_mask_from_mesh has no docstring. Please update all five changed methods with complete, factual docstrings.

As per coding guidelines, update docstrings for every changed public method.

Also applies to: 74-75, 138-139, 167-168, 363-364

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiotwin4d/contour_tools.py` around lines 34 - 35, Update the
docstrings for all five changed public methods, including extract_contours,
transform_contours, create_reference_image, create_mask_from_mesh, and the
method at the additional referenced location. Document every parameter and the
return value with accurate descriptions, ensuring transform_contours includes
its omitted parameters and create_reference_image and create_mask_from_mesh have
complete argument and return documentation.

Source: Coding guidelines

src/physiotwin4d/segment_anatomy_base.py (1)

456-463: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Group labelmaps aren't cast to uint8 like the main "labelmap" key.

labelmap_image is explicitly re-cast to uint8 before being returned, but the labelmaps dict (heart/lung/etc.) is built from the pre-cast image and returned as-is. All current segmenters happen to already produce uint8 labelmaps, so this is not currently observable, but the returned dict has no guaranteed-consistent pixel type across its values, which downstream consumers (writers, comparisons) may implicitly assume.

♻️ Suggested fix
         labelmaps = self.create_anatomy_group_labelmaps(labelmap_image)

         labelmap_image = itk.GetImageFromArray(
             itk.GetArrayFromImage(labelmap_image).astype(np.uint8)
         )
         labelmap_image.CopyInformation(input_image)

-        return {"labelmap": labelmap_image, **labelmaps}
+        labelmaps = {
+            group: itk.GetImageFromArray(
+                itk.GetArrayFromImage(group_labelmap).astype(np.uint8)
+            )
+            for group, group_labelmap in labelmaps.items()
+        }
+        for group_labelmap, source in zip(labelmaps.values(), labelmaps.values()):
+            group_labelmap.CopyInformation(input_image)
+
+        return {"labelmap": labelmap_image, **labelmaps}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiotwin4d/segment_anatomy_base.py` around lines 456 - 463, Update the
labelmap construction flow around create_anatomy_group_labelmaps so every group
labelmap in labelmaps is converted to uint8 before returning. Apply the same
pixel-type normalization used for the main "labelmap" while preserving each
group's existing image metadata and the return structure.
tutorials/tutorial_04_fit_statistical_model_to_patient.py (1)

101-110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Persisted ITK images/transforms omit compression=True in two tutorials. Both itk.imwrite and itk.transformwrite default their compression parameter to False; the coding guidelines require compressed storage for both ITK images and .hdf transforms.

  • tutorials/tutorial_04_fit_statistical_model_to_patient.py#L101-L110: add compression=True to the three itk.imwrite(...) calls (patient image, patient labelmap, heart labelmap).
  • tutorials/tutorial_06_reconstruct_highres_4d_ct.py#L107-L111: add compression=True to the two itk.transformwrite(...) calls (forward and inverse transform).
🤖 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 `@tutorials/tutorial_04_fit_statistical_model_to_patient.py` around lines 101 -
110, Enable compression on all persisted ITK outputs: update the three
itk.imwrite calls in tutorials/tutorial_04_fit_statistical_model_to_patient.py
for patient_image, patient_labelmap, and heart_labelmap, and the two
itk.transformwrite calls in tutorials/tutorial_06_reconstruct_highres_4d_ct.py
for the forward and inverse transforms, by passing compression=True.

Source: Coding guidelines

🤖 Prompt for all review comments with 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.

Inline comments:
In `@experiments/Heart-Create_Statistical_Model/README.md`:
- Line 164: Update the workflow result assignment around
WorkflowFitStatisticalModelToPatient.process() so it selects
registered_template_model_surface from the returned dictionary instead of
assigning the entire dictionary to registered_mesh.

In `@src/physiotwin4d/segment_chest_total_segmentator.py`:
- Line 286: Remove the three unconditional debug itk.imwrite calls for
preprocessed_image, labelmap_arr_heart, and labelmap_arr_body from the
segmentation flow. Do not replace them with other production writes; preserve
the segmentation behavior while eliminating the working-directory artifacts and
associated debug image creation if it is used only by those writes.
- Around line 369-393: Binarize the heart interior mask before constructing
interior_image in the high-resolution heart expansion block: make interior_arr
contain 1 for labels 141–144 and 0 elsewhere, while preserving the existing
dilation and erosion calls using foreground value 1. Keep the subsequent
exterior mask application and labelmap replacement behavior unchanged.

In `@src/physiotwin4d/workflow_convert_image_to_usd.py`:
- Line 228: Wrap the assignment to self.reference_segmentation_results in the
workflow conversion code so each resulting line is no longer than 88 characters,
preserving the existing segmenter.segment(self.reference_image) behavior.

In `@tests/conftest.py`:
- Around line 517-529: Remove the unconditional "contrast" lookup from the
fixture result assembled with segmenter_total_segmentator, since
create_anatomy_group_labelmaps only returns registered groups. Keep the existing
groups unchanged; if contrast output is required, use the
segmenter_total_segmentator_with_contrast fixture instead.

In `@tutorials/tutorial_04_fit_statistical_model_to_patient.py`:
- Around line 128-143: Guard PCA registration in the workflow setup using the
loaded pca_model: call set_use_pca_registration only when pca_model is not None,
while preserving the existing enabled configuration when it is available. Update
the result-saving flow around workflow.pca_coefficients so it does not call
tolist() when PCA was skipped, and save registered coefficients only when they
exist.

---

Outside diff comments:
In `@docs/developer/segmentation.rst`:
- Around line 40-44: Update the taxonomy documentation for the default
“contrast” placeholder to use id 155 instead of 135, while retaining the
“soft_tissue” id 133 reference and the surrounding description.

In `@src/physiotwin4d/segment_chest_total_segmentator.py`:
- Around line 46-47: Restore self.target_spacing to 1.5 in the TotalSegmentator
class so it matches the documented contract and existing initialization test,
preserving the expected resampling behavior through
SegmentAnatomyBase.preprocess_input().

In `@src/physiotwin4d/workflow_convert_image_to_usd.py`:
- Around line 83-84: Update the frames_per_second parameter documentation in the
workflow conversion function’s docstring to state that the default is 1.0
instead of 24.0, matching the value defined in the function signature.

---

Nitpick comments:
In `@src/physiotwin4d/contour_tools.py`:
- Around line 34-35: Update the docstrings for all five changed public methods,
including extract_contours, transform_contours, create_reference_image,
create_mask_from_mesh, and the method at the additional referenced location.
Document every parameter and the return value with accurate descriptions,
ensuring transform_contours includes its omitted parameters and
create_reference_image and create_mask_from_mesh have complete argument and
return documentation.

In `@src/physiotwin4d/segment_anatomy_base.py`:
- Around line 456-463: Update the labelmap construction flow around
create_anatomy_group_labelmaps so every group labelmap in labelmaps is converted
to uint8 before returning. Apply the same pixel-type normalization used for the
main "labelmap" while preserving each group's existing image metadata and the
return structure.

In `@tutorials/tutorial_04_fit_statistical_model_to_patient.py`:
- Around line 101-110: Enable compression on all persisted ITK outputs: update
the three itk.imwrite calls in
tutorials/tutorial_04_fit_statistical_model_to_patient.py for patient_image,
patient_labelmap, and heart_labelmap, and the two itk.transformwrite calls in
tutorials/tutorial_06_reconstruct_highres_4d_ct.py for the forward and inverse
transforms, by passing compression=True.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: abdc6a98-e2d4-4726-9063-94a05139b70e

📥 Commits

Reviewing files that changed from the base of the PR and between 4cfbfbc and 5e27522.

📒 Files selected for processing (23)
  • docs/api/model_registration/index.rst
  • docs/api/workflows.rst
  • docs/developer/registration_models.rst
  • docs/developer/segmentation.rst
  • docs/tutorials.rst
  • experiments/Heart-Create_Statistical_Model/README.md
  • experiments/Heart-GatedCT_To_USD/2-generate_segmentation.py
  • experiments/Heart-Statistical_Model_To_Patient/heart_model_to_patient-CHOPValve.py
  • src/physiotwin4d/cli/fit_statistical_model_to_patient.py
  • src/physiotwin4d/cli/reconstruct_highres_4d_ct.py
  • src/physiotwin4d/contour_tools.py
  • src/physiotwin4d/segment_anatomy_base.py
  • src/physiotwin4d/segment_chest_total_segmentator.py
  • src/physiotwin4d/segment_chest_total_segmentator_with_contrast.py
  • src/physiotwin4d/segment_heart_simpleware.py
  • src/physiotwin4d/workflow_convert_image_to_usd.py
  • src/physiotwin4d/workflow_fit_statistical_model_to_patient.py
  • src/physiotwin4d/workflow_reconstruct_highres_4d_ct.py
  • tests/conftest.py
  • tests/test_segment_chest_total_segmentator.py
  • tutorials/tutorial_04_fit_statistical_model_to_patient.py
  • tutorials/tutorial_06_reconstruct_highres_4d_ct.py
  • tutorials/tutorial_08_cardiac_fit_model.py

)

registered_mesh = workflow.run_workflow()
registered_mesh = workflow.process()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Select the mesh from the workflow result dictionary.

WorkflowFitStatisticalModelToPatient.process() returns a dictionary, not a mesh directly. Assigning it to registered_mesh makes this example incorrect for callers expecting a mesh.

Suggested fix
-registered_mesh = workflow.process()
+result = workflow.process()
+registered_mesh = result["registered_template_model"]

Use registered_template_model_surface instead if this experiment needs the surface mesh.

🤖 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/Heart-Create_Statistical_Model/README.md` at line 164, Update the
workflow result assignment around WorkflowFitStatisticalModelToPatient.process()
so it selects registered_template_model_surface from the returned dictionary
instead of assigning the entire dictionary to registered_mesh.

Comment thread src/physiotwin4d/segment_chest_total_segmentator.py Outdated
Comment on lines +369 to +393
# Add heart around interior regions.
if self.has_highres_heart_license:
interior_mask = np.isin(labelmap_arr, [141, 142, 143, 144])
interior_arr = labelmap_arr * interior_mask
interior_image = itk.GetImageFromArray(interior_arr.astype(np.uint8))
interior_image.CopyInformation(preprocessed_image)
imMath = ImageTools()
spacing = interior_image.GetSpacing()
exterior_image = imMath.binary_dilate_image(
interior_image, round(7 / spacing[0]), 1, 0
)
exterior_image = imMath.binary_erode_image(
exterior_image, round(4 / spacing[0]), 1, 0
)
exterior_arr = itk.GetArrayFromImage(exterior_image)
mask_id = 51 # Heart mask id
exterior_arr = exterior_arr * mask_id
labelmap_arr = np.where(
labelmap_arr == 0, exterior_arr, labelmap_arr
)
replace_arr = np.where(labelmap_arr == 133, exterior_arr, 0)
labelmap_arr = np.where(
replace_arr > 0, exterior_arr, labelmap_arr
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Highres-heart expansion is a no-op: interior_arr is never binarized before dilation with foreground_value=1.

interior_arr = labelmap_arr * interior_mask keeps original label values {0, 141, 142, 143, 144}, but imMath.binary_dilate_image(interior_image, ..., 1, 0) / binary_erode_image(..., 1, 0) treat pixel value 1 as foreground (per ImageTools.binary_dilate_image/binary_erode_image, which sets SetForegroundValue(foreground_value) on BinaryDilateImageFilter/BinaryErodeImageFilter). Since interior_image never contains the value 1, the filters find zero foreground voxels, exterior_arr ends up all zeros, and the entire "add heart around interior regions" block silently does nothing — this appears to be the exact mechanism this PR needs for incomplete non-contrast heart models.

Compare with SegmentHeartSimpleware.segmentation_method, which correctly builds a true binary interior_array (tmp_array = (mask_array > 128).astype(np.uint8)) before the equivalent dilate/erode calls.

🐛 Suggested fix
                 interior_mask = np.isin(labelmap_arr, [141, 142, 143, 144])
-                interior_arr = labelmap_arr * interior_mask
+                interior_arr = interior_mask.astype(np.uint8)
                 interior_image = itk.GetImageFromArray(interior_arr.astype(np.uint8))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Add heart around interior regions.
if self.has_highres_heart_license:
interior_mask = np.isin(labelmap_arr, [141, 142, 143, 144])
interior_arr = labelmap_arr * interior_mask
interior_image = itk.GetImageFromArray(interior_arr.astype(np.uint8))
interior_image.CopyInformation(preprocessed_image)
imMath = ImageTools()
spacing = interior_image.GetSpacing()
exterior_image = imMath.binary_dilate_image(
interior_image, round(7 / spacing[0]), 1, 0
)
exterior_image = imMath.binary_erode_image(
exterior_image, round(4 / spacing[0]), 1, 0
)
exterior_arr = itk.GetArrayFromImage(exterior_image)
mask_id = 51 # Heart mask id
exterior_arr = exterior_arr * mask_id
labelmap_arr = np.where(
labelmap_arr == 0, exterior_arr, labelmap_arr
)
replace_arr = np.where(labelmap_arr == 133, exterior_arr, 0)
labelmap_arr = np.where(
replace_arr > 0, exterior_arr, labelmap_arr
)
# Add heart around interior regions.
if self.has_highres_heart_license:
interior_mask = np.isin(labelmap_arr, [141, 142, 143, 144])
interior_arr = interior_mask.astype(np.uint8)
interior_image = itk.GetImageFromArray(interior_arr.astype(np.uint8))
interior_image.CopyInformation(preprocessed_image)
imMath = ImageTools()
spacing = interior_image.GetSpacing()
exterior_image = imMath.binary_dilate_image(
interior_image, round(7 / spacing[0]), 1, 0
)
exterior_image = imMath.binary_erode_image(
exterior_image, round(4 / spacing[0]), 1, 0
)
exterior_arr = itk.GetArrayFromImage(exterior_image)
mask_id = 51 # Heart mask id
exterior_arr = exterior_arr * mask_id
labelmap_arr = np.where(
labelmap_arr == 0, exterior_arr, labelmap_arr
)
replace_arr = np.where(labelmap_arr == 133, exterior_arr, 0)
labelmap_arr = np.where(
replace_arr > 0, exterior_arr, labelmap_arr
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiotwin4d/segment_chest_total_segmentator.py` around lines 369 - 393,
Binarize the heart interior mask before constructing interior_image in the
high-resolution heart expansion block: make interior_arr contain 1 for labels
141–144 and 0 elsewhere, while preserving the existing dilation and erosion
calls using foreground value 1. Keep the subsequent exterior mask application
and labelmap replacement behavior unchanged.


self.reference_segmentation = self.segmenter.segment(self.reference_image)
labelmap = self.reference_segmentation["labelmap"]
self.reference_segmentation_results = self.segmenter.segment(self.reference_image)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep lines at or below 88 characters.

This line is 90 characters long, which exceeds the maximum allowed length of 88 characters. As per coding guidelines, lines must be kept at or below 88 characters.

📝 Proposed fix
-        self.reference_segmentation_results = self.segmenter.segment(self.reference_image)
+        self.reference_segmentation_results = self.segmenter.segment(
+            self.reference_image
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.reference_segmentation_results = self.segmenter.segment(self.reference_image)
self.reference_segmentation_results = self.segmenter.segment(
self.reference_image
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiotwin4d/workflow_convert_image_to_usd.py` at line 228, Wrap the
assignment to self.reference_segmentation_results in the workflow conversion
code so each resulting line is no longer than 88 characters, preserving the
existing segmenter.segment(self.reference_image) behavior.

Source: Coding guidelines

Comment thread tests/conftest.py
Comment on lines 517 to 529
labelmap = itk.imread(str(labelmap_file))
masks = segmenter_total_segmentator.create_anatomy_group_masks(labelmap)
labelmaps = segmenter_total_segmentator.create_anatomy_group_labelmaps(labelmap)
results.append(
{
"labelmap": labelmap,
"lung": masks["lung"],
"heart": masks["heart"],
"major_vessels": masks["major_vessels"],
"bone": masks["bone"],
"soft_tissue": masks["soft_tissue"],
"other": masks["other"],
"contrast": masks["contrast"],
"lung": labelmaps["lung"],
"heart": labelmaps["heart"],
"major_vessels": labelmaps["major_vessels"],
"bone": labelmaps["bone"],
"soft_tissue": labelmaps["soft_tissue"],
"other": labelmaps["other"],
"contrast": labelmaps["contrast"],
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

labelmaps["contrast"] will raise KeyErrorsegmenter_total_segmentator has no "contrast" group.

This fixture is parametrized with the plain segmenter_total_segmentator (SegmentChestTotalSegmentator), which never registers a "contrast" taxonomy group — only SegmentChestTotalSegmentatorWithContrast does via _add_extra_taxonomy_groups(). create_anatomy_group_labelmaps() only returns keys present in taxonomy.group_names() (plus "other"), so labelmaps["contrast"] will raise KeyError for every invocation of this fixture.

🐛 Suggested fix
-                "contrast": labelmaps["contrast"],
+                "contrast": labelmaps.get("contrast"),

Or, if contrast results are actually required here, switch this fixture to use segmenter_total_segmentator_with_contrast instead.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
labelmap = itk.imread(str(labelmap_file))
masks = segmenter_total_segmentator.create_anatomy_group_masks(labelmap)
labelmaps = segmenter_total_segmentator.create_anatomy_group_labelmaps(labelmap)
results.append(
{
"labelmap": labelmap,
"lung": masks["lung"],
"heart": masks["heart"],
"major_vessels": masks["major_vessels"],
"bone": masks["bone"],
"soft_tissue": masks["soft_tissue"],
"other": masks["other"],
"contrast": masks["contrast"],
"lung": labelmaps["lung"],
"heart": labelmaps["heart"],
"major_vessels": labelmaps["major_vessels"],
"bone": labelmaps["bone"],
"soft_tissue": labelmaps["soft_tissue"],
"other": labelmaps["other"],
"contrast": labelmaps["contrast"],
}
labelmap = itk.imread(str(labelmap_file))
labelmaps = segmenter_total_segmentator.create_anatomy_group_labelmaps(labelmap)
results.append(
{
"labelmap": labelmap,
"lung": labelmaps["lung"],
"heart": labelmaps["heart"],
"major_vessels": labelmaps["major_vessels"],
"bone": labelmaps["bone"],
"soft_tissue": labelmaps["soft_tissue"],
"other": labelmaps["other"],
"contrast": labelmaps.get("contrast"),
}
🤖 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 `@tests/conftest.py` around lines 517 - 529, Remove the unconditional
"contrast" lookup from the fixture result assembled with
segmenter_total_segmentator, since create_anatomy_group_labelmaps only returns
registered groups. Keep the existing groups unchanged; if contrast output is
required, use the segmenter_total_segmentator_with_contrast fixture instead.

Comment on lines +128 to +143
workflow.set_use_pca_registration(
use_pca_registration=True,
pca_model=pca_model,
use_surface=False,
)
if pca_model is not None:
workflow.set_use_pca_registration(True, pca_model=pca_model)

# %%
# Workflow execution
result = workflow.run_workflow()
workflow_results = workflow.process()

# %%
# Result saving
registered_surface: pv.PolyData = result["registered_template_model_surface"]
registered_file = output_dir / "registered_template.vtp"
registered_surface.save(str(registered_file))
registered_coefficients = workflow.pca_coefficients
registered_coefficients_path = output_dir / "registered_coefficients.json"
with registered_coefficients_path.open(mode="w", encoding="utf-8") as f:
json.dump(registered_coefficients.tolist(), f)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

PCA registration unconditionally enabled even when pca_model is None — will raise ValueError.

use_pca_registration=True is hardcoded regardless of whether pca_model was actually loaded. pca_json is only conditionally loaded (line 83-85), and the docs' Recommended Run Order note says Tutorial 4 "can consume" the PCA model output, implying it's optional. But WorkflowFitStatisticalModelToPatient.set_use_pca_registration raises ValueError("When enabling PCA registration, pca_model must be provided.") whenever use_pca_registration=True and pca_model is None, so this tutorial will crash if Tutorial 3's pca_model.json hasn't been generated yet.

Compare with docs/tutorials.rst's Tutorial 4 "Inner API usage" snippet, which correctly guards this call: if pca_model is not None: workflow.set_use_pca_registration(True, pca_model=pca_model).

Additionally, even if this call didn't raise, registered_coefficients = workflow.pca_coefficients (line 140) would be None when PCA is skipped, and .tolist() (line 143) would then raise AttributeError.

🐛 Proposed fix
-    workflow.set_use_pca_registration(
-        use_pca_registration=True,
-        pca_model=pca_model,
-        use_surface=False,
-    )
+    if pca_model is not None:
+        workflow.set_use_pca_registration(
+            use_pca_registration=True,
+            pca_model=pca_model,
+            use_surface=False,
+        )

     # %%
     # Workflow execution
     workflow_results = workflow.process()

     # %%
     # Result saving
     registered_coefficients = workflow.pca_coefficients
-    registered_coefficients_path = output_dir / "registered_coefficients.json"
-    with registered_coefficients_path.open(mode="w", encoding="utf-8") as f:
-        json.dump(registered_coefficients.tolist(), f)
+    if registered_coefficients is not None:
+        registered_coefficients_path = output_dir / "registered_coefficients.json"
+        with registered_coefficients_path.open(mode="w", encoding="utf-8") as f:
+            json.dump(registered_coefficients.tolist(), f)

Based on learnings and cross-file evidence: docs/tutorials.rst's Tutorial 4 usage snippet demonstrates the guarded pattern if pca_model is not None: workflow.set_use_pca_registration(...), which this tutorial script does not follow.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
workflow.set_use_pca_registration(
use_pca_registration=True,
pca_model=pca_model,
use_surface=False,
)
if pca_model is not None:
workflow.set_use_pca_registration(True, pca_model=pca_model)
# %%
# Workflow execution
result = workflow.run_workflow()
workflow_results = workflow.process()
# %%
# Result saving
registered_surface: pv.PolyData = result["registered_template_model_surface"]
registered_file = output_dir / "registered_template.vtp"
registered_surface.save(str(registered_file))
registered_coefficients = workflow.pca_coefficients
registered_coefficients_path = output_dir / "registered_coefficients.json"
with registered_coefficients_path.open(mode="w", encoding="utf-8") as f:
json.dump(registered_coefficients.tolist(), f)
if pca_model is not None:
workflow.set_use_pca_registration(
use_pca_registration=True,
pca_model=pca_model,
use_surface=False,
)
# %%
# Workflow execution
workflow_results = workflow.process()
# %%
# Result saving
registered_coefficients = workflow.pca_coefficients
if registered_coefficients is not None:
registered_coefficients_path = output_dir / "registered_coefficients.json"
with registered_coefficients_path.open(mode="w", encoding="utf-8") as f:
json.dump(registered_coefficients.tolist(), f)
🤖 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 `@tutorials/tutorial_04_fit_statistical_model_to_patient.py` around lines 128 -
143, Guard PCA registration in the workflow setup using the loaded pca_model:
call set_use_pca_registration only when pca_model is not None, while preserving
the existing enabled configuration when it is available. Update the
result-saving flow around workflow.pca_coefficients so it does not call tolist()
when PCA was skipped, and save registered coefficients only when they exist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants