from __future__ import annotations import numpy as np import pytest from supervision.detection.utils.converters import ( xcycwh_to_xyxy, xywh_to_xyxy, xyxy_to_mask, xyxy_to_xcycarh, xyxy_to_xywh, ) @pytest.mark.parametrize( ("xywh", "expected_result"), [ (np.array([[10, 20, 30, 40]]), np.array([[10, 20, 40, 60]])), # standard case (np.array([[0, 0, 0, 0]]), np.array([[0, 0, 0, 0]])), # zero size bounding box ( np.array([[50, 50, 100, 100]]), np.array([[50, 50, 150, 150]]), ), # large bounding box ( np.array([[-10, -20, 30, 40]]), np.array([[-10, -20, 20, 20]]), ), # negative coordinates (np.array([[50, 50, 0, 30]]), np.array([[50, 50, 50, 80]])), # zero width (np.array([[50, 50, 20, 0]]), np.array([[50, 50, 70, 50]])), # zero height (np.array([]).reshape(0, 4), np.array([]).reshape(0, 4)), # empty array ], ) def test_xywh_to_xyxy(xywh: np.ndarray, expected_result: np.ndarray) -> None: result = xywh_to_xyxy(xywh) np.testing.assert_array_equal(result, expected_result) @pytest.mark.parametrize( ("xyxy", "expected_result"), [ (np.array([[10, 20, 40, 60]]), np.array([[10, 20, 30, 40]])), # standard case (np.array([[0, 0, 0, 0]]), np.array([[0, 0, 0, 0]])), # zero size bounding box ( np.array([[50, 50, 150, 150]]), np.array([[50, 50, 100, 100]]), ), # large bounding box ( np.array([[-10, -20, 20, 20]]), np.array([[-10, -20, 30, 40]]), ), # negative coordinates (np.array([[50, 50, 50, 80]]), np.array([[50, 50, 0, 30]])), # zero width (np.array([[50, 50, 70, 50]]), np.array([[50, 50, 20, 0]])), # zero height (np.array([]).reshape(0, 4), np.array([]).reshape(0, 4)), # empty array ], ) def test_xyxy_to_xywh(xyxy: np.ndarray, expected_result: np.ndarray) -> None: result = xyxy_to_xywh(xyxy) np.testing.assert_array_equal(result, expected_result) @pytest.mark.parametrize( ("xyxy", "expected_result"), [ # Empty and zero cases (np.array([]).reshape(0, 4), np.array([]).reshape(0, 4)), # empty array ( np.array([[0, 0, 0, 0]]), np.array([[0, 0, 0.0, 0]]), ), # zero size bounding box ( np.array([[10, 10, 10, 10]]), np.array([[10, 10, 0.0, 0]]), ), # point (x1=x2, y1=y2) # Zero width/height cases (np.array([[50, 50, 80, 50]]), np.array([[65, 50, 0.0, 0]])), # zero height (np.array([[50, 50, 50, 80]]), np.array([[50, 65, 0.0, 30]])), # zero width # Standard cases (np.array([[10, 20, 40, 60]]), np.array([[25, 40, 0.75, 40]])), # standard case ( np.array([[-30, -40, -10, -20]]), np.array([[-20, -30, 1.0, 20]]), ), # all negative values ( np.array([[0.1, 0.2, 0.4, 0.6]]), np.array([[0.25, 0.4, 0.75, 0.4]]), ), # values between 0-1 # Different aspect ratios ( np.array([[10, 20, 50, 100]]), np.array([[30, 60, 0.5, 80]]), ), # tall rectangle (height > width) ( np.array([[20, 10, 100, 50]]), np.array([[60, 30, 2.0, 40]]), ), # wide rectangle (width > height) ( np.array([[50, 50, 150, 150]]), np.array([[100, 100, 1.0, 100]]), ), # height == width # Multiple boxes in one array ( np.array([[0, 0, 0, 0], [10, 20, 40, 60]]), np.array([[0, 0, 0.0, 0], [25, 40, 0.75, 40]]), ), # one zero-sized box and one normal box ], ) def test_xyxy_to_xcycarh(xyxy: np.ndarray, expected_result: np.ndarray) -> None: result = xyxy_to_xcycarh(xyxy) np.testing.assert_allclose(result, expected_result) @pytest.mark.parametrize( ("xcycwh", "expected_result"), [ (np.array([[50, 50, 20, 30]]), np.array([[40, 35, 60, 65]])), # standard case (np.array([[0, 0, 0, 0]]), np.array([[0, 0, 0, 0]])), # zero size bounding box ( np.array([[50, 50, 100, 100]]), np.array([[0, 0, 100, 100]]), ), # large bounding box centered at (50, 50) ( np.array([[-10, -10, 20, 30]]), np.array([[-20, -25, 0, 5]]), ), # negative coordinates (np.array([[50, 50, 0, 30]]), np.array([[50, 35, 50, 65]])), # zero width (np.array([[50, 50, 20, 0]]), np.array([[40, 50, 60, 50]])), # zero height (np.array([]).reshape(0, 4), np.array([]).reshape(0, 4)), # empty array ], ) def test_xcycwh_to_xyxy(xcycwh: np.ndarray, expected_result: np.ndarray) -> None: result = xcycwh_to_xyxy(xcycwh) np.testing.assert_array_equal(result, expected_result) @pytest.mark.parametrize( ("boxes", "resolution_wh", "expected"), [ # 0) Empty input ( np.array([], dtype=float).reshape(0, 4), (5, 4), np.array([], dtype=bool).reshape(0, 4, 5), ), # 1) Single pixel box ( np.array([[2, 1, 2, 1]], dtype=float), (5, 4), np.array( [ [ [False, False, False, False, False], [False, False, True, False, False], [False, False, False, False, False], [False, False, False, False, False], ] ], dtype=bool, ), ), # 2) Horizontal line, inclusive bounds ( np.array([[1, 2, 3, 2]], dtype=float), (5, 4), np.array( [ [ [False, False, False, False, False], [False, False, False, False, False], [False, True, True, True, False], [False, False, False, False, False], ] ], dtype=bool, ), ), # 3) Vertical line, inclusive bounds ( np.array([[3, 0, 3, 2]], dtype=float), (5, 4), np.array( [ [ [False, False, False, True, False], [False, False, False, True, False], [False, False, False, True, False], [False, False, False, False, False], ] ], dtype=bool, ), ), # 4) Proper rectangle fill ( np.array([[1, 1, 3, 2]], dtype=float), (5, 4), np.array( [ [ [False, False, False, False, False], [False, True, True, True, False], [False, True, True, True, False], [False, False, False, False, False], ] ], dtype=bool, ), ), # 5) Negative coordinates clipped to [0, 0] ( np.array([[-2, -1, 1, 1]], dtype=float), (5, 4), np.array( [ [ [True, True, False, False, False], [True, True, False, False, False], [False, False, False, False, False], [False, False, False, False, False], ] ], dtype=bool, ), ), # 6) Overflow coordinates clipped to width-1 and height-1 ( np.array([[3, 2, 10, 10]], dtype=float), (5, 4), np.array( [ [ [False, False, False, False, False], [False, False, False, False, False], [False, False, False, True, True], [False, False, False, True, True], ] ], dtype=bool, ), ), # 7) Invalid box where max < min after ints, mask stays empty ( np.array([[3, 2, 1, 4]], dtype=float), (5, 4), np.array( [ [ [False, False, False, False, False], [False, False, False, False, False], [False, False, False, False, False], [False, False, False, False, False], ] ], dtype=bool, ), ), # 8) Fractional coordinates are floored by int conversion # (0.2,0.2)-(2.8,1.9) -> (0,0)-(2,1) ( np.array([[0.2, 0.2, 2.8, 1.9]], dtype=float), (5, 4), np.array( [ [ [True, True, True, False, False], [True, True, True, False, False], [False, False, False, False, False], [False, False, False, False, False], ] ], dtype=bool, ), ), # 9) Multiple boxes, separate masks ( np.array([[0, 0, 1, 0], [2, 1, 4, 3]], dtype=float), (5, 4), np.array( [ # Box 0: row 0, cols 0..1 [ [True, True, False, False, False], [False, False, False, False, False], [False, False, False, False, False], [False, False, False, False, False], ], # Box 1: rows 1..3, cols 2..4 [ [False, False, False, False, False], [False, False, True, True, True], [False, False, True, True, True], [False, False, True, True, True], ], ], dtype=bool, ), ), ], ) def test_xyxy_to_mask(boxes: np.ndarray, resolution_wh, expected: np.ndarray) -> None: result = xyxy_to_mask(boxes, resolution_wh) assert result.dtype == np.bool_ assert result.shape == expected.shape np.testing.assert_array_equal(result, expected)