Skip to content

Add affine-aware landmark heatmap generation#8957

Open
mustafamm072 wants to merge 8 commits into
Project-MONAI:devfrom
mustafamm072:dev
Open

Add affine-aware landmark heatmap generation#8957
mustafamm072 wants to merge 8 commits into
Project-MONAI:devfrom
mustafamm072:dev

Conversation

@mustafamm072

Copy link
Copy Markdown

Description

Adds opt-in world-coordinate landmark support to GenerateHeatmapd.

By default, behavior is unchanged. With coordinate_space="world", landmarks are transformed into the reference image voxel space using ref_image_keys before heatmap generation. The transform can also emit per-landmark visibility masks via visibility_keys.

Related to #7486.

Testing

  • Added tests for translated, rotated, anisotropic, and scaled reference affines.
  • Added tests for MetaTensor point affines and visibility masks.
  • Ran py_compile and git diff --check.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c3ca7a97-2841-483a-b111-a1d4c8628546

📥 Commits

Reviewing files that changed from the base of the PR and between 47685db and 2ce3ef8.

📒 Files selected for processing (1)
  • monai/transforms/post/dictionary.py

📝 Walkthrough

Walkthrough

GenerateHeatmapd now accepts voxel- or world-space landmarks, converts world points using reference affine data, and optionally emits per-point visibility masks. It validates coordinate-space values, key lengths, and required affine information while preserving relevant tensor, device, array, and metadata behavior. Tests cover affine transformations, visibility, metadata, tensor inputs, and validation errors.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the feature and testing, but it omits the required Types of changes checklist from the template. Add the template's Types of changes section with the relevant checkboxes, and include the Fixes #... line if applicable.
Docstring Coverage ⚠️ Warning Docstring coverage is 9.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the new affine-aware heatmap generation feature.
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
monai/transforms/post/dictionary.py (1)

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

Make the supported coordinate spaces immutable.

Ruff flags this mutable class attribute; frozenset matches constant intent.

Proposed fix
-    _SUPPORTED_COORDINATE_SPACES = {"voxel", "world"}
+    _SUPPORTED_COORDINATE_SPACES = frozenset({"voxel", "world"})
🤖 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 `@monai/transforms/post/dictionary.py` at line 616, The
_SUPPORTED_COORDINATE_SPACES class constant in the coordinate-space transform
logic is currently a mutable set; replace it with an immutable frozenset to
match constant intent and satisfy Ruff. Update the definition in the relevant
class within dictionary.py where _SUPPORTED_COORDINATE_SPACES is declared,
keeping the same values ("voxel" and "world") but making the attribute
immutable.

Source: Linters/SAST tools

tests/transforms/test_generate_heatmapd.py (1)

317-328: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the remaining new public contracts.

Please add direct tests for coordinate_space length mismatch and NumPy visibility output; both are new branches in GenerateHeatmapd.

Example additions
+    def test_coordinate_space_length_mismatch_raises(self):
+        with self.assertRaises(ValueError):
+            GenerateHeatmapd(
+                keys=["pts1", "pts2"],
+                heatmap_keys=["hm1", "hm2"],
+                coordinate_space=["voxel", "world", "voxel"],
+                spatial_shape=(8, 8),
+            )
+
+    def test_numpy_points_visibility_is_numpy(self):
+        image = MetaTensor(torch.zeros((1, 8, 8, 8), dtype=torch.float32), affine=torch.eye(4))
+        image.meta["spatial_shape"] = (8, 8, 8)
+        points = np.array([[1.0, 2.0, 3.0]], dtype=np.float32)
+
+        result = GenerateHeatmapd(
+            keys="points",
+            heatmap_keys="heatmap",
+            ref_image_keys="image",
+            coordinate_space="world",
+            visibility_keys="visible",
+            sigma=1.0,
+        )({"points": points, "image": image})
+
+        self.assertIsInstance(result["visible"], np.ndarray)
+        self.assertEqual(result["visible"].dtype, np.bool_)

