Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
---
description: Full version history of the supervision Python library — release notes, breaking changes, new features, and deprecations for every version.
date_modified: 2026-08-04
date_modified: 2026-08-11
---

# Changelog

### Unreleased <small>upcoming</small>

### Fixed

- `sv.box_iou` now calculates overlap in `float64`, preventing `int32` area overflow for large boxes. Its scalar result now matches `sv.box_iou_batch` for the same input.

### 0.30.0 <small>Aug 4, 2026</small>

!!! failure "Python 3.9 Support Terminated"
Expand Down
34 changes: 25 additions & 9 deletions src/supervision/detection/utils/iou_and_nms.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,23 @@ def _validate_iou_threshold(iou_threshold: float) -> None:
)


def _coordinate_difference(upper: np.number, lower: np.number) -> float:
"""Subtract box coordinates without overflowing integer NumPy scalars."""
return float(np.asarray(upper).item() - np.asarray(lower).item())


def box_iou(
box_true: list[float] | npt.NDArray[np.floating],
box_detection: list[float] | npt.NDArray[np.floating],
box_true: list[float] | npt.NDArray[np.number],
box_detection: list[float] | npt.NDArray[np.number],
overlap_metric: OverlapMetric | str = OverlapMetric.IOU,
) -> float:
"""
Compute overlap metric between two bounding boxes.

Supports standard IOU (intersection-over-union) and IOS
(intersection-over-smaller-area) metrics. Returns the overlap value in range
`[0, 1]`.
`[0, 1]`. Integer coordinates are subtracted before conversion to floating
point so large origins remain precise while area products avoid integer overflow.

Args:
box_true: Ground truth box in format
Expand All @@ -122,6 +128,7 @@ def box_iou(
Overlap value between boxes in `[0, 1]`.

Raises:
TypeError: If either box contains complex coordinates.
ValueError: If `overlap_metric` is not IOU or IOS.

Examples:
Expand All @@ -137,21 +144,30 @@ def box_iou(
```
"""
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)
box_true_array = np.asarray(box_true)
box_detection_array = np.asarray(box_detection)
if np.iscomplexobj(box_true_array) or np.iscomplexobj(box_detection_array):
raise TypeError("box coordinates must be real-valued")

x_min_true, y_min_true, x_max_true, y_max_true = box_true_array
x_min_det, y_min_det, x_max_det, y_max_det = box_detection_array

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.0, x_max_inter - x_min_inter)
inter_h = max(0.0, y_max_inter - y_min_inter)
inter_w = max(0.0, _coordinate_difference(x_max_inter, x_min_inter))
inter_h = max(0.0, _coordinate_difference(y_max_inter, y_min_inter))

area_inter = inter_w * inter_h

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)
area_true = _coordinate_difference(x_max_true, x_min_true) * _coordinate_difference(
y_max_true, y_min_true
)
area_det = _coordinate_difference(x_max_det, x_min_det) * _coordinate_difference(
y_max_det, y_min_det
)

if overlap_metric == OverlapMetric.IOU:
area_norm = area_true + area_det - area_inter
Expand Down
51 changes: 51 additions & 0 deletions tests/detection/utils/test_iou_and_nms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1605,6 +1605,57 @@ def test_box_iou_batch_int32_input_does_not_overflow() -> None:
assert result[0, 0] == pytest.approx(1.0 / 3.0, rel=1e-6)


@pytest.mark.parametrize(
("overlap_metric", "expected"),
[
pytest.param(OverlapMetric.IOU, 1.0 / 3.0, id="iou"),
pytest.param(OverlapMetric.IOS, 0.5, id="ios"),
],
)
def test_box_iou_int32_input_does_not_overflow(
overlap_metric: OverlapMetric, expected: float
) -> None:
"""Large int32 boxes preserve their analytic overlap scores."""
side, shift = 60000, 30000
box_a, box_b = _boundary_box_pair(0, side=side, shift=shift, dtype=np.int32)

result = box_iou(
box_true=box_a[0],
box_detection=box_b[0],
overlap_metric=overlap_metric,
)

assert result == pytest.approx(expected, rel=1e-6)


@pytest.mark.parametrize(
("overlap_metric", "expected"),
[
pytest.param(OverlapMetric.IOU, 1.0 / 3.0, id="iou"),
pytest.param(OverlapMetric.IOS, 0.5, id="ios"),
],
)
def test_box_iou_large_int64_origin_preserves_coordinate_differences(
overlap_metric: OverlapMetric, expected: float
) -> None:
"""Large int64 origins do not collapse distinct box endpoints."""
origin = 2**53
box_true = np.array([origin, 0, origin + 2, 2], dtype=np.int64)
box_detection = np.array([origin + 1, 0, origin + 3, 2], dtype=np.int64)

result = box_iou(box_true, box_detection, overlap_metric)

assert result == pytest.approx(expected, rel=1e-6)


def test_box_iou_rejects_complex_coordinates() -> None:
"""Complex coordinates are rejected instead of silently truncating values."""
box = np.array([0, 0, 2, 2], dtype=np.complex128)

with pytest.raises(TypeError, match="real-valued"):
box_iou(box, box)


@pytest.mark.parametrize(
"scale",
[
Expand Down
Loading