refactor(cv2): add optional backend facade (#2430)

* refactor(cv2): add optional backend facade
* test: guard real cv2 oracle import for cv2-less environments
* test: preserve existing PYTHONPATH in subprocess import tests
* lint: auto-fix violations after resolve cycle
* fix(typing): remove obsolete suppressions
* test(cv2): parametrize constant alignment tests and refactor fallback validation
* test(cv2): simplify constant grouping and optimize REQUIRED_SYMBOLS validation

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Jirka Borovec 2026-07-15 20:53:24 +02:00 committed by GitHub
parent 16814acff3
commit 8ecd9a6680
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 478 additions and 56 deletions

View File

@ -0,0 +1,232 @@
"""Private OpenCV compatibility surface used by Supervision."""
from __future__ import annotations
from typing import NoReturn
class BackendUnavailableError(RuntimeError):
"""Raised when an OpenCV operation is used without an available backend."""
try:
import cv2
except (ImportError, OSError):
_IS_CV2_AVAILABLE = False
else:
_IS_CV2_AVAILABLE = True
_BORDER_CONSTANT = 0
_CAP_PROP_FPS = 5
_CAP_PROP_FRAME_COUNT = 7
_CAP_PROP_FRAME_HEIGHT = 4
_CAP_PROP_FRAME_WIDTH = 3
_CAP_PROP_POS_FRAMES = 1
_CC_STAT_AREA = 4
_CHAIN_APPROX_SIMPLE = 2
_COLOR_BGR2GRAY = 6
_COLOR_BGR2RGB = 4
_COLOR_GRAY2BGR = 8
_COLOR_HSV2BGR = 54
_COLOR_RGB2BGR = 4
_DIST_L2 = 2
_FONT_HERSHEY_SIMPLEX = 0
_IMREAD_COLOR = 1
_IMREAD_UNCHANGED = -1
_INTER_LINEAR = 1
_INTER_NEAREST = 0
_LINE_4 = 4
_LINE_AA = 16
_RETR_CCOMP = 2
_RETR_TREE = 3
if _IS_CV2_AVAILABLE:
from cv2 import (
BORDER_CONSTANT,
CAP_PROP_FPS,
CAP_PROP_FRAME_COUNT,
CAP_PROP_FRAME_HEIGHT,
CAP_PROP_FRAME_WIDTH,
CAP_PROP_POS_FRAMES,
CC_STAT_AREA,
CHAIN_APPROX_SIMPLE,
COLOR_BGR2GRAY,
COLOR_BGR2RGB,
COLOR_GRAY2BGR,
COLOR_HSV2BGR,
COLOR_RGB2BGR,
DIST_L2,
FONT_HERSHEY_SIMPLEX,
IMREAD_COLOR,
IMREAD_UNCHANGED,
INTER_LINEAR,
INTER_NEAREST,
LINE_4,
LINE_AA,
RETR_CCOMP,
RETR_TREE,
VideoCapture,
VideoWriter,
VideoWriter_fourcc,
addWeighted,
approxPolyDP,
blur,
circle,
connectedComponents,
connectedComponentsWithStats,
contourArea,
convertScaleAbs,
copyMakeBorder,
cvtColor,
distanceTransform,
drawContours,
ellipse,
fillPoly,
findContours,
flip,
getRotationMatrix2D,
getTextSize,
imread,
imwrite,
intersectConvexConvex,
line,
mean,
merge,
polylines,
putText,
rectangle,
resize,
split,
warpAffine,
)
BACKEND_NAME = "opencv"
else:
BACKEND_NAME = "fallback"
BORDER_CONSTANT = _BORDER_CONSTANT
CAP_PROP_FPS = _CAP_PROP_FPS
CAP_PROP_FRAME_COUNT = _CAP_PROP_FRAME_COUNT
CAP_PROP_FRAME_HEIGHT = _CAP_PROP_FRAME_HEIGHT
CAP_PROP_FRAME_WIDTH = _CAP_PROP_FRAME_WIDTH
CAP_PROP_POS_FRAMES = _CAP_PROP_POS_FRAMES
CC_STAT_AREA = _CC_STAT_AREA
CHAIN_APPROX_SIMPLE = _CHAIN_APPROX_SIMPLE
COLOR_BGR2GRAY = _COLOR_BGR2GRAY
COLOR_BGR2RGB = _COLOR_BGR2RGB
COLOR_GRAY2BGR = _COLOR_GRAY2BGR
COLOR_HSV2BGR = _COLOR_HSV2BGR
COLOR_RGB2BGR = _COLOR_RGB2BGR
DIST_L2 = _DIST_L2
FONT_HERSHEY_SIMPLEX = _FONT_HERSHEY_SIMPLEX
IMREAD_COLOR = _IMREAD_COLOR
IMREAD_UNCHANGED = _IMREAD_UNCHANGED
INTER_LINEAR = _INTER_LINEAR
INTER_NEAREST = _INTER_NEAREST
LINE_4 = _LINE_4
LINE_AA = _LINE_AA
RETR_CCOMP = _RETR_CCOMP
RETR_TREE = _RETR_TREE
def _unavailable(*args: object, **kwargs: object) -> NoReturn:
"""Fail clearly until the corresponding fallback is implemented."""
del args, kwargs
raise BackendUnavailableError(
"OpenCV is not installed and this operation has no fallback yet."
)
VideoCapture = _unavailable
VideoWriter = _unavailable
VideoWriter_fourcc = _unavailable
addWeighted = _unavailable
approxPolyDP = _unavailable
blur = _unavailable
circle = _unavailable
connectedComponents = _unavailable
connectedComponentsWithStats = _unavailable
contourArea = _unavailable
convertScaleAbs = _unavailable
copyMakeBorder = _unavailable
cvtColor = _unavailable
distanceTransform = _unavailable
drawContours = _unavailable
ellipse = _unavailable
fillPoly = _unavailable
findContours = _unavailable
flip = _unavailable
getRotationMatrix2D = _unavailable
getTextSize = _unavailable
imread = _unavailable
imwrite = _unavailable
intersectConvexConvex = _unavailable
line = _unavailable
mean = _unavailable
merge = _unavailable
polylines = _unavailable
putText = _unavailable
rectangle = _unavailable
resize = _unavailable
split = _unavailable
warpAffine = _unavailable
__all__ = [
"BACKEND_NAME",
"BORDER_CONSTANT",
"CAP_PROP_FPS",
"CAP_PROP_FRAME_COUNT",
"CAP_PROP_FRAME_HEIGHT",
"CAP_PROP_FRAME_WIDTH",
"CAP_PROP_POS_FRAMES",
"CC_STAT_AREA",
"CHAIN_APPROX_SIMPLE",
"COLOR_BGR2GRAY",
"COLOR_BGR2RGB",
"COLOR_GRAY2BGR",
"COLOR_HSV2BGR",
"COLOR_RGB2BGR",
"DIST_L2",
"FONT_HERSHEY_SIMPLEX",
"IMREAD_COLOR",
"IMREAD_UNCHANGED",
"INTER_LINEAR",
"INTER_NEAREST",
"LINE_4",
"LINE_AA",
"RETR_CCOMP",
"RETR_TREE",
"BackendUnavailableError",
"VideoCapture",
"VideoWriter",
"VideoWriter_fourcc",
"addWeighted",
"approxPolyDP",
"blur",
"circle",
"connectedComponents",
"connectedComponentsWithStats",
"contourArea",
"convertScaleAbs",
"copyMakeBorder",
"cvtColor",
"distanceTransform",
"drawContours",
"ellipse",
"fillPoly",
"findContours",
"flip",
"getRotationMatrix2D",
"getTextSize",
"imread",
"imwrite",
"intersectConvexConvex",
"line",
"mean",
"merge",
"polylines",
"putText",
"rectangle",
"resize",
"split",
"warpAffine",
]

View File

@ -3,13 +3,13 @@ from functools import lru_cache
from math import sqrt
from typing import Any, ClassVar, cast
import cv2
import numpy as np
import numpy.typing as npt
from deprecate import deprecated, void # type: ignore[import-untyped,unused-ignore]
from PIL import Image, ImageDraw, ImageFont
from scipy.interpolate import splev, splprep
from supervision import _cv2 as cv2
from supervision.annotators.base import BaseAnnotator
from supervision.annotators.utils import (
PENDING_TRACK_ID,
@ -330,7 +330,7 @@ class OrientedBoxAnnotator(BaseAnnotator):
Example:
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from ultralytics import YOLO

View File

@ -115,7 +115,7 @@ class Classifications:
Example:
```python
import cv2
from supervision import _cv2 as cv2
from ultralytics import YOLO
import supervision as sv

View File

@ -9,11 +9,11 @@ from itertools import chain
from pathlib import Path
from typing import cast
import cv2
import numpy as np
import numpy.typing as npt
from tqdm.auto import tqdm
from supervision import _cv2 as cv2
from supervision.classification.core import Classifications
from supervision.config import CLASS_NAME_DATA_FIELD
from supervision.dataset.formats.coco import (

View File

@ -6,13 +6,13 @@ from xml.etree.ElementTree import Element, SubElement
if TYPE_CHECKING:
from supervision.dataset.core import DetectionDataset
import cv2
import numpy as np
import numpy.typing as npt
from defusedxml.ElementTree import parse, tostring
from defusedxml.minidom import parseString
from tqdm.auto import tqdm
from supervision import _cv2 as cv2
from supervision.dataset.utils import (
approximate_mask_with_polygons,
check_no_basename_collisions,

View File

@ -10,12 +10,12 @@ from collections.abc import Callable
from pathlib import Path
from typing import TYPE_CHECKING, TypeVar, cast
import cv2
import numpy as np
import numpy.typing as npt
from deprecate import deprecated, void # type: ignore[import-untyped,unused-ignore]
from tqdm.auto import tqdm
from supervision import _cv2 as cv2
from supervision.detection.core import Detections
from supervision.detection.utils.converters import mask_to_polygons
from supervision.detection.utils.converters import (

View File

@ -542,7 +542,7 @@ def _resize_crop(
Returns:
int32 RLE array for the resized crop.
"""
import cv2
from supervision import _cv2 as cv2
# All-False: skip decode entirely.
if _rle_area(rle) == 0:

View File

@ -100,7 +100,7 @@ class Detections:
method, which accepts model results from both detection and segmentation models.
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from inference import get_model
@ -116,7 +116,7 @@ class Detections:
method, which accepts model results from both detection and segmentation models.
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from ultralytics import YOLO
@ -275,7 +275,7 @@ class Detections:
Example:
```python
import cv2
from supervision import _cv2 as cv2
import torch
import supervision as sv
@ -314,7 +314,7 @@ class Detections:
Example:
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from ultralytics import YOLO
@ -396,7 +396,7 @@ class Detections:
Example:
```python
import cv2
from supervision import _cv2 as cv2
from super_gradients.training import models
import supervision as sv
@ -451,7 +451,7 @@ class Detections:
import tensorflow as tf
import tensorflow_hub as hub
import numpy as np
import cv2
from supervision import _cv2 as cv2
module_handle = "https://tfhub.dev/tensorflow/centernet/hourglass_512x512_kpts/1"
model = hub.load(module_handle)
@ -528,7 +528,7 @@ class Detections:
Example:
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from mmdet.apis import init_detector, inference_detector
@ -648,7 +648,7 @@ class Detections:
Example:
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from detectron2.engine import DefaultPredictor
from detectron2.config import get_cfg
@ -742,7 +742,7 @@ class Detections:
Example:
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from inference import get_model
@ -874,7 +874,7 @@ class Detections:
Example:
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from inference.models.sam3 import SegmentAnything3
from inference.core.entities.requests.sam3 import Sam3Prompt
@ -2207,7 +2207,7 @@ class Detections:
Example:
```python
import cv2
from supervision import _cv2 as cv2
from ncnn.model_zoo import get_model
import supervision as sv
@ -2750,7 +2750,7 @@ class Detections:
Example:
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from ultralytics import YOLO
@ -3329,7 +3329,7 @@ def merge_inner_detection_object_pair(
Example:
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from inference import get_model

View File

@ -5,10 +5,10 @@ from collections.abc import Iterable
from functools import lru_cache
from typing import Literal
import cv2
import numpy as np
import numpy.typing as npt
from supervision import _cv2 as cv2
from supervision.config import CLASS_NAME_DATA_FIELD
from supervision.detection.core import Detections
from supervision.detection.utils.internal import cross_product

View File

@ -183,7 +183,7 @@ class InferenceSlicer:
Example:
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from rfdetr import RFDETRMedium
@ -237,7 +237,7 @@ class InferenceSlicer:
that benefit from batched forward passes:
```python
import cv2
from supervision import _cv2 as cv2
import numpy as np
import supervision as sv

View File

@ -1,11 +1,11 @@
from collections.abc import Iterable
from typing import Any, cast
import cv2
import numpy as np
import numpy.typing as npt
from supervision import Detections
from supervision import _cv2 as cv2
from supervision.detection.utils.converters import polygon_to_mask
from supervision.draw.color import Color
from supervision.draw.utils import draw_filled_polygon, draw_polygon, draw_text

View File

@ -2,10 +2,11 @@ from __future__ import annotations
from typing import Any, Literal, cast
import cv2
import numpy as np
import numpy.typing as npt
from supervision import _cv2 as cv2
MIN_POLYGON_POINT_COUNT = 3
CoordinateConvention = Literal["inclusive", "exclusive"]

View File

@ -2,10 +2,10 @@ import logging
from itertools import chain
from typing import Any, Literal, cast, overload
import cv2
import numpy as np
import numpy.typing as npt
from supervision import _cv2 as cv2
from supervision.config import CLASS_NAME_DATA_FIELD
from supervision.detection.compact_mask import CompactMask
from supervision.detection.utils._typing import _DetectionDataType, _MetadataType

View File

@ -5,10 +5,10 @@ from collections.abc import Callable, Sequence
from enum import Enum
from typing import Any, cast
import cv2
import numpy as np
import numpy.typing as npt
from supervision import _cv2 as cv2
from supervision.detection.compact_mask import CompactMask
from supervision.detection.utils.converters import mask_to_xyxy
from supervision.utils.internal import warn_deprecated

View File

@ -1,9 +1,9 @@
from typing import Any, Literal, cast
import cv2
import numpy as np
import numpy.typing as npt
from supervision import _cv2 as cv2
from supervision.detection.compact_mask import CompactMask

View File

@ -1,7 +1,8 @@
import cv2
import numpy as np
import numpy.typing as npt
from supervision import _cv2 as cv2
def filter_polygons_by_area(
polygons: list[npt.NDArray[np.number]],

View File

@ -1,10 +1,10 @@
import os
from typing import cast
import cv2
import numpy as np
import numpy.typing as npt
from supervision import _cv2 as cv2
from supervision.draw.color import Color
from supervision.geometry.core import Point, Rect

View File

@ -2,10 +2,10 @@ from abc import ABC, abstractmethod
from collections.abc import Sequence
from typing import cast
import cv2
import numpy as np
import numpy.typing as npt
from supervision import _cv2 as cv2
from supervision.detection.utils.boxes import pad_boxes, spread_out_boxes
from supervision.draw.base import ImageType
from supervision.draw.color import Color

View File

@ -83,7 +83,7 @@ class KeyPoints:
conversion is needed.
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from rfdetr import RFDETRKeypointPreview
@ -100,7 +100,7 @@ class KeyPoints:
[pose](https://docs.ultralytics.com/tasks/pose/) result.
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from ultralytics import YOLO
@ -117,7 +117,7 @@ class KeyPoints:
method, which accepts [Inference](https://inference.roboflow.com/) pose result.
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from inference import get_model
@ -136,7 +136,7 @@ class KeyPoints:
```python
import cv2
from supervision import _cv2 as cv2
import mediapipe as mp
import supervision as sv
@ -398,7 +398,7 @@ class KeyPoints:
Examples:
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from inference import get_model
@ -410,7 +410,7 @@ class KeyPoints:
```
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from inference_sdk import InferenceHTTPClient
@ -489,7 +489,7 @@ class KeyPoints:
Examples:
```python
import cv2
from supervision import _cv2 as cv2
import mediapipe as mp
import supervision as sv
@ -515,7 +515,7 @@ class KeyPoints:
```
```python
import cv2
from supervision import _cv2 as cv2
import mediapipe as mp
import supervision as sv
@ -610,7 +610,7 @@ class KeyPoints:
Examples:
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from ultralytics import YOLO
@ -647,7 +647,7 @@ class KeyPoints:
Examples:
```python
import cv2
from supervision import _cv2 as cv2
import torch
import supervision as sv
import super_gradients
@ -706,7 +706,7 @@ class KeyPoints:
Examples:
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from detectron2.engine import DefaultPredictor
from detectron2.config import get_cfg
@ -1112,7 +1112,7 @@ class KeyPoints:
Examples:
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from ultralytics import YOLO
@ -1323,7 +1323,7 @@ class KeyPoints:
Examples:
```python
import cv2
from supervision import _cv2 as cv2
import supervision as sv
from rfdetr import RFDETRKeypointPreview

View File

@ -453,8 +453,9 @@ def _annotate_detection_panel(
Returns:
Annotated copy of ``scene`` as a ``np.uint8`` array.
"""
import cv2 # lazy: only needed when save_directory_path is set
from supervision import (
_cv2 as cv2, # lazy: only needed when save_directory_path is set
)
from supervision.annotators.core import BoxAnnotator, LabelAnnotator
from supervision.annotators.utils import ColorLookup
from supervision.draw.color import ColorPalette
@ -543,7 +544,9 @@ def _save_detection_validation_visualization(
class_names: Optional list mapping class integer ids to name strings.
metric_target: Coordinate representation used for IoU matching.
"""
import cv2 # lazy: only needed when save_directory_path is set
from supervision import (
_cv2 as cv2, # lazy: only needed when save_directory_path is set
)
tp_predictions, fp_predictions, fn_targets = _split_detections_by_outcome(
predictions=predictions,

View File

@ -2,12 +2,12 @@ import functools
from collections.abc import Callable
from typing import Any, TypeVar, cast
import cv2
import numpy as np
import numpy.typing as npt
from deprecate import deprecated, void # type: ignore[import-untyped,unused-ignore]
from PIL import Image
from supervision import _cv2 as cv2
from supervision.draw.base import ImageType
F = TypeVar("F", bound=Callable[..., Any])

View File

@ -9,7 +9,6 @@ from functools import partial
from types import TracebackType
from typing import Literal, cast
import cv2
import numpy as np
import numpy.typing as npt
from deprecate import ( # type: ignore[import-untyped,unused-ignore]
@ -18,6 +17,7 @@ from deprecate import ( # type: ignore[import-untyped,unused-ignore]
)
from PIL import Image
from supervision import _cv2 as cv2
from supervision.draw.base import ImageType
from supervision.draw.color import Color, unify_to_bgr
from supervision.draw.utils import calculate_optimal_text_scale, draw_text

View File

@ -1,8 +1,8 @@
import cv2
import numpy as np
import numpy.typing as npt
from PIL import Image
from supervision import _cv2 as cv2
from supervision.draw.base import ImageType
from supervision.utils.conversion import pillow_to_cv2

View File

@ -13,11 +13,11 @@ from queue import Empty, Full, Queue
from types import TracebackType
from typing import cast
import cv2
import numpy as np
import numpy.typing as npt
from tqdm.auto import tqdm
from supervision import _cv2 as cv2
from supervision.utils.logger import _get_logger
logger = _get_logger(__name__)
@ -293,7 +293,7 @@ def get_video_frames_generator(
sources; `cv2.VideoCapture` must be released by the caller when done:
```python
import cv2
from supervision import _cv2 as cv2
cap = cv2.VideoCapture(0) # 0 = default webcam
try:

View File

@ -6,12 +6,12 @@ import warnings
from collections.abc import Iterator
from typing import Any, cast
import cv2
import numpy as np
import pytest
from PIL import Image
import supervision.annotators.core as annotators_core
from supervision import _cv2 as cv2
from supervision.annotators.base import BaseAnnotator
from supervision.annotators.core import (
BackgroundOverlayAnnotator,

1
tests/cv2/__init__.py Normal file
View File

@ -0,0 +1 @@
"""Tests for the private media backend boundary."""

184
tests/cv2/test_cv2.py Normal file
View File

@ -0,0 +1,184 @@
"""Tests for the private OpenCV compatibility surface."""
from __future__ import annotations
import importlib
import os
import subprocess
import sys
from pathlib import Path
import numpy as np
import pytest
from supervision import _cv2
# Use the real OpenCV module as the oracle for compatibility comparisons.
try:
cv2 = importlib.import_module("cv2")
except (ImportError, OSError):
pytest.skip(
"OpenCV is required as the reference implementation for this test module",
allow_module_level=True,
)
OPENCV_CONSTANTS = [
"BORDER_CONSTANT",
"CAP_PROP_FPS",
"CAP_PROP_FRAME_COUNT",
"CAP_PROP_FRAME_HEIGHT",
"CAP_PROP_FRAME_WIDTH",
"CAP_PROP_POS_FRAMES",
"CC_STAT_AREA",
"CHAIN_APPROX_SIMPLE",
"COLOR_BGR2GRAY",
"COLOR_BGR2RGB",
"COLOR_GRAY2BGR",
"COLOR_HSV2BGR",
"COLOR_RGB2BGR",
"DIST_L2",
"FONT_HERSHEY_SIMPLEX",
"IMREAD_COLOR",
"IMREAD_UNCHANGED",
"INTER_LINEAR",
"INTER_NEAREST",
"LINE_4",
"LINE_AA",
"RETR_CCOMP",
"RETR_TREE",
]
REQUIRED_SYMBOLS = {
"VideoCapture",
"VideoWriter",
"VideoWriter_fourcc",
"addWeighted",
"approxPolyDP",
"blur",
"circle",
"connectedComponents",
"connectedComponentsWithStats",
"contourArea",
"convertScaleAbs",
"copyMakeBorder",
"cvtColor",
"distanceTransform",
"drawContours",
"ellipse",
"fillPoly",
"findContours",
"flip",
"getRotationMatrix2D",
"getTextSize",
"imread",
"imwrite",
"intersectConvexConvex",
"line",
"mean",
"merge",
"polylines",
"putText",
"rectangle",
"resize",
"split",
"warpAffine",
} | set(OPENCV_CONSTANTS)
def test_facade_exports_the_required_opencv_surface() -> None:
"""Expose every OpenCV symbol used by production call sites."""
assert REQUIRED_SYMBOLS <= set(_cv2.__all__)
assert all(hasattr(_cv2, symbol) for symbol in REQUIRED_SYMBOLS)
assert _cv2.BACKEND_NAME == "opencv"
@pytest.mark.parametrize("name", OPENCV_CONSTANTS)
def test_fallback_constant_matches_opencv(name: str) -> None:
"""Keep each private fallback constant aligned with the OpenCV reference."""
actual = getattr(_cv2, f"_{name}")
expected = getattr(cv2, name)
assert actual == expected
def test_facade_calls_the_package_imported_surface() -> None:
"""Route production-style calls through the Supervision facade."""
image = np.array([[[10, 20, 30], [40, 50, 60]]], dtype=np.uint8)
np.testing.assert_array_equal(
_cv2.cvtColor(image, _cv2.COLOR_BGR2RGB),
cv2.cvtColor(image, cv2.COLOR_BGR2RGB),
)
np.testing.assert_array_equal(
_cv2.resize(image, (4, 2), interpolation=_cv2.INTER_NEAREST),
cv2.resize(image, (4, 2), interpolation=cv2.INTER_NEAREST),
)
def test_facade_imports_without_opencv() -> None:
"""Keep fallback imports and constants valid when cv2 is unavailable."""
env = os.environ.copy()
source_path = str(Path(__file__).resolve().parents[2] / "src")
env["PYTHONPATH"] = os.pathsep.join(
filter(None, (source_path, env.get("PYTHONPATH")))
)
expected_constants = {name: getattr(cv2, name) for name in OPENCV_CONSTANTS}
code = f"""
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 import _cv2
assert _cv2._IS_CV2_AVAILABLE is False
assert _cv2.BACKEND_NAME == "fallback"
expected = {expected_constants!r}
actual = {{name: getattr(_cv2, name) for name in expected}}
fallback = {{name: getattr(_cv2, f"_{{name}}") for name in expected}}
assert fallback == expected
assert actual == expected
try:
_cv2.resize(None, (1, 1), interpolation=_cv2.INTER_NEAREST)
except _cv2.BackendUnavailableError:
pass
else:
raise AssertionError("missing OpenCV must fail explicitly")
"""
subprocess.run( # noqa: S603
[sys.executable, "-c", code],
check=True,
env=env,
)
def test_facade_does_not_hide_a_breaking_opencv_import() -> None:
"""Raise when cv2 imports but no longer exposes a required symbol."""
env = os.environ.copy()
source_path = str(Path(__file__).resolve().parents[2] / "src")
env["PYTHONPATH"] = os.pathsep.join(
filter(None, (source_path, env.get("PYTHONPATH")))
)
code = """
import sys
import types
sys.modules["cv2"] = types.ModuleType("cv2")
from supervision import _cv2
"""
result = subprocess.run( # noqa: S603
[sys.executable, "-c", code],
capture_output=True,
text=True,
env=env,
)
assert result.returncode != 0
assert "cannot import name" in result.stderr

View File

@ -1,10 +1,10 @@
import warnings
import cv2
import numpy as np
import pytest
from PIL import Image, ImageChops
from supervision import _cv2 as cv2
from supervision.utils.image import (
ImageSink,
_overlay_image,

View File

@ -6,10 +6,10 @@ from queue import Queue as StdQueue
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import cv2
import numpy as np
import pytest
from supervision import _cv2 as cv2
from supervision.utils.video import (
FPSMonitor,
VideoInfo,

View File

@ -3426,7 +3426,7 @@ requires-dist = [
{ name = "defusedxml", specifier = ">=0.7.1" },
{ name = "matplotlib", specifier = ">=3.6" },
{ name = "numpy", specifier = ">=1.21.2" },
{ name = "opencv-python", specifier = ">=4.5.5.64" },
{ name = "opencv-python", specifier = ">=4.5.5.64,<5" },
{ name = "pandas", marker = "extra == 'metrics'", specifier = ">=2" },
{ name = "pillow", specifier = ">=9.4" },
{ name = "pydeprecate", specifier = ">=0.9,<0.11" },