Files
wehub-resource-sync 9194ef5abd
Docs/Test Workflow / Test docs build (push) Failing after 0s
Check links & references / links-check (push) Failing after 1s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.10) (push) Failing after 0s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.11) (push) Failing after 0s
PR Conflict Labeler / main (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.12) (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.13) (push) Failing after 0s
Pytest/Test Workflow / Build this Package (push) Failing after 5s
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / testing-guardian (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:06:10 +08:00

499 lines
19 KiB
Python

"""Correctness and integration tests for CompactMask IoU and NMS.
These tests verify that:
- compact_mask_iou_batch gives numerically identical results to the
dense mask_iou_batch (raster IoU) for all overlap patterns.
- mask_iou_batch dispatches correctly when given CompactMask inputs.
- mask_non_max_suppression and mask_non_max_merge work with CompactMask
and produce the same keep-set as when given equivalent dense arrays.
"""
import numpy as np
import pytest
from supervision.detection.compact_mask import CompactMask
from supervision.detection.utils.iou_and_nms import (
OverlapMetric,
compact_mask_iou_batch,
mask_iou_batch,
mask_non_max_merge,
mask_non_max_suppression,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _cm_from_masks(masks: np.ndarray, image_shape: tuple[int, int]) -> CompactMask:
"""Build a CompactMask using full-image bounding boxes (lossless)."""
num_masks = len(masks)
img_h, img_w = image_shape
xyxy = np.tile(
np.array([0, 0, img_w - 1, img_h - 1], dtype=np.float32), (num_masks, 1)
)
return CompactMask.from_dense(masks, xyxy, image_shape=image_shape)
def _cm_tight(masks: np.ndarray, image_shape: tuple[int, int]) -> CompactMask:
"""Build a CompactMask using tight per-mask bounding boxes."""
from supervision.detection.utils.converters import mask_to_xyxy
xyxy = mask_to_xyxy(masks).astype(np.float32)
return CompactMask.from_dense(masks, xyxy, image_shape=image_shape)
def _dense_iou(
masks_a: np.ndarray,
masks_b: np.ndarray,
metric: OverlapMetric = OverlapMetric.IOU,
) -> np.ndarray:
"""Reference pairwise IoU using the existing dense implementation."""
return mask_iou_batch(masks_a, masks_b, overlap_metric=metric)
class TestCompactMaskIouBatch:
"""Verify that compact_mask_iou_batch matches dense raster IoU exactly.
Every test builds a pair of CompactMask collections from known boolean
arrays, runs compact_mask_iou_batch, and compares the result to the dense
reference computed by mask_iou_batch on the raw numpy arrays.
"""
def test_no_overlap_gives_zero(self) -> None:
"""Non-overlapping masks should always produce IoU = 0."""
img_h, img_w = 20, 20
masks_a = np.zeros((1, img_h, img_w), dtype=bool)
masks_a[0, 0:5, 0:5] = True # top-left
masks_b = np.zeros((1, img_h, img_w), dtype=bool)
masks_b[0, 10:15, 10:15] = True # bottom-right
cm_a = _cm_from_masks(masks_a, (img_h, img_w))
cm_b = _cm_from_masks(masks_b, (img_h, img_w))
result = compact_mask_iou_batch(cm_a, cm_b)
assert result.shape == (1, 1)
assert result[0, 0] == pytest.approx(0.0)
def test_identical_masks_give_one(self) -> None:
"""IoU of a mask with itself must be 1.0."""
img_h, img_w = 20, 20
masks = np.zeros((2, img_h, img_w), dtype=bool)
masks[0, 2:8, 2:8] = True
masks[1, 10:18, 10:18] = True
cm = _cm_from_masks(masks, (img_h, img_w))
result = compact_mask_iou_batch(cm, cm)
assert result.shape == (2, 2)
np.testing.assert_allclose(np.diag(result), [1.0, 1.0], atol=1e-9)
def test_matches_dense_random(self) -> None:
"""compact_mask_iou_batch must be numerically identical to dense IoU."""
rng = np.random.default_rng(0)
img_h, img_w = 30, 30
masks_a = rng.integers(0, 2, size=(5, img_h, img_w)).astype(bool)
masks_b = rng.integers(0, 2, size=(4, img_h, img_w)).astype(bool)
cm_a = _cm_from_masks(masks_a, (img_h, img_w))
cm_b = _cm_from_masks(masks_b, (img_h, img_w))
compact_result = compact_mask_iou_batch(cm_a, cm_b)
dense_result = _dense_iou(masks_a, masks_b)
assert compact_result.shape == (5, 4)
np.testing.assert_allclose(compact_result, dense_result, atol=1e-9)
def test_matches_dense_with_tight_bboxes(self) -> None:
"""Using tight bounding boxes (mask_to_xyxy) must still be accurate."""
rng = np.random.default_rng(1)
img_h, img_w = 40, 40
masks_a = rng.integers(0, 2, size=(4, img_h, img_w)).astype(bool)
masks_b = rng.integers(0, 2, size=(3, img_h, img_w)).astype(bool)
cm_a = _cm_tight(masks_a, (img_h, img_w))
cm_b = _cm_tight(masks_b, (img_h, img_w))
compact_result = compact_mask_iou_batch(cm_a, cm_b)
dense_result = _dense_iou(masks_a, masks_b)
np.testing.assert_allclose(compact_result, dense_result, atol=1e-9)
def test_partial_overlap(self) -> None:
"""Partially overlapping masks: IoU should match the analytic value."""
img_h, img_w = 10, 10
# Mask A: columns 0-4 (5 wide), Mask B: columns 3-7 (5 wide).
# Overlap: columns 3-4 (2 wide) x full height (10 rows) = 20 px.
masks_a = np.zeros((1, img_h, img_w), dtype=bool)
masks_a[0, :, 0:5] = True # area = 50
masks_b = np.zeros((1, img_h, img_w), dtype=bool)
masks_b[0, :, 3:8] = True # area = 50
cm_a = _cm_from_masks(masks_a, (img_h, img_w))
cm_b = _cm_from_masks(masks_b, (img_h, img_w))
result = compact_mask_iou_batch(cm_a, cm_b)
# inter=20, union=50+50-20=80 → IoU=0.25
assert result[0, 0] == pytest.approx(0.25, abs=1e-9)
np.testing.assert_allclose(result, _dense_iou(masks_a, masks_b), atol=1e-9)
def test_ios_metric(self) -> None:
"""IOS = intersection / min(area_a, area_b) must match dense reference."""
rng = np.random.default_rng(2)
img_h, img_w = 25, 25
masks_a = rng.integers(0, 2, size=(3, img_h, img_w)).astype(bool)
masks_b = rng.integers(0, 2, size=(3, img_h, img_w)).astype(bool)
cm_a = _cm_from_masks(masks_a, (img_h, img_w))
cm_b = _cm_from_masks(masks_b, (img_h, img_w))
compact_result = compact_mask_iou_batch(cm_a, cm_b, OverlapMetric.IOS)
dense_result = _dense_iou(masks_a, masks_b, OverlapMetric.IOS)
np.testing.assert_allclose(compact_result, dense_result, atol=1e-9)
def test_all_false_masks(self) -> None:
"""Zero-area masks should produce IoU = 0, not NaN."""
img_h, img_w = 10, 10
masks_a = np.zeros((2, img_h, img_w), dtype=bool)
masks_b = np.zeros((2, img_h, img_w), dtype=bool)
cm_a = _cm_from_masks(masks_a, (img_h, img_w))
cm_b = _cm_from_masks(masks_b, (img_h, img_w))
result = compact_mask_iou_batch(cm_a, cm_b)
assert not np.any(np.isnan(result))
np.testing.assert_array_equal(result, 0.0)
def test_empty_inputs(self) -> None:
"""Empty CompactMask collections should return a zero-shaped matrix."""
img_h, img_w = 10, 10
empty = CompactMask(
[],
np.empty((0, 2), dtype=np.int32),
np.empty((0, 2), dtype=np.int32),
(img_h, img_w),
)
masks = np.zeros((3, img_h, img_w), dtype=bool)
cm = _cm_from_masks(masks, (img_h, img_w))
result_a = compact_mask_iou_batch(empty, cm)
assert result_a.shape == (0, 3)
result_b = compact_mask_iou_batch(cm, empty)
assert result_b.shape == (3, 0)
def test_n_by_n_pairwise(self) -> None:
"""N x N pairwise IoU: diagonal must be 1.0 for non-zero-area masks."""
img_h, img_w = 50, 50
rng = np.random.default_rng(3)
masks = rng.integers(0, 2, size=(8, img_h, img_w)).astype(bool)
# Ensure no all-false mask (diagonal would be undefined).
for mask_idx in range(8):
masks[mask_idx, mask_idx * 5, mask_idx * 5] = True
cm = _cm_from_masks(masks, (img_h, img_w))
result = compact_mask_iou_batch(cm, cm)
assert result.shape == (8, 8)
np.testing.assert_allclose(np.diag(result), 1.0, atol=1e-9)
np.testing.assert_allclose(result, _dense_iou(masks, masks), atol=1e-9)
class TestMaskIouBatchDispatch:
"""Verify mask_iou_batch dispatches correctly for CompactMask inputs.
When both arguments are CompactMask, the function must route to the
efficient RLE implementation and produce identical results to the dense
path. When one argument is dense and the other is CompactMask, the
CompactMask must be materialised transparently before computation.
"""
def test_both_compact_dispatches_to_rle(self) -> None:
img_h, img_w = 20, 20
rng = np.random.default_rng(10)
masks_a = rng.integers(0, 2, size=(3, img_h, img_w)).astype(bool)
masks_b = rng.integers(0, 2, size=(2, img_h, img_w)).astype(bool)
cm_a = _cm_from_masks(masks_a, (img_h, img_w))
cm_b = _cm_from_masks(masks_b, (img_h, img_w))
result_compact = mask_iou_batch(cm_a, cm_b)
result_dense = mask_iou_batch(masks_a, masks_b)
np.testing.assert_allclose(result_compact, result_dense, atol=1e-9)
def test_mixed_compact_and_dense(self) -> None:
"""One CompactMask + one dense array must still work correctly."""
img_h, img_w = 20, 20
rng = np.random.default_rng(11)
masks_a = rng.integers(0, 2, size=(3, img_h, img_w)).astype(bool)
masks_b = rng.integers(0, 2, size=(2, img_h, img_w)).astype(bool)
cm_a = _cm_from_masks(masks_a, (img_h, img_w))
result = mask_iou_batch(cm_a, masks_b)
expected = mask_iou_batch(masks_a, masks_b)
np.testing.assert_allclose(result, expected, atol=1e-9)
class TestNmsWithCompactMask:
"""Verify mask NMS produces identical keep-sets for CompactMask and dense inputs.
Both paths now use exact full-resolution IoU — no resize approximation.
Tests use images larger than 640 px to ensure the old resize-to-640 path
would have introduced lossy approximation (catching the regression).
"""
def test_nms_compact_matches_dense(self) -> None:
"""NMS keep-set is identical for CompactMask and the equivalent dense array."""
# Use > 640 px so the old resize-to-640 path would have been lossy.
img_h, img_w = 720, 720
masks = np.zeros((3, img_h, img_w), dtype=bool)
masks[0, 0:360, 0:360] = True # top-left
masks[1, 0:324, 0:324] = True # heavily overlaps mask 0
masks[2, 360:720, 360:720] = True # bottom-right, no overlap
scores = np.array([0.9, 0.8, 0.7])
predictions = np.column_stack(
[np.zeros((3, 4)), scores] # dummy xyxy, real scores
)
cm = _cm_from_masks(masks, (img_h, img_w))
keep_dense = mask_non_max_suppression(predictions, masks, iou_threshold=0.3)
keep_compact = mask_non_max_suppression(predictions, cm, iou_threshold=0.3)
np.testing.assert_array_equal(keep_compact, keep_dense)
def test_nms_compact_matches_dense_borderline(self) -> None:
"""Borderline IoU pair (≈ threshold) must agree — catches the resize bug.
With resize-to-640, sub-pixel rounding on a pair whose true IoU is very
close to the threshold flips the keep/suppress decision. Both paths now
compute exact pixel-level IoU so results are identical.
"""
img_h, img_w = 1080, 1920
masks = np.zeros((2, img_h, img_w), dtype=bool)
# Mask 0: 200x200 square; mask 1: shifted 141 px → true IoU ≈ 0.50.
masks[0, 100:300, 100:300] = True
masks[1, 241:441, 241:441] = True
scores = np.array([0.9, 0.8])
predictions = np.column_stack([np.zeros((2, 4)), scores])
cm = _cm_from_masks(masks, (img_h, img_w))
keep_dense = mask_non_max_suppression(predictions, masks, iou_threshold=0.5)
keep_compact = mask_non_max_suppression(predictions, cm, iou_threshold=0.5)
np.testing.assert_array_equal(keep_compact, keep_dense)
def test_nms_compact_no_suppression(self) -> None:
"""Non-overlapping masks: all should be kept."""
img_h, img_w = 20, 20
masks = np.zeros((3, img_h, img_w), dtype=bool)
masks[0, 0:5, 0:5] = True
masks[1, 7:12, 7:12] = True
masks[2, 14:19, 14:19] = True
scores = np.array([0.9, 0.8, 0.7])
predictions = np.column_stack([np.zeros((3, 4)), scores])
cm = _cm_from_masks(masks, (img_h, img_w))
keep = mask_non_max_suppression(predictions, cm, iou_threshold=0.5)
assert keep.all(), "All non-overlapping masks should be kept"
def test_nms_compact_full_suppression(self) -> None:
"""Identical masks: only the highest-confidence one should survive."""
img_h, img_w = 20, 20
mask = np.zeros((1, img_h, img_w), dtype=bool)
mask[0, 5:15, 5:15] = True
masks = np.repeat(mask, 3, axis=0)
scores = np.array([0.9, 0.8, 0.7])
predictions = np.column_stack([np.zeros((3, 4)), scores])
cm = _cm_from_masks(masks, (img_h, img_w))
keep = mask_non_max_suppression(predictions, cm, iou_threshold=0.5)
assert keep.sum() == 1
assert keep[0], "Highest-confidence mask should survive"
class TestNmmWithCompactMask:
"""Verify mask_non_max_merge produces the same groups for CompactMask and dense.
NMM materialises CompactMask to a downscaled dense array internally, so
results must be numerically identical to the dense path.
"""
def test_nmm_compact_matches_dense(self) -> None:
"""Merge groups must match between CompactMask and dense inputs."""
img_h, img_w = 40, 40
masks = np.zeros((3, img_h, img_w), dtype=bool)
masks[0, 0:20, 0:20] = True # top-left
masks[1, 0:18, 0:18] = True # heavily overlaps mask 0
masks[2, 20:40, 20:40] = True # bottom-right, no overlap
scores = np.array([0.9, 0.8, 0.7])
predictions = np.column_stack([np.zeros((3, 4)), scores])
cm = _cm_from_masks(masks, (img_h, img_w))
groups_dense = mask_non_max_merge(predictions, masks, iou_threshold=0.3)
groups_compact = mask_non_max_merge(predictions, cm, iou_threshold=0.3)
def normalise(groups: list[list[int]]) -> list[list[int]]:
return sorted(sorted(group) for group in groups)
assert normalise(groups_compact) == normalise(groups_dense)
def test_nmm_no_merge(self) -> None:
"""Non-overlapping masks: every mask should be its own group."""
img_h, img_w = 20, 20
masks = np.zeros((3, img_h, img_w), dtype=bool)
masks[0, 0:5, 0:5] = True
masks[1, 7:12, 7:12] = True
masks[2, 14:19, 14:19] = True
scores = np.array([0.9, 0.8, 0.7])
predictions = np.column_stack([np.zeros((3, 4)), scores])
cm = _cm_from_masks(masks, (img_h, img_w))
groups = mask_non_max_merge(predictions, cm, iou_threshold=0.5)
assert len(groups) == 3, "Each non-overlapping mask gets its own group"
assert all(len(group) == 1 for group in groups)
def test_nmm_full_merge(self) -> None:
"""Identical masks: all predictions should merge into one group."""
img_h, img_w = 20, 20
single = np.zeros((1, img_h, img_w), dtype=bool)
single[0, 5:15, 5:15] = True
masks = np.repeat(single, 3, axis=0)
scores = np.array([0.9, 0.8, 0.7])
predictions = np.column_stack([np.zeros((3, 4)), scores])
cm = _cm_from_masks(masks, (img_h, img_w))
groups = mask_non_max_merge(predictions, cm, iou_threshold=0.5)
assert len(groups) == 1, "Identical masks must collapse to one group"
assert len(groups[0]) == 3
# ---------------------------------------------------------------------------
# Random scenario helpers
# ---------------------------------------------------------------------------
# Small (N, h, w) configs to keep IoU tests fast.
_IOU_RANDOM_CONFIGS = [
(5, 30, 30),
(8, 40, 40),
(10, 25, 25),
(6, 50, 50),
(12, 30, 40),
(5, 60, 60),
(15, 20, 20),
(7, 35, 35),
(10, 40, 50),
(8, 45, 45),
]
def _random_masks(
rng: np.random.Generator,
num_masks: int,
img_h: int,
img_w: int,
fill_prob: float = 0.25,
) -> np.ndarray:
"""Generate *num_masks* random boolean masks with at least one True pixel each."""
masks = np.zeros((num_masks, img_h, img_w), dtype=bool)
for mask_idx in range(num_masks):
y1 = rng.integers(0, img_h)
y2 = rng.integers(y1, img_h)
x1 = rng.integers(0, img_w)
x2 = rng.integers(x1, img_w)
region = rng.random((y2 - y1 + 1, x2 - x1 + 1)) < fill_prob
if not region.any():
region[0, 0] = True
masks[mask_idx, y1 : y2 + 1, x1 : x2 + 1] = region
return masks
class TestCompactMaskIouRandom:
"""compact_mask_iou_batch matches dense mask_iou_batch across 10 random seeds.
Uses small mask counts (5-15) and image sizes (20x20 to 60x60) to keep
individual test runs under 1 second.
"""
@pytest.mark.parametrize("seed", list(range(10)))
def test_parity_seed(self, seed: int) -> None:
rng = np.random.default_rng(seed)
num_masks_a, img_h, img_w = _IOU_RANDOM_CONFIGS[seed]
num_masks_b = max(3, num_masks_a - 2)
masks_a = _random_masks(rng, num_masks_a, img_h, img_w)
masks_b = _random_masks(rng, num_masks_b, img_h, img_w)
cm_a = _cm_from_masks(masks_a, (img_h, img_w))
cm_b = _cm_from_masks(masks_b, (img_h, img_w))
compact_result = compact_mask_iou_batch(cm_a, cm_b)
dense_result = _dense_iou(masks_a, masks_b)
assert compact_result.shape == (num_masks_a, num_masks_b), (
f"Shape mismatch: {compact_result.shape} vs ({num_masks_a}, {num_masks_b})"
)
np.testing.assert_allclose(
compact_result,
dense_result,
atol=1e-9,
err_msg=f"IoU mismatch: seed={seed}, N_a={num_masks_a}, N_b={num_masks_b}",
)
@pytest.mark.parametrize("seed", list(range(10)))
def test_self_iou_diagonal(self, seed: int) -> None:
"""Self-IoU diagonal must be 1.0 for masks with at least one True pixel."""
rng = np.random.default_rng(seed + 50)
num_masks, img_h, img_w = _IOU_RANDOM_CONFIGS[seed]
masks = _random_masks(rng, num_masks, img_h, img_w)
cm = _cm_from_masks(masks, (img_h, img_w))
result = compact_mask_iou_batch(cm, cm)
np.testing.assert_allclose(
np.diag(result),
1.0,
atol=1e-9,
err_msg=f"Diagonal not 1.0 for seed={seed}",
)
@pytest.mark.parametrize("seed", list(range(10)))
def test_tight_bbox_parity(self, seed: int) -> None:
"""Tight bounding boxes (mask_to_xyxy) must still produce identical IoU."""
from supervision.detection.utils.converters import mask_to_xyxy
rng = np.random.default_rng(seed + 200)
num_masks, img_h, img_w = _IOU_RANDOM_CONFIGS[seed]
num_masks_b = max(3, num_masks - 2)
masks_a = _random_masks(rng, num_masks, img_h, img_w)
masks_b = _random_masks(rng, num_masks_b, img_h, img_w)
xyxy_a = mask_to_xyxy(masks_a).astype(np.float32)
xyxy_b = mask_to_xyxy(masks_b).astype(np.float32)
cm_a = CompactMask.from_dense(masks_a, xyxy_a, image_shape=(img_h, img_w))
cm_b = CompactMask.from_dense(masks_b, xyxy_b, image_shape=(img_h, img_w))
compact_result = compact_mask_iou_batch(cm_a, cm_b)
dense_result = _dense_iou(masks_a, masks_b)
np.testing.assert_allclose(
compact_result,
dense_result,
atol=1e-9,
err_msg=f"Tight bbox IoU mismatch for seed={seed}",
)