* 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>
- Added `KeyPoints.visible` mask support for per-keypoint visibility
- Split confidence into `keypoint_confidence` and `detection_confidence`
- Kept legacy `KeyPoints.confidence` as deprecated forwarding alias
- Updated `KeyPoints` slicing/filtering to preserve visibility and confidence fields
- Fixed `KeyPoints.__getitem__` row-index normalization for NumPy scalar, 0-D array, and boolean indexing
- Fixed `detection_confidence` indexing to use normalized row indices
- Updated `VertexAnnotator` to skip invisible keypoints
- Updated `EdgeAnnotator` to skip invisible keypoints and edges
- Added per-class skeleton support to `EdgeAnnotator`
- Added multi-skeleton support to `VertexLabelAnnotator`
- Added label validation for `VertexLabelAnnotator`
- Added color-list length validation for keypoint annotators
- Added `VertexEllipseAreaAnnotator`
- Added `VertexEllipseOutlineAnnotator`
- Added `VertexEllipseHaloAnnotator`
- Kept/exported `VertexEllipseAnnotator` alongside the new ellipse variants
- Standardized keypoint annotator docstrings and executable examples
- Added/updated `validate_detection_confidence` and `validate_visible`
- Removed/cleaned old keypoint validator shims
- Added regression tests for visibility, confidence fields, multi-skeleton behavior, and indexing edge cases
- Updated helpers and RF-DETR/keypoint tests for the new confidence/visibility model
- Added API design principles to contributing docs
- Ignored local multi-skeleton test script in `.gitignore`
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.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: OpenAI Codex <codex@openai.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>
DetectionDataset.from_yolo accepts is_obb=True and stores the four
corners in detections.data["xyxyxyxy"], but DetectionDataset.as_yolo
has no matching option and only reads xyxy/mask. The standard
from_yolo -> split -> as_yolo flow silently writes 5-token
axis-aligned lines, and re-loading the saved file with is_obb=True
crashes the validator because it expects 9 tokens.
Add is_obb to as_yolo, save_yolo_annotations, and
detections_to_yolo_annotations. When True, the four corners from
data["xyxyxyxy"] are serialized via the existing object_to_yolo
polygon path. Masks are ignored, mirroring from_yolo(is_obb=True)
semantics. A missing xyxyxyxy raises ValueError early.
- Add UserWarning in as_yolo when area/approx params passed with is_obb=True (silently ignored)
- Add UserWarning in detections_to_yolo_annotations when mask present + is_obb=True
- Update ValueError message to include expected shape (N, 4, 2) for manual callers
- Add Google-style docstrings to detections_to_yolo_annotations and save_yolo_annotations
- Add test: N>1 OBB detections per image (corner indexing via data-dict slicing)
- Add test: dataset round-trip with background-only (no label file) image
- Add test: as_yolo() without is_obb=True on OBB-loaded dataset emits 5-token lines
- Replace all tempfile.TemporaryDirectory / os.path.join / os.makedirs
with pytest tmp_path and pathlib Path
- Merge 3 load-mask tests into parametrized test_load_yolo_annotations_mask_behaviour
(obb-no-mask, obb-force_masks-ignored, segmentation-produces-mask)
- Merge token-count tests into parametrized test_dataset_as_yolo_obb_output_token_count
(obb-save-nine-tokens, default-save-five-tokens)
- Split corner accuracy into dedicated test_dataset_as_yolo_obb_round_trip_corner_accuracy
- Drop import os and import tempfile
---------
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
- Delete letterbox_image alpha block (lines 266-270): block wrote to
caller's input array, not image_with_borders; used wrong coordinate
system (resized vs original dims); redundant since cv2.copyMakeBorder
already sets alpha=0 in padded regions when given a 3-element value
- Add test_letterbox_image_for_rgba_opencv_image: asserts padded alpha=0,
interior alpha preserved, and input array not mutated after call
- Update letterbox_image docstring: image param lists (H,W,3)/(H,W,4)/
(H,W)/PIL shapes; add Note on BGRA alpha behavior; add grayscale doctest
---------
Co-authored-by: jirka <6035284+Borda@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Replaces the static python code block with a pycon doctest using
primitive numpy inputs, so the example is now executed and verified
by `pytest --doctest-modules`. Removes the external YOLO, ByteTrack,
and cv2 dependencies from the example.
Follows the same pattern as PR #2207 (LineZone). Part of the
documentation-as-tests effort tracked in #2106.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
When HeatMapAnnotator is called on a fresh annotator with empty detections
(common on the first frames of a video before the model produces any output),
self.heat_mask is all zeros, so temp / temp.max() raises
RuntimeWarning: invalid value encountered in divide and produces nan/inf
in-flight. Skip the normalisation when temp.max() == 0; the resulting
all-zero heat mask filters out via the > 0 check below, so the scene is
returned unchanged.
- Fix `kernel_size: int = 25` → `int | None = 25`; document None disables blur
- Add Note to annotate docstring: empty detections returns scene unchanged
- Add happy path test: single detection must produce visible heat output
- Add stateful tests: empty→real and real→empty sequence coverage
---------
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 Code <noreply@anthropic.com>
* fix(dataset): make COCO annotation/image ids chainable across splits (#768)
Exporting train/valid/test splits with DetectionDataset.as_coco
previously restarted image_id and annotation_id at 1 for every split,
producing three JSON files whose ids collided and could not be safely
merged into a single COCO collection.
Adds optional starting_image_id and starting_annotation_id parameters
to save_coco_annotations and DetectionDataset.as_coco (default 1 to
preserve existing behavior) and returns a (next_image_id,
next_annotation_id) tuple so callers can feed the result of one
export straight into the next:
next_image, next_ann = train.as_coco(annotations_path="train.json")
next_image, next_ann = valid.as_coco(
annotations_path="valid.json",
starting_image_id=next_image,
starting_annotation_id=next_ann,
)
test.as_coco(
annotations_path="test.json",
starting_image_id=next_image,
starting_annotation_id=next_ann,
)
The images-only branch of as_coco (annotations_path=None) round-trips
the starting ids unchanged so chaining still works there.
Adds 4 regression tests covering defaults, custom starting ids,
end-to-end three-split chaining with global uniqueness assertions,
and the images-only round-trip.
* docs: address review polish on COCO id-chaining
* fix(dataset): align save_coco_annotations approximation_percentage default to 0.0
* docs(dataset): add one-line summary to save_coco_annotations docstring
* docs: add changelog entry for COCO id chaining (PR #2267)
* docs(dataset): document file_name uniqueness limitation in save_coco_annotations
* feat(dataset): validate starting_image_id and starting_annotation_id >= 1
* docs(dataset): add Example section to save_coco_annotations docstring
* docs(dataset): unpack final as_coco return value in chaining example
* test(dataset): add COCO chaining tests and fix test helper for zero detections
---------
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
The README listed `--track_threshold` and `--match_threshold`, but
script.py exposes `--track_activation_threshold` and
`--minimum_matching_threshold` (the CLI surface is derived from the
main() signature by jsonargparse.auto_cli). Following the README as
written produced "unknown argument" errors.
Aligns the README with the actual CLI surface.
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>
- Move `tempfile.mkstemp` + `os.close` inside `try` block so OSError (disk full,
unwritable dir) is caught by the existing `except Exception` handler instead of
propagating to the caller, preserving the warn-and-degrade contract
- Pass `dir=os.path.dirname(os.path.abspath(video_path))` so the temp file is on
the same filesystem as the output, restoring `os.rename` semantics in `shutil.move`
- Initialise `tmp_path = None` before `try`; guard `finally` with
`tmp_path is not None` to satisfy mypy and avoid referencing an unbound name
- Add `-loglevel error -nostats` so ffmpeg only writes actual errors to stderr
(eliminates progress/stats spam that would buffer in PIPE indefinitely)
- Decode `result.stderr` and include it in the warning when ffmpeg exits
non-zero, so failure messages surface diagnostically instead of being discarded
- Change bare `process_video(...)` call to `sv.process_video(...)` so the
example matches the public API pattern and does not raise NameError for users
copying the snippet
- Remove unused `import cv2` which was never referenced in the example body
- Clarify that missing/failing ffmpeg warns and continues rather than raising
- Add install hint for ffmpeg (apt/brew)
- Note that audio is truncated to match the processed video duration (-shortest)
- test_mux_audio_moves_file_on_success: mock subprocess.run returncode=0;
assert shutil.move is called once with video_path as destination — catches
any regression that drops the move call after a successful ffmpeg run
- test_mux_audio_swallows_subprocess_exception: mock subprocess.run raising
OSError; assert no exception escapes _mux_audio and original file is intact
- Fix failed_result.stderr = b"" in test_mux_audio_warns_on_ffmpeg_failure
to match the updated _mux_audio which now decodes result.stderr
- Skip _mux_audio when writer_worker.is_alive() after join timeout to avoid
muxing an incomplete output file
- Fix test_mux_audio_moves_file_on_success: patch os.replace (not shutil.move)
to match implementation changed in 2027938d
- Move four test_mux_audio_* free functions into TestMuxAudio class
- Strip mux_audio_ prefix from method names; class carries the unit
- Condense multi-line docstrings to single-line per testing rules
- Collapse test_warns_when_ffmpeg_missing, test_warns_on_ffmpeg_failure,
test_swallows_subprocess_exception into one parametrized
test_file_unchanged_on_failure[ffmpeg_missing|ffmpeg_fails|subprocess_raises]
- Promote two class methods back to module-level functions
- Collapse nested with-patch statements into single with a, b: form
---------
Co-authored-by: jirka <6035284+Borda@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
- Skip mike deploy for RC releases (tags containing rc/RC)
- Strip .postX suffix before deploying (0.27.0.post1 → 0.27.0)
- Add -u flag to latest deploy to handle existing alias
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>