"""Tests for private drawing fallbacks.""" from __future__ import annotations import importlib import os import subprocess import sys from pathlib import Path import numpy as np import pytest from supervision._cv2._drawing import ( _circle, _draw_contours, _ellipse, _fill_poly, _line, _polylines, _rectangle, ) try: cv2 = importlib.import_module("cv2") except (ImportError, OSError): cv2 = None requires_cv2 = pytest.mark.skipif( cv2 is None, reason="OpenCV is required as the reference implementation for this test", ) def _assert_draws_on_canvas(result: np.ndarray, image: np.ndarray) -> None: """Assert that a fallback drawing operation mutates and returns its image.""" assert result is image assert np.any(image) @requires_cv2 def test_fallback_line_draws_and_matches_opencv_extent() -> None: """Draw a line in place with the same raster extent as OpenCV.""" actual = np.zeros((20, 20, 3), dtype=np.uint8) expected = np.zeros_like(actual) result = _line(actual, (2, 3), (15, 12), (10, 20, 30), thickness=2) cv2.line(expected, (2, 3), (15, 12), (10, 20, 30), thickness=2) _assert_draws_on_canvas(result, actual) actual_points = np.argwhere(np.any(actual != 0, axis=2)) expected_points = np.argwhere(np.any(expected != 0, axis=2)) np.testing.assert_allclose( actual_points.min(axis=0), expected_points.min(axis=0), atol=1 ) np.testing.assert_allclose( actual_points.max(axis=0), expected_points.max(axis=0), atol=1 ) def test_fallback_rectangle_fills_the_requested_region() -> None: """Fill a rectangle using OpenCV's inclusive corner convention.""" image = np.zeros((12, 14, 3), dtype=np.uint8) result = _rectangle(image, (2, 3), (7, 8), (1, 2, 3), thickness=-1) _assert_draws_on_canvas(result, image) np.testing.assert_array_equal( image[3:9, 2:8], np.full((6, 6, 3), (1, 2, 3), dtype=np.uint8) ) @requires_cv2 def test_fallback_scalar_color_matches_opencv_channel_padding() -> None: """Pad short scalar colors in BGR channel order like OpenCV.""" actual = np.zeros((8, 8, 3), dtype=np.uint8) expected = np.zeros_like(actual) _rectangle(actual, (1, 1), (3, 3), 7, thickness=-1) cv2.rectangle(expected, (1, 1), (3, 3), 7, thickness=-1) np.testing.assert_array_equal(actual, expected) def test_fallback_circle_draws_a_filled_disk() -> None: """Fill a circle while keeping pixels outside its radius unchanged.""" image = np.zeros((15, 15, 3), dtype=np.uint8) result = _circle(image, (7, 7), 4, (1, 2, 3), thickness=-1) _assert_draws_on_canvas(result, image) assert tuple(image[7, 7]) == (1, 2, 3) assert tuple(image[0, 0]) == (0, 0, 0) def test_fallback_ellipse_draws_a_rotated_outline() -> None: """Draw a rotated ellipse outline on a multi-channel image.""" image = np.zeros((24, 24, 3), dtype=np.uint8) result = _ellipse( image, center=(12, 12), axes=(7, 4), angle=30, startAngle=0, endAngle=360, color=(1, 2, 3), thickness=2, ) _assert_draws_on_canvas(result, image) def test_fallback_polylines_respects_closed_flag() -> None: """Close a polyline only when the caller requests a closed path.""" image = np.zeros((12, 12, 3), dtype=np.uint8) points = [np.array([[2, 2], [8, 2], [8, 8]], dtype=np.int32)] result = _polylines(image, points, isClosed=True, color=(1, 2, 3), thickness=1) _assert_draws_on_canvas(result, image) assert tuple(image[5, 5]) == (1, 2, 3) def test_fallback_fill_poly_fills_multiple_channels() -> None: """Fill a polygon with a BGR color in place.""" image = np.zeros((12, 12, 3), dtype=np.uint8) polygon = np.array([[2, 2], [9, 2], [6, 9]], dtype=np.int32) result = _fill_poly(image, [polygon], color=(4, 5, 6)) _assert_draws_on_canvas(result, image) assert tuple(image[4, 5]) == (4, 5, 6) @requires_cv2 def test_fallback_fill_poly_matches_opencv_for_mask_conversion() -> None: """Fill an integer polygon with the same inclusive mask boundary as OpenCV.""" polygon = np.array([[2, 2], [6, 2], [6, 6], [2, 6]], dtype=np.int32) actual = np.zeros((10, 10), dtype=np.uint8) expected = np.zeros((10, 10), dtype=np.uint8) _fill_poly(actual, [polygon], color=(1,)) cv2.fillPoly(expected, [polygon], color=(1,)) np.testing.assert_array_equal(actual, expected) def test_fallback_draw_contours_fills_selected_contour() -> None: """Fill the selected contour when drawContours receives a negative thickness.""" image = np.zeros((12, 12, 3), dtype=np.uint8) contours = [np.array([[[2, 2]], [[9, 2]], [[9, 9]], [[2, 9]]], dtype=np.int32)] result = _draw_contours( image, contours, contourIdx=0, color=(7, 8, 9), thickness=-1 ) _assert_draws_on_canvas(result, image) assert tuple(image[5, 5]) == (7, 8, 9) def test_fallback_draw_contours_rejects_hierarchy() -> None: """Reject hierarchy-guided drawing the flat fallback cannot reproduce.""" image = np.zeros((12, 12, 3), dtype=np.uint8) contours = [np.array([[[2, 2]], [[9, 2]], [[9, 9]], [[2, 9]]], dtype=np.int32)] hierarchy = np.array([[[-1, -1, -1, -1]]], dtype=np.int32) with pytest.raises(ValueError, match="hierarchy"): _draw_contours( image, contours, contourIdx=0, color=(7, 8, 9), hierarchy=hierarchy ) def test_draw_line_consumer_uses_fallback_without_opencv() -> None: """Exercise a Supervision drawing consumer in a cv2-blocked process.""" source = """ import numpy as np import sys class BlockCv2: def find_spec(self, fullname, path=None, target=None): if fullname == "cv2": raise ModuleNotFoundError("blocked for test") return None sys.meta_path.insert(0, BlockCv2()) from supervision.draw.color import Color from supervision.draw.utils import draw_line from supervision.geometry.core import Point image = np.zeros((12, 12, 3), dtype=np.uint8) result = draw_line(image, Point(2, 2), Point(8, 8), color=Color.RED) assert result is image assert np.any(result) """ environment = os.environ.copy() source_path = str(Path(__file__).resolve().parents[2] / "src") environment["PYTHONPATH"] = os.pathsep.join( filter(None, (source_path, environment.get("PYTHONPATH"))) ) completed = subprocess.run( [sys.executable, "-c", source], capture_output=True, text=True, env=environment, ) assert completed.returncode == 0, completed.stderr