Files
roboflow--supervision/tests/detection/test_compact_mask_integration.py
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

277 lines
9.9 KiB
Python

"""Integration tests: CompactMask <-> Detections, annotators, merge."""
from contextlib import ExitStack as DoesNotRaise
import numpy as np
import pytest
import supervision as sv
from supervision.detection.compact_mask import CompactMask
from supervision.detection.core import Detections
def _full_xyxy(n: int, h: int, w: int) -> np.ndarray:
"""N boxes covering the whole image (ensures crop == full mask)."""
return np.tile(np.array([0, 0, w, h], dtype=np.float32), (n, 1))
def _make_compact_detections(
n: int, h: int = 40, w: int = 40
) -> tuple[Detections, np.ndarray]:
"""Detections with a CompactMask backed by full-image bounding boxes.
Using full-image xyxy means all True pixels are within the crop region,
so from_dense -> to_dense is lossless.
"""
rng = np.random.default_rng(42)
masks = rng.integers(0, 2, size=(n, h, w)).astype(bool)
xyxy = _full_xyxy(n, h, w)
cm = CompactMask.from_dense(masks, xyxy, image_shape=(h, w))
det = Detections(
xyxy=xyxy,
mask=cm,
confidence=np.ones(n, dtype=np.float32) * 0.9,
class_id=np.arange(n),
)
return det, masks
class TestConstruction:
"""Tests for building Detections with a CompactMask.
Verifies that a CompactMask is accepted as a valid mask argument and that
the validator raises ValueError when the mask length does not match the
number of bounding boxes.
"""
def test_detections_construction_with_compact_mask(self) -> None:
with DoesNotRaise():
det, _ = _make_compact_detections(3)
assert isinstance(det.mask, CompactMask)
assert len(det) == 3
def test_detections_compact_mask_validation_mismatch(self) -> None:
n, h, w = 3, 20, 20
xyxy = _full_xyxy(n, h, w)
masks_wrong_n = np.zeros((n + 1, h, w), dtype=bool)
cm = CompactMask.from_dense(masks_wrong_n, _full_xyxy(n + 1, h, w), (h, w))
with pytest.raises(ValueError, match="mask must contain"):
Detections(xyxy=xyxy, mask=cm)
class TestFiltering:
"""Tests for Detections.__getitem__ with a CompactMask.
Verifies that integer, slice, and boolean-array indexing all preserve the
CompactMask type and return the correct subset of masks.
"""
def test_int_wraps_to_compact_mask(self) -> None:
det, _ = _make_compact_detections(3)
# Detections converts int to [int] internally -> subset has 1 element
subset = det[1]
assert isinstance(subset.mask, CompactMask)
assert len(subset) == 1
def test_slice_preserves_compact_mask(self) -> None:
det, masks = _make_compact_detections(4)
subset = det[1:3]
assert isinstance(subset.mask, CompactMask)
assert len(subset) == 2
np.testing.assert_array_equal(subset.mask.to_dense(), masks[1:3])
def test_bool_array_preserves_compact_mask(self) -> None:
det, masks = _make_compact_detections(4)
selector = np.array([True, False, True, False])
subset = det[selector]
assert isinstance(subset.mask, CompactMask)
assert len(subset) == 2
np.testing.assert_array_equal(subset.mask.to_dense(), masks[[0, 2]])
class TestIteration:
"""Tests for iterating over Detections with a CompactMask.
Verifies that each iteration step yields a 2-D boolean (H, W) array
identical to the corresponding dense mask, so downstream code that
iterates over detections needs no changes.
"""
def test_iter_yields_2d_dense(self) -> None:
h, w = 20, 20
det, masks = _make_compact_detections(3, h, w)
for i, (_, mask_2d, *_) in enumerate(det):
assert mask_2d is not None
assert isinstance(mask_2d, np.ndarray)
assert mask_2d.shape == (h, w)
assert mask_2d.dtype == bool
np.testing.assert_array_equal(mask_2d, masks[i])
class TestEquality:
"""Tests for Detections.__eq__ mixing CompactMask and dense arrays.
Verifies that a Detections object backed by a CompactMask compares equal
to an otherwise identical Detections object backed by a dense ndarray.
"""
def test_compact_vs_dense(self) -> None:
h, w = 20, 20
det_compact, masks = _make_compact_detections(2, h, w)
xyxy = det_compact.xyxy.copy()
det_dense = Detections(
xyxy=xyxy,
mask=masks,
confidence=np.ones(2, dtype=np.float32) * 0.9,
class_id=np.arange(2),
)
assert det_compact == det_dense
class TestArea:
"""Tests for the Detections.area property with a CompactMask.
Verifies that the fast CompactMask path in Detections.area returns the
same per-detection pixel counts as summing the equivalent dense array.
"""
def test_compact_matches_dense(self) -> None:
det_compact, masks = _make_compact_detections(3)
expected_area = np.array([m.sum() for m in masks])
np.testing.assert_array_equal(det_compact.area, expected_area)
class TestMerge:
"""Tests for merging Detections objects that contain CompactMask instances.
Covers three scenarios:
- All-compact merge: result is a CompactMask.
- Mixed compact + dense: dense inputs are converted; result is a CompactMask.
- Inner pair merge (merge_inner_detection_object_pair): used during NMS-like
operations, each input must contain exactly one detection.
"""
def test_all_compact(self) -> None:
h, w = 30, 30
det1, masks1 = _make_compact_detections(2, h, w)
rng = np.random.default_rng(7)
masks2 = rng.integers(0, 2, size=(3, h, w)).astype(bool)
xyxy2 = _full_xyxy(3, h, w)
cm2 = CompactMask.from_dense(masks2, xyxy2, (h, w))
det2 = Detections(
xyxy=xyxy2,
mask=cm2,
confidence=np.ones(3, dtype=np.float32) * 0.8,
class_id=np.arange(3),
)
merged = Detections.merge([det1, det2])
assert isinstance(merged.mask, CompactMask)
assert len(merged) == 5
expected = np.concatenate([masks1, masks2], axis=0)
np.testing.assert_array_equal(merged.mask.to_dense(), expected)
def test_mixed_compact_and_dense(self) -> None:
"""Merging a CompactMask with a dense ndarray returns a CompactMask."""
h, w = 20, 20
det_compact, masks_compact = _make_compact_detections(2, h, w)
masks_dense = np.zeros((1, h, w), dtype=bool)
masks_dense[0, 3:8, 3:8] = True
xyxy_dense = _full_xyxy(1, h, w)
det_dense = Detections(
xyxy=xyxy_dense,
mask=masks_dense,
confidence=np.array([0.5], dtype=np.float32),
class_id=np.array([0]),
)
merged = Detections.merge([det_compact, det_dense])
assert isinstance(merged.mask, CompactMask)
assert len(merged) == 3
expected = np.concatenate([masks_compact, masks_dense], axis=0)
np.testing.assert_array_equal(merged.mask.to_dense(), expected)
assert merged.mask.image_shape == (h, w)
def test_inner_pair_with_compact(self) -> None:
from supervision.detection.core import merge_inner_detection_object_pair
h, w = 20, 20
masks_a = np.zeros((1, h, w), dtype=bool)
masks_a[0, 0:5, 0:5] = True
xyxy_a = _full_xyxy(1, h, w)
cm_a = CompactMask.from_dense(masks_a, xyxy_a, (h, w))
det_a = Detections(
xyxy=xyxy_a,
mask=cm_a,
confidence=np.array([0.9], dtype=np.float32),
class_id=np.array([1]),
)
masks_b = np.zeros((1, h, w), dtype=bool)
masks_b[0, 5:10, 5:10] = True
xyxy_b = _full_xyxy(1, h, w)
cm_b = CompactMask.from_dense(masks_b, xyxy_b, (h, w))
det_b = Detections(
xyxy=xyxy_b,
mask=cm_b,
confidence=np.array([0.7], dtype=np.float32),
class_id=np.array([1]),
)
with DoesNotRaise():
result = merge_inner_detection_object_pair(det_a, det_b)
assert len(result) == 1
class TestAnnotators:
"""Tests for annotators that consume CompactMask via Detections.
Verifies that MaskAnnotator and PolygonAnnotator produce pixel-identical
output when given Detections backed by a CompactMask versus the equivalent
dense ndarray, confirming that the annotators are transparent to the mask
representation.
"""
def test_mask_annotator(self) -> None:
h, w = 40, 40
det_compact, masks = _make_compact_detections(2, h, w)
det_dense = Detections(
xyxy=det_compact.xyxy.copy(),
mask=masks,
confidence=det_compact.confidence.copy(),
class_id=det_compact.class_id.copy(),
)
image = np.zeros((h, w, 3), dtype=np.uint8)
annotator = sv.MaskAnnotator(color_lookup=sv.ColorLookup.INDEX)
annotated_compact = annotator.annotate(image.copy(), det_compact)
annotated_dense = annotator.annotate(image.copy(), det_dense)
np.testing.assert_array_equal(
annotated_compact,
annotated_dense,
err_msg="MaskAnnotator output differs between CompactMask and dense mask",
)
def test_polygon_annotator(self) -> None:
h, w = 40, 40
# Use solid rectangular masks for stable polygon results.
masks = np.zeros((2, h, w), dtype=bool)
masks[0, 5:15, 5:15] = True
masks[1, 20:30, 20:30] = True
xyxy = _full_xyxy(2, h, w)
cm = CompactMask.from_dense(masks, xyxy, (h, w))
det_compact = Detections(xyxy=xyxy, mask=cm, class_id=np.array([0, 1]))
det_dense = Detections(xyxy=xyxy, mask=masks, class_id=np.array([0, 1]))
image = np.zeros((h, w, 3), dtype=np.uint8)
annotator = sv.PolygonAnnotator(color_lookup=sv.ColorLookup.INDEX)
annotated_compact = annotator.annotate(image.copy(), det_compact)
annotated_dense = annotator.annotate(image.copy(), det_dense)
np.testing.assert_array_equal(annotated_compact, annotated_dense)