Repository navigation
fix(annotators): exclude empty compact masks from the mask blend region - #2741
Conversation
All-False CompactMask crops contributed their full bounding boxes to the ROI that MaskAnnotator copies and blends. Filter crop bounds by RLE foreground count so empty crops no longer widen the region, and return None when no foreground remains so the existing early return skips the blend entirely. Co-authored-by: Codex <codex@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2a2e4aef4f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
MaskAnnotator passes detection boxes to _masks_to_roi, whose dense fast path took the union of every box, including boxes of all-False masks. Drop those boxes before the union so dense and compact masks blend the same region. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
The focused fix includes regression coverage for both mask formats, with no unresolved findings.
0 open findings
What changed in this PR
Completes the mask-blending optimization from #2368 by excluding empty masks from the blend region.
Changes:
- Filters empty dense and compact masks before computing region bounds.
- Adds regression coverage for skipped blending, region size, and preserved rendering.
- Updates helper docstrings and the changelog.
🟢 Approve — no actionable issues identified. Behavior, regression coverage, performance tradeoffs, and documentation are addressed; tests were not independently run.
Quality: code 5/5, tests 5/5, documentation 5/5. Overall risk: 1/5 (focused change with targeted coverage).
| File | Description |
|---|---|
| tests/detection/utils/test_masks.py | Tests empty-mask exclusion from region bounds. |
| tests/annotators/test_core.py | Tests blend behavior for dense and compact masks. |
| src/supervision/detection/utils/masks.py | Excludes empty masks from bounding-box unions. |
| docs/changelog.md | Documents the blending optimization. |
🧠 Review effort: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Changes: - Merge develop at 74c9574 into the mask ROI PR branch and preserve the target release updates plus the PR changelog entry. Impact: - The PR branch now includes the latest target source, docs, and version metadata while retaining its mask ROI fix. Verification: - Reviewed all ten staged paths and resolved the changelog conflict; git diff --cached --check passed. - All pre-commit hooks passed. - Full suite: 5,702 passed, 105 skipped, 94% coverage. Residual limits: - The admitted review report has no independent specialist evidence; no independent review coverage is claimed. --- Co-authored-by: Codex <codex@openai.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #2741 +/- ##
=======================================
- Coverage 94% 94% -0%
=======================================
Files 83 83
Lines 13489 13491 +2
=======================================
Hits 12675 12675
- Misses 814 816 +2 🚀 New features to boost your workflow:
|
Merge develop at ffd0ac0, which released 0.31.0 and merged roboflow#2741, roboflow#2740 and roboflow#2743. Only docs/changelog.md conflicted: the CompactMask.area entry now opens the new Unreleased section and the rest matches develop. roboflow#2741 now filters MaskAnnotator ROI crops with CompactMask.area > 0, so that path uses the vectorized area; its tests pass unchanged. Verification: full suite 5,735 passed, 123 skipped, 94% coverage (NumPy fallback); compact mask, mask utility and annotator tests 601 passed with both the fallback and OpenCV 5.0.0; pre-commit passed on the PR files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Changes: - Merge the fetched develop target into the Pascal VOC rounding branch, including the Unreleased notes for PRs roboflow#2740, roboflow#2741, and roboflow#2743. - Integrate OpenCV-compatible ellipse rasterization and its regression coverage. - Integrate mask ROI handling that excludes all-False masks and its tests. - Integrate overflow-safe mean-average-precision box extents and dtype regression tests. Impact: - Keeps the PR branch current so the roboflow#2748 changelog note can be added beside the target's Unreleased entries. - Brings the target's drawing, mask annotation, and metric fixes into the local PR branch. Verification: - The three-tree merge completed automatically without conflicts. - The staged merge diff check passed; all eight staged paths were reviewed. Residual limits: - None known. --- Co-authored-by: Codex <codex@openai.com>
Description
MaskAnnotatoronly copies and blends the region covered by masks (#2368). An all-False mask still widened that region to its box:CompactMask: the region is the union of crop boxes, and an empty crop still contributes its box.MaskAnnotatorpassesxyxy, and the box fast path takes the union of every box, including boxes of all-False masks.develop(dense / compact)Painted pixels are the same in both versions.
This PR drops the boxes of masks without foreground before taking the union:
CompactMask.area > 0(counted from the RLE, no decoding) for compact masks, andmasks.any(axis=(1, 2))for dense masks. When no compact crop remains, the helper returnsNone, so the annotator's existing early return applies, as it already did for dense masks.Motivation and Context
Completes the empty-mask handling from #2368: it covered collections with no foreground at all, but not empty masks next to non-empty ones. No issue was opened for this case.
Changes Made
_compact_masks_to_roi: union only crops that have foreground._masks_to_roi: in the dense box fast path, drop boxes of all-False masks before the union. Docstrings updated for both helpers.Unreleased.Out of scope: vectorizing
CompactMask.area(see notes).Testing
Windows x64, Python 3.11.9, locked environment (
uv sync --frozen),developat6ecb279.New and parametrized tests on
develop: 8 failed, 1 passed (the pass is the dense all-False case, which already worked), with both the NumPy fallback and OpenCV 4.12.0. With this PR: 9 passed.pytest tests/detection/utils/test_masks.py tests/annotators/test_core.py src/supervision/detection/utils/masks.py: 278 passed on both backends.Full suite,
pytest --cov=supervision: 5,702 passed, 105 skipped, 94% coverage.pre-commit runon the changed files: all hooks pass.Added/updated tests, and the full suite passes locally
Updated docs (docstrings / mkdocs entry) for new or changed public API
Added a changelog entry in
docs/changelog.mdunderUnreleased(skip for lint/type/format-only or pure doc changes)Additional Notes
Cost, measured at 1280×720:
CompactMask.arealoops over masks in Python, so the ROI helper goes from ~10 µs to roughly 3 µs per mask (≈0.3 ms at 100 masks, ≈1.5 ms at 500, OpenCV). Without empty masks,annotate()timings stayed within run-to-run noise on my machine (500 masks: 28–31 ms on both branches).anyreduction costs ~0.1 ms at 10 masks and ~1.9 ms at 100, against ~0.5 / 7.6 ms for the existing dense helper and ~4 / 49 ms for the area sum that_paint_masks_by_areaalready computes.annotate()went from ~6.5 ms to ~2.4 ms with OpenCV and from ~67 ms to ~3.5 ms with the NumPy fallback.Happy to share the benchmark script.
If the per-mask cost matters,
CompactMask.areacan be vectorized (concatenate the RLE arrays and sum odd runs withnp.bincount). A local prototype matches the current results and is ~8–12× faster at 100–500 masks; it would also speed up the area sort in_paint_masks_by_area. I left it out to keep this fix minimal and can open it separately.The dense-path case was flagged by the automated Codex review on this PR.
Prepared with Codex and reviewed with Claude Code; the test runs and timings above come from those local sessions.
🤖 Generated with Claude Code