From 17123bca63ef44f61b28e9585048ced8d76c456f Mon Sep 17 00:00:00 2001 From: Ritwij Aryan Parmar Date: Fri, 29 May 2026 13:47:04 -0400 Subject: [PATCH] Fix OBB IoU canvas dimensions --- .../detection/utils/iou_and_nms.py | 4 +- tests/detection/utils/test_iou_and_nms.py | 17 +++++++ tests/metrics/test_mean_average_precision.py | 28 +++++++++++ .../test_oriented_bounding_box_metrics.py | 47 +++++++++++++++++++ 4 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 tests/metrics/test_oriented_bounding_box_metrics.py diff --git a/src/supervision/detection/utils/iou_and_nms.py b/src/supervision/detection/utils/iou_and_nms.py index c2aa230f06..455852b65d 100644 --- a/src/supervision/detection/utils/iou_and_nms.py +++ b/src/supervision/detection/utils/iou_and_nms.py @@ -381,9 +381,9 @@ def oriented_box_iou_batch( boxes_true = boxes_true.reshape(-1, 4, 2) boxes_detection = boxes_detection.reshape(-1, 4, 2) - max_height = int(max(boxes_true[:, :, 0].max(), boxes_detection[:, :, 0].max()) + 1) # adding 1 because we are 0-indexed - max_width = int(max(boxes_true[:, :, 1].max(), boxes_detection[:, :, 1].max()) + 1) + max_width = int(max(boxes_true[:, :, 0].max(), boxes_detection[:, :, 0].max()) + 1) + max_height = int(max(boxes_true[:, :, 1].max(), boxes_detection[:, :, 1].max()) + 1) mask_true = np.zeros((boxes_true.shape[0], max_height, max_width), dtype=np.uint8) for box_idx, box_true in enumerate(boxes_true): diff --git a/tests/detection/utils/test_iou_and_nms.py b/tests/detection/utils/test_iou_and_nms.py index a43830fc6e..9c4ade4cf8 100644 --- a/tests/detection/utils/test_iou_and_nms.py +++ b/tests/detection/utils/test_iou_and_nms.py @@ -13,6 +13,7 @@ box_non_max_suppression, mask_non_max_merge, mask_non_max_suppression, + oriented_box_iou_batch, ) from tests.helpers import _generate_random_boxes @@ -1128,3 +1129,19 @@ def test_box_iou_batch_symmetric_large( rtol=1e-6, atol=1e-12, ) + + +def test_oriented_box_iou_batch_is_invariant_to_non_square_scaling() -> None: + boxes_true = np.array([[[1, 0], [0, 1], [3, 4], [4, 3]]], dtype=np.float32) + boxes_detection = np.array([[[1, 1], [2, 0], [4, 2], [3, 3]]], dtype=np.float32) + + baseline_iou = oriented_box_iou_batch(boxes_true, boxes_detection) + scaled_iou = oriented_box_iou_batch( + boxes_true * np.array([[10, 1]], dtype=np.float32), + boxes_detection * np.array([[10, 1]], dtype=np.float32), + ) + + assert baseline_iou.shape == (1, 1) + assert scaled_iou.shape == (1, 1) + assert baseline_iou[0, 0] > 0.35 + assert np.allclose(scaled_iou, baseline_iou, rtol=0.03, atol=0.02) diff --git a/tests/metrics/test_mean_average_precision.py b/tests/metrics/test_mean_average_precision.py index 9d19fd7904..a6efd624d1 100644 --- a/tests/metrics/test_mean_average_precision.py +++ b/tests/metrics/test_mean_average_precision.py @@ -1,6 +1,8 @@ import numpy as np +from supervision.config import ORIENTED_BOX_COORDINATES from supervision.detection.core import Detections +from supervision.metrics.core import MetricTarget from supervision.metrics.mean_average_precision import MeanAveragePrecision @@ -33,6 +35,32 @@ def test_multiple_perfect_detections(self): # Should be perfect 1.0 mAP assert abs(result.map50_95 - 1.0) < 1e-6 + def test_perfect_non_square_oriented_boxes_get_full_map(self): + """Perfect OBB detections must not fail when the canvas is non-square.""" + obb = np.array( + [[[10, 0], [0, 1], [30, 4], [40, 3]]], + dtype=np.float32, + ) + detections = Detections( + xyxy=np.array([[0, 0, 40, 4]], dtype=np.float64), + class_id=np.array([0]), + confidence=np.array([0.9]), + data={ORIENTED_BOX_COORDINATES: obb}, + ) + targets = Detections( + xyxy=np.array([[0, 0, 40, 4]], dtype=np.float64), + class_id=np.array([0]), + data={ORIENTED_BOX_COORDINATES: obb}, + ) + + metric = MeanAveragePrecision( + metric_target=MetricTarget.ORIENTED_BOUNDING_BOXES + ) + metric.update([detections], [targets]) + result = metric.compute() + + assert abs(result.map50_95 - 1.0) < 1e-6 + def test_batch_updates_perfect_detections(self, detections_50_50, targets_50_50): """Test that batch updates with perfect detections get 1.0 mAP""" metric = MeanAveragePrecision() diff --git a/tests/metrics/test_oriented_bounding_box_metrics.py b/tests/metrics/test_oriented_bounding_box_metrics.py new file mode 100644 index 0000000000..8391a0a716 --- /dev/null +++ b/tests/metrics/test_oriented_bounding_box_metrics.py @@ -0,0 +1,47 @@ +import numpy as np +import pytest + +from supervision.config import ORIENTED_BOX_COORDINATES +from supervision.detection.core import Detections +from supervision.metrics.core import MetricTarget +from supervision.metrics.f1_score import F1Score +from supervision.metrics.mean_average_precision import MeanAveragePrecision +from supervision.metrics.mean_average_recall import MeanAverageRecall +from supervision.metrics.precision import Precision +from supervision.metrics.recall import Recall + + +def _non_square_obb_detections(confidence: bool = False) -> Detections: + obb = np.array( + [[[10, 0], [0, 1], [30, 4], [40, 3]]], + dtype=np.float32, + ) + return Detections( + xyxy=np.array([[0, 0, 40, 4]], dtype=np.float64), + class_id=np.array([0]), + confidence=np.array([0.9]) if confidence else None, + data={ORIENTED_BOX_COORDINATES: obb}, + ) + + +@pytest.mark.parametrize( + ("metric_cls", "score_name"), + [ + (Precision, "precision_at_50"), + (Recall, "recall_at_50"), + (F1Score, "f1_50"), + (MeanAveragePrecision, "map50_95"), + (MeanAverageRecall, "mAR_at_100"), + ], +) +def test_perfect_non_square_oriented_boxes_score_as_perfect( + metric_cls: type, + score_name: str, +) -> None: + predictions = _non_square_obb_detections(confidence=True) + targets = _non_square_obb_detections() + + metric = metric_cls(metric_target=MetricTarget.ORIENTED_BOUNDING_BOXES) + result = metric.update([predictions], [targets]).compute() + + assert getattr(result, score_name) == pytest.approx(1.0)