Count components via bincount over full field in get_largest_connected_component_mask#9000
Conversation
…d_component_mask Fixes Project-MONAI#8974. Rank component sizes with a single bincount over the whole label field and zero out the background bin, instead of gathering every foreground voxel via features[nonzero(features)] first. This removes the per-spatial-dim index arrays produced by nonzero and the full gathered copy of the non-background voxels, lowering peak memory and runtime. Output is bit-identical: background is label 0, all foreground labels keep their original counts, and the top num_components labels selected by argsort are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Vishnu Kannaujia <vishnu.kannaujia@gmail.com>
📝 WalkthroughWalkthrough
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
monai/transforms/utils.py (2)
1231-1231: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valuePrefer
.ravel()over.reshape(-1).Use
.ravel()to flatten the array. It is more idiomatic and explicitly conveys the intent to return a flattened view.♻️ Proposed refactor
- counts = lib.bincount(features.reshape(-1)) + counts = lib.bincount(features.ravel())🤖 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/utils.py` at line 1231, Update the flattening expression in the counts computation to use features.ravel() instead of features.reshape(-1), preserving the existing lib.bincount behavior.
1194-1204: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd
ReturnsandRaisessections to the docstring.As per path instructions, describe the return value and raised exceptions using the appropriate sections of the Google-style docstrings.
📝 Proposed docstring update
""" Gets the largest connected component mask of an image. Args: img: Image to get largest connected component from. Shape is (spatial_dim1 [, spatial_dim2, ...]) connectivity: Maximum number of orthogonal hops to consider a pixel/voxel as a neighbor. Accepted values are ranging from 1 to input.ndim. If ``None``, a full connectivity of ``input.ndim`` is used. for more details: https://scikit-image.org/docs/dev/api/skimage.measure.html#skimage.measure.label. num_components: The number of largest components to preserve. + + Returns: + A boolean mask tensor or array of the same shape as `img` containing the largest connected components. + + Raises: + RuntimeError: If `skimage.measure` is required but not installed. """🤖 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/utils.py` around lines 1194 - 1204, Update the docstring for the largest connected component utility to add Google-style Returns and Raises sections, documenting the returned component mask and the exceptions raised for invalid inputs or processing failures. Keep the existing Args documentation unchanged.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.
Nitpick comments:
In `@monai/transforms/utils.py`:
- Line 1231: Update the flattening expression in the counts computation to use
features.ravel() instead of features.reshape(-1), preserving the existing
lib.bincount behavior.
- Around line 1194-1204: Update the docstring for the largest connected
component utility to add Google-style Returns and Raises sections, documenting
the returned component mask and the exceptions raised for invalid inputs or
processing failures. Keep the existing Args documentation unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0aec11f9-d860-46fd-aaae-bd29eefc365f
📒 Files selected for processing (2)
monai/transforms/utils.pytests/transforms/test_keep_largest_connected_component.py
Fixes #8974.
Summary
get_largest_connected_component_mask(monai/transforms/utils.py) ranked component sizes with:lib.nonzero(features)materializes one full index array per spatial dim, andfeatures[...]then gathers a full copy of every non-background voxel — all purely to drop background beforebincount. That is unnecessary: abincountover the whole field already counts every label, and background is label0.This PR counts directly over the field and masks background out:
This avoids both the
nonzeroindex arrays and the gathered copy, so it lowers peak memory and runtime on large volumes. The change applies to both the NumPy and CuPy (lib) paths.Why the output is unchanged (bit-identical)
0; settingcounts[0] = 0guarantees it is never selected.bincount, which only affected bin0, whose count was already0).num_features > num_components, so all foreground labels have count>= 1and thenum_componentslargest selected byargsort(...)[::-1]are identical.isinmembership is order-independent.Tests
Existing
test_keep_largest_connected_component[d]suites (174 cases) still pass. Added directget_largest_connected_component_masktests acrossTEST_NDARRAYSbackends:test_num_components_selection: a field with four blobs (6/4/3/1 voxels) keeps exactly the largestnum_componentsforn = 1..4.test_background_never_selected: background dominates by voxel count but is never kept.