As per path instructions, **/*.py: “Ensure new or modified definitions will be covered by existing or new unit tests.”

🤖 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/transforms/test_generate_heatmapd.py` around lines 317 - 328, Add
direct coverage for the remaining public branches in GenerateHeatmapd: the
constructor should reject a coordinate_space length mismatch, and __call__
should be tested for NumPy visibility output when visibility_keys is provided.
Extend the existing tests in test_generate_heatmapd.py to exercise these cases
using GenerateHeatmapd with mismatched coordinate_space alongside the current
visibility_keys mismatch test, and add an assertion that the visibility output
is a NumPy array for the relevant GenerateHeatmapd invocation.

Source: Path instructions

🤖 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 `@monai/transforms/post/dictionary.py`:
- Around line 796-804: The _get_reference_affine helper in the dictionary
transform is incorrectly treating any raw 3x3/4x4 tensor or ndarray as an
affine, which can misclassify normal reference images. Update this logic so only
objects with an explicit affine attribute are accepted for
coordinate_space='world', and otherwise raise the documented missing-affine
ValueError instead of inferring from shape; keep the fix localized to
_get_reference_affine and its callers.
- Around line 583-591: The world-space example in GenerateHeatmapd is
inconsistent with the sample data: it references landmarks_world even though the
example data only defines landmarks, and it uses an ndarray image without affine
metadata. Update the doc example to either define the matching world-space
landmark key and a reference image object with affine information, or change the
example to use the existing landmarks with a valid image/affine setup so the
GenerateHeatmapd usage is runnable.

---

Nitpick comments:
In `@monai/transforms/post/dictionary.py`:
- Line 616: The _SUPPORTED_COORDINATE_SPACES class constant in the
coordinate-space transform logic is currently a mutable set; replace it with an
immutable frozenset to match constant intent and satisfy Ruff. Update the
definition in the relevant class within dictionary.py where
_SUPPORTED_COORDINATE_SPACES is declared, keeping the same values ("voxel" and
"world") but making the attribute immutable.

In `@tests/transforms/test_generate_heatmapd.py`:
- Around line 317-328: Add direct coverage for the remaining public branches in
GenerateHeatmapd: the constructor should reject a coordinate_space length
mismatch, and __call__ should be tested for NumPy visibility output when
visibility_keys is provided. Extend the existing tests in
test_generate_heatmapd.py to exercise these cases using GenerateHeatmapd with
mismatched coordinate_space alongside the current visibility_keys mismatch test,
and add an assertion that the visibility output is a NumPy array for the
relevant GenerateHeatmapd invocation.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d6e314af-388d-48b3-a672-b87902ab2768

📥 Commits

Reviewing files that changed from the base of the PR and between b7d14c8 and b71a614.

📒 Files selected for processing (3)
  • docs/source/whatsnew_1_6.md
  • monai/transforms/post/dictionary.py
  • tests/transforms/test_generate_heatmapd.py

Comment thread monai/transforms/post/dictionary.py
Comment thread monai/transforms/post/dictionary.py
Signed-off-by: Mustafa Merchant <mustafamerchant072@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
monai/transforms/post/dictionary.py (1)

618-641: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add Google-style docstrings to the changed definitions.

The new/modified methods should document Args, Returns, and raised ValueErrors.

As per path instructions, **/*.py: “Docstrings should be present for all definition which describe each variable, return value, and raised exception in the appropriate section of the Google-style of docstrings.”

Also applies to: 643-672, 695-721, 780-818

🤖 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 `@monai/transforms/post/dictionary.py` around lines 618 - 641, Add Google-style
docstrings to the changed definitions in dictionary.py, including the
constructor and the related helper/transform methods referenced by the review,
so each one documents Args, Returns, and any ValueError raised. Update the
docstrings on the affected symbols such as __init__, _prepare_heatmap_keys,
_prepare_optional_keys, _prepare_coordinate_spaces, _prepare_visibility_keys,
_prepare_shapes, and any other modified definitions in the same area, keeping
the descriptions aligned with the existing parameter names and behavior.

