Skip to content

Add optional RankSEG decoding to AsDiscrete#8908

Open
ZixunWang wants to merge 8 commits into
Project-MONAI:devfrom
rankseg:8857-rankseg-as-optional-asdiscrete
Open

Add optional RankSEG decoding to AsDiscrete#8908
ZixunWang wants to merge 8 commits into
Project-MONAI:devfrom
rankseg:8857-rankseg-as-optional-asdiscrete

Conversation

@ZixunWang

Copy link
Copy Markdown

Related to #8857.

Description

This PR adds optional RankSEG decoding support to MONAI post-processing.

RankSEG is integrated as an extension of the existing discrete post-processing API:

AsDiscrete(rankseg=True)
AsDiscreted(keys="pred", rankseg=True)

rankseg=False remains the default, so existing behavior is unchanged.

Types of changes

  • Non-breaking change (fix or new feature that would not break existing functionality).
  • Breaking change (fix or new feature that would cause existing functionality to change).
  • New tests added to cover the changes.
  • Integration tests passed locally by running ./runtests.sh -f -u --net --coverage.
  • Quick tests passed locally by running ./runtests.sh --quick --unittests --disttests.
  • In-line docstrings updated.
  • Documentation updated, tested make html command in the docs/ folder.

Changes

  • Add rankseg support to AsDiscrete and AsDiscreted.
  • Import RankSEG through MONAI's optional dependency mechanism.
  • Add optional dependency config, documentation, and tests.

Many thanks to @statmlben and @Leev1s for their collaboration and helpful discussions on the RankSEG integration.

Feedback Requested

This PR is intended to start from a conservative integration point. We would especially appreciate feedback on:

  • whether AsDiscrete(rankseg=True) / AsDiscreted(..., rankseg=True) is the preferred API shape;
  • whether combining argmax=True and rankseg=True should raise a ValueError as implemented, or only emit a warning.

ZixunWang and others added 6 commits May 30, 2026 16:16
Signed-off-by: Zixun Wang <craddywang@gmail.com>
Signed-off-by: Zixun Wang <craddywang@gmail.com>
Signed-off-by: LI Junxing <83260380+Leev1s@users.noreply.github.com>
Signed-off-by: Zixun Wang <craddywang@gmail.com>
Signed-off-by: statmlben <bdai.hk@protonmail.com>
Signed-off-by: Zixun Wang <craddywang@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

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: 76cb4c46-a46a-4374-bc03-6085c1250164

📥 Commits

Reviewing files that changed from the base of the PR and between 1866926 and f08231b.

📒 Files selected for processing (4)
  • monai/transforms/post/array.py
  • monai/transforms/post/dictionary.py
  • tests/transforms/test_as_discrete.py
  • tests/transforms/test_as_discreted.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • monai/transforms/post/dictionary.py
  • tests/transforms/test_as_discreted.py
  • tests/transforms/test_as_discrete.py
  • monai/transforms/post/array.py

📝 Walkthrough

Walkthrough

This PR integrates RankSEG as an optional external dependency for probabilistic decoding in MONAI transforms. The AsDiscrete array transform and its AsDiscreted dictionary wrapper gain a new rankseg parameter that enables RankSEG-based decoding. The implementation enforces incompatibility with argmax=True, handles channel remapping to RankSEG's expected layout, and preserves dimensionality according to keepdim settings. Dependency metadata is updated across setup.cfg and requirements-dev.txt, installation documentation reflects the new extra, and comprehensive test coverage verifies correct behavior and error handling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly summarizes the main change: adding optional RankSEG decoding to AsDiscrete.
Description check ✅ Passed Description covers all key aspects: related issue, clear explanation of changes, checked relevant boxes, and specific feedback requests.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

@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.

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

198-200: ⚡ Quick win

Add strict=True to zip() for robustness.

Since the project requires Python ≥ 3.10, use zip(..., strict=True) to catch configuration errors early when argmax and rankseg sequences have mismatched lengths.

🔒 Proposed fix
         self.argmax = ensure_tuple_rep(argmax, len(self.keys))
         self.rankseg = ensure_tuple_rep(rankseg, len(self.keys))
-        if any(argmax_ and rankseg_ for argmax_, rankseg_ in zip(self.argmax, self.rankseg)):
+        if any(argmax_ and rankseg_ for argmax_, rankseg_ in zip(self.argmax, self.rankseg, strict=True)):
             raise ValueError("`rankseg=True` is incompatible with `argmax=True`.")
