from contextlib import ExitStack as DoesNotRaise from typing import Any import cv2 import numpy as np import pytest from supervision.config import CLASS_NAME_DATA_FIELD from supervision.detection.compact_mask import CompactMask from supervision.detection.utils.internal import ( extract_ultralytics_masks, get_data_item, merge_data, merge_metadata, process_roboflow_result, ) class _FakeMasksData: """Ultralytics-like mask tensor exposing shape and cpu().numpy().""" def __init__(self, arr: np.ndarray) -> None: self._arr = np.asarray(arr, dtype=np.float32) self.shape = self._arr.shape def cpu(self) -> "_FakeMasksData": return self def numpy(self) -> np.ndarray: return self._arr class _FakeMasks: """Ultralytics-like masks container holding a data tensor.""" def __init__(self, data: _FakeMasksData) -> None: self.data = data def __bool__(self) -> bool: return True class _FakeYOLOMaskResults: """Minimal Ultralytics results exposing masks and orig_shape.""" def __init__(self, masks_arr: np.ndarray, orig_shape: tuple[int, int]) -> None: self.masks = _FakeMasks(_FakeMasksData(masks_arr)) self.orig_shape = orig_shape def test_extract_ultralytics_masks_thresholds_resized_proto_at_half() -> None: """Resized proto masks threshold at 0.5, so interpolated edges don't dilate.""" orig_shape = (4, 4) proto = np.array([[[1.0, 1.0], [0.0, 0.0]]], dtype=np.float32) results = _FakeYOLOMaskResults(masks_arr=proto, orig_shape=orig_shape) masks = extract_ultralytics_masks(results) resized = cv2.resize(proto[0], (orig_shape[1], orig_shape[0])) assert masks is not None assert masks.dtype == bool np.testing.assert_array_equal(masks[0], resized > 0.5) # A naive `> 0` cast would flag the interpolated boundary row as True. assert masks[0].sum() < int((resized > 0).sum()) def _pred( yx: tuple[float, float] = (1.5, 1.5), size: tuple[float, float] = (2.0, 2.0), confidence: float = 0.9, class_id: int = 0, class_name: str = "person", **extra: Any, ) -> dict[str, Any]: """Build a minimal Roboflow prediction dict; `extra` adds/overrides fields.""" pred: dict[str, Any] = { "x": yx[1], "y": yx[0], "width": size[0], "height": size[1], "confidence": confidence, "class_id": class_id, "class": class_name, } pred.update(extra) return pred def _result( *predictions: dict[str, Any], img_w: int = 4, img_h: int = 4, ) -> dict[str, Any]: """Wrap predictions in a Roboflow result envelope.""" return { "predictions": list(predictions), "image": {"width": img_w, "height": img_h}, } def _result_1k(*predictions: dict[str, Any]) -> dict[str, Any]: """Wrap predictions in a 1000x1000 Roboflow result envelope.""" return _result(*predictions, img_w=1000, img_h=1000) TEST_MASK = np.zeros((1, 1000, 1000), dtype=bool) TEST_MASK[:, 300:351, 200:251] = True TEST_RLE_MASK = np.zeros((1, 4, 4), dtype=bool) TEST_RLE_MASK[0, 1:3, 1:3] = True TEST_RLE_NONCONTIGUOUS_MASK = np.zeros((1, 4, 4), dtype=bool) TEST_RLE_NONCONTIGUOUS_MASK[0, 0:2, 0:2] = True TEST_RLE_NONCONTIGUOUS_MASK[0, 3, 2:4] = True @pytest.mark.parametrize( ("roboflow_result", "expected_result", "exception"), [ ( {"predictions": [], "image": {"width": 1000, "height": 1000}}, ( np.empty((0, 4)), np.empty(0), np.empty(0), None, None, {CLASS_NAME_DATA_FIELD: np.empty(0, dtype=str)}, ), DoesNotRaise(), ), # empty result ( _result_1k(_pred(yx=(300.0, 200.0), size=(50.0, 50.0))), ( np.array([[175.0, 275.0, 225.0, 325.0]]), np.array([0.9]), np.array([0]), None, None, {CLASS_NAME_DATA_FIELD: np.array(["person"])}, ), DoesNotRaise(), ), # single correct object detection result ( _result_1k( _pred(yx=(300.0, 200.0), size=(50.0, 50.0), tracker_id=1), _pred( yx=(500.0, 500.0), size=(100.0, 100.0), confidence=0.8, class_id=7, class_name="truck", tracker_id=2, ), ), ( np.array([[175.0, 275.0, 225.0, 325.0], [450.0, 450.0, 550.0, 550.0]]), np.array([0.9, 0.8]), np.array([0, 7]), None, np.array([1, 2]), {CLASS_NAME_DATA_FIELD: np.array(["person", "truck"])}, ), DoesNotRaise(), ), # two correct object detection result ( _result_1k( _pred( yx=(300.0, 200.0), size=(50.0, 50.0), points=[], tracker_id=None, ), ), ( np.array([[175.0, 275.0, 225.0, 325.0]]), np.array([0.9]), np.array([0]), None, None, {CLASS_NAME_DATA_FIELD: np.array(["person"])}, ), DoesNotRaise(), ), # single invalid polygon result with no points falls back to box-only ( _result_1k( _pred( yx=(300.0, 200.0), size=(50.0, 50.0), points=[{"x": 200.0, "y": 300.0}, {"x": 250.0, "y": 300.0}], ), ), ( np.array([[175.0, 275.0, 225.0, 325.0]]), np.array([0.9]), np.array([0]), None, None, {CLASS_NAME_DATA_FIELD: np.array(["person"])}, ), DoesNotRaise(), ), # single invalid polygon result with too few points falls back to box-only ( _result_1k( _pred( yx=(300.0, 200.0), size=(50.0, 50.0), points=[ {"x": 200.0, "y": 300.0}, {"x": 250.0, "y": 300.0}, {"x": 250.0, "y": 350.0}, {"x": 200.0, "y": 350.0}, ], ), ), ( np.array([[175.0, 275.0, 225.0, 325.0]]), np.array([0.9]), np.array([0]), TEST_MASK, None, {CLASS_NAME_DATA_FIELD: np.array(["person"])}, ), DoesNotRaise(), ), # single incorrect instance segmentation result with no enough points ( _result_1k( _pred( yx=(300.0, 200.0), size=(50.0, 50.0), points=[ {"x": 200.0, "y": 300.0}, {"x": 250.0, "y": 300.0}, {"x": 250.0, "y": 350.0}, {"x": 200.0, "y": 350.0}, ], ), _pred( yx=(500.0, 500.0), size=(100.0, 100.0), confidence=0.8, class_id=7, class_name="truck", points=[], ), ), ( np.array([[175.0, 275.0, 225.0, 325.0], [450.0, 450.0, 550.0, 550.0]]), np.array([0.9, 0.8]), np.array([0, 7]), None, None, {CLASS_NAME_DATA_FIELD: np.array(["person", "truck"])}, ), DoesNotRaise(), ), # mixed valid polygon and invalid polygon keeps boxes and drops masks ( _result(_pred(rle={"size": [4, 4], "counts": "52203"})), ( np.array([[0.5, 0.5, 2.5, 2.5]]), np.array([0.9]), np.array([0]), TEST_RLE_MASK, None, {CLASS_NAME_DATA_FIELD: np.array(["person"])}, ), DoesNotRaise(), ), # single RLE prediction with compressed string counts ( _result( _pred( yx=(2.0, 2.0), size=(4.0, 4.0), confidence=0.85, class_id=1, class_name="cat", rle={"size": [4, 4], "counts": "02203ON0"}, ) ), ( np.array([[0.0, 0.0, 4.0, 4.0]]), np.array([0.85]), np.array([1]), TEST_RLE_NONCONTIGUOUS_MASK, None, {CLASS_NAME_DATA_FIELD: np.array(["cat"])}, ), DoesNotRaise(), ), # single RLE prediction with non-contiguous mask ( _result(_pred(rle={"size": [4, 4], "counts": "52203"}, tracker_id=5)), ( np.array([[0.5, 0.5, 2.5, 2.5]]), np.array([0.9]), np.array([0]), TEST_RLE_MASK, np.array([5]), {CLASS_NAME_DATA_FIELD: np.array(["person"])}, ), DoesNotRaise(), ), # RLE prediction with tracker_id ( _result(_pred(rle_mask={"size": [4, 4], "counts": "52203"})), ( np.array([[0.5, 0.5, 2.5, 2.5]]), np.array([0.9]), np.array([0]), TEST_RLE_MASK, None, {CLASS_NAME_DATA_FIELD: np.array(["person"])}, ), DoesNotRaise(), ), # single RLE prediction with compressed string counts under rle_mask key ( _result(_pred(rle="bad_string")), ( np.array([[0.5, 0.5, 2.5, 2.5]]), np.array([0.9]), np.array([0]), None, None, {CLASS_NAME_DATA_FIELD: np.array(["person"])}, ), DoesNotRaise(), ), # malformed RLE payload should fall through to box-only detection ( _result(_pred(rle={"size": [4, 4]})), ( np.array([[0.5, 0.5, 2.5, 2.5]]), np.array([0.9]), np.array([0]), None, None, {CLASS_NAME_DATA_FIELD: np.array(["person"])}, ), DoesNotRaise(), ), # RLE dict missing counts falls through to box-only detection ( _result(_pred(rle={"size": [4, 4], "counts": "!"})), ( np.array([[0.5, 0.5, 2.5, 2.5]]), np.array([0.9]), np.array([0]), None, None, {CLASS_NAME_DATA_FIELD: np.array(["person"])}, ), DoesNotRaise(), ), # malformed compressed counts falls through to box-only detection ( _result( _pred(rle={"size": [4, 4], "counts": "52203"}), _pred(yx=(3.0, 3.0), confidence=0.8, class_id=1, class_name="car"), ), ( np.array([[0.5, 0.5, 2.5, 2.5], [2.0, 2.0, 4.0, 4.0]]), np.array([0.9, 0.8]), np.array([0, 1]), # Mixed-modality batch: only a subset carries masks. # All masks dropped to preserve xyxy alignment (mirrors # the tracker_id mixed-batch handling). None, None, {CLASS_NAME_DATA_FIELD: np.array(["person", "car"])}, ), DoesNotRaise(), ), # mixed RLE + box-only batch — masks dropped to preserve xyxy alignment pytest.param( _result( _pred(tracker_id=7), _pred(yx=(3.0, 3.0), confidence=0.8, class_id=1, class_name="car"), ), ( np.array([[0.5, 0.5, 2.5, 2.5], [2.0, 2.0, 4.0, 4.0]]), np.array([0.9, 0.8]), np.array([0, 1]), None, # tracker_id is None when only some detections carry one, rather # than an array misaligned with xyxy that would raise ValueError. None, {CLASS_NAME_DATA_FIELD: np.array(["person", "car"])}, ), DoesNotRaise(), id="mixed_tracker_id_batch_drops_to_none", ), pytest.param( _result( _pred(), _pred(yx=(3.0, 3.0), confidence=0.8, class_id=1, class_name="car"), ), ( np.array([[0.5, 0.5, 2.5, 2.5], [2.0, 2.0, 4.0, 4.0]]), np.array([0.9, 0.8]), np.array([0, 1]), None, # None not in tracker_ids prevents np.array([None, None], dtype=int64) None, {CLASS_NAME_DATA_FIELD: np.array(["person", "car"])}, ), DoesNotRaise(), id="all_absent_tracker_id_no_raise", ), ], ) def test_process_roboflow_result( roboflow_result: dict, expected_result: tuple[ np.ndarray, np.ndarray, np.ndarray, np.ndarray | None, np.ndarray ], exception: Exception, ) -> None: with exception: result = process_roboflow_result(roboflow_result=roboflow_result) assert np.array_equal(result[0], expected_result[0]) assert np.array_equal(result[1], expected_result[1]) assert np.array_equal(result[2], expected_result[2]) assert (result[3] is None and expected_result[3] is None) or ( np.array_equal(result[3], expected_result[3]) ) assert (result[4] is None and expected_result[4] is None) or ( np.array_equal(result[4], expected_result[4]) ) for key in result[5]: if isinstance(result[5][key], np.ndarray): assert np.array_equal(result[5][key], expected_result[5][key]), ( f"Mismatch in arrays for key {key}" ) assert result[5][key].dtype == expected_result[5][key].dtype, ( f"dtype mismatch for key {key}: " f"got {result[5][key].dtype}, " f"expected {expected_result[5][key].dtype}" ) else: assert result[5][key] == expected_result[5][key], ( f"Mismatch in non-array data for key {key}" ) def test_process_roboflow_result_compact_masks_returns_compact_mask() -> None: """compact_masks=True should return CompactMask for valid RLE predictions.""" roboflow_result = _result(_pred(rle={"size": [4, 4], "counts": "52203"})) result = process_roboflow_result( roboflow_result=roboflow_result, compact_masks=True ) assert isinstance(result[3], CompactMask) np.testing.assert_array_equal(result[3].to_dense(), TEST_RLE_MASK) def test_process_roboflow_result_compact_masks_matches_resized_dense_rle() -> None: """compact_masks=True should preserve current RLE resize behavior.""" roboflow_result = _result( _pred(yx=(2.0, 2.0), size=(4.0, 4.0), rle={"size": [2, 2], "counts": [0, 4]}) ) dense_result = process_roboflow_result(roboflow_result=roboflow_result) compact_result = process_roboflow_result( roboflow_result=roboflow_result, compact_masks=True ) assert isinstance(compact_result[3], CompactMask) np.testing.assert_array_equal(compact_result[3].to_dense(), dense_result[3]) def test_process_roboflow_result_compact_masks_invalid_rle_is_box_only() -> None: """compact_masks=True should keep malformed RLE fallback behavior.""" roboflow_result = _result(_pred(rle={"size": [4, 4], "counts": "!"})) result = process_roboflow_result( roboflow_result=roboflow_result, compact_masks=True ) assert result[3] is None np.testing.assert_array_equal(result[0], np.array([[0.5, 0.5, 2.5, 2.5]])) def test_process_roboflow_result_compact_masks_overflow_rle_is_box_only() -> None: """compact_masks=True should not leak OverflowError from invalid counts.""" roboflow_result = _result(_pred(rle={"size": [4, 4], "counts": [2**31]})) result = process_roboflow_result( roboflow_result=roboflow_result, compact_masks=True ) assert result[3] is None np.testing.assert_array_equal(result[0], np.array([[0.5, 0.5, 2.5, 2.5]])) def test_process_roboflow_result_compact_masks_partial_failure_drops_all_masks() -> ( None ): """One invalid RLE in a two-prediction batch drops all masks but keeps both xyxy.""" roboflow_result = _result( _pred(rle={"size": [4, 4], "counts": "52203"}), # valid: sum == 16 _pred(rle={"size": [4, 4], "counts": [1, 2, 3]}), # invalid: sum == 6, not 16 ) result = process_roboflow_result( roboflow_result=roboflow_result, compact_masks=True ) # Mixed-modality: one mask decoded, one failed → all masks dropped for alignment. assert result[3] is None # Both detections preserved in xyxy. assert result[0].shape == (2, 4) def test_process_roboflow_result_uses_rle_mask_when_rle_invalid() -> None: """Valid rle_mask should be used when rle is present but invalid.""" roboflow_result = _result( _pred(rle={"foo": "bar"}, rle_mask={"size": [4, 4], "counts": "52203"}) ) dense_result = process_roboflow_result(roboflow_result=roboflow_result) compact_result = process_roboflow_result( roboflow_result=roboflow_result, compact_masks=True ) assert isinstance(dense_result[3], np.ndarray) assert isinstance(compact_result[3], CompactMask) np.testing.assert_array_equal(compact_result[3].to_dense(), dense_result[3]) def test_process_roboflow_result_invalid_polygon_is_box_only( caplog: pytest.LogCaptureFixture, ) -> None: """Predictions with fewer than three polygon points are kept as box-only.""" roboflow_result = _result(_pred(points=[{"x": 1, "y": 1}, {"x": 2, "y": 2}])) with caplog.at_level("WARNING"): xyxy, confidence, class_id, masks, tracker_ids, data = process_roboflow_result( roboflow_result=roboflow_result ) np.testing.assert_array_equal(xyxy, np.array([[0.5, 0.5, 2.5, 2.5]])) np.testing.assert_array_equal(confidence, np.array([0.9])) np.testing.assert_array_equal(class_id, np.array([0])) assert masks is None assert tracker_ids is None np.testing.assert_array_equal(data[CLASS_NAME_DATA_FIELD], np.array(["person"])) assert "fewer than 3 points" in caplog.text compact_result = process_roboflow_result( roboflow_result=roboflow_result, compact_masks=True ) np.testing.assert_array_equal(compact_result[0], xyxy) assert compact_result[3] is None def test_polygon_prediction_compact_masks_true() -> None: """polygon prediction with compact_masks=True returns a CompactMask.""" roboflow_result = _result( _pred( yx=(2.5, 2.5), size=(4.0, 4.0), confidence=0.75, class_name="dog", points=[ {"x": 1, "y": 1}, {"x": 4, "y": 1}, {"x": 4, "y": 4}, {"x": 1, "y": 4}, ], ), img_w=6, img_h=6, ) _, _, _, masks, _, _ = process_roboflow_result(roboflow_result, compact_masks=True) assert isinstance(masks, CompactMask) assert len(masks) == 1 def test_box_only_compact_masks_true_returns_none_mask() -> None: """box-only predictions with compact_masks=True yield None mask.""" roboflow_result = _result( _pred(yx=(2.0, 2.0), size=(3.0, 3.0), class_name="cat"), img_w=5, img_h=5, ) _, _, _, masks, _, _ = process_roboflow_result(roboflow_result, compact_masks=True) assert masks is None def test_rle_size_mismatch_resizes_dense_mask() -> None: """Dense path resizes mask when RLE size differs from image dimensions.""" # counts=[0, 4]: 0 False runs then 4 True runs — all-True 2x2 mask. # Image is 4x4, so cv2.resize must expand the decoded 2x2 to 4x4. roboflow_result = _result( _pred( yx=(2.0, 2.0), size=(4.0, 4.0), class_name="cat", rle_mask={"size": [2, 2], "counts": [0, 4]}, ) ) _, _, _, masks, _, _ = process_roboflow_result(roboflow_result, compact_masks=False) assert masks is not None assert masks.shape == (1, 4, 4) assert masks[0].sum() > 0 @pytest.mark.parametrize( ("data_list", "expected_result", "exception"), [ ( [], {}, DoesNotRaise(), ), # empty data list ( [{}], {}, DoesNotRaise(), ), # single empty data dict ( [{}, {}], {}, DoesNotRaise(), ), # two empty data dicts ( [ {"test_1": []}, ], {"test_1": []}, DoesNotRaise(), ), # single data dict with a single field name and empty list values ( [ {"test_1": []}, {"test_1": []}, ], {"test_1": []}, DoesNotRaise(), ), # two data dicts with the same field name and empty list values ( [ {"test_1": np.array([])}, ], {"test_1": np.array([])}, DoesNotRaise(), ), # single data dict with a single field name and empty np.array values ( [ {"test_1": np.array([])}, {"test_1": np.array([])}, ], {"test_1": np.array([])}, DoesNotRaise(), ), # two data dicts with the same field name and empty np.array values ( [ {"test_1": [1, 2, 3]}, ], {"test_1": [1, 2, 3]}, DoesNotRaise(), ), # single data dict with a single field name and list values ( [ {"test_1": []}, {"test_1": [3, 2, 1]}, ], {"test_1": [3, 2, 1]}, DoesNotRaise(), ), # two data dicts with the same field name; one of with empty list as value ( [ {"test_1": [1, 2, 3]}, {"test_1": [3, 2, 1]}, ], {"test_1": [1, 2, 3, 3, 2, 1]}, DoesNotRaise(), ), # two data dicts with the same field name and list values ( [ {"test_1": [1, 2, 3]}, {"test_1": [3, 2, 1]}, {"test_1": [1, 2, 3]}, ], {"test_1": [1, 2, 3, 3, 2, 1, 1, 2, 3]}, DoesNotRaise(), ), # three data dicts with the same field name and list values ( [ {"test_1": [1, 2, 3]}, {"test_2": [3, 2, 1]}, ], None, pytest.raises(ValueError, match="same keys to merge"), ), # two data dicts with different field names ( [ {"test_1": np.array([1, 2, 3])}, {"test_1": np.array([3, 2, 1])}, ], {"test_1": np.array([1, 2, 3, 3, 2, 1])}, DoesNotRaise(), ), # two data dicts with the same field name and np.array values as 1D arrays ( [ {"test_1": np.array([[1, 2, 3]])}, {"test_1": np.array([[3, 2, 1]])}, ], {"test_1": np.array([[1, 2, 3], [3, 2, 1]])}, DoesNotRaise(), ), # two data dicts with the same field name and np.array values as 2D arrays ( [ {"test_1": np.array([1, 2, 3]), "test_2": np.array(["a", "b", "c"])}, {"test_1": np.array([3, 2, 1]), "test_2": np.array(["c", "b", "a"])}, ], { "test_1": np.array([1, 2, 3, 3, 2, 1]), "test_2": np.array(["a", "b", "c", "c", "b", "a"]), }, DoesNotRaise(), ), # two data dicts with the same field names and np.array values ( [ {"test_1": [1, 2, 3], "test_2": np.array(["a", "b", "c"])}, {"test_1": [3, 2, 1], "test_2": np.array(["c", "b", "a"])}, ], { "test_1": [1, 2, 3, 3, 2, 1], "test_2": np.array(["a", "b", "c", "c", "b", "a"]), }, DoesNotRaise(), ), # two data dicts with the same field names and mixed values ( [ {"test_1": np.array([1, 2, 3])}, {"test_1": np.array([[3, 2, 1]])}, ], None, pytest.raises(ValueError, match="same number of dimensions"), ), # two data dicts with the same field name and 1D and 2D arrays values ( [ {"test_1": np.array([1, 2, 3]), "test_2": np.array(["a", "b"])}, {"test_1": np.array([3, 2, 1]), "test_2": np.array(["c", "b", "a"])}, ], None, pytest.raises(ValueError, match="equal length"), ), # two data dicts with the same field name and different length arrays values ( [{}, {"test_1": [1, 2, 3]}], None, pytest.raises(ValueError, match="same keys to merge"), ), # two data dicts; one empty and one non-empty dict ( [{"test_1": [], "test_2": []}, {"test_1": [1, 2, 3], "test_2": [1, 2, 3]}], {"test_1": [1, 2, 3], "test_2": [1, 2, 3]}, DoesNotRaise(), ), # two data dicts; one empty and one non-empty dict; same keys ( [{"test_1": []}, {"test_1": [1, 2, 3], "test_2": [4, 5, 6]}], None, pytest.raises(ValueError, match="same keys to merge"), ), # two data dicts; one empty and one non-empty dict; different keys ( [ { "test_1": [1, 2, 3], "test_2": [4, 5, 6], "test_3": [7, 8, 9], }, {"test_1": [1, 2, 3], "test_2": [4, 5, 6]}, ], None, pytest.raises(ValueError, match="same keys to merge"), ), # two data dicts; one with three keys, one with two keys ( [ {"test_1": [1, 2, 3]}, {"test_1": [1, 2, 3], "test_2": [1, 2, 3]}, ], None, pytest.raises(ValueError, match="same keys to merge"), ), # some keys missing in one dict ( [ {"test_1": [1, 2, 3], "test_2": ["a", "b"]}, {"test_1": [4, 5], "test_2": ["c", "d", "e"]}, ], None, pytest.raises(ValueError, match="equal length"), ), # different value lengths for the same key ], ) def test_merge_data( data_list: list[dict[str, Any]], expected_result: dict[str, Any] | None, exception: Exception, ) -> None: with exception: result = merge_data(data_list=data_list) if expected_result is None: pytest.fail(f"Expected an error, but got result {result}") for key in result: if isinstance(result[key], np.ndarray): assert np.array_equal(result[key], expected_result[key]), ( f"Mismatch in arrays for key {key}" ) else: assert result[key] == expected_result[key], ( f"Mismatch in non-array data for key {key}" ) @pytest.mark.parametrize( ("data", "index", "expected_result", "exception"), [ ({}, 0, {}, DoesNotRaise()), # empty data dict ( { "test_1": [1, 2, 3], }, 0, { "test_1": [1], }, DoesNotRaise(), ), # data dict with a single list field and integer index ( { "test_1": np.array([1, 2, 3]), }, 0, { "test_1": np.array([1]), }, DoesNotRaise(), ), # data dict with a single np.array field and integer index ( { "test_1": [1, 2, 3], }, slice(0, 2), { "test_1": [1, 2], }, DoesNotRaise(), ), # data dict with a single list field and slice index ( { "test_1": np.array([1, 2, 3]), }, slice(0, 2), { "test_1": np.array([1, 2]), }, DoesNotRaise(), ), # data dict with a single np.array field and slice index ( { "test_1": [1, 2, 3], }, -1, { "test_1": [3], }, DoesNotRaise(), ), # data dict with a single list field and negative integer index ( { "test_1": np.array([1, 2, 3]), }, -1, { "test_1": np.array([3]), }, DoesNotRaise(), ), # data dict with a single np.array field and negative integer index ( { "test_1": [1, 2, 3], }, [0, 2], { "test_1": [1, 3], }, DoesNotRaise(), ), # data dict with a single list field and integer list index ( { "test_1": np.array([1, 2, 3]), }, [0, 2], { "test_1": np.array([1, 3]), }, DoesNotRaise(), ), # data dict with a single np.array field and integer list index ( { "test_1": [1, 2, 3], }, np.array([0, 2]), { "test_1": [1, 3], }, DoesNotRaise(), ), # data dict with a single list field and integer np.array index ( { "test_1": np.array([1, 2, 3]), }, np.array([0, 2]), { "test_1": np.array([1, 3]), }, DoesNotRaise(), ), # data dict with a single np.array field and integer np.array index ( { "test_1": np.array([1, 2, 3]), }, np.array([True, True, True]), { "test_1": np.array([1, 2, 3]), }, DoesNotRaise(), ), # data dict with a single np.array field and all-true bool np.array index ( { "test_1": np.array([1, 2, 3]), }, np.array([False, False, False]), { "test_1": np.array([]), }, DoesNotRaise(), ), # data dict with a single np.array field and all-false bool np.array index ( { "test_1": np.array([1, 2, 3]), }, np.array([False, True, False]), { "test_1": np.array([2]), }, DoesNotRaise(), ), # data dict with a single np.array field and mixed bool np.array index ( {"test_1": np.array([1, 2, 3]), "test_2": ["a", "b", "c"]}, 0, {"test_1": np.array([1]), "test_2": ["a"]}, DoesNotRaise(), ), # data dict with two fields and integer index ( {"test_1": np.array([1, 2, 3]), "test_2": ["a", "b", "c"]}, -1, {"test_1": np.array([3]), "test_2": ["c"]}, DoesNotRaise(), ), # data dict with two fields and negative integer index ( {"test_1": np.array([1, 2, 3]), "test_2": ["a", "b", "c"]}, np.array([False, True, False]), {"test_1": np.array([2]), "test_2": ["b"]}, DoesNotRaise(), ), # data dict with two fields and mixed bool np.array index ], ) def test_get_data_item( data: dict[str, Any], index: Any, expected_result: dict[str, Any] | None, exception: Exception, ) -> None: with exception: result = get_data_item(data=data, index=index) for key in result: if isinstance(result[key], np.ndarray): assert np.array_equal(result[key], expected_result[key]), ( f"Mismatch in arrays for key {key}" ) else: assert result[key] == expected_result[key], ( f"Mismatch in non-array data for key {key}" ) @pytest.mark.parametrize( ("metadata_list", "expected_result", "exception"), [ # Identical metadata with a single key ([{"key1": "value1"}, {"key1": "value1"}], {"key1": "value1"}, DoesNotRaise()), # Identical metadata with multiple keys ( [ {"key1": "value1", "key2": "value2"}, {"key1": "value1", "key2": "value2"}, ], {"key1": "value1", "key2": "value2"}, DoesNotRaise(), ), # Conflicting values for the same key ( [{"key1": "value1"}, {"key1": "value2"}], None, pytest.raises(ValueError, match="Conflicting metadata for key: 'key1'\\."), ), # Different sets of keys across dictionaries ( [{"key1": "value1"}, {"key2": "value2"}], None, pytest.raises(ValueError, match="same keys to merge"), ), # Empty metadata list ([], {}, DoesNotRaise()), # Empty metadata dictionaries ([{}, {}], {}, DoesNotRaise()), # Different declaration order for keys ( [ {"key1": "value1", "key2": "value2"}, {"key2": "value2", "key1": "value1"}, ], {"key1": "value1", "key2": "value2"}, DoesNotRaise(), ), # Nested metadata dictionaries ( [{"key1": {"sub_key": "sub_value"}}, {"key1": {"sub_key": "sub_value"}}], {"key1": {"sub_key": "sub_value"}}, DoesNotRaise(), ), # Large metadata dictionaries with many keys ( [ {f"key{i}": f"value{i}" for i in range(100)}, {f"key{i}": f"value{i}" for i in range(100)}, ], {f"key{i}": f"value{i}" for i in range(100)}, DoesNotRaise(), ), # Mixed types in list metadata values ( [{"key1": ["value1", 2, True]}, {"key1": ["value1", 2, True]}], {"key1": ["value1", 2, True]}, DoesNotRaise(), ), # Identical lists across metadata dictionaries ( [{"key1": [1, 2, 3]}, {"key1": [1, 2, 3]}], {"key1": [1, 2, 3]}, DoesNotRaise(), ), # Identical numpy arrays across metadata dictionaries ( [{"key1": np.array([1, 2, 3])}, {"key1": np.array([1, 2, 3])}], {"key1": np.array([1, 2, 3])}, DoesNotRaise(), ), # Identical numpy arrays across metadata dictionaries, different datatype ( [ {"key1": np.array([1, 2, 3], dtype=np.int32)}, {"key1": np.array([1, 2, 3], dtype=np.int64)}, ], {"key1": np.array([1, 2, 3])}, DoesNotRaise(), ), # Conflicting lists for the same key ( [{"key1": [1, 2, 3]}, {"key1": [4, 5, 6]}], None, pytest.raises(ValueError, match="Conflicting metadata for key: 'key1'\\."), ), # Conflicting numpy arrays for the same key ( [{"key1": np.array([1, 2, 3])}, {"key1": np.array([4, 5, 6])}], None, pytest.raises(ValueError, match="Conflicting metadata for key: 'key1':"), ), # Mixed data types: list and numpy array for the same key ( [{"key1": [1, 2, 3]}, {"key1": np.array([1, 2, 3])}], None, pytest.raises( ValueError, match=( r"Conflicting metadata for key: 'key1': " r"(?:, |" r", )\." ), ), ), # Empty lists and numpy arrays for the same key ( [{"key1": []}, {"key1": np.array([])}], None, pytest.raises( ValueError, match=( r"Conflicting metadata for key: 'key1': " r"(?:, |" r", )\." ), ), ), # Identical multi-dimensional lists across metadata dictionaries ( [{"key1": [[1, 2], [3, 4]]}, {"key1": [[1, 2], [3, 4]]}], {"key1": [[1, 2], [3, 4]]}, DoesNotRaise(), ), # Identical multi-dimensional numpy arrays across metadata dictionaries ( [ {"key1": np.arange(4).reshape(2, 2)}, {"key1": np.arange(4).reshape(2, 2)}, ], {"key1": np.arange(4).reshape(2, 2)}, DoesNotRaise(), ), # Conflicting multi-dimensional lists for the same key ( [{"key1": [[1, 2], [3, 4]]}, {"key1": [[5, 6], [7, 8]]}], None, pytest.raises(ValueError, match="Conflicting metadata for key: 'key1'\\."), ), # Conflicting multi-dimensional numpy arrays for the same key ( [ {"key1": np.arange(4).reshape(2, 2)}, {"key1": np.arange(4, 8).reshape(2, 2)}, ], None, pytest.raises(ValueError, match="Conflicting metadata for key: 'key1':"), ), # Mixed types with multi-dimensional list and array for the same key ( [{"key1": [[1, 2], [3, 4]]}, {"key1": np.arange(4).reshape(2, 2)}], None, pytest.raises( ValueError, match=( r"Conflicting metadata for key: 'key1': " r"(?:, |" r", )\." ), ), ), # Identical higher-dimensional (3D) numpy arrays across # metadata dictionaries ( [ {"key1": np.arange(8).reshape(2, 2, 2)}, {"key1": np.arange(8).reshape(2, 2, 2)}, ], {"key1": np.arange(8).reshape(2, 2, 2)}, DoesNotRaise(), ), # Differently-shaped higher-dimensional (3D) numpy arrays # across metadata dictionaries ( [ {"key1": np.arange(8).reshape(2, 2, 2)}, {"key1": np.arange(8).reshape(4, 1, 2)}, ], None, pytest.raises(ValueError, match="Conflicting metadata for key: 'key1':"), ), ], ) def test_merge_metadata(metadata_list, expected_result, exception) -> None: with exception: result = merge_metadata(metadata_list) if expected_result is None: assert result is None, f"Expected an error, but got a result {result}" for key, value in result.items(): assert key in expected_result if isinstance(value, np.ndarray): np.testing.assert_array_equal(value, expected_result[key]) else: assert value == expected_result[key] def test_process_roboflow_result_compact_masks_batch_retry_logs_warning( caplog: pytest.LogCaptureFixture, ) -> None: """Batch RLE failure triggers per-prediction retry; mixed-modality drops masks.""" import logging roboflow_result = _result( _pred(rle={"size": [4, 4], "counts": "52203"}), _pred(rle={"size": [4, 4], "counts": "52203"}), # sum=6 != 16 — triggers batch failure → per-prediction retry _pred(rle={"size": [4, 4], "counts": [1, 2, 3]}), ) with caplog.at_level(logging.WARNING): result = process_roboflow_result( roboflow_result=roboflow_result, compact_masks=True ) # Batch call fails, per-prediction retry produces 2/3 decoded → mixed-modality drop. assert "Batch compact RLE decode failed" in caplog.text assert result[3] is None assert result[0].shape == (3, 4) def test_process_roboflow_result_compact_masks_rle_mask_size_mismatch() -> None: """rle_mask key + size mismatch triggers resize fallback with compact_masks=True.""" # RLE is 2x2; image is 4x4 — size mismatch triggers resize fallback. roboflow_result = _result( _pred( yx=(2.0, 2.0), size=(4.0, 4.0), rle_mask={"size": [2, 2], "counts": [0, 4]}, ) ) dense_result = process_roboflow_result(roboflow_result=roboflow_result) compact_result = process_roboflow_result( roboflow_result=roboflow_result, compact_masks=True ) assert isinstance(compact_result[3], CompactMask) np.testing.assert_array_equal(compact_result[3].to_dense(), dense_result[3]) # --------------------------------------------------------------------------- # cross_product — regression + unit tests (GitHub #2384) # --------------------------------------------------------------------------- import warnings # noqa: E402 from supervision.detection.utils.internal import cross_product # noqa: E402 from supervision.geometry.core import Point, Vector # noqa: E402 def test_cross_product_no_deprecation_warning() -> None: """Regression for #2384: cross_product must not fire DeprecationWarning.""" anchors = np.array([[[5.0, 5.0]]]) v = Vector(start=Point(0, 0), end=Point(10, 0)) with warnings.catch_warnings(): warnings.simplefilter("error", DeprecationWarning) cross_product(anchors, v) @pytest.mark.parametrize( ("anchors", "vector", "expected_sign"), [ pytest.param( np.array([[[5.0, 5.0]]]), Vector(Point(0, 0), Point(10, 0)), 1, id="above", ), pytest.param( np.array([[[5.0, -5.0]]]), Vector(Point(0, 0), Point(10, 0)), -1, id="below", ), pytest.param( np.array([[[5.0, 0.0]]]), Vector(Point(0, 0), Point(10, 0)), 0, id="on-line", ), pytest.param( np.array([[[3.0, 3.0]]]), Vector(Point(1, 1), Point(5, 1)), 1, id="offset-start", ), ], ) def test_cross_product_sign( anchors: np.ndarray, vector: Vector, expected_sign: int ) -> None: """Verify cross_product returns correct sign for known anchor/vector pairs.""" result = cross_product(anchors, vector) assert int(np.sign(result[0, 0])) == expected_sign