From 9518918f2224b5cfa53a6147b7c89ec4a55014ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:54:40 +0000 Subject: [PATCH 01/10] Initial plan From 45c9402a749a5bc487b98e98192b0456f1ea369c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:57:43 +0000 Subject: [PATCH 02/10] Make OpenCV an optional dependency with multiple variants Co-authored-by: Borda <6035284+Borda@users.noreply.github.com> --- README.md | 13 +++++++++++++ docs/index.md | 39 +++++++++++++++++++++++++++++++++++---- pyproject.toml | 13 ++++++++++++- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index cece681e84..dcb6e19189 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,19 @@ Pip install the supervision package in a [**Python>=3.9**](https://www.python.org/) environment. +```bash +pip install supervision[headless] +``` + +Supervision requires OpenCV. We don't enforce a specific version to avoid conflicts with other packages. Choose the OpenCV variant that best fits your needs: + +- `supervision[headless]` - Install with `opencv-python-headless` (recommended for servers) +- `supervision[desktop]` - Install with `opencv-python` (includes GUI support) +- `supervision[desktop-contrib]` - Install with `opencv-contrib-python` (extra modules + GUI) +- `supervision[headless-contrib]` - Install with `opencv-contrib-python-headless` (extra modules, no GUI) + +If you have OpenCV already installed, you can install supervision without any extras: + ```bash pip install supervision ``` diff --git a/docs/index.md b/docs/index.md index 251ec76782..6c158dce4a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -48,6 +48,30 @@ You can install `supervision` in a [![license](https://img.shields.io/pypi/l/supervision)](../LICENSE.md) [![python-version](https://img.shields.io/pypi/pyversions/supervision)](https://badge.fury.io/py/supervision) + Supervision requires OpenCV. Choose the variant that best fits your needs: + + ```bash + # Recommended for servers (no GUI) + pip install supervision[headless] + ``` + + ```bash + # For desktop applications (includes GUI support) + pip install supervision[desktop] + ``` + + ```bash + # For desktop with extra modules (e.g., CSRT tracker) + pip install supervision[desktop-contrib] + ``` + + ```bash + # For servers with extra modules (no GUI) + pip install supervision[headless-contrib] + ``` + + If you already have OpenCV installed: + ```bash pip install supervision ``` @@ -60,7 +84,13 @@ You can install `supervision` in a [![python-version](https://img.shields.io/pypi/pyversions/supervision)](https://badge.fury.io/py/supervision) ```bash - poetry add supervision + # With headless OpenCV (recommended for servers) + poetry add supervision[headless] + ``` + + ```bash + # With desktop OpenCV (includes GUI) + poetry add supervision[desktop] ``` === "uv" @@ -71,13 +101,14 @@ You can install `supervision` in a [![python-version](https://img.shields.io/pypi/pyversions/supervision)](https://badge.fury.io/py/supervision) ```bash - uv pip install supervision + # With headless OpenCV (recommended for servers) + uv pip install supervision[headless] ``` For uv projects: ```bash - uv add supervision + uv add supervision --extra headless ``` === "rye" @@ -88,7 +119,7 @@ You can install `supervision` in a [![python-version](https://img.shields.io/pypi/pyversions/supervision)](https://badge.fury.io/py/supervision) ```bash - rye add supervision + rye add supervision --features headless ``` !!! example "conda/mamba install" diff --git a/pyproject.toml b/pyproject.toml index 7b987a21a6..c4eb2b01a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,6 @@ dependencies = [ "defusedxml>=0.7.1", "matplotlib>=3.6", "numpy>=1.21.2", - "opencv-python>=4.5.5.64", "pillow>=9.4", "pyyaml>=5.3", "requests>=2.26", @@ -59,6 +58,18 @@ dependencies = [ "tqdm>=4.62.3", ] +optional-dependencies.headless = [ + "opencv-python-headless>=4.5.5.64", +] +optional-dependencies.desktop = [ + "opencv-python>=4.5.5.64", +] +optional-dependencies.desktop-contrib = [ + "opencv-contrib-python>=4.5.5.64", +] +optional-dependencies.headless-contrib = [ + "opencv-contrib-python-headless>=4.5.5.64", +] optional-dependencies.metrics = [ "pandas>=2", ] From 4d9133be13796fbc866227873a9f7b80e7901d28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:58:48 +0000 Subject: [PATCH 03/10] Add changelog entry for OpenCV optional dependency change Co-authored-by: Borda <6035284+Borda@users.noreply.github.com> --- docs/changelog.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index e59cd17e08..726dfb4008 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,19 @@ # Changelog +### 0.28.0rc0 Unreleased + +!!! breaking "Breaking Change" + **OpenCV is now an optional dependency.** To ensure compatibility with different OpenCV variants (standard, contrib, headless), `opencv-python` has been removed from the core dependencies. Users must now explicitly choose their preferred OpenCV package: + + - `pip install supervision[headless]` - Installs `opencv-python-headless` (recommended for servers) + - `pip install supervision[desktop]` - Installs `opencv-python` (includes GUI support) + - `pip install supervision[desktop-contrib]` - Installs `opencv-contrib-python` (extra modules + GUI) + - `pip install supervision[headless-contrib]` - Installs `opencv-contrib-python-headless` (extra modules, no GUI) + + If you already have OpenCV installed, you can install supervision without extras: `pip install supervision` + + This change resolves conflicts where `opencv-python` would override `opencv-contrib-python`, preventing access to extra modules like CSRT tracker. + ### 0.27.0 Nov 16, 2025 - Added [#2008](https://github.com/roboflow/supervision/pull/2008): [`sv.filter_segments_by_distance`](https://supervision.roboflow.com/0.27.0/detection/utils/masks/#supervision.detection.utils.masks.filter_segments_by_distance) to keep the largest connected component and nearby components within an absolute or relative distance threshold. Useful for cleaning segmentation predictions from models such as SAM, SAM2, YOLO segmentation, and RF-DETR segmentation. From b4b34229c6a515415ef7c530190f079953a97541 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:24:24 +0000 Subject: [PATCH 04/10] Wrap all cv2 imports with try/except and add ensure_cv2_installed() calls - Wrapped import cv2 with try/except in 16 files across detection, dataset, key_points, draw, and utils modules - Added ensure_cv2_installed() import and calls to all __init__ methods, public functions, and classmethods that use cv2 - Follows pattern established in utils/internal.py, conversion.py, video.py, and image.py - Files updated: line_zone.py, polygon_zone.py, polygons.py, masks.py, converters.py, internal.py, dataset/core.py, dataset/utils.py, pascal_voc.py, key_points/annotators.py, draw/utils.py, utils/notebook.py - All files compile successfully with py_compile Co-authored-by: Borda <6035284+Borda@users.noreply.github.com> --- .github/workflows/ci-build-docs.yml | 2 +- .github/workflows/ci-tests.yml | 2 +- src/supervision/annotators/core.py | 6 +++- src/supervision/dataset/core.py | 11 +++++-- src/supervision/dataset/formats/pascal_voc.py | 8 ++++- src/supervision/dataset/utils.py | 8 ++++- src/supervision/detection/line_zone.py | 12 +++++-- .../detection/tools/polygon_zone.py | 9 +++++- src/supervision/detection/utils/converters.py | 10 +++++- src/supervision/detection/utils/internal.py | 8 ++++- src/supervision/detection/utils/masks.py | 11 ++++++- src/supervision/detection/utils/polygons.py | 9 +++++- src/supervision/draw/utils.py | 15 ++++++++- src/supervision/key_points/annotators.py | 10 +++++- src/supervision/utils/conversion.py | 12 ++++++- src/supervision/utils/image.py | 9 +++++- src/supervision/utils/internal.py | 32 +++++++++++++++++++ src/supervision/utils/notebook.py | 9 +++++- src/supervision/utils/video.py | 9 +++++- 19 files changed, 172 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci-build-docs.yml b/.github/workflows/ci-build-docs.yml index e1cc1e245d..64b9d00b14 100644 --- a/.github/workflows/ci-build-docs.yml +++ b/.github/workflows/ci-build-docs.yml @@ -31,7 +31,7 @@ jobs: activate-environment: true - name: ๐Ÿ—๏ธ Install dependencies - run: uv pip install -r pyproject.toml --group docs + run: uv pip install -r pyproject.toml --group docs --extra headless - name: ๐Ÿงช Test Docs Build run: uv run mkdocs build --verbose diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 977b3f8bdd..d73940b970 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -42,7 +42,7 @@ jobs: activate-environment: true - name: ๐Ÿš€ Install Packages - run: uv pip install -r pyproject.toml --group dev --group docs --extra metrics + run: uv pip install -r pyproject.toml --group dev --group docs --extra metrics --extra headless - name: ๐Ÿ“ฆ Run the Import test run: uv run python -c "import supervision; from supervision import assets; from supervision import metrics; print(supervision.__version__)" diff --git a/src/supervision/annotators/core.py b/src/supervision/annotators/core.py index af8440993e..b5c97667a8 100644 --- a/src/supervision/annotators/core.py +++ b/src/supervision/annotators/core.py @@ -4,12 +4,16 @@ from math import sqrt from typing import Any -import cv2 import numpy as np import numpy.typing as npt from PIL import Image, ImageDraw, ImageFont from scipy.interpolate import splev, splprep +try: + import cv2 +except ImportError: + cv2 = None # type: ignore + from supervision.annotators.base import BaseAnnotator from supervision.annotators.utils import ( PENDING_TRACK_ID, diff --git a/src/supervision/dataset/core.py b/src/supervision/dataset/core.py index f0b3d33918..93b19af0ce 100644 --- a/src/supervision/dataset/core.py +++ b/src/supervision/dataset/core.py @@ -7,9 +7,13 @@ from itertools import chain from pathlib import Path -import cv2 import numpy as np +try: + import cv2 +except ImportError: + cv2 = None # type: ignore + from supervision.classification.core import Classifications from supervision.dataset.formats.coco import ( load_coco_annotations, @@ -32,7 +36,7 @@ train_test_split, ) from supervision.detection.core import Detections -from supervision.utils.internal import warn_deprecated +from supervision.utils.internal import warn_deprecated, ensure_cv2_installed from supervision.utils.iterables import find_duplicates @@ -90,6 +94,7 @@ def __init__( def _get_image(self, image_path: str) -> np.ndarray: """Assumes that image is in dataset""" + ensure_cv2_installed() if self._images_in_memory: return self._images_in_memory[image_path] return cv2.imread(image_path) @@ -691,6 +696,7 @@ def __init__( def _get_image(self, image_path: str) -> np.ndarray: """Assumes that image is in dataset""" + ensure_cv2_installed() if self._images_in_memory: return self._images_in_memory[image_path] return cv2.imread(image_path) @@ -825,6 +831,7 @@ def as_folder_structure(self, root_directory_path: str) -> None: root_directory_path (str): The path to the directory where the dataset will be saved. """ + ensure_cv2_installed() os.makedirs(root_directory_path, exist_ok=True) for class_name in self.classes: diff --git a/src/supervision/dataset/formats/pascal_voc.py b/src/supervision/dataset/formats/pascal_voc.py index c5172c33da..7e9f6eb0c3 100644 --- a/src/supervision/dataset/formats/pascal_voc.py +++ b/src/supervision/dataset/formats/pascal_voc.py @@ -4,15 +4,20 @@ from pathlib import Path from xml.etree.ElementTree import Element, SubElement -import cv2 import numpy as np from defusedxml.ElementTree import parse, tostring from defusedxml.minidom import parseString +try: + import cv2 +except ImportError: + cv2 = None # type: ignore + from supervision.dataset.utils import approximate_mask_with_polygons from supervision.detection.core import Detections from supervision.detection.utils.converters import polygon_to_mask, polygon_to_xyxy from supervision.utils.file import list_files_with_extensions +from supervision.utils.internal import ensure_cv2_installed def object_to_pascal_voc( @@ -156,6 +161,7 @@ def load_pascal_voc_annotations( of class names, a list of paths to images, and a dictionary with image paths as keys and corresponding Detections instances as values. """ + ensure_cv2_installed() image_paths = [ str(path) diff --git a/src/supervision/dataset/utils.py b/src/supervision/dataset/utils.py index 37d9bf9112..b4c5245e37 100644 --- a/src/supervision/dataset/utils.py +++ b/src/supervision/dataset/utils.py @@ -7,16 +7,21 @@ from pathlib import Path from typing import TYPE_CHECKING, TypeVar -import cv2 import numpy as np import numpy.typing as npt +try: + import cv2 +except ImportError: + cv2 = None # type: ignore + from supervision.detection.core import Detections from supervision.detection.utils.converters import mask_to_polygons from supervision.detection.utils.polygons import ( approximate_polygon, filter_polygons_by_area, ) +from supervision.utils.internal import ensure_cv2_installed if TYPE_CHECKING: from supervision.dataset.core import DetectionDataset @@ -101,6 +106,7 @@ def map_detections_class_id( def save_dataset_images(dataset: DetectionDataset, images_directory_path: str) -> None: + ensure_cv2_installed() Path(images_directory_path).mkdir(parents=True, exist_ok=True) for image_path in dataset.image_paths: final_path = os.path.join(images_directory_path, Path(image_path).name) diff --git a/src/supervision/detection/line_zone.py b/src/supervision/detection/line_zone.py index 574860f3a5..5319e0f05b 100644 --- a/src/supervision/detection/line_zone.py +++ b/src/supervision/detection/line_zone.py @@ -7,10 +7,14 @@ from functools import lru_cache from typing import Any, Literal -import cv2 import numpy as np import numpy.typing as npt +try: + import cv2 +except ImportError: + cv2 = None # type: ignore + from supervision.config import CLASS_NAME_DATA_FIELD from supervision.detection.core import Detections from supervision.detection.utils.internal import cross_product @@ -18,7 +22,7 @@ from supervision.draw.utils import draw_rectangle, draw_text from supervision.geometry.core import Point, Position, Rect, Vector from supervision.utils.image import overlay_image -from supervision.utils.internal import SupervisionWarnings +from supervision.utils.internal import SupervisionWarnings, ensure_cv2_installed TEXT_MARGIN = 10 @@ -354,6 +358,7 @@ def __init__( when the label overlaps something important. """ + ensure_cv2_installed() self.thickness: int = thickness self.color: Color = color self.text_thickness: int = text_thickness @@ -382,6 +387,7 @@ def annotate(self, frame: np.ndarray, line_counter: LineZone) -> np.ndarray: (np.ndarray): The image with the line drawn on it. """ + ensure_cv2_installed() line_start = line_counter.vector.start.as_xy_int_tuple() line_end = line_counter.vector.end.as_xy_int_tuple() cv2.line( @@ -742,6 +748,7 @@ def __init__( " TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT." ) + ensure_cv2_installed() self.table_position = table_position self.table_color = table_color self.table_margin = table_margin @@ -771,6 +778,7 @@ def annotate( (np.ndarray): The image with the table drawn on it. """ + ensure_cv2_installed() if line_zone_labels is None: line_zone_labels = [f"Line {i + 1}:" for i in range(len(line_zones))] if len(line_zones) != len(line_zone_labels): diff --git a/src/supervision/detection/tools/polygon_zone.py b/src/supervision/detection/tools/polygon_zone.py index 3319c1783e..e0369611dd 100644 --- a/src/supervision/detection/tools/polygon_zone.py +++ b/src/supervision/detection/tools/polygon_zone.py @@ -3,10 +3,14 @@ from collections.abc import Iterable from dataclasses import replace -import cv2 import numpy as np import numpy.typing as npt +try: + import cv2 +except ImportError: + cv2 = None # type: ignore + from supervision import Detections from supervision.detection.utils.boxes import clip_boxes from supervision.detection.utils.converters import polygon_to_mask @@ -14,6 +18,7 @@ from supervision.draw.utils import draw_filled_polygon, draw_polygon, draw_text from supervision.geometry.core import Position from supervision.geometry.utils import get_polygon_center +from supervision.utils.internal import ensure_cv2_installed class PolygonZone: @@ -144,6 +149,7 @@ def __init__( display_in_zone_count: bool = True, opacity: float = 0, ): + ensure_cv2_installed() self.zone = zone self.color = color self.thickness = thickness @@ -168,6 +174,7 @@ def annotate(self, scene: np.ndarray, label: str | None = None) -> np.ndarray: Returns: np.ndarray: The image with the polygon zone and count of detected objects """ + ensure_cv2_installed() if self.opacity == 0: annotated_frame = draw_polygon( scene=scene, diff --git a/src/supervision/detection/utils/converters.py b/src/supervision/detection/utils/converters.py index cc886cfff2..f00bdea16c 100644 --- a/src/supervision/detection/utils/converters.py +++ b/src/supervision/detection/utils/converters.py @@ -1,6 +1,12 @@ -import cv2 import numpy as np +try: + import cv2 +except ImportError: + cv2 = None # type: ignore + +from supervision.utils.internal import ensure_cv2_installed + MIN_POLYGON_POINT_COUNT = 3 @@ -35,6 +41,7 @@ def polygon_to_mask(polygon: np.ndarray, resolution_wh: tuple[int, int]) -> np.n np.ndarray: The generated 2D mask, where the polygon is marked with `1`'s and the rest is filled with `0`'s. """ + ensure_cv2_installed() width, height = map(int, resolution_wh) mask = np.zeros((height, width), dtype=np.uint8) cv2.fillPoly(mask, [polygon.astype(np.int32)], color=1) @@ -287,6 +294,7 @@ def mask_to_polygons(mask: np.ndarray) -> list[np.ndarray]: points. Polygons with fewer points than `MIN_POLYGON_POINT_COUNT = 3` are excluded from the output. """ + ensure_cv2_installed() contours, _ = cv2.findContours( mask.astype(np.uint8), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE diff --git a/src/supervision/detection/utils/internal.py b/src/supervision/detection/utils/internal.py index f5d6dc9fbf..d1f03c64ff 100644 --- a/src/supervision/detection/utils/internal.py +++ b/src/supervision/detection/utils/internal.py @@ -3,16 +3,22 @@ from itertools import chain from typing import Any -import cv2 import numpy as np import numpy.typing as npt +try: + import cv2 +except ImportError: + cv2 = None # type: ignore + from supervision.config import CLASS_NAME_DATA_FIELD from supervision.detection.utils.converters import polygon_to_mask from supervision.geometry.core import Vector +from supervision.utils.internal import ensure_cv2_installed def extract_ultralytics_masks(yolov8_results) -> np.ndarray | None: + ensure_cv2_installed() if not yolov8_results.masks: return None diff --git a/src/supervision/detection/utils/masks.py b/src/supervision/detection/utils/masks.py index a03bb2bf99..888fa22f49 100644 --- a/src/supervision/detection/utils/masks.py +++ b/src/supervision/detection/utils/masks.py @@ -2,10 +2,16 @@ from typing import Literal -import cv2 import numpy as np import numpy.typing as npt +try: + import cv2 +except ImportError: + cv2 = None # type: ignore + +from supervision.utils.internal import ensure_cv2_installed + def move_masks( masks: npt.NDArray[np.bool_], @@ -155,6 +161,7 @@ def contains_holes(mask: npt.NDArray[np.bool_]) -> bool: ![contains_holes](https://media.roboflow.com/supervision-docs/contains-holes.png){ align=center width="800" } """ # noqa E501 // docs + ensure_cv2_installed() mask_uint8 = mask.astype(np.uint8) _, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) @@ -216,6 +223,7 @@ def contains_multiple_segments( ![contains_multiple_segments](https://media.roboflow.com/supervision-docs/contains-multiple-segments.png){ align=center width="800" } """ # noqa E501 // docs + ensure_cv2_installed() if connectivity != 4 and connectivity != 8: raise ValueError( "Incorrect connectivity value. Possible connectivity values: 4 or 8." @@ -337,6 +345,7 @@ def filter_segments_by_distance( The nearby 2ร—2 block at columns 6โ€“7 is kept because its edge distance is within 3 pixels. The distant block at columns 9-10 is removed. """ # noqa E501 // docs + ensure_cv2_installed() if mask.dtype != bool: raise TypeError("mask must be boolean") diff --git a/src/supervision/detection/utils/polygons.py b/src/supervision/detection/utils/polygons.py index c3ded1e70c..2106da2339 100644 --- a/src/supervision/detection/utils/polygons.py +++ b/src/supervision/detection/utils/polygons.py @@ -1,8 +1,14 @@ from __future__ import annotations -import cv2 import numpy as np +try: + import cv2 +except ImportError: + cv2 = None # type: ignore + +from supervision.utils.internal import ensure_cv2_installed + def filter_polygons_by_area( polygons: list[np.ndarray], @@ -65,6 +71,7 @@ def approximate_polygon( the `x`, `y` coordinates of the approximated polygon's points. """ + ensure_cv2_installed() if percentage < 0 or percentage >= 1: raise ValueError("Percentage must be in the range [0, 1).") diff --git a/src/supervision/draw/utils.py b/src/supervision/draw/utils.py index 3109d57937..8dfb49f6ec 100644 --- a/src/supervision/draw/utils.py +++ b/src/supervision/draw/utils.py @@ -3,12 +3,17 @@ import os from typing import cast -import cv2 import numpy as np import numpy.typing as npt +try: + import cv2 +except ImportError: + cv2 = None # type: ignore + from supervision.draw.color import Color from supervision.geometry.core import Point, Rect +from supervision.utils.internal import ensure_cv2_installed def draw_line( @@ -31,6 +36,7 @@ def draw_line( Returns: The scene with the line drawn on it """ + ensure_cv2_installed() cv2.line( scene, start.as_xy_int_tuple(), @@ -59,6 +65,7 @@ def draw_rectangle( Returns: The scene with the rectangle drawn on it """ + ensure_cv2_installed() cv2.rectangle( scene, rect.top_left.as_xy_int_tuple(), @@ -87,6 +94,7 @@ def draw_filled_rectangle( Returns: The scene with the rectangle drawn on it """ + ensure_cv2_installed() if opacity == 1: cv2.rectangle( scene, @@ -129,6 +137,7 @@ def draw_rounded_rectangle( Returns: The image with the rounded rectangle drawn on it. """ + ensure_cv2_installed() x1, y1, x2, y2 = rect.as_xyxy_int_tuple() width, height = x2 - x1, y2 - y1 border_radius = min(border_radius, min(width, height) // 2) @@ -180,6 +189,7 @@ def draw_polygon( Returns: The scene with the polygon drawn on it. """ + ensure_cv2_installed() cv2.polylines( scene, [polygon], isClosed=True, color=color.as_bgr(), thickness=thickness ) @@ -203,6 +213,7 @@ def draw_filled_polygon( Returns: The scene with the polygon drawn on it. """ + ensure_cv2_installed() if opacity == 1: cv2.fillPoly(scene, [polygon], color=color.as_bgr()) else: @@ -262,6 +273,7 @@ def draw_text( ``` """ + ensure_cv2_installed() text_width, text_height = cv2.getTextSize( text=text, fontFace=text_font, @@ -318,6 +330,7 @@ def draw_image( FileNotFoundError: If the image path does not exist. ValueError: For invalid opacity or rectangle dimensions. """ + ensure_cv2_installed() # Validate and load image if isinstance(image, str): diff --git a/src/supervision/key_points/annotators.py b/src/supervision/key_points/annotators.py index a13115b168..8962e277ab 100644 --- a/src/supervision/key_points/annotators.py +++ b/src/supervision/key_points/annotators.py @@ -3,9 +3,13 @@ from abc import ABC, abstractmethod from logging import warn -import cv2 import numpy as np +try: + import cv2 +except ImportError: + cv2 = None # type: ignore + from supervision.detection.utils.boxes import pad_boxes, spread_out_boxes from supervision.draw.base import ImageType from supervision.draw.color import Color @@ -14,6 +18,7 @@ from supervision.key_points.core import KeyPoints from supervision.key_points.skeletons import SKELETONS_BY_VERTEX_COUNT from supervision.utils.conversion import ensure_cv2_image_for_class_method +from supervision.utils.internal import ensure_cv2_installed class BaseKeyPointAnnotator(ABC): @@ -40,6 +45,7 @@ def __init__( radius (int): The radius of the circles used to represent the key points. """ + ensure_cv2_installed() self.color = color self.radius = radius @@ -117,6 +123,7 @@ def __init__( edges (Optional[List[Tuple[int, int]]]): The edges to draw. If set to `None`, will attempt to select automatically. """ + ensure_cv2_installed() self.color = color self.thickness = thickness self.edges = edges @@ -222,6 +229,7 @@ def __init__( boxes. Set to a high value to produce circles. smart_position (bool): Spread out the labels to avoid overlap. """ + ensure_cv2_installed() self.border_radius: int = border_radius self.color: Color | list[Color] = color self.text_color: Color | list[Color] = text_color diff --git a/src/supervision/utils/conversion.py b/src/supervision/utils/conversion.py index d58deffb50..5fd68b2200 100644 --- a/src/supervision/utils/conversion.py +++ b/src/supervision/utils/conversion.py @@ -1,13 +1,17 @@ from functools import wraps from typing import Any, Callable -import cv2 import numpy as np import numpy.typing as npt from PIL import Image from supervision.draw.base import ImageType +try: + import cv2 +except ImportError: + cv2 = None # type: ignore + def ensure_cv2_image_for_class_method( annotate_func: Callable[..., ImageType], @@ -119,6 +123,9 @@ def pillow_to_cv2(image: Image.Image) -> npt.NDArray[np.uint8]: Returns: Input image converted to OpenCV format. """ + from supervision.utils.internal import ensure_cv2_installed + + ensure_cv2_installed() scene = np.array(image) scene = cv2.cvtColor(scene, cv2.COLOR_RGB2BGR) return scene.astype(np.uint8) @@ -135,5 +142,8 @@ def cv2_to_pillow(image: npt.NDArray[np.uint8]) -> Image.Image: Returns: Input image converted to Pillow format. """ + from supervision.utils.internal import ensure_cv2_installed + + ensure_cv2_installed() image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) return Image.fromarray(image) diff --git a/src/supervision/utils/image.py b/src/supervision/utils/image.py index 717cd5a165..153ef0c07b 100644 --- a/src/supervision/utils/image.py +++ b/src/supervision/utils/image.py @@ -4,11 +4,15 @@ import shutil from typing import Any -import cv2 import numpy as np import numpy.typing as npt from PIL import Image +try: + import cv2 +except ImportError: + cv2 = None # type: ignore + from supervision.draw.base import ImageType from supervision.draw.color import Color, unify_to_bgr from supervision.utils.conversion import ( @@ -521,6 +525,9 @@ def save_image( `None`, generates name using `image_name_pattern`. Defaults to `None`. """ + from supervision.utils.internal import ensure_cv2_installed + + ensure_cv2_installed() if image_name is None: image_name = self.image_name_pattern.format(self.image_count) diff --git a/src/supervision/utils/internal.py b/src/supervision/utils/internal.py index 4b92c43b17..d32d26e154 100644 --- a/src/supervision/utils/internal.py +++ b/src/supervision/utils/internal.py @@ -190,6 +190,38 @@ def __get__(self, owner_self: Any, owner_cls: type | None = None) -> T: return self.fget(owner_cls) +def ensure_cv2_installed() -> None: + """ + Check if OpenCV (cv2) is installed and raise an ImportError with installation + instructions if it is not available. + + Raises: + ImportError: If OpenCV is not installed, with instructions on how to install it. + + Usage: + ```pycon + >>> from supervision.utils.internal import ensure_cv2_installed + >>> ensure_cv2_installed() # Will raise ImportError if cv2 is not installed + + ``` + """ + try: + import cv2 # noqa: F401 + except ImportError: + raise ImportError( + "OpenCV is required but not installed. " + "Install supervision with one of the following options:\n" + " pip install supervision[headless] # For servers (no GUI)\n" + " pip install supervision[desktop] # For desktop (with GUI)\n" + " pip install supervision[desktop-contrib] # For desktop with extra modules\n" + " pip install supervision[headless-contrib] # For servers with extra modules\n" + "Or install OpenCV separately:\n" + " pip install opencv-python-headless # Recommended for servers\n" + " pip install opencv-python # For desktop applications\n" + " pip install opencv-contrib-python # For extra modules" + ) + + def get_instance_variables(instance: Any, include_properties: bool = False) -> set[str]: """ Get the public variables of a class instance. diff --git a/src/supervision/utils/notebook.py b/src/supervision/utils/notebook.py index ed8f4f68f7..158cf8cd64 100644 --- a/src/supervision/utils/notebook.py +++ b/src/supervision/utils/notebook.py @@ -1,11 +1,16 @@ from __future__ import annotations -import cv2 import matplotlib.pyplot as plt from PIL import Image +try: + import cv2 +except ImportError: + cv2 = None # type: ignore + from supervision.draw.base import ImageType from supervision.utils.conversion import pillow_to_cv2 +from supervision.utils.internal import ensure_cv2_installed def plot_image( @@ -33,6 +38,7 @@ def plot_image( ``` """ + ensure_cv2_installed() if isinstance(image, Image.Image): image = pillow_to_cv2(image) @@ -89,6 +95,7 @@ def plot_images_grid( ``` """ + ensure_cv2_installed() nrows, ncols = grid_size for idx, img in enumerate(images): diff --git a/src/supervision/utils/video.py b/src/supervision/utils/video.py index d3b7776f19..0db5e4a562 100644 --- a/src/supervision/utils/video.py +++ b/src/supervision/utils/video.py @@ -8,11 +8,15 @@ from queue import Empty, Full, Queue from typing import Any -import cv2 import numpy as np import numpy.typing as npt from tqdm.auto import tqdm +try: + import cv2 +except ImportError: + cv2 = None # type: ignore + @dataclass class VideoInfo: @@ -48,6 +52,9 @@ class VideoInfo: @classmethod def from_video_path(cls, video_path: str) -> VideoInfo: + from supervision.utils.internal import ensure_cv2_installed + + ensure_cv2_installed() video = cv2.VideoCapture(video_path) if not video.isOpened(): raise Exception(f"Could not open video at {video_path}") From b7aba154653ba5fd8af591b3d78056e8a36351bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:27:46 +0000 Subject: [PATCH 05/10] Fix cv2 constant usage at module level and update CI workflows Co-authored-by: Borda <6035284+Borda@users.noreply.github.com> --- src/supervision/annotators/core.py | 24 ++++++++++++++++++------ src/supervision/draw/utils.py | 4 +++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/supervision/annotators/core.py b/src/supervision/annotators/core.py index b5c97667a8..217d55ae38 100644 --- a/src/supervision/annotators/core.py +++ b/src/supervision/annotators/core.py @@ -49,7 +49,19 @@ scale_image, ) -CV2_FONT = cv2.FONT_HERSHEY_SIMPLEX +# Lazy initialization for cv2 constants to avoid import errors +CV2_FONT = None + + +def _get_cv2_font() -> int: + """Get cv2.FONT_HERSHEY_SIMPLEX constant, ensuring cv2 is installed.""" + global CV2_FONT + if CV2_FONT is None: + from supervision.utils.internal import ensure_cv2_installed + + ensure_cv2_installed() + CV2_FONT = cv2.FONT_HERSHEY_SIMPLEX + return CV2_FONT class _BaseLabelAnnotator(BaseAnnotator): @@ -1277,7 +1289,7 @@ def _get_label_properties( for line in wrapped_lines: (text_w, text_h) = cv2.getTextSize( text=line, - fontFace=CV2_FONT, + fontFace=_get_cv2_font(), fontScale=self.text_scale, thickness=self.text_thickness, )[0] @@ -1360,7 +1372,7 @@ def _draw_labels( # Use a character with ascenders and descenders as height reference (_, text_h) = cv2.getTextSize( text="Tg", - fontFace=CV2_FONT, + fontFace=_get_cv2_font(), fontScale=self.text_scale, thickness=self.text_thickness, )[0] @@ -1369,7 +1381,7 @@ def _draw_labels( (_, text_h) = cv2.getTextSize( text=line, - fontFace=CV2_FONT, + fontFace=_get_cv2_font(), fontScale=self.text_scale, thickness=self.text_thickness, )[0] @@ -1381,7 +1393,7 @@ def _draw_labels( img=scene, text=line, org=(text_x, text_y), - fontFace=CV2_FONT, + fontFace=_get_cv2_font(), fontScale=self.text_scale, color=text_color.as_bgr(), thickness=self.text_thickness, @@ -3106,7 +3118,7 @@ def _draw_labels(self, scene: npt.NDArray[np.uint8]) -> None: (text_w, _) = cv2.getTextSize( text=text, - fontFace=CV2_FONT, + fontFace=_get_cv2_font(), fontScale=self.label_scale, thickness=self.text_thickness, )[0] diff --git a/src/supervision/draw/utils.py b/src/supervision/draw/utils.py index 8dfb49f6ec..06e61a3964 100644 --- a/src/supervision/draw/utils.py +++ b/src/supervision/draw/utils.py @@ -234,7 +234,7 @@ def draw_text( text_scale: float = 0.5, text_thickness: int = 1, text_padding: int = 10, - text_font: int = cv2.FONT_HERSHEY_SIMPLEX, + text_font: int | None = None, background_color: Color | None = None, ) -> npt.NDArray[np.uint8]: """ @@ -274,6 +274,8 @@ def draw_text( ``` """ ensure_cv2_installed() + if text_font is None: + text_font = cv2.FONT_HERSHEY_SIMPLEX text_width, text_height = cv2.getTextSize( text=text, fontFace=text_font, From a59d3c200d607c3e6db8cf9aca298375fd96e96a Mon Sep 17 00:00:00 2001 From: Jirka Borovec <6035284+Borda@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:01:05 +0900 Subject: [PATCH 06/10] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dcb6e19189..5eb0c62d68 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ Supervision requires OpenCV. We don't enforce a specific version to avoid confli - `supervision[desktop-contrib]` - Install with `opencv-contrib-python` (extra modules + GUI) - `supervision[headless-contrib]` - Install with `opencv-contrib-python-headless` (extra modules, no GUI) +โš ๏ธ **Important**: Only choose one variant โ€” the different `opencv-python` packages cannot coexist in the same environment. If you have OpenCV already installed, you can install supervision without any extras: ```bash From e04444b74c1e9e1bb86840f207cbcdac06200064 Mon Sep 17 00:00:00 2001 From: Jirka Borovec <6035284+Borda@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:01:16 +0900 Subject: [PATCH 07/10] Update docs/index.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/index.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/index.md b/docs/index.md index 6c158dce4a..e05cb21929 100644 --- a/docs/index.md +++ b/docs/index.md @@ -93,6 +93,15 @@ You can install `supervision` in a poetry add supervision[desktop] ``` + ```bash + # For desktop with extra modules (e.g., CSRT tracker) + poetry add supervision[desktop-contrib] + ``` + + ```bash + # For servers with extra modules (no GUI) + poetry add supervision[headless-contrib] + ``` === "uv" [![version](https://badge.fury.io/py/supervision.svg)](https://badge.fury.io/py/supervision) From fa05b195a11684c1db81709c675eaa8456bafed9 Mon Sep 17 00:00:00 2001 From: Jirka Borovec <6035284+Borda@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:01:25 +0900 Subject: [PATCH 08/10] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5eb0c62d68..9d36cef127 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Pip install the supervision package in a pip install supervision[headless] ``` -Supervision requires OpenCV. We don't enforce a specific version to avoid conflicts with other packages. Choose the OpenCV variant that best fits your needs: +Supervision requires OpenCV to be installed. We don't automatically install it to avoid conflicts with other packages that may need specific OpenCV variants. Choose the OpenCV variant that best fits your needs: - `supervision[headless]` - Install with `opencv-python-headless` (recommended for servers) - `supervision[desktop]` - Install with `opencv-python` (includes GUI support) From f88b612c8a06e144fa1b914e7f621b9215441b10 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:02:20 +0000 Subject: [PATCH 09/10] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/index.md | 1 + pyproject.toml | 7 +++---- src/supervision/dataset/core.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/index.md b/docs/index.md index e05cb21929..4dac01a923 100644 --- a/docs/index.md +++ b/docs/index.md @@ -102,6 +102,7 @@ You can install `supervision` in a # For servers with extra modules (no GUI) poetry add supervision[headless-contrib] ``` + === "uv" [![version](https://badge.fury.io/py/supervision.svg)](https://badge.fury.io/py/supervision) diff --git a/pyproject.toml b/pyproject.toml index 919111c5ee..5fe099f8fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,16 +57,15 @@ dependencies = [ "scipy>=1.10", "tqdm>=4.62.3", ] - -optional-dependencies.headless = [ - "opencv-python-headless>=4.5.5.64", -] optional-dependencies.desktop = [ "opencv-python>=4.5.5.64", ] optional-dependencies.desktop-contrib = [ "opencv-contrib-python>=4.5.5.64", ] +optional-dependencies.headless = [ + "opencv-python-headless>=4.5.5.64", +] optional-dependencies.headless-contrib = [ "opencv-contrib-python-headless>=4.5.5.64", ] diff --git a/src/supervision/dataset/core.py b/src/supervision/dataset/core.py index 072341e51b..7808ab370a 100644 --- a/src/supervision/dataset/core.py +++ b/src/supervision/dataset/core.py @@ -37,7 +37,7 @@ train_test_split, ) from supervision.detection.core import Detections -from supervision.utils.internal import warn_deprecated, ensure_cv2_installed +from supervision.utils.internal import ensure_cv2_installed, warn_deprecated from supervision.utils.iterables import find_duplicates From 2b67b3db1f9e8f69c54e1163cef7a8c416b233fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:03:15 +0000 Subject: [PATCH 10/10] Address PR review feedback: complete installation docs and clarify runtime behavior Co-authored-by: Borda <6035284+Borda@users.noreply.github.com> --- docs/changelog.md | 11 ++++++++--- docs/index.md | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 726dfb4008..5bbc30bc46 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,16 +3,21 @@ ### 0.28.0rc0 Unreleased !!! breaking "Breaking Change" - **OpenCV is now an optional dependency.** To ensure compatibility with different OpenCV variants (standard, contrib, headless), `opencv-python` has been removed from the core dependencies. Users must now explicitly choose their preferred OpenCV package: + **OpenCV is required at runtime but is now an optional install-time dependency.** To ensure compatibility with different OpenCV variants (standard, contrib, headless), `opencv-python` has been removed from the core dependencies. Users must now have OpenCV installed either by: + + - Explicitly choosing an OpenCV extra when installing supervision (recommended) + - Having a compatible OpenCV package already installed + + Installation options: - `pip install supervision[headless]` - Installs `opencv-python-headless` (recommended for servers) - `pip install supervision[desktop]` - Installs `opencv-python` (includes GUI support) - `pip install supervision[desktop-contrib]` - Installs `opencv-contrib-python` (extra modules + GUI) - `pip install supervision[headless-contrib]` - Installs `opencv-contrib-python-headless` (extra modules, no GUI) - If you already have OpenCV installed, you can install supervision without extras: `pip install supervision` + If you already have OpenCV installed: `pip install supervision` - This change resolves conflicts where `opencv-python` would override `opencv-contrib-python`, preventing access to extra modules like CSRT tracker. + **Note**: Without OpenCV installed, functions that require OpenCV will raise an `ImportError` with instructions on how to install it. This change resolves conflicts where `opencv-python` would override `opencv-contrib-python`, preventing access to extra modules like CSRT tracker. ### 0.27.0 Nov 16, 2025 diff --git a/docs/index.md b/docs/index.md index 4dac01a923..c7bf285395 100644 --- a/docs/index.md +++ b/docs/index.md @@ -50,6 +50,8 @@ You can install `supervision` in a Supervision requires OpenCV. Choose the variant that best fits your needs: + โš ๏ธ **Important**: Only install one OpenCV variant at a time. The different `opencv-python` packages (standard, contrib, headless, etc.) are mutually exclusive and cannot coexist in the same environment. + ```bash # Recommended for servers (no GUI) pip install supervision[headless] @@ -115,6 +117,27 @@ You can install `supervision` in a uv pip install supervision[headless] ``` + ```bash + # For desktop applications (includes GUI support) + uv pip install supervision[desktop] + ``` + + ```bash + # For desktop with extra modules (e.g., CSRT tracker) + uv pip install supervision[desktop-contrib] + ``` + + ```bash + # For servers with extra modules (no GUI) + uv pip install supervision[headless-contrib] + ``` + + If you already have OpenCV installed: + + ```bash + uv pip install supervision + ``` + For uv projects: ```bash @@ -129,9 +152,25 @@ You can install `supervision` in a [![python-version](https://img.shields.io/pypi/pyversions/supervision)](https://badge.fury.io/py/supervision) ```bash + # With headless OpenCV (recommended for servers) rye add supervision --features headless ``` + ```bash + # For desktop applications (includes GUI support) + rye add supervision --features desktop + ``` + + ```bash + # For desktop with extra modules (e.g., CSRT tracker) + rye add supervision --features desktop-contrib + ``` + + ```bash + # For servers with extra modules (no GUI) + rye add supervision --features headless-contrib + ``` + !!! example "conda/mamba install" === "conda"