🤖 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 198 - 200, The zip call
combining self.argmax and self.rankseg should use strict=True to fail fast on
length mismatches; update the check in the Dictionary-based transform (around
ensure_tuple_rep and the subsequent if that zips self.argmax and self.rankseg)
to call zip(self.argmax, self.rankseg, strict=True) so a configuration error is
raised if their lengths differ.

Source: Linters/SAST tools

🤖 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.

Nitpick comments:
In `@monai/transforms/post/dictionary.py`:
- Around line 198-200: The zip call combining self.argmax and self.rankseg
should use strict=True to fail fast on length mismatches; update the check in
the Dictionary-based transform (around ensure_tuple_rep and the subsequent if
that zips self.argmax and self.rankseg) to call zip(self.argmax, self.rankseg,
strict=True) so a configuration error is raised if their lengths differ.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fd566a60-5ae0-431b-89cc-bc846b4edb2f

📥 Commits

Reviewing files that changed from the base of the PR and between eccefc5 and b10b7e1.

📒 Files selected for processing (7)
  • docs/source/installation.md
  • monai/transforms/post/array.py
  • monai/transforms/post/dictionary.py
  • requirements-dev.txt
  • setup.cfg
  • tests/transforms/test_as_discrete.py
  • tests/transforms/test_as_discreted.py

@ZixunWang ZixunWang changed the title 8857 Add optional RankSEG decoding to AsDiscrete Add optional RankSEG decoding to AsDiscrete Jun 9, 2026
Signed-off-by: Zixun Wang <craddywang@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.

Caution

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

⚠️ Outside diff range comments (2)
tests/transforms/test_as_discrete.py (2)

89-94: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add docstring per coding guidelines.

Test methods should include Google-style docstrings describing what is tested. As per coding guidelines, "Docstrings should be present for all definition which describe each variable, return value, and raised exception."

📝 Proposed docstring
 def test_rankseg_argmax_incompatible(self):
+    """Test that rankseg=True raises ValueError when combined with argmax=True."""
     with self.assertRaises(ValueError):
🤖 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_as_discrete.py` around lines 89 - 94, Add a
Google-style docstring to the test method test_rankseg_argmax_incompatible
describing what the test verifies (that AsDiscrete raises ValueError when argmax
and rankseg are both requested), list any parameters (none), the return (None),
and the raised exception (ValueError) — reference the AsDiscrete constructor and
call sites used in the test to explain the two asserted failure cases; keep the
docstring concise and follow existing test docstring formatting in the file.

Source: Coding guidelines


96-99: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add docstring per coding guidelines.

Test method should include Google-style docstring describing what is tested. As per coding guidelines, "Docstrings should be present for all definition which describe each variable, return value, and raised exception."

📝 Proposed docstring
 def test_rankseg_missing_dependency(self):
+    """Test that rankseg=True raises OptionalImportError when rankseg package is unavailable."""
     with mock.patch("monai.transforms.post.array.has_rankseg", False):
🤖 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_as_discrete.py` around lines 96 - 99, Add a
Google-style docstring to the test_rankseg_missing_dependency function that
succinctly describes the test purpose (verifying that AsDiscrete(...,
rankseg=True) raises OptionalImportError when the rankseg dependency is
missing), documents parameters (none), the return value (None), and the raised
exception (OptionalImportError), and mentions the mocked condition
(monai.transforms.post.array.has_rankseg set to False); place the docstring
immediately under the def test_rankseg_missing_dependency(self): line to satisfy
the project's docstring guideline and reference AsDiscrete and
OptionalImportError in the description.

Source: Coding guidelines

🧹 Nitpick comments (2)
tests/transforms/test_as_discrete.py (2)

99-99: 💤 Low value

Use p() wrapper for consistency.

Line 99 uses a raw list. Wrap with p() from TEST_NDARRAYS to test across multiple array types consistently.

♻️ Proposed change
+    for p in TEST_NDARRAYS:
         with mock.patch("monai.transforms.post.array.has_rankseg", False):
             with self.assertRaises(OptionalImportError):