Source: Path instructions

🤖 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 `@monai/transforms/post/dictionary.py`:
- Line 616: The _SUPPORTED_COORDINATE_SPACES class attribute in the
coordinate-space transform code is a mutable set and should be made immutable.
Update the attribute to use frozenset in the same class/module where it is
defined so Ruff no longer flags it, keeping the existing "voxel" and "world"
values unchanged.

---

Nitpick comments:
In `@monai/transforms/post/dictionary.py`:
- Around line 618-641: Add Google-style docstrings to the changed definitions in
dictionary.py, including the constructor and the related helper/transform
methods referenced by the review, so each one documents Args, Returns, and any
ValueError raised. Update the docstrings on the affected symbols such as
__init__, _prepare_heatmap_keys, _prepare_optional_keys,
_prepare_coordinate_spaces, _prepare_visibility_keys, _prepare_shapes, and any
other modified definitions in the same area, keeping the descriptions aligned
with the existing parameter names and behavior.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2ec5fb77-ebfa-4a14-a352-002f6bae4b54

📥 Commits

Reviewing files that changed from the base of the PR and between b71a614 and dbbfbb3.

📒 Files selected for processing (3)
  • docs/source/whatsnew_1_6.md
  • monai/transforms/post/dictionary.py
  • tests/transforms/test_generate_heatmapd.py
✅ Files skipped from review due to trivial changes (1)
  • docs/source/whatsnew_1_6.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/transforms/test_generate_heatmapd.py

Comment thread monai/transforms/post/dictionary.py Outdated
Signed-off-by: Mustafa Merchant <mustafamerchant072@gmail.com>
Signed-off-by: Mustafa Merchant <mustafamerchant072@gmail.com>

@ericspod ericspod left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

hi @mustafamm072 thanks for this addition, I had some comments but with changes we should be good to go.

Comment thread docs/source/whatsnew_1_6.md Outdated
Comment thread monai/transforms/post/dictionary.py Outdated
Comment thread monai/transforms/post/dictionary.py
Comment thread monai/transforms/post/dictionary.py Outdated
if points_t.ndim != 2:
raise ValueError(f"{self._ERR_INVALID_POINTS} Got {points_t.ndim}D tensor.")
bounds = torch.as_tensor(spatial_shape, dtype=points_t.dtype, device=points_t.device)
return torch.isfinite(points_t).all(dim=1) & (points_t >= 0).all(dim=1) & (points_t < bounds).all(dim=1)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
return torch.isfinite(points_t).all(dim=1) & (points_t >= 0).all(dim=1) & (points_t < bounds).all(dim=1)
return points_t.mul(torch.isfinite(points_t)).clamp_(0, bounds - 1).all(dim=1)

My concern is that the original expression involves a lot of tensor copies that's not efficient. Please check this is correct if you think it's worth changing.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Reduced the copies by combining the checks into a single boolean mask and doing one .all(dim=1) reduction. I didn't use the exact points_t.mul(...).clamp_(0, bounds-1) form because it changes the semantics. clamp_ forces every point in bounds so all points would report visible, and multiplying by the finite-mask doesn't drop NaNs (0.0 * nan == nan). The new version keeps correctness while removing the redundant reductions. Let me know if you'd prefer a different approach.

Comment thread monai/transforms/post/dictionary.py
…oordinate support

- Move coordinate_space/visibility_keys to end of __init__ to preserve
  positional-argument compatibility; reorder docstring to match
- Remove ambiguous shape-based affine inference from raw 3x3/4x4 arrays;
  require an explicit .affine attribute
- Compute visibility mask only when visibility_keys is provided
- Build a single boolean mask in _compute_visibility to reduce tensor copies
- Use frozenset for _SUPPORTED_COORDINATE_SPACES (Ruff RUF012)
- Make the world-space docstring example runnable
- Drop whatsnew_1_6 entry (1.6 already released)

Signed-off-by: Mustafa Merchant <mustafamerchant072@gmail.com>
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