fix(annotators): clip CropAnnotator boxes to the scene before cropping (#2391)

This commit is contained in:
Abhijith Neil Abraham 2026-07-03 01:33:49 -07:00 committed by GitHub
parent f196e15f26
commit 0d4c3a4fcf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 63 additions and 7 deletions

View File

@ -3019,17 +3019,21 @@ class CropAnnotator(BaseAnnotator):
"""
if not isinstance(scene, np.ndarray):
return scene
crops = [
crop_image(image=scene, xyxy=xyxy) for xyxy in detections.xyxy.astype(int)
]
resized_crops = [
scale_image(image=crop, scale_factor=self.scale_factor) for crop in crops
]
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(int)
anchors: npt.NDArray[np.int32] = detections.get_anchors_coordinates(
anchor=self.position
).astype(int)
for idx, (resized_crop, anchor) in enumerate(zip(resized_crops, anchors)):
for idx, (xyxy, anchor) in enumerate(zip(clipped_xyxy, anchors)):
x_min, y_min, x_max, y_max = xyxy
if x_max - x_min <= 0 or y_max - y_min <= 0:
continue
crop = crop_image(image=scene, xyxy=xyxy)
resized_crop = scale_image(image=crop, scale_factor=self.scale_factor)
crop_wh = resized_crop.shape[1], resized_crop.shape[0]
(x1, y1), (x2, y2) = self.calculate_crop_coordinates(
anchor=anchor, crop_wh=crop_wh, position=self.position

View File

@ -1323,6 +1323,58 @@ class TestCropAnnotator:
result = annotator.annotate(scene=gradient_image.copy(), detections=detections)
assert not np.array_equal(gradient_image, result)
@pytest.mark.parametrize(
"xyxy",
[
pytest.param([-5, 20, 40, 60], id="negative-x-min"),
pytest.param([20, -5, 60, 40], id="negative-y-min"),
pytest.param([-10, -10, 30, 30], id="negative-x-and-y-min"),
pytest.param([60, 20, 140, 60], id="past-right-edge"),
pytest.param([-20, -20, 140, 140], id="larger-than-scene"),
],
)
def test_annotate_with_box_crossing_scene_border(
self, gradient_image, xyxy: list[int]
) -> None:
"""Boxes extending past the scene border are clipped instead of raising"""
detections = _create_detections(xyxy=[xyxy], class_id=[0])
annotator = CropAnnotator()
result = annotator.annotate(scene=gradient_image.copy(), detections=detections)
assert result.shape == gradient_image.shape
@pytest.mark.parametrize(
"xyxy",
[
pytest.param([150, 150, 200, 200], id="fully-outside"),
pytest.param([-50, -50, -10, -10], id="fully-negative"),
pytest.param([30, 20, 30, 60], id="zero-width"),
pytest.param([30, 30, 30, 30], id="zero-area"),
],
)
def test_annotate_skips_boxes_empty_after_clipping(
self, gradient_image, xyxy: list[int]
) -> None:
"""Boxes with no visible area are skipped instead of raising cv2.error"""
detections = _create_detections(xyxy=[xyxy], class_id=[0])
annotator = CropAnnotator()
result = annotator.annotate(scene=gradient_image.copy(), detections=detections)
assert np.array_equal(gradient_image, result)
def test_annotate_mixed_valid_and_degenerate_boxes(self, gradient_image) -> None:
"""A degenerate box does not prevent valid boxes from being drawn"""
detections = _create_detections(
xyxy=[[150, 150, 200, 200], [10, 10, 90, 90]], class_id=[0, 1]
)
annotator = CropAnnotator()
result = annotator.annotate(scene=gradient_image.copy(), detections=detections)
assert not np.array_equal(gradient_image, result)
class TestBackgroundOverlayAnnotator:
"""Tests for BackgroundOverlayAnnotator class"""