Re-verify remaining supervision review backlog against develop HEAD; most items were already resolved by an intervening commit, only genuinely-open gaps got new fixes.
Fix float32 precision loss in box_iou_batch for large coordinates (GeoTIFF-scale) by accumulating in float64.
Raise ValueError instead of a strippable assert in EvaluationDataset.load_predictions for unknown image ids.
Add HeatMapAnnotator.reset() to clear accumulated heat for annotator reuse.
Add missing coverage: labelme export basename collisions, _greedy_match matcher, metrics.core ABC/enum contracts, metrics.utils.utils pandas guard; remove a global RNG-seed pollution site in a metrics test.
Document the last two undocumented public exports (calculate_masks_centroids, is_compressed_rle) and add usage examples to 17 previously-example-less public functions/classes (NMS/NMM helpers, draw utils, PolygonZoneAnnotator, mask/polygon converters).
* tests: load_predictions ValueError branch + empty-dataset coverage
* fix: box_iou_batch int-dtype overflow, narrow float32 precision claim
* feat: add reset() to TraceAnnotator/DetectionsSmoother, fix docstrings
* docs: fix temp file leak in coco.py docstring, rename misnamed test
---------
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
- Added `sv.mask_to_roi` as an explicit migration path for exclusive mask bounds
- Fixed COCO, CreateML, and Pascal VOC export validation to reject ambiguous or colliding dataset paths before writing
- Fixed in-memory `DetectionDataset` split and merge behavior
- Fixed `supervision` imports to avoid loading ByteTrack until it is used
- Fixed detection conversion helpers to support coordinate-convention migration while preserving legacy inclusive defaults
- Fixed Azure tag mapping, anchor rounding, and line-zone smoothing to avoid incorrect or ghost detections
- Fixed video processing shutdown handling for timeout and full-queue cases
- Improved downloader, validator, documentation, and regression coverage for the shipped dataset, detection, annotator, image, and video behavior
---------
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
- Fixed detection medium findings across adapters, mask non-max merge, sinks, segmentation parsing, LineZone history, and mask ROI handling
- Fixed mask non-max merge deprecation warnings to honor the standard warning opt-out and include version context
- Fixed mask non-max merge validation for invalid IoU thresholds
- Fixed CompactMask non-max merge grouping to update merged mask candidates correctly
- Fixed selected and compacted detections to copy arrays and metadata, preventing mutations from leaking back to source detections
- Fixed LineZone crossing history eviction to tolerate short tracking gaps and evict stale state per tracker/class key
- Fixed semantic segmentation handling to preserve class ID 0
- Improved mask ROI conversion performance by avoiding unnecessary full-frame copies and repeated scans
- Updated JSONSink changelog/docs to document native bool/int/float output while leaving CSVSink unchanged
- Updated detection docstrings for mask parsing, selection copy semantics, validation errors, and argument readability guidance
---------
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
- Fixed crop annotation so overlapping detections sample from the original scene
- Fixed dataset exports to reject basename collisions, including case-insensitive collisions
- Fixed LMM connector mapping to support mirror enum aliases without a hand-maintained dispatch table
- Updated benchmark documentation to install the released inference package with metrics support
---------
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* test: cover public API gaps and dataset split
* test(sinks): switch VideoSink to AVI/MJPG and add ImageSink clearing test
* test(detection): add box_non_max_merge 6-column class-separation tests
* test(dataset): drop deprecated dict API and strengthen class-id assertion
* test(public_api): strengthen importability check with getattr
---------
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
- `from_tensorflow` scaled boxes in place on the array returned by `.numpy()`, which can share memory with the source tensor — corrupting caller data and double-scaling on a repeat call; copy before scaling
- `from_lmm` raised a bare `KeyError` for `MOONDREAM` and `QWEN_3_VL`, which the enum and docstring advertise; map both to their `VLM` members
- `from_deepseek_vl_2` returned a `(0,)`-shaped `xyxy` on empty output, so a zero-detection response crashed the `Detections` constructor; return `(0, 4)` like the other parsers
- `extract_ultralytics_masks` binarized bilinear-resized masks with `> 0`, dilating every mask at object boundaries; threshold at 0.5 to match Ultralytics
- add connector coverage: fake-result shims and round-trip tests (N>1, N=1, empty) for the nine previously untested `from_*` connectors and the `detection/tools/transformers.py` processors; one empty-`segments_info` panoptic case is xfail-marked pending a separate fix
* test(ci-fix): drop deprecated Pillow mode arg from panoptic helpers
* test(coverage): add from_qwen_3_vl end-to-end parametrized tests
* test(quality): harden test isolation, xfail strictness, and kwarg forwarding
* fix(detection): fix class_name empty dtype; annotate mask threshold asymmetry
* chore: ruff-format cleanup (blank lines)
---------
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
- Fixed `get_anchors_coordinates` to compute anchor positions from oriented bounding boxes when OBB geometry is available, ensuring anchor-based operations (such as zone counting and annotators) align with the rotated object instead of its axis-aligned bounding box.
- Preserved existing behavior for axis-aligned boxes, while continuing to use mask centroids for `CENTER_OF_MASS` anchors when masks are available.
- Improved the `get_anchors_coordinates` documentation with the updated anchor selection order, OBB usage examples, and notes describing OBB winding-order requirements and anchor tie-breaking behavior.
---------
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
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.
---------
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
`Detections.from_tensorflow` scaled the normalized box coordinates by the
wrong image dimensions: the y coordinates (ymin/ymax, columns 0 and 2) were
multiplied by width and the x coordinates (xmin/xmax, columns 1 and 3) by
height. Tensorflow Hub object-detection models emit `detection_boxes` as
normalized `[ymin, xmin, ymax, xmax]`, so y must scale by height and x by
width.
The bug is masked on square images (width == height) but corrupts every
coordinate on the common non-square case — e.g. a box normalized to
`[0.1, 0.2, 0.5, 0.6]` on a 1000x500 image came out as
`[100, 100, 300, 500]` instead of the correct `[200, 50, 600, 250]`.
Swap the two multipliers so y scales by `resolution_wh[1]` (height) and x by
`resolution_wh[0]` (width). Adds a non-square regression test (the connector
was previously untested).
- Expand tensorflow_results arg to document required dict keys and tensor
shapes so callers know what to pass before getting a KeyError
- Add Note: section documenting the [ymin, xmin, ymax, xmax] normalized
box format; the inline comment was only visible to code readers
- Fix SOURCE_IMAGE_PATH undefined identifier → "<SOURCE_IMAGE_PATH>"
string placeholder (consistent with other connector examples in file)
---------
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
`box_iou_batch_with_jaccard` computed COCO-style Jaccard IoU with a double
Python `for` loop calling a scalar `_jaccard` helper once per (detection,
ground-truth) pair — an O(N*M) per-element pattern in otherwise pure-NumPy
code. It is the inner IoU of `COCOEvaluator._compute_iou`, called once per
(image, category) during mAP evaluation, and is also public API
(`sv.box_iou_batch_with_jaccard`).
Replace the loop with a broadcasted NumPy implementation and drop the now
unused scalar `_jaccard`. The far corners are built as `x2 = x + w` and the
union is associated as `(area_det + area_gt - area_inter) + eps` so the
result is bit-identical to the previous per-pair output (verified to
`max|diff| = 0` over 4000 randomized trials including zero/negative-width
degenerate boxes and crowd flags). Crowd semantics are preserved: a crowd
ground truth uses the detection area as the union.
Speedup scales with batch size — ~1.6x at 5x5, ~27x at 15x60, ~66x at
50x100 — and is faster even at the smallest sizes, so there is no regime
where it regresses. End-to-end COCO mAP results are unchanged (the existing
metrics suite passes without modification).
Adds `TestBoxIouBatchWithJaccard`: parity against an independent per-pair
reference across empty / single / busy / degenerate+crowd batches, the crowd
union semantics, the empty-input contract, and the `is_crowd` length guard.
- Improved COCO-style Jaccard IoU batch evaluation performance while preserving existing results, crowd handling, degenerate-box behavior, and public API semantics
- Fixed empty-input returns to preserve the documented `(len(boxes_detection), len(boxes_true))` output shape
- Fixed `is_crowd` length validation to raise a descriptive `ValueError`
- Updated Jaccard IoU documentation to clarify COCO `[x, y, w, h]` input format, output orientation, and NaN propagation
---------
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
- Improved `Detections.merge()` to preserve `CompactMask` output when merging dense and compact masks by converting dense masks to compact form, avoiding unnecessary full-mask materialization while keeping all-dense and all-compact behavior unchanged.
- Added validation to mixed-mask merging that raises `ValueError` when compact masks have inconsistent image shapes or dense mask dimensions do not match the compact mask image size.
- Added the public `CompactMask.image_shape` property for safe access to compact mask dimensions.
- Updated `Detections.merge()` documentation to describe mixed-mask merge behavior, output types, validation errors, the lossy dense-to-compact conversion outside detection bounding boxes, and that NMS/NMM pairwise operations do not preserve `CompactMask`.
- Added a comprehensive "Use Compact Masks" how-to guide covering compact mask ingestion, inference, annotator mask requirements, and mixed-mask merging, and integrated it into the documentation navigation.
---------
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
- Improved MaskAnnotator performance by blending mask overlays only within the affected ROI while preserving dense mask and CompactMask rendering behavior
- Fixed all-false masks to skip unnecessary ROI blending
- Updated compact-mask benchmark output to clarify annotation speedup reporting
---------
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
- Added compact COCO RLE mask ingestion with a `CompactMask` representation and optional compact mask parsing during inference for substantially lower memory usage on sparse segmentation results.
- Added `Detections.to_compact_masks()` to convert existing dense masks into compact masks while preserving detection and collection metadata.
- Improved compact mask decoding performance with cropped RLE processing, batched decoding on the fast path, vectorized decoding for small images, optimized RLE traversal, and faster delta decoding.
- Improved mask metrics to operate directly on `CompactMask` instances, preserving the compact representation while producing results equivalent to dense masks.
- Fixed mixed-modality inference handling by keeping detections and masks aligned, isolating malformed RLE failures to individual predictions where possible, and falling back safely when decoding cannot be completed.
- Fixed compact mask conversion and parsing to preserve dense-mask pixel content across public parsing and slicing paths, while correctly documenting and applying the intended bbox-cropping behavior for compact COCO RLE masks.
- Improved COCO RLE validation with checks for malformed payloads, invalid dimensions, count overflows, image size limits, count-sum mismatches, bounding-box mismatches, and safe fallback behavior for incompatible mask sizes.
- Added inference benchmarks and documentation demonstrating the memory and inference-time characteristics of compact masks, including guidance on their performance tradeoffs and behavior.
---------
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
process_roboflow_result appended a mask only for predictions carrying one
(RLE or polygon), while xyxy/confidence/class_id were appended for every
prediction. A result mixing masked and box-only predictions (e.g. a
segmentation batch where one polygon is empty) produced a mask array shorter
than the boxes, so Detections.from_inference raised a shape-mismatch error.
Append None for box-only predictions and build the mask array only when every
prediction has a mask, otherwise drop masks to preserve alignment, mirroring
the tracker_id handling. Fully-masked and mask-free results are unchanged.
- Update `masks` Returns clause to document partial-drop case and corrupt-RLE blast radius (D1+C1)
- Remove stale "known limitation" note from from_inference docstring; describe actual behavior (D2)
- Extract _all_present_or_none() helper; eliminate duplicated partial-drop-warn pattern (S1)
---------
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* Add explicit selection helpers
* improve typing in detection metrics and update pre-commit dependencies
- Add explicit type annotation for `panel_array` in `_draw_panel` function.
- Update `.pre-commit-config.yaml` to include `tomli>=2.0.1` as an additional dependency for `pyproject-fmt`.
---------
Co-authored-by: Codex <codex@openai.com>
Add postponed annotations to test modules and modernize one test helper annotation for Python 3.9-compatible collection.
---------
Co-authored-by: Codex <codex@openai.com>
- Port OBB sequential fallback to batch path (same guard as single-image path)
- Port compact_masks RLE compression into _run_callback_batch
- Port out-of-slice-bounds SupervisionWarnings to _run_callback_batch
- Add list-type and length-match guard before zip in _run_callback_batch
- Add OBB-with-thread_workers warning to batch path
- Widen callback param annotation to union of single-image and batch signatures
- Update class docstring: dual callback contract, batch_size arg, new Raises, usage example
- Remove redundant list() re-wraps in batch execution path
- Add TestInferenceSlicerBatch: 12 parametrised tests covering all new batch behaviours
---------
Co-authored-by: Linas Kondrackis <linas.ko+dev@skiff.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: jirka <6035284+Borda@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
process_roboflow_result appended tracker_id only for predictions that carried
one, while xyxy/confidence/class_id were appended for every prediction. A
result where some predictions are tracked and others are not produced a
tracker_id array shorter than the boxes, so Detections.from_inference raised
"tracker_id must be a 1D np.ndarray with shape (N,)".
Collect tracker_id for every prediction (None when absent) and build the array
only when all detections carry one, otherwise leave it None. Fully-tracked and
untracked results are unchanged.
---------
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
InferenceSlicer can now accept an open rasterio-style dataset and read each tile via a windowed read instead of loading the whole image into memory, enabling tiled inference on multi-GB aerial/drone GeoTIFFs. Detection is duck-typed so rasterio stays an optional dependency (supervision[geotiff]) and the library imports no rasterio symbols. Adds CRS projected validation and tests. Closes#2027.
- Add threading.Lock around raster.read() in _run_callback to prevent
data race when thread_workers > 1 shares a DatasetReader (GDAL releases
GIL inside GDALRasterIO — reads are genuinely concurrent C code)
- Return TypeGuard[WindowedRasterDataset] from _is_windowed_raster;
TYPE_CHECKING guard imports typing_extensions for Python 3.9 compat
- Add @runtime_checkable to WindowedRasterDataset Protocol; crs typed
as object|None; guard .is_projected via getattr(..., True)
- Extract _get_resolution_wh and _apply_overlap_filter helpers from
__call__ to bring cyclomatic complexity under PLR0912 limit (16 → ~4)
- Widen callback type to Callable[[NDArray[Any]], Detections] to accept
any dtype (uint16 raster tiles are not NDArray[uint8])
- Add Raises section to __call__ docstring for geographic CRS ValueError
- Add one-line summary to move_detections docstring
- Export WindowedRasterDataset from sv.__init__
- Move changelog entry from 0.29.1 (released) to UnReleased
- Add comment explaining rasterio>=1.3 lower bound in pyproject.toml
- Restructure tests: class grouping, parametrize CRS cases, add
docstrings; add compact_masks, thread_workers>1, single-band,
single-tile test cases
---------
Co-authored-by: madhavcodez <madhavcodez@users.noreply.github.com>
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Two ways the VLM parsers crashed on adversarial model output instead of
degrading gracefully (the contract they already honor for invalid JSON):
1. Gemini 2.5: a mask value that is not a 'data:image/png;base64,' string
appended an empty mask and then 'continue'd, skipping the confidence
handler at the bottom of the loop. The item's box was recorded but its
confidence was not, so the confidence array ended up shorter than xyxy
and Detections.from_vlm raised a shape ValueError. Replaced the
'continue' with an if/else so the confidence handler always runs.
2. Gemini 2.0 / Gemini 2.5 / Qwen 2.5: valid JSON whose top level is not a
list, or whose elements are not dicts (e.g. '[1, 2, 3]'), raised
TypeError from the 'key not in item' membership test. Added a top-level
list guard (Gemini 2.0/2.5; Qwen already had one) and a per-element
dict guard so wrong-shaped JSON degrades to empty Detections.
Add regression tests for the mask/confidence alignment and for graceful
degradation across all three parsers.
---------
Co-authored-by: jirka <6035284+Borda@users.noreply.github.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
- Reimplemented dense mask IoU/IoS computation using matrix multiplication on flattened masks instead of constructing a full `(N, M, H, W)` overlap tensor
- Significantly reduced memory usage and improved performance for large mask sets while preserving identical IoU/IoS results
- Added automatic precision handling for large masks to keep intersection and area counts numerically accurate
- Improved memory-limit handling and chunking logic to reflect actual matmul memory usage
- Added validation that compared mask sets share the same spatial dimensions
- Added validation for invalid mask tensor ranks and input shapes
- Added safe handling for empty-mask inputs
- Added warnings when inputs exceed the minimum memory footprint that chunking cannot reduce
- Fixed large-mask area calculations that could produce incorrect IoU values
- Suppressed spurious runtime warnings during valid matrix-multiplication computations
- Added regression coverage for correctness, chunking, rectangular matrices, empty inputs, shape mismatches, large-mask precision, and memory-limit edge cases
---------
Co-authored-by: jirka <6035284+Borda@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
- Fixed `JSONSink` to correctly serialize NumPy scalar values stored in `custom_data`
- Added JSON serialization support for NumPy arrays by converting them to standard JSON-compatible lists
- Prevented buffered export failures caused by non-serializable NumPy values during `json.dump`
- Added regression coverage for multiple NumPy scalar types, NumPy arrays, and unsupported-object error handling
- Updated documentation and changelog to describe NumPy serialization behavior in `JSONSink`
---------
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
- Fixed `DetectionsSmoother` to work with detections that have no confidence scores
- Changed confidence aggregation to average only the confidence values that are present, leaving confidence as `None` when no values exist
- Fixed smoothing of mixed-confidence tracks (some frames with confidence, some without) while preserving available confidence information
- Fixed crashes when merging smoothed tracks that disagree on confidence availability by normalizing confidence fields before merge
- Added regression coverage for no-confidence, mixed-confidence, multi-track, full-window, and tracker-id-missing scenarios
- Updated documentation and changelog to reflect the new confidence-handling behavior
---------
Co-authored-by: jirka <6035284+Borda@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
- Vectorized `mask_to_xyxy` by replacing per-mask pixel scans with batched occupancy-profile reductions, yielding large speedups while preserving identical outputs
- Added direct test coverage for `mask_to_xyxy`, including edge and corner-pixel mask cases
- Vectorized `KeyPoints.as_detections` by computing all bounding boxes in a single batch operation instead of constructing and merging per-skeleton `Detections`
- Vectorized keypoint-confidence aggregation in `KeyPoints.as_detections` using NumPy reductions
- Preserved exact output behavior for bounding boxes, confidence values, class IDs, metadata, selected-keypoint subsets, and missing-keypoint handling
- Added regression coverage for selected-keypoint indexing, mixed valid/invalid skeleton batches, detection-confidence paths, and confidence aggregation behavior
- Fixed strict mypy typing issues introduced by the vectorized implementations
- Improved documentation for `mask_to_xyxy` and `selected_keypoint_indices` behavior
---------
Co-authored-by: jirka <6035284+Borda@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* fix(detection): OBB NMM now computes geometric union via min-area rotated rect
Previously with_nmm for OBB detections kept the winner's OBB geometry unchanged
(only confidence was merged), making it inconsistent with AABB NMM which expands
to the union envelope. Now computes cv2.minAreaRect over all N×4 corners from
the merge group — the MARC degenerates to the axis-aligned union for zero-rotation
OBBs, preserving full consistency with AABB NMM.
- Replace winner-OBB xyxy patch with MARC of all merged corners
- Update ORIENTED_BOX_COORDINATES in data to reflect merged geometry
- Rename test to reflect new expected behaviour (union, not winner AABB)
- Add consistency test asserting axis-aligned OBB NMM == AABB NMM xyxy
---
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* code(detection): defensive reshape + clarify xyxy-override intent in OBB NMM
- Add .reshape(4, 2) to OBB corner extraction loop so flat-adjacent shapes are normalised before cv2.minAreaRect
- Add inline comment at xyxy override: OBB groups intentionally discard AABB-union xyxy from reduce() to stay consistent with MARC corners
---
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* test(detection): expand OBB NMM coverage — rotated, 3-group, passthrough, class-agnostic, IOS, flat-format
- Add test_rotated_obb_merge_produces_marc: two 45-degree OBBs, assert MARC encompasses all corners
- Add test_three_detection_group_merge: three overlapping OBBs, assert merged len==1 and envelope spans all inputs
- Add test_single_detection_passthrough_preserves_obb: non-overlapping OBB passes through unchanged
- Add test_class_agnostic_obb_merge: class_agnostic=True merges cross-class OBBs
- Add test_overlap_metric_ios_obb_merge: IOS metric merges contained OBBs
- Add test_flat_n8_obb_format_raises_value_error: documents that (N,8) flat format is unsupported (canonical is (N,4,2))
- Import OverlapMetric for IOS test
---
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* docs(detection): document OBB NMM MARC semantics in with_nmm + changelog entry
- Add Note section to with_nmm docstring explaining MARC behavior: union for zero-rotation OBBs, MARC for rotated OBBs, single-group passthrough
- Add changelog UnReleased entry for #2312 behavioral change
---
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* fix(detection): OBB NMM uses winner's angle instead of free MARC to avoid overshoot
cv2.minAreaRect picks a 45-degree rect for diagonal staircase arrangements of
axis-aligned boxes, producing an AABB like [-10,-10,54,54] that extends outside
every input. Fix: lock merged OBB to winner's angle by projecting all corners
onto the winner's principal axes (from first edge vector), computing AABB there,
and back-rotating — for zero-rotation inputs this gives exactly the axis-aligned
union; for same-angle groups the result equals the prior MARC.
- Remove cv2 dependency from the OBB merge block (pure numpy now)
- Add test_diagonal_staircase_obb_merge_stays_within_union regression test
- Rename test to reflect winner-angle semantics
---
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* fix(detection): fix changelog wording + add explicit OBB shape guard in NMM
- docs/changelog.md: replace stale MARC/cv2.minAreaRect wording with
winner's-angle description matching the actual implementation
- core.py: validate ORIENTED_BOX_COORDINATES shape is (N, 4, 2) at the
start of the OBB merge block; raises ValueError("corners must have
shape (N, 4, 2)") for flat (N, 8) input instead of silently mis-reshaping
---
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
* refactor: parametrize OBB NMM tests in TestDetectionsWithNmm Consolidate 7 individual OBB NMM test methods into a single parametrized test_obb_nmm_merge with explicit expected_confidence and expected_corners assertions. Add cases for mixed-angle merges, multiple merge groups, and degenerate collinear OBBs. Add standalone test_obb_nmm_empty_detections for empty inputs.
* refactor(tests): simplify OBB NMM test cases by replacing `np.array` usage with nested lists
- Update test parameters to use plain Python lists instead of `numpy` arrays for corner definitions.
- Adjust the `_make_obb_detections` setup to preprocess corners into `numpy` arrays.
- Add explicit conversion of `expected_corners` to `numpy` arrays in the assertions.
* feat: add xyxyxyxy_to_xyxy utility for OBB-to-AABB conversion Vectorized conversion of oriented bounding box corners (N, 4, 2) to axis-aligned bounding boxes (N, 4). Used internally in with_nmm and exposed via top-level import.
* deprecate: mark merge_inner_detections_objects for removal in 0.34.0 Function is unused dead code with no external callers. Decorator emits FutureWarning while preserving existing behavior.
* refactor: extract _merge_obb_corners and _merge_detection_group from with_nmm Replace inline OBB post-processing and reduce-based merging with two private helpers using single-pass area-weighted confidence. Deprecate merge_inner_detection_object_pair and merge_inner_detections_objects_without_iou (0.29.0 -> 0.34.0). Rename TestDetectionsWithNmm -> TestDetectionsWithNMM and expand TestMergeDetectionGroup to assert all output fields via expected_detections.
---------
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: SkalskiP <piotr.skalski92@gmail.com>
* fix(detection): make Detections.area OBB-aware
When detections carry ORIENTED_BOX_COORDINATES (the four xyxyxyxy corners),
the area property returned the area of the derived axis-aligned bounding
box instead of the rotated body. The AABB overestimates by up to ~2x for a
45-degree rotation, which silently miscomputes downstream values — most
visibly the area-sorted z-ordering inside MaskAnnotator / HaloAnnotator,
and any user code that filters detections by area.
* docs(detection): use string literal in Detections.area doctest
* test(detection): single-line docstring on test_uses_oriented_box_corners_when_present
* fix(detection): validate (N,4,2) shape of OBB data field in Detections.area
* perf(detection): replace np.roll pair with cross-diagonal shoelace in Detections.area
* perf(detection): cast x/y slices to float64 instead of full corners array
* refactor(detection): extract obb_polygon_area to detection/utils/boxes.py
* test(detection): add test_raises_on_malformed_obb_coordinates_shape
* test(detection): assert per-branch dtype contract for Detections.area
* docs(detection): document OBB dispatch contract and dtype in Detections.area docstring
---------
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
When from_paligemma or from_google_gemini_2_0 find no detections (no regex
matches, JSON decode error, or empty bounding-box list), they previously
returned None for class_id. All other early-exit and filter paths already
return a zero-length ndarray of dtype int. This inconsistency causes
downstream AttributeError when callers unconditionally call .shape or
iterate over the result.
Affected paths:
- from_paligemma: matches.shape[0] == 0 branch
- from_google_gemini_2_0: JSONDecodeError branch and len(xyxy) == 0 branch
---------
Co-authored-by: YousefZahran1 <youssefzahran.y@gmail.com>
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
* test: add regression tests for list/tuple custom_data slicing
* fix: slice list and tuple custom_data values per row
* docs: document custom_data slicing contract in append() docstrings
* docs: add docstring to _slice_value in CSVSink and JSONSink
* docs: add docstring to parse_detection_data in CSVSink and JSONSink
* test: add test for detections.data with plain Python list values
* test: add _slice_value edge-case unit tests
* docs: add per-row slicing note to CSVSink and JSONSink class docstrings
---------
Co-authored-by: jirka <6035284+Borda@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
* Detections.from_inference works on RLE-encoded masks
* Apply suggestions from code review
* fix: harden RLE handling in from_inference and decoder
* lint: fix cv2.fillPoly color type in polygon_to_mask
* fix: resize RLE mask to image dims when size mismatches
* fix: pass RLE counts directly in coco_annotations_to_masks
* test: document mixed RLE + box-only batch misalignment
* test: add compressed RLE iscrowd case to coco_annotations_to_detections
* fix: cast polygon mask to bool in process_roboflow_result
* fix: log warning when RLE decode fails in process_roboflow_result
* fix: replace assert with ValueError in rle_to_mask
* test: add bytes invalid UTF-8 case to rle_to_mask tests
* docs: note rle_to_mask dtype change from uint8 to bool in changelog
* refactor: tighten rle_to_mask NDArray input type to np.integer[Any]
* refactor: drop mask_to_rle overloads; cast at call site
* docs: clarify COCO column-major RLE order in rle_to_mask/mask_to_rle
* refactor: update @deprecated annotations and docstrings for mask_to_rle/rle_to_mask; add pydeprecate dependency
* refactor: replace mask_to_rle body with `void` function to suppress unused argument warnings
---------
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
When a user's callback accidentally runs inference on the full image instead
of the provided slice, detections get incorrect offsets applied, causing a
repeating grid pattern. Add a validation check in _run_callback that emits a
SupervisionWarnings warning when any detection coordinate exceeds the slice
dimensions or is negative. An instance flag prevents repeated warnings across
many slices.
- Wrap _out_of_slice_bounds_warned check-and-set in threading.Lock to prevent duplicate warnings under ThreadPoolExecutor with thread_workers > 1
- Change stacklevel=2 to stacklevel=1 — under executor.submit the stacklevel=2 frame points into concurrent.futures internals, not user code
- Assert exactly 1 warning fires with thread_workers=4 (validates Lock fix)
- Assert no warning for detection touching but not exceeding slice boundary (pins > vs >= semantics)
- Assert second slicer call does not re-warn (documents once-per-instance semantic)
- Extract warning message into `msg` variable to satisfy E501 line-length limit
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Borda <6035284+Borda@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* fix: prevent single object from appearing in multiple polygon zones
when checking if a detection is inside a polygon zone, the previous implementation
would clip the bounding box to fit within each ROI's dimensions before calculating
anchor points. This caused the same detection to produce different anchor points
for different ROIs, allowing it to be counted as present in multiple zones.
* Add regression test for PolygonZone trigger issue #1987 and remove unused `frame_resolution_wh` attribute
* refactor(polygon_zone): vectorize trigger() and strengthen tests
Replace the O(n×m) Python double-loop in PolygonZone.trigger() with
vectorized NumPy. Semantics are identical: compute a (num_anchors,
num_detections) in_bounds mask, use np.clip solely for safe fancy-index
access, then AND with the polygon mask and reduce with np.all(axis=0).
Also removes the now-unused `from dataclasses import replace` import and
a latent np.all(axis=1) call on a 1D array.
Test improvements:
- Group into TestPolygonZoneInit / TestPolygonZoneTrigger classes
- Replace the trivially-passing regression (sum=0 on both old and new
code) with adjacent zones + straddling detection that gives sum=2 on
the old clip_boxes implementation and sum=1 on the fix
- Rename tests to describe behaviour, not issue numbers
- Add test_out_of_bounds_anchor_excluded and
test_anchor_on_polygon_boundary_included edge cases
* test(polygon_zone): verify current_count updates with expected results during trigger
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: jirka <6035284+Borda@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor docstrings in `scr/supervision/detection`
* Enhance docstrings across multiple modules: clarify attributes/args, improve formatting, and update logic for handling sentinel values in metrics calculation.
* Ensure consistent handling of `class_id` as integer across YOLO and Pascal VOC formats, fix NoneType handling in line zone logic, and add test coverage for multiclass annotator with None `class_id`.
* Enforce `class_id` as integer in YOLO export, update line zone class count docstrings, and add test for non-integer `class_id`.
* Apply suggestions from code review
---------
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Fix mask_annotate for int dtypes
* Add depreciation warning
* Add dtype=bool to test masks
* Remove ValueError (testing)
* Ensure boolean masks are consistently used in `Detections` and update validations, tests, and warnings for stricter type handling.
* Apply suggestions from code review
---------
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>