feat/ speed up box iou - replaced original function; added tests

This commit is contained in:
AnonymDevOSS 2025-11-06 22:52:25 +01:00
parent 35ff87b08d
commit ceefe0bab2
4 changed files with 139 additions and 134 deletions

View File

@ -172,93 +172,6 @@ def box_iou_batch(
`shape = (N, M)` where `N` is number of true objects and
`M` is number of detected objects.
Examples:
```python
import numpy as np
import supervision as sv
boxes_true = np.array([
[100, 100, 200, 200],
[300, 300, 400, 400]
])
boxes_detection = np.array([
[150, 150, 250, 250],
[320, 320, 420, 420]
])
sv.box_iou_batch(boxes_true=boxes_true, boxes_detection=boxes_detection)
# array([
# [0.14285714, 0. ],
# [0. , 0.47058824]
# ])
```
"""
def box_area(box):
return (box[2] - box[0]) * (box[3] - box[1])
area_true = box_area(boxes_true.T)
area_detection = box_area(boxes_detection.T)
top_left = np.maximum(boxes_true[:, None, :2], boxes_detection[:, :2])
bottom_right = np.minimum(boxes_true[:, None, 2:], boxes_detection[:, 2:])
area_inter = np.prod(np.clip(bottom_right - top_left, a_min=0, a_max=None), 2)
if overlap_metric == OverlapMetric.IOU:
union_area = area_true[:, None] + area_detection - area_inter
ious = np.divide(
area_inter,
union_area,
out=np.zeros_like(area_inter, dtype=float),
where=union_area != 0,
)
elif overlap_metric == OverlapMetric.IOS:
small_area = np.minimum(area_true[:, None], area_detection)
ious = np.divide(
area_inter,
small_area,
out=np.zeros_like(area_inter, dtype=float),
where=small_area != 0,
)
else:
raise ValueError(
f"overlap_metric {overlap_metric} is not supported, "
"only 'IOU' and 'IOS' are supported"
)
ious = np.nan_to_num(ious)
return ious
def box_iou_batch_alt(
boxes_true: np.ndarray,
boxes_detection: np.ndarray,
overlap_metric: OverlapMetric = OverlapMetric.IOU,
) -> np.ndarray:
"""
Compute Intersection over Union (IoU) of two sets of bounding boxes -
`boxes_true` and `boxes_detection`. Both sets
of boxes are expected to be in `(x_min, y_min, x_max, y_max)` format.
Note:
Use `box_iou` when computing IoU between two individual boxes.
For comparing multiple boxes (arrays of boxes), use `box_iou_batch` for better
performance.
Args:
boxes_true (np.ndarray): 2D `np.ndarray` representing ground-truth boxes.
`shape = (N, 4)` where `N` is number of true objects.
boxes_detection (np.ndarray): 2D `np.ndarray` representing detection boxes.
`shape = (M, 4)` where `M` is number of detected objects.
overlap_metric (OverlapMetric): Metric used to compute the degree of overlap
between pairs of boxes (e.g., IoU, IoS).
Returns:
np.ndarray: Pairwise IoU of boxes from `boxes_true` and `boxes_detection`.
`shape = (N, M)` where `N` is number of true objects and
`M` is number of detected objects.
Examples:
```python
import numpy as np

View File

@ -1,38 +0,0 @@
import random
import numpy as np
def generate_boxes(
n: int,
W: int = 1920,
H: int = 1080,
min_size: int = 20,
max_size: int = 200,
seed: int | None = 1,
):
"""
Generate N valid bounding boxes of format [x_min, y_min, x_max, y_max].
Args:
n (int): Number of boexs to generate
W (int): Image width
H (int): Image height
min_size (int): Minimum box size (width/height)
max_size (int): Maximum box size (width/height)
seed (int | None): Random seed for reproducibility
Returns:
list[list[float]] | np.ndarray: List of boxes
"""
random.seed(seed)
boxes = []
for _ in range(n):
w = random.uniform(min_size, max_size)
h = random.uniform(min_size, max_size)
x1 = random.uniform(0, W - w)
y1 = random.uniform(0, H - h)
x2 = x1 + w
y2 = y1 + h
boxes.append([x1, y1, x2, y2])
return np.array(boxes, dtype=np.float32)

View File

@ -6,14 +6,15 @@ import numpy as np
import pytest
from supervision.detection.utils.iou_and_nms import (
OverlapMetric,
_group_overlapping_boxes,
box_iou,
box_iou_batch,
box_iou_batch_alt,
box_non_max_suppression,
mask_non_max_merge,
mask_non_max_suppression,
)
from test.detection.utils.functions import generate_boxes
from test.test_utils import mock_boxes
@pytest.mark.parametrize(
@ -636,12 +637,105 @@ def test_mask_non_max_merge(
assert sorted_result == sorted_expected_result
def test_box_iou_batch_and_alt_equivalence():
boxes_true = generate_boxes(20, seed=1)
boxes_detection = generate_boxes(30, seed=2)
@pytest.mark.parametrize(
"boxes_true, boxes_detection, expected_iou, exception",
[
(
np.empty((0, 4), dtype=np.float32),
np.empty((0, 4), dtype=np.float32),
np.empty((0, 0), dtype=np.float32),
DoesNotRaise(),
), # empty
(
np.array([[0, 0, 10, 10]], dtype=np.float32),
np.empty((0, 4), dtype=np.float32),
np.empty((1, 0), dtype=np.float32),
DoesNotRaise(),
), # one true box, no detections
(
np.empty((0, 4), dtype=np.float32),
np.array([[0, 0, 10, 10]], dtype=np.float32),
np.empty((0, 1), dtype=np.float32),
DoesNotRaise(),
), # no true boxes, one detection
(
np.array([[0, 0, 10, 10]], dtype=np.float32),
np.array([[0, 0, 10, 10]], dtype=np.float32),
np.array([[1.0]]),
DoesNotRaise(),
), # perfect overlap
(
np.array([[0, 0, 10, 10]], dtype=np.float32),
np.array([[20, 20, 30, 30]], dtype=np.float32),
np.array([[0.0]]),
DoesNotRaise(),
), # no overlap
(
np.array([[0, 0, 10, 10]], dtype=np.float32),
np.array([[5, 5, 15, 15]], dtype=np.float32),
np.array([[25.0 / 175.0]]), # intersection: 5x5=25, union: 100+100-25=175
DoesNotRaise(),
), # partial overlap
(
np.array([[0, 0, 10, 10]], dtype=np.float32),
np.array([[0, 0, 5, 5]], dtype=np.float32),
np.array([[25.0 / 100.0]]), # intersection: 5x5=25, union: 100
DoesNotRaise(),
), # detection inside true box
(
np.array([[0, 0, 5, 5]], dtype=np.float32),
np.array([[0, 0, 10, 10]], dtype=np.float32),
np.array([[25.0 / 100.0]]), # true box inside detection
DoesNotRaise(),
),
(
np.array([[0, 0, 10, 10], [20, 20, 30, 30]], dtype=np.float32),
np.array([[0, 0, 10, 10], [20, 20, 30, 30]], dtype=np.float32),
np.array([[1.0, 0.0], [0.0, 1.0]]),
DoesNotRaise(),
), # two boxes, perfect matches
],
)
def test_box_iou_batch(
boxes_true: np.ndarray,
boxes_detection: np.ndarray,
expected_iou: np.ndarray,
exception: Exception,
) -> None:
with exception:
result = box_iou_batch(boxes_true, boxes_detection)
assert result.shape == expected_iou.shape
assert np.allclose(result, expected_iou, rtol=1e-5, atol=1e-5)
iou_a = box_iou_batch(boxes_true, boxes_detection)
iou_b = box_iou_batch_alt(boxes_true, boxes_detection)
assert iou_a.shape == iou_b.shape
assert np.allclose(iou_a, iou_b, rtol=1e-6, atol=1e-6)
def test_box_iou_batch_consistency_with_box_iou():
"""Test that box_iou_batch gives same results as box_iou for single boxes."""
boxes_true = np.array(mock_boxes(5, seed=1), dtype=np.float32)
boxes_detection = np.array(mock_boxes(5, seed=2), dtype=np.float32)
batch_result = box_iou_batch(boxes_true, boxes_detection)
for i, box_true in enumerate(boxes_true):
for j, box_detection in enumerate(boxes_detection):
single_result = box_iou(box_true, box_detection)
assert np.allclose(
batch_result[i, j], single_result, rtol=1e-5, atol=1e-5
)
def test_box_iou_batch_with_mock_detections():
""" Test box_iou_batch with generated boxes and verify results are valid. """
boxes_true = np.array(mock_boxes(10, seed=1), dtype=np.float32)
boxes_detection = np.array(mock_boxes(15, seed=2), dtype=np.float32)
result = box_iou_batch(boxes_true, boxes_detection)
assert result.shape == (10, 15)
assert np.all(result >= 0)
assert np.all(result <= 1.0)
# and symetric
result_reversed = box_iou_batch(boxes_detection, boxes_true)
assert result_reversed.shape == (15, 10)
assert np.allclose(result.T, result_reversed, rtol=1e-5, atol=1e-5)

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import random
from typing import Any
import numpy as np
@ -52,5 +53,40 @@ def mock_key_points(
)
def mock_boxes(
n: int,
resolution_wh: tuple[int, int] = (1920, 1080),
min_size: int = 20,
max_size: int = 200,
seed: int | None = None,
) -> list[list[float]]:
"""
Generate N valid bounding boxes of format [x_min, y_min, x_max, y_max].
Args:
n: Number of boxes to generate.
resolution_wh: Image resolution as (width, height). Defaults to (1920, 1080).
min_size: Minimum box size (width/height). Defaults to 20.
max_size: Maximum box size (width/height). Defaults to 200.
seed: Random seed for reproducibility. Defaults to None.
Returns:
List of boxes, each as [x_min, y_min, x_max, y_max].
"""
if seed is not None:
random.seed(seed)
width, height = resolution_wh
boxes = []
for _ in range(n):
w = random.uniform(min_size, max_size)
h = random.uniform(min_size, max_size)
x1 = random.uniform(0, width - w)
y1 = random.uniform(0, height - h)
x2 = x1 + w
y2 = y1 + h
boxes.append([x1, y1, x2, y2])
return boxes
def assert_almost_equal(actual, expected, tolerance=1e-5):
assert abs(actual - expected) < tolerance, f"Expected {expected}, but got {actual}."