diff --git a/.github/workflows/ci-build-docs.yml b/.github/workflows/ci-build-docs.yml
index 0be071826d..6db1132dad 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 sync --frozen --group docs
+ run: uv sync --frozen --group docs --extra headless
- name: ๐งช Test Docs Build
run: mkdocs build --verbose
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index 56c07309f1..9bfe9a338d 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 sync --frozen --group dev --group docs --extra metrics
+ run: uv sync --frozen --group dev --group docs --extra metrics --extra headless
- name: ๐ฆ Run the Import test
run: python -c "import supervision; from supervision import assets; from supervision import metrics; print(supervision.__version__)"
diff --git a/README.md b/README.md
index cece681e84..9d36cef127 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,20 @@
Pip install the supervision package in a
[**Python>=3.9**](https://www.python.org/) environment.
+```bash
+pip install supervision[headless]
+```
+
+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)
+- `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
pip install supervision
```
diff --git a/docs/changelog.md b/docs/changelog.md
index e59cd17e08..5bbc30bc46 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,5 +1,24 @@
# Changelog
+### 0.28.0rc0 Unreleased
+
+!!! breaking "Breaking Change"
+ **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: `pip install supervision`
+
+ **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
- 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.
diff --git a/docs/index.md b/docs/index.md
index 251ec76782..c7bf285395 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -48,6 +48,32 @@ You can install `supervision` in a
[](../LICENSE.md)
[](https://badge.fury.io/py/supervision)
+ 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]
+ ```
+
+ ```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 +86,23 @@ You can install `supervision` in a
[](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]
+ ```
+
+ ```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"
@@ -70,6 +112,28 @@ You can install `supervision` in a
[](../LICENSE.md)
[](https://badge.fury.io/py/supervision)
+ ```bash
+ # With headless OpenCV (recommended for servers)
+ 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
```
@@ -77,7 +141,7 @@ You can install `supervision` in a
For uv projects:
```bash
- uv add supervision
+ uv add supervision --extra headless
```
=== "rye"
@@ -88,7 +152,23 @@ You can install `supervision` in a
[](https://badge.fury.io/py/supervision)
```bash
- rye add supervision
+ # 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"
diff --git a/pyproject.toml b/pyproject.toml
index 05e33eb957..5fe099f8fc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -51,13 +51,24 @@ 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",
"scipy>=1.10",
"tqdm>=4.62.3",
]
+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",
+]
optional-dependencies.metrics = [
"pandas>=2",
]
diff --git a/src/supervision/annotators/core.py b/src/supervision/annotators/core.py
index af8440993e..217d55ae38 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,
@@ -45,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):
@@ -1273,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]
@@ -1356,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]
@@ -1365,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]
@@ -1377,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,
@@ -3102,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/dataset/core.py b/src/supervision/dataset/core.py
index 823339ab53..7808ab370a 100644
--- a/src/supervision/dataset/core.py
+++ b/src/supervision/dataset/core.py
@@ -7,10 +7,14 @@
from itertools import chain
from pathlib import Path
-import cv2
import numpy as np
import numpy.typing as npt
+try:
+ import cv2
+except ImportError:
+ cv2 = None # type: ignore
+
from supervision.classification.core import Classifications
from supervision.dataset.formats.coco import (
load_coco_annotations,
@@ -33,7 +37,7 @@
train_test_split,
)
from supervision.detection.core import Detections
-from supervision.utils.internal import warn_deprecated
+from supervision.utils.internal import ensure_cv2_installed, warn_deprecated
from supervision.utils.iterables import find_duplicates
@@ -91,6 +95,7 @@ def __init__(
def _get_image(self, image_path: str) -> npt.NDArray[np.uint8]:
"""Assumes that image is in dataset."""
+ ensure_cv2_installed()
if self._images_in_memory:
return self._images_in_memory[image_path]
image = cv2.imread(image_path)
@@ -693,6 +698,7 @@ def __init__(
def _get_image(self, image_path: str) -> npt.NDArray[np.uint8]:
"""Assumes that image is in dataset."""
+ ensure_cv2_installed()
if self._images_in_memory:
return self._images_in_memory[image_path]
image = cv2.imread(image_path)
diff --git a/src/supervision/dataset/formats/pascal_voc.py b/src/supervision/dataset/formats/pascal_voc.py
index 83ec55a85e..38c2a1a87c 100644
--- a/src/supervision/dataset/formats/pascal_voc.py
+++ b/src/supervision/dataset/formats/pascal_voc.py
@@ -4,16 +4,21 @@
from pathlib import Path
from xml.etree.ElementTree import Element, SubElement
-import cv2
import numpy as np
import numpy.typing as npt
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(
@@ -161,6 +166,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 f52111a3e4..ca5fe8bc45 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 4078b69b92..1258e4c3ae 100644
--- a/src/supervision/detection/utils/converters.py
+++ b/src/supervision/detection/utils/converters.py
@@ -1,7 +1,13 @@
-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
+
MIN_POLYGON_POINT_COUNT = 3
@@ -39,6 +45,7 @@ def polygon_to_mask(
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)
@@ -293,6 +300,7 @@ def mask_to_polygons(mask: npt.NDArray[np.bool_]) -> list[npt.NDArray[np.int32]]
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 03f197d3b4..7ebd8a1d73 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, cast
-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: Any) -> npt.NDArray[np.bool_] | 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 1f7c8baad9..56acf8dc5e 100644
--- a/src/supervision/detection/utils/masks.py
+++ b/src/supervision/detection/utils/masks.py
@@ -2,10 +2,16 @@
from typing import Any, Literal, cast
-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_],
@@ -161,6 +167,7 @@ def contains_holes(mask: npt.NDArray[np.bool_]) -> bool:
{ 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)
@@ -222,6 +229,7 @@ def contains_multiple_segments(
{ 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."
@@ -343,6 +351,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 0981d73ac1..a76a89ea41 100644
--- a/src/supervision/detection/utils/polygons.py
+++ b/src/supervision/detection/utils/polygons.py
@@ -6,6 +6,13 @@
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 filter_polygons_by_area(
polygons: list[npt.NDArray[np.number]],
@@ -68,6 +75,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..06e61a3964 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:
@@ -223,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]:
"""
@@ -262,6 +273,9 @@ 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,
@@ -318,6 +332,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}")