refactor: centralize geometry-aware dispatch for detection_area and detection_iou - #2374
Conversation
There was a problem hiding this comment.
Pull request overview
This PR centralizes geometry-precedence dispatch (mask → OBB → AABB) for per-detection area and pairwise IoU into a single internal helper module, then routes Detections.area through that shared implementation to reduce recurring “silent AABB fallback” regressions.
Changes:
- Added internal
detection_area(...)anddetection_iou(...)helpers undersrc/supervision/detection/utils/geometry.py. - Refactored
Detections.areato delegate todetection_area(removing duplicated dispatch logic incore.py). - Added contract-style tests ensuring area/IoU respect richer geometry and catch envelope-vs-OBB regressions.
Review scores (1–5):
- Code quality: 4/5
- Testing: 5/5
- Documentation: 4/5
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/supervision/detection/utils/geometry.py | Introduces centralized geometry-aware dispatch helpers for area and IoU. |
| src/supervision/detection/core.py | Refactors Detections.area to call the shared helper. |
| tests/detection/utils/test_geometry.py | Adds parity + metamorphic/contract tests for dispatch correctness across mask/OBB/AABB. |
@mvanhorn mind check this out ^^ 🦝 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #2374 +/- ##
=======================================
Coverage 87% 87%
=======================================
Files 84 85 +1
Lines 11838 11855 +17
=======================================
+ Hits 10251 10277 +26
+ Misses 1587 1578 -9 🚀 New features to boost your workflow:
|
- Route deprecated inner merges through shared OBB/mask/AABB dispatch - Add mixed-geometry regression coverage and vectorized mask-area docs --- Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Codex <codex@openai.com>
[resolve group] PR roboflow#2374 — items 1, 2, 3, 7 Detection_area's dense-mask branch now delegates to the shared count_mask_pixels SIMD-popcount helper instead of a duplicated np.count_nonzero(axis=(1,2)) reduction, restoring the ~6x fast path also used by metrics.utils.object_size (item roboflow#1). Relocates detection_area/detection_iou from the pure-array detection/utils/ package to detection/geometry_dispatch.py, since both consume Detections objects rather than raw arrays — this also resolves the module-name collision with the top-level supervision.geometry package (item roboflow#2, item roboflow#7's module-rename part). Documents detection_iou's shared-geometry, box-IoU-last-resort fallback and its asymmetry vs detection_area's per-operand dispatch (item roboflow#3). Renames detection_iou's detections_true/detections_detection params to detections_a/detections_b — the sole caller passes two peer merge candidates with no true/pred relationship (item roboflow#7's param-rename part). --- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: OpenAI Codex <codex@openai.com>
[resolve group] PR roboflow#2374 — items 4, 5, 7 Splits the changelog bullet that fused the merge-IoU bugfix with the unrelated mask-area perf change into two, names the affected APIs, adds the PR link, and drops the "batched reduction" framing that mischaracterized a measured regression as an improvement (item roboflow#4). Restores the blank line before ### Added that this PR's diff had removed (item roboflow#7's changelog part). Rewords the confusing filter_detections.md sentence so "if oriented-box coordinates are present" reads as a condition rather than the data field acting as the sentence's subject (item roboflow#5). --- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: OpenAI Codex <codex@openai.com>
[resolve group] PR roboflow#2374 — item 6 Adds a regression guard asserting detection_area's dense-mask branch delegates to count_mask_pixels rather than reimplementing it, empty CompactMask (N=0) coverage through detection_area, degenerate (collinear, zero-area) OBB coverage through both detection_area and detection_iou, and a 3-detection merge_inner_detections_objects chain covering the per-iteration detection_iou call as geometry richness changes mid-chain (OBB pair merges, then falls back to box IoU against an AABB-only third detection). Updates the moved test file's import path to supervision.detection.geometry_dispatch. --- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…e tests A prior docstring-collapse edit left two test methods with an unindented triple-quoted string immediately after `def ...():`, raising IndentationError at import time and breaking pytest collection for both files. [resolve group] PR roboflow#2374 — pre-existing regression found during resolve baseline check (not a review action item)
`geometry_dispatch.py` sat at a top-level, non-underscored path under `detection/`, making it directly importable (`supervision.detection.geometry_dispatch`) despite the PR's stated intent that these dispatch helpers stay module-private with no public API surface. Renamed to `_geometry_dispatch.py`, the standard convention for signalling "internal, not part of the public API," and updated the two import sites. [resolve group] PR roboflow#2374 — item roboflow#6 (@Copilot, roboflow#2374 (comment))
…aware-dispatch # Conflicts: # src/supervision/detection/core.py
|
Thanks for shepherding the refactor, @Borda - geometry-aware dispatch living in one place beats four copies. |
Before submitting
Description
Centralizes the "which geometry is the real one?" dispatch for detection area and IoU into a single internal module so the mask -> OBB -> box precedence lives in one place instead of being re-implemented (and silently forgotten) at each call site.
Type of Change
Motivation and Context
A recurring "silent AABB" bug class keeps reappearing: code reaches for
detections.xyxyand forgets the real geometry lives indata["xyxyxyxy"](OBB) ormask. Becausexyxyis always present, the axis-aligned answer is the silent default. Recent fixes (#2306 area returning AABB area, #2303with_nms/with_nmmdropping crossed OBBs via AABB IoU, #2289as_yolodropping OBB rotation) share this root cause: every consumer re-derives geometry precedence and forgetting it fails silently.@kounelisagis endorsed (2026-06-17) keeping the policy internal ("exact when richer geometry is present") behind a single geometry-resolution helper rather than a public per-op mode argument, and suggested a metamorphic test.
This PR is a deliberate first slice of the multi-part issue: the read-only dispatch helpers plus the contract test land first.
move_detectionsconsolidation and thewith_nms/with_nmm/as_yolocall-site migration are left as independent follow-ups per the issue's incremental rollout.Refs #2318
Changes Made
src/supervision/detection/utils/geometry.pywith two batched, read-only helpers:detection_area(mask sum /CompactMask.area-> shoelace OBB area ->box_area) anddetection_iou(mask_iou_batch->oriented_box_iou_batch->box_iou_batch, by the richest geometry both operands carry).Detections.areaincore.pythroughdetection_areaso the property is a thin call into the shared helper, with no behavior change and the vectorized design preserved.tests/detection/utils/test_geometry.py: area-parity across mask/OBB/AABB plus the metamorphic contract (identical envelopes with different OBB corners must differ; rotating an OBB off-axis leavesdetection_areainvariant while the envelope area changes).Testing
Added
tests/detection/utils/test_geometry.pycovering area parity (mask/OBB/AABB), the metamorphic rotation-invariance contract, emptyDetections,CompactMaskrouting, and the AABB IoU fallback. The helpers are read-only and preserveDetections.areabehavior exactly.Note: I was unable to run the full suite in my local sandbox (the test stack pulls
cv2/matplotlib, which aren't installed in my environment); the new tests are written to run under the repo's CI. Happy to adjust if CI surfaces anything.Google Colab (optional)
N/A
Screenshots/Videos (optional)
N/A (internal refactor; no user-visible output change)
Additional Notes
The new helpers are intentionally module-private (no public API surface, no per-op mode argument) to match the maintainer's stated preference. Follow-ups can migrate
with_nms/with_nmm/as_yoloand consolidatemove_detectionsonto the same dispatch.