improve docstrings, examples, variable naming conventions and unit tests
This commit is contained in:
parent
eee6a85eaa
commit
f716b7fa3b
|
|
@ -64,7 +64,7 @@ class OverlapMetric(Enum):
|
|||
IOS = "IOS"
|
||||
|
||||
@classmethod
|
||||
def list(cls):
|
||||
def list(cls) -> list[str]:
|
||||
return list(map(lambda c: c.value, cls))
|
||||
|
||||
@classmethod
|
||||
|
|
@ -72,7 +72,7 @@ class OverlapMetric(Enum):
|
|||
if isinstance(value, cls):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
value = value.lower()
|
||||
value = value.upper()
|
||||
try:
|
||||
return cls(value)
|
||||
except ValueError:
|
||||
|
|
@ -86,91 +86,107 @@ class OverlapMetric(Enum):
|
|||
def box_iou(
|
||||
box_true: list[float] | np.ndarray,
|
||||
box_detection: list[float] | np.ndarray,
|
||||
overlap_metric: OverlapMetric | str = OverlapMetric.IOU,
|
||||
) -> float:
|
||||
r"""
|
||||
Compute the Intersection over Union (IoU) between two bounding boxes.
|
||||
"""
|
||||
Compute overlap metric between two bounding boxes.
|
||||
|
||||
\[
|
||||
\text{IoU} = \frac{|\text{box}_{\text{true}} \cap \text{box}_{\text{detection}}|}{|\text{box}_{\text{true}} \cup \text{box}_{\text{detection}}|}
|
||||
\]
|
||||
|
||||
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.
|
||||
Supports standard IOU (intersection-over-union) and IOS
|
||||
(intersection-over-smaller-area) metrics. Returns the overlap value in range
|
||||
`[0, 1]`.
|
||||
|
||||
Args:
|
||||
box_true (Union[List[float], np.ndarray]): A single bounding box represented as
|
||||
[x_min, y_min, x_max, y_max].
|
||||
box_detection (Union[List[float], np.ndarray]):
|
||||
A single bounding box represented as [x_min, y_min, x_max, y_max].
|
||||
box_true (`list[float]` or `numpy.array`): Ground truth box in format
|
||||
`(x_min, y_min, x_max, y_max)`.
|
||||
box_detection (`list[float]` or `numpy.array`): Detected box in format
|
||||
`(x_min, y_min, x_max, y_max)`.
|
||||
overlap_metric (`OverlapMetric` or `str`): Overlap type.
|
||||
Use `OverlapMetric.IOU` for IOU or
|
||||
`OverlapMetric.IOS` for IOS. Defaults to `OverlapMetric.IOU`.
|
||||
|
||||
Returns:
|
||||
IoU (float): IoU score between the two boxes. Ranges from 0.0 (no overlap)
|
||||
to 1.0 (perfect overlap).
|
||||
(`float`): Overlap value between boxes in `[0, 1]`.
|
||||
|
||||
Raises:
|
||||
ValueError: If `overlap_metric` is not IOU or IOS.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import numpy as np
|
||||
```
|
||||
import supervision as sv
|
||||
|
||||
box_true = np.array([100, 100, 200, 200])
|
||||
box_detection = np.array([150, 150, 250, 250])
|
||||
box_true = [100, 100, 200, 200]
|
||||
box_detection = [150, 150, 250, 250]
|
||||
|
||||
sv.box_iou(box_true=box_true, box_detection=box_detection)
|
||||
# 0.14285814285714285
|
||||
sv.box_iou(box_true, box_detection, overlap_metric=sv.OverlapMetric.IOU)
|
||||
# 0.14285714285714285
|
||||
|
||||
sv.box_iou(box_true, box_detection, overlap_metric=sv.OverlapMetric.IOS)
|
||||
# 0.25
|
||||
```
|
||||
""" # noqa: E501
|
||||
box_true = np.array(box_true)
|
||||
box_detection = np.array(box_detection)
|
||||
"""
|
||||
overlap_metric = OverlapMetric.from_value(overlap_metric)
|
||||
x_min_true, y_min_true, x_max_true, y_max_true = np.array(box_true)
|
||||
x_min_det, y_min_det, x_max_det, y_max_det = np.array(box_detection)
|
||||
|
||||
inter_x1 = max(box_true[0], box_detection[0])
|
||||
inter_y1 = max(box_true[1], box_detection[1])
|
||||
inter_x2 = min(box_true[2], box_detection[2])
|
||||
inter_y2 = min(box_true[3], box_detection[3])
|
||||
x_min_inter = max(x_min_true, x_min_det)
|
||||
y_min_inter = max(y_min_true, y_min_det)
|
||||
x_max_inter = min(x_max_true, x_max_det)
|
||||
y_max_inter = min(y_max_true, y_max_det)
|
||||
|
||||
inter_w = max(0, inter_x2 - inter_x1)
|
||||
inter_h = max(0, inter_y2 - inter_y1)
|
||||
inter_w = max(0.0, x_max_inter - x_min_inter)
|
||||
inter_h = max(0.0, y_max_inter - y_min_inter)
|
||||
|
||||
inter_area = inter_w * inter_h
|
||||
area_inter = inter_w * inter_h
|
||||
|
||||
area_true = (box_true[2] - box_true[0]) * (box_true[3] - box_true[1])
|
||||
area_detection = (box_detection[2] - box_detection[0]) * (
|
||||
box_detection[3] - box_detection[1]
|
||||
)
|
||||
area_true = (x_max_true - x_min_true) * (y_max_true - y_min_true)
|
||||
area_det = (x_max_det - x_min_det) * (y_max_det - y_min_det)
|
||||
|
||||
union_area = area_true + area_detection - inter_area
|
||||
if overlap_metric == OverlapMetric.IOU:
|
||||
area_norm = area_true + area_det - area_inter
|
||||
elif overlap_metric == OverlapMetric.IOS:
|
||||
area_norm = min(area_true, area_det)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"overlap_metric {overlap_metric} is not supported, "
|
||||
"only 'IOU' and 'IOS' are supported"
|
||||
)
|
||||
|
||||
return inter_area / union_area + 1e-6
|
||||
if area_norm <= 0.0:
|
||||
return 0.0
|
||||
|
||||
return float(area_inter / area_norm)
|
||||
|
||||
|
||||
def box_iou_batch(
|
||||
boxes_true: np.ndarray,
|
||||
boxes_detection: np.ndarray,
|
||||
overlap_metric: OverlapMetric = OverlapMetric.IOU,
|
||||
overlap_metric: OverlapMetric | str = 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.
|
||||
Compute pairwise overlap scores between batches of bounding boxes.
|
||||
|
||||
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.
|
||||
Supports standard IOU (intersection-over-union) and IOS
|
||||
(intersection-over-smaller-area) metrics for all `boxes_true` and
|
||||
`boxes_detection` pairs. Returns a matrix of overlap values in range
|
||||
`[0, 1]`, matching each box from the first batch to each from the second.
|
||||
|
||||
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).
|
||||
boxes_true (`numpy.array`): Array of reference boxes in
|
||||
shape `(N, 4)` as `(x_min, y_min, x_max, y_max)`.
|
||||
boxes_detection (`numpy.array`): Array of detected boxes in
|
||||
shape `(M, 4)` as `(x_min, y_min, x_max, y_max)`.
|
||||
overlap_metric (`OverlapMetric` or `str`): Overlap type.
|
||||
Use `OverlapMetric.IOU` for intersection-over-union,
|
||||
`OverlapMetric.IOS` for intersection-over-smaller-area.
|
||||
Defaults to `OverlapMetric.IOU`.
|
||||
|
||||
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.
|
||||
(`numpy.array`): Overlap matrix of shape `(N, M)`, where entry
|
||||
`[i, j]` is the overlap between `boxes_true[i]` and
|
||||
`boxes_detection[j]`.
|
||||
|
||||
Raises:
|
||||
ValueError: If `overlap_metric` is not IOU or IOS.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
|
|
@ -186,43 +202,48 @@ def box_iou_batch(
|
|||
[320, 320, 420, 420]
|
||||
])
|
||||
|
||||
sv.box_iou_batch(boxes_true=boxes_true, boxes_detection=boxes_detection)
|
||||
# array([
|
||||
# [0.14285714, 0. ],
|
||||
# [0. , 0.47058824]
|
||||
# ])
|
||||
sv.box_iou_batch(boxes_true, boxes_detection, overlap_metric=OverlapMetric.IOU)
|
||||
# array([[0.14285715, 0. ],
|
||||
# [0. , 0.47058824]])
|
||||
|
||||
sv.box_iou_batch(boxes_true, boxes_detection, overlap_metric=OverlapMetric.IOS)
|
||||
# array([[0.25, 0. ],
|
||||
# [0. , 0.64]])
|
||||
```
|
||||
|
||||
"""
|
||||
overlap_metric = OverlapMetric.from_value(overlap_metric)
|
||||
x_min_true, y_min_true, x_max_true, y_max_true = boxes_true.T
|
||||
x_min_det, y_min_det, x_max_det, y_max_det = boxes_detection.T
|
||||
count_true, count_det = boxes_true.shape[0], boxes_detection.shape[0]
|
||||
|
||||
tx1, ty1, tx2, ty2 = boxes_true.T
|
||||
dx1, dy1, dx2, dy2 = boxes_detection.T
|
||||
N, M = boxes_true.shape[0], boxes_detection.shape[0]
|
||||
if count_true == 0 or count_det == 0:
|
||||
return np.empty((count_true, count_det), dtype=np.float32)
|
||||
|
||||
top_left_x = np.empty((N, M), dtype=np.float32)
|
||||
bottom_right_x = np.empty_like(top_left_x)
|
||||
top_left_y = np.empty_like(top_left_x)
|
||||
bottom_right_y = np.empty_like(top_left_x)
|
||||
x_min_inter = np.empty((count_true, count_det), dtype=np.float32)
|
||||
x_max_inter = np.empty_like(x_min_inter)
|
||||
y_min_inter = np.empty_like(x_min_inter)
|
||||
y_max_inter = np.empty_like(x_min_inter)
|
||||
|
||||
np.maximum(tx1[:, None], dx1[None, :], out=top_left_x)
|
||||
np.minimum(tx2[:, None], dx2[None, :], out=bottom_right_x)
|
||||
np.maximum(ty1[:, None], dy1[None, :], out=top_left_y)
|
||||
np.minimum(ty2[:, None], dy2[None, :], out=bottom_right_y)
|
||||
np.maximum(x_min_true[:, None], x_min_det[None, :], out=x_min_inter)
|
||||
np.minimum(x_max_true[:, None], x_max_det[None, :], out=x_max_inter)
|
||||
np.maximum(y_min_true[:, None], y_min_det[None, :], out=y_min_inter)
|
||||
np.minimum(y_max_true[:, None], y_max_det[None, :], out=y_max_inter)
|
||||
|
||||
np.subtract(bottom_right_x, top_left_x, out=bottom_right_x) # W
|
||||
np.subtract(bottom_right_y, top_left_y, out=bottom_right_y) # H
|
||||
np.clip(bottom_right_x, 0.0, None, out=bottom_right_x)
|
||||
np.clip(bottom_right_y, 0.0, None, out=bottom_right_y)
|
||||
# we reuse x_max_inter and y_max_inter to store inter_w and inter_h
|
||||
np.subtract(x_max_inter, x_min_inter, out=x_max_inter) # inter_w
|
||||
np.subtract(y_max_inter, y_min_inter, out=y_max_inter) # inter_h
|
||||
np.clip(x_max_inter, 0.0, None, out=x_max_inter)
|
||||
np.clip(y_max_inter, 0.0, None, out=y_max_inter)
|
||||
|
||||
area_inter = bottom_right_x * bottom_right_y
|
||||
area_inter = x_max_inter * y_max_inter # inter_w * inter_h
|
||||
|
||||
area_true = (tx2 - tx1) * (ty2 - ty1)
|
||||
area_detection = (dx2 - dx1) * (dy2 - dy1)
|
||||
area_true = (x_max_true - x_min_true) * (y_max_true - y_min_true)
|
||||
area_det = (x_max_det - x_min_det) * (y_max_det - y_min_det)
|
||||
|
||||
if overlap_metric == OverlapMetric.IOU:
|
||||
denom = area_true[:, None] + area_detection[None, :] - area_inter
|
||||
area_norm = area_true[:, None] + area_det[None, :] - area_inter
|
||||
elif overlap_metric == OverlapMetric.IOS:
|
||||
denom = np.minimum(area_true[:, None], area_detection[None, :])
|
||||
area_norm = np.minimum(area_true[:, None], area_det[None, :])
|
||||
else:
|
||||
raise ValueError(
|
||||
f"overlap_metric {overlap_metric} is not supported, "
|
||||
|
|
@ -230,7 +251,7 @@ def box_iou_batch(
|
|||
)
|
||||
|
||||
out = np.zeros_like(area_inter, dtype=np.float32)
|
||||
np.divide(area_inter, denom, out=out, where=denom > 0)
|
||||
np.divide(area_inter, area_norm, out=out, where=area_norm > 0)
|
||||
return out
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ from supervision.detection.utils.iou_and_nms import (
|
|||
box_non_max_suppression,
|
||||
mask_non_max_merge,
|
||||
mask_non_max_suppression,
|
||||
OverlapMetric
|
||||
)
|
||||
from test.test_utils import mock_boxes
|
||||
from test.test_utils import random_boxes
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -637,102 +638,502 @@ def test_mask_non_max_merge(
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"boxes_true, boxes_detection, expected_iou, exception",
|
||||
"box_true, box_detection, overlap_metric, expected_overlap, exception",
|
||||
[
|
||||
(
|
||||
[100.0, 100.0, 200.0, 200.0],
|
||||
[150.0, 150.0, 250.0, 250.0],
|
||||
OverlapMetric.IOU,
|
||||
0.14285714285714285,
|
||||
DoesNotRaise(),
|
||||
), # partial overlap, IOU
|
||||
(
|
||||
[100.0, 100.0, 200.0, 200.0],
|
||||
[150.0, 150.0, 250.0, 250.0],
|
||||
OverlapMetric.IOS,
|
||||
0.25,
|
||||
DoesNotRaise(),
|
||||
), # partial overlap, IOS
|
||||
|
||||
(
|
||||
np.array([0.0, 0.0, 10.0, 10.0], dtype=np.float32),
|
||||
np.array([0.0, 0.0, 10.0, 10.0], dtype=np.float32),
|
||||
OverlapMetric.IOU,
|
||||
1.0,
|
||||
DoesNotRaise(),
|
||||
), # identical boxes, both boxes are arrays, IOU
|
||||
(
|
||||
np.array([0.0, 0.0, 10.0, 10.0], dtype=np.float32),
|
||||
np.array([0.0, 0.0, 10.0, 10.0], dtype=np.float32),
|
||||
OverlapMetric.IOS,
|
||||
1.0,
|
||||
DoesNotRaise(),
|
||||
), # identical boxes, both boxes are arrays, IOS
|
||||
(
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
"iou",
|
||||
1.0,
|
||||
DoesNotRaise(),
|
||||
), # identical boxes, both boxes are arrays, IOU as lowercase string
|
||||
(
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
"ios",
|
||||
1.0,
|
||||
DoesNotRaise(),
|
||||
), # identical boxes, both boxes are arrays, IOS as lowercase string
|
||||
(
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
"IOU",
|
||||
1.0,
|
||||
DoesNotRaise(),
|
||||
), # identical boxes, both boxes are arrays, IOU as uppercase string
|
||||
(
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
"IOU",
|
||||
1.0,
|
||||
DoesNotRaise(),
|
||||
), # identical boxes, both boxes are arrays, IOS as uppercase string
|
||||
|
||||
(
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
[20.0, 20.0, 30.0, 30.0],
|
||||
OverlapMetric.IOU,
|
||||
0.0,
|
||||
DoesNotRaise(),
|
||||
), # no overlap, IOU
|
||||
(
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
[20.0, 20.0, 30.0, 30.0],
|
||||
OverlapMetric.IOS,
|
||||
0.0,
|
||||
DoesNotRaise(),
|
||||
), # no overlap, IOS
|
||||
|
||||
(
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
[10.0, 0.0, 20.0, 10.0],
|
||||
OverlapMetric.IOU,
|
||||
0.0,
|
||||
DoesNotRaise(),
|
||||
), # boxes touch at edge, zero intersection, IOU
|
||||
(
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
[10.0, 0.0, 20.0, 10.0],
|
||||
OverlapMetric.IOS,
|
||||
0.0,
|
||||
DoesNotRaise(),
|
||||
), # boxes touch at edge, zero intersection, IOU
|
||||
|
||||
(
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
[2.0, 2.0, 8.0, 8.0],
|
||||
OverlapMetric.IOU,
|
||||
0.36,
|
||||
DoesNotRaise(),
|
||||
), # one box inside another, IOU
|
||||
(
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
[2.0, 2.0, 8.0, 8.0],
|
||||
OverlapMetric.IOS,
|
||||
1.0,
|
||||
DoesNotRaise(),
|
||||
), # one box inside another, IOS
|
||||
|
||||
(
|
||||
[0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
OverlapMetric.IOU,
|
||||
0.0,
|
||||
DoesNotRaise(),
|
||||
), # degenerate true box with zero area, IOU
|
||||
(
|
||||
[0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
OverlapMetric.IOS,
|
||||
0.0,
|
||||
DoesNotRaise(),
|
||||
), # degenerate true box with zero area, IOS
|
||||
|
||||
(
|
||||
[0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 0.0],
|
||||
OverlapMetric.IOU,
|
||||
0.0,
|
||||
DoesNotRaise(),
|
||||
), # both boxes fully degenerate, IOU
|
||||
(
|
||||
[0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 0.0],
|
||||
OverlapMetric.IOS,
|
||||
0.0,
|
||||
DoesNotRaise(),
|
||||
), # both boxes fully degenerate, IOS
|
||||
|
||||
(
|
||||
[-5.0, 0.0, 5.0, 10.0],
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
OverlapMetric.IOU,
|
||||
1.0 / 3.0,
|
||||
DoesNotRaise(),
|
||||
), # negative x_min, overlapping boxes, IOU is 1/3
|
||||
(
|
||||
[-5.0, 0.0, 5.0, 10.0],
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
OverlapMetric.IOS,
|
||||
0.5,
|
||||
DoesNotRaise(),
|
||||
), # negative x_min, overlapping boxes, IOS is 0.5
|
||||
|
||||
(
|
||||
[0.0, 0.0, 1.0, 1.0],
|
||||
[0.5, 0.5, 1.5, 1.5],
|
||||
OverlapMetric.IOU,
|
||||
0.14285714285714285,
|
||||
DoesNotRaise(),
|
||||
), # partial overlap with fractional coordinates, IOU
|
||||
(
|
||||
[0.0, 0.0, 1.0, 1.0],
|
||||
[0.5, 0.5, 1.5, 1.5],
|
||||
OverlapMetric.IOS,
|
||||
0.25,
|
||||
DoesNotRaise(),
|
||||
), # partial overlap with fractional coordinates, IOS
|
||||
],
|
||||
)
|
||||
def test_box_iou(
|
||||
box_true: list[float] | np.ndarray,
|
||||
box_detection: list[float] | np.ndarray,
|
||||
overlap_metric: str | OverlapMetric,
|
||||
expected_overlap: float,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
with exception:
|
||||
result = box_iou(
|
||||
box_true=box_true,
|
||||
box_detection=box_detection,
|
||||
overlap_metric=overlap_metric,
|
||||
)
|
||||
assert result == pytest.approx(expected_overlap, rel=1e-6, abs=1e-12)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"boxes_true, boxes_detection, overlap_metric, expected_overlap, exception",
|
||||
[
|
||||
# both inputs empty
|
||||
(
|
||||
np.empty((0, 4), dtype=np.float32),
|
||||
np.empty((0, 4), dtype=np.float32),
|
||||
OverlapMetric.IOU,
|
||||
np.empty((0, 0), dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
), # empty
|
||||
),
|
||||
# one true box, no detections
|
||||
(
|
||||
np.array([[0, 0, 10, 10]], dtype=np.float32),
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
np.empty((0, 4), dtype=np.float32),
|
||||
OverlapMetric.IOU,
|
||||
np.empty((1, 0), dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
), # one true box, no detections
|
||||
),
|
||||
# no true boxes, one detection
|
||||
(
|
||||
np.empty((0, 4), dtype=np.float32),
|
||||
np.array([[0, 0, 10, 10]], dtype=np.float32),
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
OverlapMetric.IOU,
|
||||
np.empty((0, 1), dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
), # no true boxes, one detection
|
||||
),
|
||||
# 1x1 partial overlap, IOU
|
||||
(
|
||||
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
|
||||
np.array([[100.0, 100.0, 200.0, 200.0]], dtype=np.float32),
|
||||
np.array([[150.0, 150.0, 250.0, 250.0]], dtype=np.float32),
|
||||
OverlapMetric.IOU,
|
||||
np.array([[0.14285715]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 partial overlap, IOS
|
||||
(
|
||||
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]]),
|
||||
np.array([[100.0, 100.0, 200.0, 200.0]], dtype=np.float32),
|
||||
np.array([[150.0, 150.0, 250.0, 250.0]], dtype=np.float32),
|
||||
OverlapMetric.IOS,
|
||||
np.array([[0.25]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
), # two boxes, perfect matches
|
||||
),
|
||||
# 1x1 identical boxes, IOU as lowercase string
|
||||
(
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
"iou",
|
||||
np.array([[1.0]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 identical boxes, IOS as lowercase string
|
||||
(
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
"ios",
|
||||
np.array([[1.0]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 identical boxes, IOU as uppercase string
|
||||
(
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
"IOU",
|
||||
np.array([[1.0]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 identical boxes, IOS as uppercase string
|
||||
(
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
"IOS",
|
||||
np.array([[1.0]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 no overlap, IOU
|
||||
(
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
np.array([[20.0, 20.0, 30.0, 30.0]], dtype=np.float32),
|
||||
OverlapMetric.IOU,
|
||||
np.array([[0.0]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 no overlap, IOS
|
||||
(
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
np.array([[20.0, 20.0, 30.0, 30.0]], dtype=np.float32),
|
||||
OverlapMetric.IOS,
|
||||
np.array([[0.0]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 touching at edge, zero intersection, IOU
|
||||
(
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
np.array([[10.0, 0.0, 20.0, 10.0]], dtype=np.float32),
|
||||
OverlapMetric.IOU,
|
||||
np.array([[0.0]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 touching at edge, zero intersection, IOS
|
||||
(
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
np.array([[10.0, 0.0, 20.0, 10.0]], dtype=np.float32),
|
||||
OverlapMetric.IOS,
|
||||
np.array([[0.0]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 box inside another, IOU
|
||||
(
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
np.array([[2.0, 2.0, 8.0, 8.0]], dtype=np.float32),
|
||||
OverlapMetric.IOU,
|
||||
np.array([[0.36]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 box inside another, IOS
|
||||
(
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
np.array([[2.0, 2.0, 8.0, 8.0]], dtype=np.float32),
|
||||
OverlapMetric.IOS,
|
||||
np.array([[1.0]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 degenerate true box, IOU
|
||||
(
|
||||
np.array([[0.0, 0.0, 0.0, 0.0]], dtype=np.float32),
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
OverlapMetric.IOU,
|
||||
np.array([[0.0]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 degenerate true box, IOS
|
||||
(
|
||||
np.array([[0.0, 0.0, 0.0, 0.0]], dtype=np.float32),
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
OverlapMetric.IOS,
|
||||
np.array([[0.0]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 both boxes degenerate, IOU
|
||||
(
|
||||
np.array([[0.0, 0.0, 0.0, 0.0]], dtype=np.float32),
|
||||
np.array([[0.0, 0.0, 0.0, 0.0]], dtype=np.float32),
|
||||
OverlapMetric.IOU,
|
||||
np.array([[0.0]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 both boxes degenerate, IOS
|
||||
(
|
||||
np.array([[0.0, 0.0, 0.0, 0.0]], dtype=np.float32),
|
||||
np.array([[0.0, 0.0, 0.0, 0.0]], dtype=np.float32),
|
||||
OverlapMetric.IOS,
|
||||
np.array([[0.0]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 negative coordinate, partial overlap, IOU
|
||||
(
|
||||
np.array([[-5.0, 0.0, 5.0, 10.0]], dtype=np.float32),
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
OverlapMetric.IOU,
|
||||
np.array([[1.0 / 3.0]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 negative coordinate, partial overlap, IOS
|
||||
(
|
||||
np.array([[-5.0, 0.0, 5.0, 10.0]], dtype=np.float32),
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
OverlapMetric.IOS,
|
||||
np.array([[0.5]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 fractional coordinates, partial overlap, IOU
|
||||
(
|
||||
np.array([[0.0, 0.0, 1.0, 1.0]], dtype=np.float32),
|
||||
np.array([[0.5, 0.5, 1.5, 1.5]], dtype=np.float32),
|
||||
OverlapMetric.IOU,
|
||||
np.array([[0.14285715]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# 1x1 fractional coordinates, partial overlap, IOS
|
||||
(
|
||||
np.array([[0.0, 0.0, 1.0, 1.0]], dtype=np.float32),
|
||||
np.array([[0.5, 0.5, 1.5, 1.5]], dtype=np.float32),
|
||||
OverlapMetric.IOS,
|
||||
np.array([[0.25]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# true batch case, 2x2, IOU
|
||||
(
|
||||
np.array(
|
||||
[
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
[10.0, 10.0, 20.0, 20.0],
|
||||
],
|
||||
dtype=np.float32,
|
||||
),
|
||||
np.array(
|
||||
[
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
[5.0, 5.0, 15.0, 15.0],
|
||||
],
|
||||
dtype=np.float32,
|
||||
),
|
||||
OverlapMetric.IOU,
|
||||
np.array(
|
||||
[
|
||||
[1.0, 0.14285715],
|
||||
[0.0, 0.14285715],
|
||||
],
|
||||
dtype=np.float32,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# true batch case, 2x2, IOS
|
||||
(
|
||||
np.array(
|
||||
[
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
[10.0, 10.0, 20.0, 20.0],
|
||||
],
|
||||
dtype=np.float32,
|
||||
),
|
||||
np.array(
|
||||
[
|
||||
[0.0, 0.0, 10.0, 10.0],
|
||||
[5.0, 5.0, 15.0, 15.0],
|
||||
],
|
||||
dtype=np.float32,
|
||||
),
|
||||
OverlapMetric.IOS,
|
||||
np.array(
|
||||
[
|
||||
[1.0, 0.25],
|
||||
[0.0, 0.25],
|
||||
],
|
||||
dtype=np.float32,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# invalid overlap_metric
|
||||
(
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
np.array([[0.0, 0.0, 10.0, 10.0]], dtype=np.float32),
|
||||
"invalid",
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_box_iou_batch(
|
||||
boxes_true: np.ndarray,
|
||||
boxes_detection: np.ndarray,
|
||||
expected_iou: np.ndarray,
|
||||
overlap_metric: str | OverlapMetric,
|
||||
expected_overlap: np.ndarray | None,
|
||||
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)
|
||||
result = box_iou_batch(
|
||||
boxes_true=boxes_true,
|
||||
boxes_detection=boxes_detection,
|
||||
overlap_metric=overlap_metric,
|
||||
)
|
||||
|
||||
assert isinstance(result, np.ndarray)
|
||||
assert result.shape == expected_overlap.shape
|
||||
assert np.allclose(
|
||||
result,
|
||||
expected_overlap,
|
||||
rtol=1e-6,
|
||||
atol=1e-12,
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
@pytest.mark.parametrize(
|
||||
"num_true, num_det",
|
||||
[
|
||||
(5, 5),
|
||||
(5, 10),
|
||||
(10, 5),
|
||||
(10, 10),
|
||||
(20, 30),
|
||||
(30, 20),
|
||||
(50, 50),
|
||||
(100, 100),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"overlap_metric",
|
||||
[OverlapMetric.IOU, OverlapMetric.IOS],
|
||||
)
|
||||
def test_box_iou_batch_symmetric_large(
|
||||
num_true: int,
|
||||
num_det: int,
|
||||
overlap_metric: OverlapMetric,
|
||||
) -> None:
|
||||
boxes_true = random_boxes(num_true)
|
||||
boxes_det = random_boxes(num_det)
|
||||
|
||||
batch_result = box_iou_batch(boxes_true, boxes_detection)
|
||||
result_ab = box_iou_batch(
|
||||
boxes_true=boxes_true,
|
||||
boxes_detection=boxes_det,
|
||||
overlap_metric=overlap_metric,
|
||||
)
|
||||
result_ba = box_iou_batch(
|
||||
boxes_true=boxes_det,
|
||||
boxes_detection=boxes_true,
|
||||
overlap_metric=overlap_metric,
|
||||
)
|
||||
|
||||
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 symmetric
|
||||
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)
|
||||
assert result_ab.shape == (num_true, num_det)
|
||||
assert result_ba.shape == (num_det, num_true)
|
||||
assert np.allclose(
|
||||
result_ab,
|
||||
result_ba.T,
|
||||
rtol=1e-6,
|
||||
atol=1e-12,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -53,39 +53,48 @@ def mock_key_points(
|
|||
)
|
||||
|
||||
|
||||
def mock_boxes(
|
||||
n: int,
|
||||
resolution_wh: tuple[int, int] = (1920, 1080),
|
||||
min_size: int = 20,
|
||||
max_size: int = 200,
|
||||
def random_boxes(
|
||||
count: int,
|
||||
image_size: tuple[int, int] = (1920, 1080),
|
||||
min_box_size: int = 20,
|
||||
max_box_size: int = 200,
|
||||
seed: int | None = None,
|
||||
) -> list[list[float]]:
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Generate N valid bounding boxes of format [x_min, y_min, x_max, y_max].
|
||||
Generate random bounding boxes within given image dimensions and size constraints.
|
||||
|
||||
Creates `count` bounding boxes randomly positioned and sized, ensuring each
|
||||
stays within image bounds and has width and height in the specified range.
|
||||
|
||||
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.
|
||||
count (`int`): Number of random bounding boxes to generate.
|
||||
image_size (`tuple[int, int]`): Image size as `(width, height)`. Defaults to `(1920, 1080)`.
|
||||
min_box_size (`int`): Minimum side length (pixels) for generated boxes. Defaults to `20`.
|
||||
max_box_size (`int`): Maximum side length (pixels) for generated boxes. Defaults to `200`.
|
||||
seed (`int` or `None`): Optional random seed for reproducibility. Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
List of boxes, each as [x_min, y_min, x_max, y_max].
|
||||
(`numpy.ndarray`): Array of shape `(count, 4)` with bounding boxes 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
|
||||
|
||||
img_w, img_h = image_size
|
||||
out = np.zeros((count, 4), dtype=np.float32)
|
||||
|
||||
for i in range(count):
|
||||
w = random.uniform(min_box_size, max_box_size)
|
||||
h = random.uniform(min_box_size, max_box_size)
|
||||
|
||||
x_min = random.uniform(0, img_w - w)
|
||||
y_min = random.uniform(0, img_h - h)
|
||||
x_max = x_min + w
|
||||
y_max = y_min + h
|
||||
|
||||
out[i] = (x_min, y_min, x_max, y_max)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def assert_almost_equal(actual, expected, tolerance=1e-5):
|
||||
|
|
|
|||
Loading…
Reference in New Issue