perf(detection): count mask pixels with count_nonzero - #2361
Merged
Conversation
Mask pixel-area counting used `np.sum` — `np.array([np.sum(m) for m in masks])` in `Detections.area` and `np.sum(mask, axis=(1, 2))` in the metrics `get_mask_size_category`. For boolean masks `np.count_nonzero` (with no axis) dispatches to NumPy's SIMD popcount over the raw byte buffer, whereas every axis-reduction form — `np.sum(..., axis=...)` and even `np.count_nonzero(..., axis=...)` — falls back to a slower generic reduction. So counting per mask with `np.count_nonzero` is several times faster than the "obvious" vectorized sum, while producing bit-identical integer counts. Route both sites through `np.fromiter((np.count_nonzero(m) for m in masks), dtype=np.int64, count=len(masks))`. `dtype=np.int64` preserves the documented `Detections.area` mask-branch dtype on every platform (a bare `np.array([...])` of Python ints would be int32 on Windows). Measured ~5x on 640x640 masks (e.g. `Detections.area`, N=300: ~24ms -> ~4ms), faster across densities. `get_mask_size_category` feeds the size-bucketed F1/Precision/Recall/mAP/mAR metrics, where it is invoked repeatedly per dataset. Counts are integer-exact (verified over 400 randomized trials plus empty / all-true / all-false / 1x1 edge cases). Adds parity tests for `Detections.area` (dense mask) and `get_mask_size_category` against an `np.sum` reference.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #2361 +/- ##
=======================================
Coverage 84% 84%
=======================================
Files 70 70
Lines 9923 9926 +3
=======================================
+ Hits 8313 8316 +3
Misses 1610 1610 🚀 New features to boost your workflow:
|
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves mask pixel-area counting performance in Supervision’s detection and metrics code paths by switching from np.sum reductions to a faster per-mask np.count_nonzero + np.fromiter approach, while preserving bit-identical results (for boolean masks) and maintaining an int64 area dtype.
Changes:
- Updated
Detections.area(mask branch) to compute per-mask areas vianp.fromiter((np.count_nonzero(m) ...), dtype=np.int64). - Updated
get_mask_size_categoryto use the samecount_nonzero+fromiterstrategy for faster size bucketing. - Added tests to verify parity with
np.sumreferences and to cover threshold boundary behavior and dense-mask area expectations.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
src/supervision/detection/core.py |
Speeds up Detections.area for boolean masks using count_nonzero + fromiter with stable int64 output. |
src/supervision/metrics/utils/object_size.py |
Speeds up mask-size categorization by computing areas via per-mask count_nonzero + fromiter. |
tests/detection/test_core.py |
Adds a dense-mask parity test to ensure Detections.area matches pixel counts / sum and remains int64. |
tests/metrics/utils/test_object_size.py |
Adds reference-parity and boundary tests for get_mask_size_category. |
tests/metrics/utils/__init__.py |
Adds/keeps the tests.metrics.utils package marker (empty file). |
count_nonzero
- Add test_empty_detections_with_mask_returns_empty_area: verifies empty mask branch returns shape (0,) with dtype int64 (locks in the float64→int64 fix introduced by count_nonzero + fromiter) - Add test_mask_boundary_fills: parametrized test for all-False (area=0) and all-True (area=H*W) boundary masks; guards count_nonzero contract at the extremes --- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
…ns.area - Add module docstring to tests/metrics/utils/test_object_size.py - Add mask-branch doctest to Detections.area: 2-mask example guarding the int64 dtype contract under --doctest-modules - Extend core.py:2639 comment to note C-contiguous assumption for SIMD speedup (F-order slices skip the popcount path) --- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Extend comment at object_size.py:162 to: - Explicitly warn that np.sum(mask, axis=(1,2)) is ~6x SLOWER, not faster, than the current count_nonzero loop — prevents future "simplification" revert - Document dtype=np.int64 rationale: Windows NumPy defaults to int32; explicit dtype guarantees consistent 64-bit areas cross-platform --- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
- Add count_mask_pixels(masks) to detection/utils/masks.py: shared helper wrapping np.fromiter + np.count_nonzero (no axis) with Google-style docstring and doctest; dtype=np.int64 guaranteed - Replace inline fromiter expression in Detections.area (core.py) and get_mask_size_category (object_size.py) with count_mask_pixels call, eliminating 3-line duplication across both sites - Apply count_mask_pixels to calculate_masks_centroids (masks.py:128), updating the missed total_pixels = masks.sum(axis=(1,2)) callsite for consistency with the count_nonzero pattern from this PR --- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Borda
approved these changes
Jul 1, 2026
Draft
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Mask pixel-area counting used
np.sum:np.array([np.sum(m) for m in masks])inDetections.area, andnp.sum(mask, axis=(1, 2))in the metrics helperget_mask_size_category.For boolean masks,
np.count_nonzero(with noaxis) dispatches to NumPy's SIMD popcount over the raw byte buffer, whereas every axis-reduction form —np.sum(..., axis=...)and evennp.count_nonzero(..., axis=...)— falls back to a slower generic reduction. So counting per-mask withnp.count_nonzerois several times faster than the "obvious" vectorized sum, while producing bit-identical integer counts. (The plainmasks.sum(axis=(1, 2))vectorization, by contrast, is only ~1.1x — it's the primitive, not the loop-removal, that matters here.)Both sites now use:
dtype=np.int64preserves the documentedDetections.areamask-branch dtype on every platform (a barenp.array([...])of Python ints is int32 on Windows).Performance
~5x on 640×640 masks, density-independent:
Detections.areaDetections.areaget_mask_size_categoryget_mask_size_categoryfeeds the size-bucketedF1Score/Precision/Recall/MeanAveragePrecision/MeanAverageRecallmetrics, where it runs repeatedly (SMALL/MEDIUM/LARGE × predictions+targets) across a dataset.Correctness
Counting
Truepixels equals summing a bool array exactly (integers, no float error). Verified bit-identical over 400 randomized trials plus empty / all-true / all-false / 1×1 edge cases, with int64 dtype preserved. ExistingDetections.areaand metrics suites pass unchanged.Tests
Adds parity tests for
Detections.area(dense mask) andget_mask_size_category, each against annp.sumreference, plus threshold-boundary coverage.ruff check/formatclean.