fix(annotators): clip BackgroundOverlayAnnotator boxes to the scene… (#2396)
* fix(annotators): clip BackgroundOverlayAnnotator boxes to the scene before restoring detection regions * fix(annotators): use explicit np.int32 cast in BackgroundOverlayAnnotator * test(annotators): strengthen BackgroundOverlayAnnotator test coverage * docs(changelog): add Unreleased entry for BackgroundOverlayAnnotator fix --------- Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
This commit is contained in:
parent
0e1056df19
commit
afcf13a6f5
|
|
@ -18,6 +18,7 @@ date_modified: 2026-06-25
|
|||
- Fixed [#2393](https://github.com/roboflow/supervision/pull/2393): `sv.HeatMapAnnotator.annotate` no longer blanks the hottest region when the per-pixel hit count exceeds 255; the heat mask is now derived from the float32 accumulator directly, avoiding uint8 wrap-around.
|
||||
- Fixed [#2393](https://github.com/roboflow/supervision/pull/2393): `sv.get_video_frames_generator` now releases the underlying `cv2.VideoCapture` via `try/finally`, so the decoder is freed when a consumer breaks out of iteration early rather than waiting for garbage collection.
|
||||
- Fixed [#2382](https://github.com/roboflow/supervision/pull/2382): `sv.Detections.get_anchors_coordinates` now uses oriented bounding box corners (`data["xyxyxyxy"]`) when OBB data is present, instead of falling back to the axis-aligned envelope. Anchors on rotated detections now lie on the oriented body rather than drifting to the envelope. Non-OBB detections and `Position.CENTER_OF_MASS` (which requires a mask) are unaffected.
|
||||
- Fixed [#2396](https://github.com/roboflow/supervision/pull/2396): `sv.BackgroundOverlayAnnotator.annotate` no longer leaves detection regions tinted when bounding boxes have negative coordinates (extend outside the left or top scene boundary); boxes are now clipped to scene bounds before the detection region is restored.
|
||||
|
||||
### Added
|
||||
- `BaseAnnotator.requires_mask` — class-level `bool` flag on all annotators; `True` for `MaskAnnotator`, `PolygonAnnotator`, and `HaloAnnotator`; `False` for all others. Integrations can inspect this before materializing expensive mask payloads ([#2370](https://github.com/roboflow/supervision/pull/2370))
|
||||
|
|
|
|||
|
|
@ -3180,7 +3180,12 @@ class BackgroundOverlayAnnotator(BaseAnnotator):
|
|||
)
|
||||
|
||||
if detections.mask is None or self.force_box:
|
||||
for x1, y1, x2, y2 in detections.xyxy.astype(int):
|
||||
image_height, image_width = scene.shape[:2]
|
||||
clipped_xyxy: npt.NDArray[np.int32] = clip_boxes(
|
||||
xyxy=detections.xyxy,
|
||||
resolution_wh=(image_width, image_height),
|
||||
).astype(np.int32)
|
||||
for x1, y1, x2, y2 in clipped_xyxy:
|
||||
colored_mask[y1:y2, x1:x2] = scene[y1:y2, x1:x2]
|
||||
else:
|
||||
for mask in detections.mask:
|
||||
|
|
|
|||
|
|
@ -1461,6 +1461,66 @@ class TestBackgroundOverlayAnnotator:
|
|||
result = annotator.annotate(scene=image.copy(), detections=detections)
|
||||
assert not np.array_equal(image, result)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("xyxy", "inside_xy", "outside_xy"),
|
||||
[
|
||||
pytest.param([-5, 20, 40, 60], (20, 30), (60, 80), id="crosses-left-edge"),
|
||||
pytest.param([20, -5, 60, 40], (30, 20), (80, 60), id="crosses-top-edge"),
|
||||
pytest.param(
|
||||
[-10, -10, 40, 40], (20, 20), (70, 70), id="crosses-both-edges"
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_annotate_preserves_detection_crossing_scene_border(
|
||||
self, xyxy: list[int], inside_xy: tuple[int, int], outside_xy: tuple[int, int]
|
||||
) -> None:
|
||||
"""The visible part of a box crossing the border keeps original pixels"""
|
||||
image = np.full((100, 100, 3), 200, dtype=np.uint8)
|
||||
detections = _create_detections(xyxy=[xyxy])
|
||||
annotator = BackgroundOverlayAnnotator(color=Color.BLACK, opacity=0.5)
|
||||
|
||||
result = annotator.annotate(scene=image.copy(), detections=detections)
|
||||
|
||||
x_in, y_in = inside_xy
|
||||
x_out, y_out = outside_xy
|
||||
assert np.array_equal(result[y_in, x_in], np.array([200, 200, 200]))
|
||||
assert np.array_equal(result[y_out, x_out], np.array([100, 100, 100]))
|
||||
|
||||
def test_annotate_fully_out_negative_box_does_not_corrupt(self) -> None:
|
||||
"""Both-negative OOB box must not restore an in-bounds region via wrap-around"""
|
||||
image = np.full((100, 100, 3), 200, dtype=np.uint8)
|
||||
detections = _create_detections(xyxy=[[-30, -30, -5, -5]])
|
||||
annotator = BackgroundOverlayAnnotator(color=Color.BLACK, opacity=0.5)
|
||||
|
||||
result = annotator.annotate(scene=image.copy(), detections=detections)
|
||||
|
||||
assert np.array_equal(result[80, 80], np.array([100, 100, 100]))
|
||||
|
||||
def test_annotate_force_box_preserves_detection_crossing_scene_border(self):
|
||||
"""force_box with a border-crossing box keeps the visible detection region"""
|
||||
image = np.full((100, 100, 3), 200, dtype=np.uint8)
|
||||
mask = np.zeros((100, 100), dtype=bool)
|
||||
mask[20:60, 0:40] = True
|
||||
detections = _create_detections(xyxy=[[-5, 20, 40, 60]], mask=[mask])
|
||||
annotator = BackgroundOverlayAnnotator(
|
||||
color=Color.BLACK, opacity=0.5, force_box=True
|
||||
)
|
||||
|
||||
result = annotator.annotate(scene=image.copy(), detections=detections)
|
||||
|
||||
assert np.array_equal(result[30, 20], np.array([200, 200, 200]))
|
||||
assert np.array_equal(result[80, 60], np.array([100, 100, 100]))
|
||||
|
||||
def test_annotate_with_fully_out_of_bounds_detection(self):
|
||||
"""A box fully outside the scene leaves the whole scene tinted"""
|
||||
image = np.full((100, 100, 3), 200, dtype=np.uint8)
|
||||
detections = _create_detections(xyxy=[[150, 150, 200, 200]])
|
||||
annotator = BackgroundOverlayAnnotator(color=Color.BLACK, opacity=0.5)
|
||||
|
||||
result = annotator.annotate(scene=image.copy(), detections=detections)
|
||||
|
||||
assert np.all(result == 100)
|
||||
|
||||
def test_annotate_uint8_mask_matches_bool_mask(self):
|
||||
"""Test that uint8 and bool masks produce identical overlays."""
|
||||
image = np.ones((100, 100, 3), dtype=np.uint8) * 255
|
||||
|
|
|
|||
Loading…
Reference in New Issue