feat/ speed up box iou - replaced original function; added tests
This commit is contained in:
parent
35ff87b08d
commit
ceefe0bab2
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}."
|
||||
|
|
|
|||
Loading…
Reference in New Issue