-                AsDiscrete(rankseg=True)([[[0.3, 0.6]], [[0.7, 0.4]]])
+                AsDiscrete(rankseg=True)(p([[[0.3, 0.6]], [[0.7, 0.4]]]))
🤖 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_as_discrete.py` at line 99, The test passes a raw
Python list to AsDiscrete(rankseg=True) instead of wrapping it with the p()
array factory from TEST_NDARRAYS; update the call
AsDiscrete(rankseg=True)([[[0.3, 0.6]], [[0.7, 0.4]]]) to use the p(...) wrapper
from TEST_NDARRAYS so the input is created across all test ndarray types (e.g.,
AsDiscrete(rankseg=True)(p([[[0.3,0.6]], [[0.7,0.4]]]))); keep the same shape
and values and ensure p is the TEST_NDARRAYS factory used elsewhere in the file.

94-94: 💤 Low value

Use p() wrapper for consistency.

Line 94 uses a raw list instead of wrapping with p() from TEST_NDARRAYS. For consistency with the test suite's pattern of testing across multiple array types, wrap the input.

♻️ Proposed change
+    for p in TEST_NDARRAYS:
         with self.assertRaises(ValueError):
-            AsDiscrete(argmax=True)([[[0.3, 0.6]], [[0.7, 0.4]]], rankseg=True)
+            AsDiscrete(argmax=True)(p([[[0.3, 0.6]], [[0.7, 0.4]]]), rankseg=True)
🤖 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_as_discrete.py` at line 94, The test call to
AsDiscrete(argmax=True) uses a raw Python list instead of using the
TEST_NDARRAYS wrapper; update the input to wrap the list with the p() factory so
the test runs across array types (use p([[ [0.3,0.6] ], [[0.7,0.4] ]]) or
equivalent) before passing to AsDiscrete(argmax=True)(..., rankseg=True); locate
the call to AsDiscrete and replace the raw list argument with p(...) to maintain
consistency with the rest of the test suite.
🤖 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.

Outside diff comments:
In `@tests/transforms/test_as_discrete.py`:
- Around line 89-94: Add a Google-style docstring to the test method
test_rankseg_argmax_incompatible describing what the test verifies (that
AsDiscrete raises ValueError when argmax and rankseg are both requested), list
any parameters (none), the return (None), and the raised exception (ValueError)
— reference the AsDiscrete constructor and call sites used in the test to
explain the two asserted failure cases; keep the docstring concise and follow
existing test docstring formatting in the file.
- Around line 96-99: Add a Google-style docstring to the
test_rankseg_missing_dependency function that succinctly describes the test
purpose (verifying that AsDiscrete(..., rankseg=True) raises OptionalImportError
when the rankseg dependency is missing), documents parameters (none), the return
value (None), and the raised exception (OptionalImportError), and mentions the
mocked condition (monai.transforms.post.array.has_rankseg set to False); place
the docstring immediately under the def test_rankseg_missing_dependency(self):
line to satisfy the project's docstring guideline and reference AsDiscrete and
OptionalImportError in the description.

---

Nitpick comments:
In `@tests/transforms/test_as_discrete.py`:
- Line 99: The test passes a raw Python list to AsDiscrete(rankseg=True) instead
of wrapping it with the p() array factory from TEST_NDARRAYS; update the call
AsDiscrete(rankseg=True)([[[0.3, 0.6]], [[0.7, 0.4]]]) to use the p(...) wrapper
from TEST_NDARRAYS so the input is created across all test ndarray types (e.g.,
AsDiscrete(rankseg=True)(p([[[0.3,0.6]], [[0.7,0.4]]]))); keep the same shape
and values and ensure p is the TEST_NDARRAYS factory used elsewhere in the file.
- Line 94: The test call to AsDiscrete(argmax=True) uses a raw Python list
instead of using the TEST_NDARRAYS wrapper; update the input to wrap the list
with the p() factory so the test runs across array types (use p([[ [0.3,0.6] ],
[[0.7,0.4] ]]) or equivalent) before passing to AsDiscrete(argmax=True)(...,
rankseg=True); locate the call to AsDiscrete and replace the raw list argument
with p(...) to maintain consistency with the rest of the test suite.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 645ba23a-cff2-41c2-b3a3-3cd897653df8

📥 Commits

Reviewing files that changed from the base of the PR and between b10b7e1 and 1866926.

📒 Files selected for processing (3)
  • monai/transforms/post/dictionary.py
  • tests/transforms/test_as_discrete.py
  • tests/transforms/test_as_discreted.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/transforms/test_as_discreted.py
  • monai/transforms/post/dictionary.py

@aymuos15

aymuos15 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Really cool! To me, the only changes would be adding a 3D test and extending out a metric arg so that people can choose something other than dice?

Signed-off-by: Zixun Wang <craddywang@gmail.com>
@ZixunWang

Copy link
Copy Markdown
Author

@aymuos15 Thanks for your review and suggestions! I have added a 3D test case:

[[[[0.3, 0.6]]], [[[0.7, 0.4]]]]  # shape: [C, H, W, D] = [2, 1, 1, 2]

RankSEG internally handles inputs with shape [B, C, *spatial], where spatial has no restriction on the number of dimensions.

The metric argument is also exposed via AsDiscrete(rankseg=True, metric=...) / AsDiscreted(..., rankseg=True, metric=...)

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.

4 participants