diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 4002566e..70e0fd6b 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -8,7 +8,7 @@ This file provides context-aware guidance for GitHub Copilot when working in the
**Supervision** is a Python library providing reusable computer vision utilities for working with object detection models (YOLO, SAM, etc.). It offers tools for detections processing, tracking, annotation, and dataset management.
-- **Languages**: Python 3.9+
+- **Languages**: Python 3.10+
- **Key Dependencies**: NumPy, OpenCV, SciPy
- **License**: MIT
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index 8ce66c8b..f8348358 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -29,7 +29,7 @@ jobs:
fail-fast: false
matrix:
os: ["ubuntu-latest", "windows-latest", "macos-latest"]
- python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
runs-on: ${{ matrix.os }}
steps:
- name: 📥 Checkout the repository
diff --git a/README.md b/README.md
index 97ea7844..c8d587e0 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@
## 💻 Install
-Pip install the supervision package in a [**Python>=3.9**](https://www.python.org/) environment.
+Pip install the supervision package in a [**Python>=3.10**](https://www.python.org/) environment.
```bash
pip install supervision
diff --git a/docs/changelog.md b/docs/changelog.md
index 53a79fcc..8b4c847c 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -5,6 +5,14 @@ date_modified: 2026-06-25
# Changelog
+### Unreleased upcoming
+
+!!! failure "Python 3.9 Support Terminated"
+
+ With the upcoming `supervision-0.30.0` release, we are terminating official support for Python 3.9, which reached end-of-life in October 2025. The minimum supported Python version is now **3.10**.
+
+ Users on Python 3.9 should upgrade their environment before updating supervision.
+
### 0.29.1 Jun 23, 2026
- Fixed [#2353](https://github.com/roboflow/supervision/pull/2353): `sv.Detections.from_inference` no longer raises `TypeError` when the Inference package returns a mixed batch where only some predictions carry a `tracker_id`. `detections.tracker_id` is `None` for the full result in that case; fully-tracked and fully-untracked batches are unchanged.
diff --git a/docs/index.md b/docs/index.md
index 67a841fa..29ca5cfe 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -45,7 +45,7 @@ We write your reusable computer vision tools. Whether you need to load your data
## 💻 Install
-You can install `supervision` in a [**Python>=3.9**](https://www.python.org/) environment.
+You can install `supervision` in a [**Python>=3.10**](https://www.python.org/) environment.
!!! example "Installation"
diff --git a/docs/llms.full.txt b/docs/llms.full.txt
index 47e9d506..4ade0ef9 100644
--- a/docs/llms.full.txt
+++ b/docs/llms.full.txt
@@ -129,7 +129,7 @@ Supervision is an open-source Python library by Roboflow for computer vision wor
### How do I install supervision?
-Install with `pip install supervision`. For optional metric dependencies use `pip install supervision[metrics]`. Sample asset utilities are included in the base package under `supervision.assets`. The current package metadata requires Python 3.9+.
+Install with `pip install supervision`. For optional metric dependencies use `pip install supervision[metrics]`. Sample asset utilities are included in the base package under `supervision.assets`. The current package metadata requires Python 3.10+.
### What can I do with supervision?
diff --git a/docs/llms.txt b/docs/llms.txt
index b2f97864..3271822f 100644
--- a/docs/llms.txt
+++ b/docs/llms.txt
@@ -120,7 +120,7 @@ Supervision is an open-source Python library by Roboflow for computer vision wor
### How do I install supervision?
-Install with `pip install supervision`. For optional metric dependencies use `pip install supervision[metrics]`. Sample asset utilities are included in the base package under `supervision.assets`. The current package metadata requires Python 3.9+.
+Install with `pip install supervision`. For optional metric dependencies use `pip install supervision[metrics]`. Sample asset utilities are included in the base package under `supervision.assets`. The current package metadata requires Python 3.10+.
### What can I do with supervision?
diff --git a/docs/theme/main.html b/docs/theme/main.html
index 45110773..3054edde 100644
--- a/docs/theme/main.html
+++ b/docs/theme/main.html
@@ -120,7 +120,7 @@
"name": "How do I install supervision?",
"acceptedAnswer": {
"@type": "Answer",
- "text": "Install supervision with pip: pip install supervision. For optional metric dependencies use pip install supervision[metrics]. Sample asset utilities are included in the base package under supervision.assets. The current package metadata requires Python 3.9+."
+ "text": "Install supervision with pip: pip install supervision. For optional metric dependencies use pip install supervision[metrics]. Sample asset utilities are included in the base package under supervision.assets. The current package metadata requires Python 3.10+."
}
},
{
diff --git a/examples/compact_mask/benchmark.py b/examples/compact_mask/benchmark.py
index 052e1b46..f1e37763 100644
--- a/examples/compact_mask/benchmark.py
+++ b/examples/compact_mask/benchmark.py
@@ -12,8 +12,6 @@ Mask complexity is controlled by ``num_vertices``: random polygons with more
vertices produce jaggier boundaries and more RLE runs per row.
"""
-from __future__ import annotations
-
import dataclasses
import gc
import json
diff --git a/examples/time_in_zone/scripts/download_from_youtube.py b/examples/time_in_zone/scripts/download_from_youtube.py
index 808d098e..029a946a 100644
--- a/examples/time_in_zone/scripts/download_from_youtube.py
+++ b/examples/time_in_zone/scripts/download_from_youtube.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import os
import sys
from typing import Any
diff --git a/examples/time_in_zone/scripts/draw_zones.py b/examples/time_in_zone/scripts/draw_zones.py
index e52d0602..a6bea0b2 100644
--- a/examples/time_in_zone/scripts/draw_zones.py
+++ b/examples/time_in_zone/scripts/draw_zones.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import json
import os
from typing import Any
diff --git a/examples/time_in_zone/ultralytics_stream_example.py b/examples/time_in_zone/ultralytics_stream_example.py
index 75935680..ca44fcfc 100644
--- a/examples/time_in_zone/ultralytics_stream_example.py
+++ b/examples/time_in_zone/ultralytics_stream_example.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import cv2
import numpy as np
from inference import InferencePipeline
diff --git a/examples/traffic_analysis/inference_example.py b/examples/traffic_analysis/inference_example.py
index c6bd739e..8bd13941 100644
--- a/examples/traffic_analysis/inference_example.py
+++ b/examples/traffic_analysis/inference_example.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import os
from collections.abc import Iterable
diff --git a/examples/traffic_analysis/ultralytics_example.py b/examples/traffic_analysis/ultralytics_example.py
index 76d916e6..03b8d776 100644
--- a/examples/traffic_analysis/ultralytics_example.py
+++ b/examples/traffic_analysis/ultralytics_example.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
from collections.abc import Iterable
import cv2
diff --git a/pyproject.toml b/pyproject.toml
index c5c24735..a064d29f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -23,7 +23,7 @@ maintainers = [
authors = [
{ name = "Roboflow et al.", email = "develop@roboflow.com" },
]
-requires-python = ">=3.9"
+requires-python = ">=3.10"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
@@ -33,7 +33,6 @@ classifiers = [
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
@@ -83,7 +82,7 @@ dev = [
]
docs = [
"mike>=2",
- "mkdocs-git-committers-plugin-2>=2.4.1; python_version>='3.9' and python_version<'4'",
+ "mkdocs-git-committers-plugin-2>=2.4.1; python_version>='3.10' and python_version<'4'",
"mkdocs-git-revision-date-localized-plugin>=1.2.4",
"mkdocs-jupyter>=0.24.3",
"mkdocs-material[imaging]>=9.7",
@@ -102,12 +101,9 @@ package-data.supervision = [ "py.typed" ]
packages.find.where = [ "src" ]
packages.find.include = [ "supervision*" ]
-[tool.uv]
-dependency-groups.docs = { requires-python = ">=3.10" }
-
# exclude = [ "docs*", "tests*", "examples*" ]
[tool.ruff]
-target-version = "py39"
+target-version = "py310"
line-length = 88
indent-width = 4
# Exclude a variety of commonly ignored directories.
@@ -190,7 +186,7 @@ quiet-level = 3
ignore-words-list = "STrack,sTrack,strack"
[tool.mypy]
-python_version = "3.9"
+python_version = "3.10"
ignore_missing_imports = false
explicit_package_bases = true
strict = true
@@ -206,6 +202,7 @@ overrides = [
"tests.*",
"examples.*",
], ignore_errors = true },
+ { module = "deprecate", ignore_missing_imports = true },
]
[tool.pytest]
diff --git a/src/supervision/annotators/core.py b/src/supervision/annotators/core.py
index 04441524..7ec07860 100644
--- a/src/supervision/annotators/core.py
+++ b/src/supervision/annotators/core.py
@@ -1,8 +1,6 @@
-from __future__ import annotations
-
from functools import lru_cache
from math import sqrt
-from typing import Any, cast, overload
+from typing import Any, cast
import cv2
import numpy as np
@@ -54,16 +52,6 @@ from supervision.utils.logger import _get_logger
logger = _get_logger(__name__)
-@overload
-def _normalize_color_input(color: Color | str) -> Color: ...
-
-
-@overload
-def _normalize_color_input(
- color: Color | ColorPalette | str,
-) -> Color | ColorPalette: ...
-
-
def _normalize_color_input(color: Color | ColorPalette | str) -> Color | ColorPalette:
"""Normalize accepted color inputs to internal color objects.
@@ -1958,7 +1946,7 @@ class BlurAnnotator(BaseAnnotator):
return scene
image_height, image_width = scene.shape[:2]
clipped_xyxy: npt.NDArray[np.int32] = clip_boxes(
- xyxy=cast(npt.NDArray[np.number], detections.xyxy),
+ xyxy=detections.xyxy,
resolution_wh=(image_width, image_height),
).astype(int)
@@ -2311,7 +2299,7 @@ class PixelateAnnotator(BaseAnnotator):
return scene
image_height, image_width = scene.shape[:2]
clipped_xyxy: npt.NDArray[np.int32] = clip_boxes(
- xyxy=cast(npt.NDArray[np.number], detections.xyxy),
+ xyxy=detections.xyxy,
resolution_wh=(image_width, image_height),
).astype(int)
@@ -2635,7 +2623,7 @@ class PercentageBarAnnotator(BaseAnnotator):
self.height: int = height
self.width: int = width
self.color: Color | ColorPalette = _normalize_color_input(color)
- self.border_color: Color = _normalize_color_input(border_color)
+ self.border_color = cast(Color, _normalize_color_input(border_color))
self.position: Position = position
self.color_lookup: ColorLookup = color_lookup
@@ -3213,7 +3201,7 @@ class ComparisonAnnotator:
return mask
resolution_wh = scene.shape[1], scene.shape[0]
- polygons = xyxy_to_polygons(cast(npt.NDArray[np.number], detections.xyxy))
+ polygons = xyxy_to_polygons(detections.xyxy)
for polygon in polygons:
polygon_mask = polygon_to_mask(polygon, resolution_wh=resolution_wh)
diff --git a/src/supervision/annotators/utils.py b/src/supervision/annotators/utils.py
index caa53757..dc0da2d3 100644
--- a/src/supervision/annotators/utils.py
+++ b/src/supervision/annotators/utils.py
@@ -1,9 +1,7 @@
-from __future__ import annotations
-
import re
import textwrap
from enum import Enum
-from typing import Any
+from typing import cast
import numpy as np
import numpy.typing as npt
@@ -156,7 +154,7 @@ def resolve_color(
return get_color_by_index(color=color, idx=idx)
-def wrap_text(text: Any, max_line_length: int | None = None) -> list[str]:
+def wrap_text(text: object, max_line_length: int | None = None) -> list[str]:
"""
Wrap `text` to the specified maximum line length, respecting existing
newlines. Falls back to str() if `text` is not already a string.
@@ -264,9 +262,9 @@ def get_labels_text(
def snap_boxes(
- xyxy: np.ndarray[Any, np.dtype[np.float32]],
+ xyxy: npt.NDArray[np.float32],
resolution_wh: tuple[int, int],
-) -> np.ndarray[Any, np.dtype[np.float32]]:
+) -> npt.NDArray[np.float32]:
"""
Shifts `label` bounding boxes into the frame so that they are fully contained
within the given resolution, prioritizing the top/left edge.
@@ -307,7 +305,7 @@ def snap_boxes(
```
"""
- result = np.copy(xyxy)
+ result: npt.NDArray[np.float32] = np.array(xyxy, dtype=np.float32, copy=True)
width, height = resolution_wh
# X-axis (prioritize left edge)
@@ -326,7 +324,7 @@ def snap_boxes(
bottom_shift = height - result[bottom_overflow, 3]
result[bottom_overflow, 1:4:2] += bottom_shift[:, np.newaxis]
- return result.astype(np.float32) # type: ignore
+ return cast(npt.NDArray[np.float32], result.astype(np.float32, copy=False))
class Trace:
@@ -340,9 +338,9 @@ class Trace:
self.max_size = max_size
self.anchor = anchor
- self.frame_id = np.array([], dtype=int)
- self.xy = np.empty((0, 2), dtype=np.float32)
- self.tracker_id = np.array([], dtype=int)
+ self.frame_id: npt.NDArray[np.int_] = np.array([], dtype=int)
+ self.xy: npt.NDArray[np.float32] = np.empty((0, 2), dtype=np.float32)
+ self.tracker_id: npt.NDArray[np.int_] = np.array([], dtype=int)
def put(self, detections: Detections) -> None:
frame_id: npt.NDArray[np.int_] = np.full(
@@ -374,11 +372,11 @@ class Trace:
self.current_frame_id += 1
- def get(self, tracker_id: int) -> np.ndarray[Any, np.dtype[np.float32]]:
- filtered: np.ndarray[Any, np.dtype[np.float32]] = (
- self.xy[self.tracker_id == tracker_id].copy().astype(np.float32, copy=False)
+ def get(self, tracker_id: int) -> npt.NDArray[np.float32]:
+ xy: npt.NDArray[np.float32] = np.asarray(
+ self.xy[self.tracker_id == tracker_id], dtype=np.float32
)
- return filtered
+ return xy
def hex_to_rgba(hex_color: str) -> tuple[int, int, int, int]:
diff --git a/src/supervision/assets/downloader.py b/src/supervision/assets/downloader.py
index ba41c579..19cd7442 100644
--- a/src/supervision/assets/downloader.py
+++ b/src/supervision/assets/downloader.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import os
from hashlib import md5
from pathlib import Path
diff --git a/src/supervision/dataset/core.py b/src/supervision/dataset/core.py
index 1f57024b..4e94cb1b 100644
--- a/src/supervision/dataset/core.py
+++ b/src/supervision/dataset/core.py
@@ -6,6 +6,7 @@ from collections.abc import Iterator
from dataclasses import dataclass
from itertools import chain
from pathlib import Path
+from typing import cast
import cv2
import numpy as np
@@ -114,7 +115,7 @@ class DetectionDataset(BaseDataset):
image = cv2.imread(image_path)
if image is None:
raise ValueError(f"Could not read image from path: {image_path}")
- return image
+ return cast(npt.NDArray[np.uint8], image)
def __len__(self) -> int:
return len(self._images_in_memory) or len(self.image_paths)
@@ -1010,7 +1011,7 @@ class ClassificationDataset(BaseDataset):
image = cv2.imread(image_path)
if image is None:
raise ValueError(f"Could not read image from path: {image_path}")
- return image
+ return cast(npt.NDArray[np.uint8], image)
def __len__(self) -> int:
return len(self._images_in_memory) or len(self.image_paths)
diff --git a/src/supervision/dataset/formats/coco.py b/src/supervision/dataset/formats/coco.py
index b7270ebd..ee331d0e 100644
--- a/src/supervision/dataset/formats/coco.py
+++ b/src/supervision/dataset/formats/coco.py
@@ -297,7 +297,7 @@ def detections_to_coco_annotations(
box_width, box_height = xyxy[2] - xyxy[0], xyxy[3] - xyxy[1]
segmentation: list[list[float]] | dict[str, list[int]] = []
if mask is not None:
- mask_bool = cast(npt.NDArray[np.bool_], mask)
+ mask_bool = mask
if "iscrowd" in data:
iscrowd = int(np.asarray(data["iscrowd"]).item())
else:
diff --git a/src/supervision/dataset/formats/pascal_voc.py b/src/supervision/dataset/formats/pascal_voc.py
index c2332377..1798b798 100644
--- a/src/supervision/dataset/formats/pascal_voc.py
+++ b/src/supervision/dataset/formats/pascal_voc.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import os
from pathlib import Path
from xml.etree.ElementTree import Element, SubElement
diff --git a/src/supervision/dataset/formats/yolo.py b/src/supervision/dataset/formats/yolo.py
index df0728d5..fd7da78a 100644
--- a/src/supervision/dataset/formats/yolo.py
+++ b/src/supervision/dataset/formats/yolo.py
@@ -2,8 +2,9 @@ from __future__ import annotations
import os
import warnings
+from collections.abc import Sequence
from pathlib import Path
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, cast
import numpy as np
import numpy.typing as npt
@@ -13,6 +14,7 @@ from tqdm.auto import tqdm
from supervision.config import ORIENTED_BOX_COORDINATES
from supervision.dataset.utils import approximate_mask_with_polygons
from supervision.detection.core import Detections
+from supervision.detection.utils._typing import _DetectionDataType
from supervision.detection.utils.converters import polygon_to_mask, polygon_to_xyxy
from supervision.utils.file import (
list_files_with_extensions,
@@ -41,7 +43,8 @@ def _parse_box(values: list[str]) -> npt.NDArray[np.float32]:
def _box_to_polygon(box: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
return np.array(
- [[box[0], box[1]], [box[2], box[1]], [box[2], box[3]], [box[0], box[3]]]
+ [[box[0], box[1]], [box[2], box[1]], [box[2], box[3]], [box[0], box[3]]],
+ dtype=np.float32,
)
@@ -50,7 +53,7 @@ def _parse_polygon(values: list[str]) -> npt.NDArray[np.float32]:
def _polygons_to_masks(
- polygons: list[npt.NDArray[np.number]], resolution_wh: tuple[int, int]
+ polygons: Sequence[npt.NDArray[np.number]], resolution_wh: tuple[int, int]
) -> npt.NDArray[np.bool_]:
return np.array(
[
@@ -145,41 +148,44 @@ def yolo_annotations_to_detections(
if len(lines) == 0:
return Detections.empty()
- class_id, relative_xyxy, relative_polygon, relative_xyxyxyxy = [], [], [], []
+ class_id_list: list[int] = []
+ relative_xyxy_list: list[npt.NDArray[np.number]] = []
+ relative_polygon_list: list[npt.NDArray[np.float32]] = []
+ relative_xyxyxyxy_list: list[npt.NDArray[np.float32]] = []
w, h = resolution_wh
for line in lines:
values = line.split()
- class_id.append(int(values[0]))
+ class_id_list.append(int(values[0]))
if len(values) == 5:
box = _parse_box(values=values[1:])
- relative_xyxy.append(box)
+ relative_xyxy_list.append(box)
if with_masks:
- relative_polygon.append(_box_to_polygon(box=box))
+ relative_polygon_list.append(_box_to_polygon(box=box))
elif len(values) > 5:
polygon = _parse_polygon(values=values[1:])
- relative_xyxy.append(polygon_to_xyxy(polygon=polygon))
+ relative_xyxy_list.append(polygon_to_xyxy(polygon=polygon))
if is_obb:
- relative_xyxyxyxy.append(np.array(values[1:]))
+ relative_xyxyxyxy_list.append(np.array(values[1:], dtype=np.float32))
if with_masks:
- relative_polygon.append(polygon)
+ relative_polygon_list.append(polygon)
- class_id = np.array(class_id, dtype=int)
- relative_xyxy = np.array(relative_xyxy, dtype=np.float32)
+ class_id = np.array(class_id_list, dtype=int)
+ relative_xyxy = np.array(relative_xyxy_list, dtype=np.float32)
xyxy = relative_xyxy * np.array([w, h, w, h], dtype=np.float32)
- data = {}
+ data: _DetectionDataType = {}
if is_obb:
- relative_xyxyxyxy = np.array(relative_xyxyxyxy, dtype=np.float32)
+ relative_xyxyxyxy = np.array(relative_xyxyxyxy_list, dtype=np.float32)
xyxyxyxy = relative_xyxyxyxy.reshape(-1, 4, 2)
xyxyxyxy *= np.array([w, h], dtype=np.float32)
- data[ORIENTED_BOX_COORDINATES] = xyxyxyxy
+ data[ORIENTED_BOX_COORDINATES] = cast(npt.NDArray[np.generic], xyxyxyxy)
if not with_masks:
return Detections(class_id=class_id, xyxy=xyxy, data=data)
polygons = [
polygon * np.array(resolution_wh, dtype=np.float32)
- for polygon in relative_polygon
+ for polygon in relative_polygon_list
]
mask = _polygons_to_masks(polygons=polygons, resolution_wh=resolution_wh)
return Detections(class_id=class_id, xyxy=xyxy, data=data, mask=mask)
diff --git a/src/supervision/dataset/utils.py b/src/supervision/dataset/utils.py
index c0bceb47..70c5b445 100644
--- a/src/supervision/dataset/utils.py
+++ b/src/supervision/dataset/utils.py
@@ -5,7 +5,7 @@ import os
import random
import shutil
from pathlib import Path
-from typing import TYPE_CHECKING, Any, TypeVar
+from typing import TYPE_CHECKING, TypeVar, cast
import cv2
import numpy as np
@@ -32,16 +32,16 @@ def mask_to_rle(
mask: npt.NDArray[np.bool_], compressed: bool = False
) -> list[int] | str:
"""Deprecated. Use `supervision.detection.utils.converters.mask_to_rle`."""
- return void(mask, compressed) # type: ignore[no-any-return]
+ return cast(list[int] | str, void(mask, compressed))
@deprecated(target=_rle_to_mask, deprecated_in="0.28.0", remove_in="0.30.0") # type: ignore[untyped-decorator]
def rle_to_mask(
- rle: npt.NDArray[np.integer[Any]] | list[int] | str | bytes,
+ rle: npt.NDArray[np.integer] | list[int] | str | bytes,
resolution_wh: tuple[int, int],
) -> npt.NDArray[np.bool_]:
"""Deprecated. Use `supervision.detection.utils.converters.rle_to_mask`."""
- return void(rle, resolution_wh)
+ return cast(npt.NDArray[np.bool_], void(rle, resolution_wh))
if TYPE_CHECKING:
@@ -61,7 +61,7 @@ def approximate_mask_with_polygons(
minimum_detection_area = min_image_area_percentage * image_area
maximum_detection_area = max_image_area_percentage * image_area
- polygons = mask_to_polygons(mask=mask)
+ polygons = cast(list[npt.NDArray[np.number]], mask_to_polygons(mask=mask))
if len(polygons) == 1:
polygons = filter_polygons_by_area(
polygons=polygons, min_area=None, max_area=maximum_detection_area
diff --git a/src/supervision/detection/compact_mask.py b/src/supervision/detection/compact_mask.py
index b2ffdac0..9244159e 100644
--- a/src/supervision/detection/compact_mask.py
+++ b/src/supervision/detection/compact_mask.py
@@ -13,7 +13,7 @@ from __future__ import annotations
import os
from collections.abc import Iterator
-from typing import Any
+from typing import cast, overload
import numpy as np
import numpy.typing as npt
@@ -323,9 +323,10 @@ def _rle_resize(
col_cache: dict[int, list[int]] = {}
scaled_cols = []
for src_c in col_map:
- if src_c not in col_cache:
- col_cache[src_c] = _rle_scale_col(per_col[src_c], crop_h, row_map)
- scaled_cols.append(col_cache[src_c])
+ src_c_int = int(src_c)
+ if src_c_int not in col_cache:
+ col_cache[src_c_int] = _rle_scale_col(per_col[src_c_int], crop_h, row_map)
+ scaled_cols.append(col_cache[src_c_int])
return _rle_join_cols(scaled_cols, new_total)
@@ -476,7 +477,7 @@ class CompactMask:
def from_dense(
cls,
masks: npt.NDArray[np.bool_],
- xyxy: npt.NDArray[Any],
+ xyxy: npt.NDArray[np.number],
image_shape: tuple[int, int],
) -> CompactMask:
"""Create a :class:`CompactMask` from a dense ``(N, H, W)`` bool array.
@@ -720,7 +721,7 @@ class CompactMask:
return np.column_stack((x1, y1, x2, y2)).astype(np.int32, copy=False)
@property
- def dtype(self) -> np.dtype[Any]:
+ def dtype(self) -> np.dtype[np.bool_]:
"""Return ``np.dtype(bool)`` — always.
Returns:
@@ -763,7 +764,9 @@ class CompactMask:
"""
return np.array([_rle_area(rle) for rle in self._rles], dtype=np.int64)
- def sum(self, axis: int | tuple[int, ...] | None = None) -> npt.NDArray[Any] | int:
+ def sum(
+ self, axis: int | tuple[int, ...] | None = None
+ ) -> npt.NDArray[np.int64] | np.int64:
"""NumPy-compatible sum with a fast path for per-mask area.
When ``axis=(1, 2)``, returns the per-mask True-pixel count via
@@ -790,11 +793,32 @@ class CompactMask:
"""
if axis == (1, 2):
return self.area
- return self.to_dense().sum(axis=axis)
+ return cast(npt.NDArray[np.int64] | np.int64, self.to_dense().sum(axis=axis))
+
+ @overload
+ def __getitem__(self, index: int | np.integer) -> npt.NDArray[np.bool_]: ...
+
+ @overload
+ def __getitem__(
+ self,
+ index: slice
+ | list[int]
+ | list[bool]
+ | npt.NDArray[np.int_]
+ | npt.NDArray[np.bool_],
+ ) -> CompactMask: ...
def __getitem__(
self,
- index: int | slice | list[Any] | npt.NDArray[Any],
+ index: (
+ int
+ | np.integer
+ | slice
+ | list[int]
+ | list[bool]
+ | npt.NDArray[np.int_]
+ | npt.NDArray[np.bool_]
+ ),
) -> npt.NDArray[np.bool_] | CompactMask:
"""Index into the mask collection.
@@ -860,7 +884,9 @@ class CompactMask:
new_offsets: npt.NDArray[np.int32] = self._offsets[idx_arr]
return CompactMask(new_rles, new_crop_shapes, new_offsets, self._image_shape)
- def __array__(self, dtype: np.dtype[Any] | None = None) -> npt.NDArray[Any]:
+ def __array__(
+ self, dtype: np.dtype[np.generic] | None = None
+ ) -> npt.NDArray[np.generic]:
"""NumPy interop: materialise as a dense ``(N, H, W)`` array.
Called by ``np.asarray(compact_mask)`` and similar NumPy functions.
diff --git a/src/supervision/detection/core.py b/src/supervision/detection/core.py
index b130d1db..23d6edbe 100644
--- a/src/supervision/detection/core.py
+++ b/src/supervision/detection/core.py
@@ -19,6 +19,7 @@ from supervision.detection.tools.transformers import (
process_transformers_v4_segmentation_result,
process_transformers_v5_segmentation_result,
)
+from supervision.detection.utils._typing import _DetectionDataType, _MetadataType
from supervision.detection.utils.boxes import obb_polygon_area, xyxyxyxy_to_xyxy
from supervision.detection.utils.converters import (
mask_to_xyxy,
@@ -153,13 +154,13 @@ class Detections:
as the video name, camera parameters, timestamp, or other global metadata.
""" # noqa: E501 // docs
- xyxy: npt.NDArray[np.generic]
- mask: npt.NDArray[np.generic] | CompactMask | None = None
- confidence: npt.NDArray[np.generic] | None = None
- class_id: npt.NDArray[np.generic] | None = None
- tracker_id: npt.NDArray[np.generic] | None = None
- data: dict[str, npt.NDArray[np.generic] | list[Any]] = field(default_factory=dict)
- metadata: dict[str, Any] = field(default_factory=dict)
+ xyxy: npt.NDArray[np.number]
+ mask: npt.NDArray[np.bool_] | CompactMask | None = None
+ confidence: npt.NDArray[np.floating] | None = None
+ class_id: npt.NDArray[np.integer] | None = None
+ tracker_id: npt.NDArray[np.integer] | None = None
+ data: _DetectionDataType = field(default_factory=dict)
+ metadata: _MetadataType = field(default_factory=dict)
def __post_init__(self) -> None:
_validate_detections_fields(
@@ -181,12 +182,12 @@ class Detections:
self,
) -> Iterator[
tuple[
- npt.NDArray[np.generic],
- npt.NDArray[np.generic] | None,
+ npt.NDArray[np.number],
+ npt.NDArray[np.bool_] | None,
np.generic | None,
np.generic | None,
np.generic | None,
- dict[str, npt.NDArray[np.generic] | list[Any]],
+ _DetectionDataType,
]
]:
"""
@@ -206,13 +207,34 @@ class Detections:
def __eq__(self, other: object) -> bool:
if not isinstance(other, Detections):
return NotImplemented
+
+ def array_equal_or_none(
+ a: npt.NDArray[np.generic] | None,
+ b: npt.NDArray[np.generic] | None,
+ ) -> bool:
+ if a is None or b is None:
+ return a is b
+ return bool(np.array_equal(a, b))
+
+ def mask_equal(
+ a: npt.NDArray[np.generic] | CompactMask | None,
+ b: npt.NDArray[np.generic] | CompactMask | None,
+ ) -> bool:
+ if a is None or b is None:
+ return a is b
+ if isinstance(a, CompactMask):
+ return bool(a == b)
+ if isinstance(b, CompactMask):
+ return bool(b == a)
+ return bool(np.array_equal(a, b))
+
return all(
[
np.array_equal(self.xyxy, other.xyxy),
- np.array_equal(self.mask, other.mask),
- np.array_equal(self.class_id, other.class_id),
- np.array_equal(self.confidence, other.confidence),
- np.array_equal(self.tracker_id, other.tracker_id),
+ mask_equal(self.mask, other.mask),
+ array_equal_or_none(self.class_id, other.class_id),
+ array_equal_or_none(self.confidence, other.confidence),
+ array_equal_or_none(self.tracker_id, other.tracker_id),
is_data_equal(self.data, other.data),
is_metadata_equal(self.metadata, other.metadata),
]
@@ -302,7 +324,9 @@ class Detections:
)
if hasattr(ultralytics_results, "boxes") and ultralytics_results.boxes is None:
- masks = extract_ultralytics_masks(ultralytics_results)
+ masks = cast(
+ npt.NDArray[np.bool_], extract_ultralytics_masks(ultralytics_results)
+ )
return cls(
xyxy=mask_to_xyxy(masks),
mask=masks,
@@ -1891,7 +1915,7 @@ class Detections:
if vlm == VLM.PALIGEMMA:
assert isinstance(result, str)
xyxy, class_id, class_name = from_paligemma(result, **kwargs)
- data: dict[str, npt.NDArray[np.generic] | list[Any]] = {
+ data: _DetectionDataType = {
CLASS_NAME_DATA_FIELD: class_name,
}
return cls(xyxy=xyxy, class_id=class_id, data=data)
@@ -2176,25 +2200,31 @@ class Detections:
xyxy = np.vstack([d.xyxy for d in detections_list])
- def stack_or_none(
- name: str,
- ) -> npt.NDArray[np.generic] | CompactMask | None:
- if all(d.__getattribute__(name) is None for d in detections_list):
+ def stack_mask_or_none() -> npt.NDArray[np.generic] | CompactMask | None:
+ masks = [d.mask for d in detections_list]
+ if all(m is None for m in masks):
return None
- if any(d.__getattribute__(name) is None for d in detections_list):
- raise ValueError(f"All or none of the '{name}' fields must be None")
- if name == "mask":
- masks = [d.__getattribute__(name) for d in detections_list]
- if all(isinstance(m, CompactMask) for m in masks):
- return CompactMask.merge(masks)
- # Mixed or all-ndarray: __array__ auto-converts any CompactMask.
- return np.vstack([np.asarray(m) for m in masks])
- return np.hstack([d.__getattribute__(name) for d in detections_list])
+ if any(m is None for m in masks):
+ raise ValueError("All or none of the 'mask' fields must be None")
+ if all(isinstance(m, CompactMask) for m in masks):
+ return CompactMask.merge(cast(list[CompactMask], masks))
+ # Mixed or all-ndarray: __array__ auto-converts any CompactMask.
+ return cast(
+ npt.NDArray[np.generic], np.vstack([np.asarray(m) for m in masks])
+ )
- mask = stack_or_none("mask")
- confidence = stack_or_none("confidence")
- class_id = stack_or_none("class_id")
- tracker_id = stack_or_none("tracker_id")
+ def stack_or_none(name: str) -> npt.NDArray[np.generic] | None:
+ values = [getattr(d, name) for d in detections_list]
+ if all(v is None for v in values):
+ return None
+ if any(v is None for v in values):
+ raise ValueError(f"All or none of the '{name}' fields must be None")
+ return cast(npt.NDArray[np.generic], np.hstack(values))
+
+ mask = cast(npt.NDArray[np.bool_] | CompactMask | None, stack_mask_or_none())
+ confidence = cast(npt.NDArray[np.floating] | None, stack_or_none("confidence"))
+ class_id = cast(npt.NDArray[np.integer] | None, stack_or_none("class_id"))
+ tracker_id = cast(npt.NDArray[np.integer] | None, stack_or_none("tracker_id"))
data = merge_data([d.data for d in detections_list])
@@ -2230,14 +2260,18 @@ class Detections:
Raises:
ValueError: If the provided `anchor` is not supported.
"""
- xyxy = cast(npt.NDArray[np.number], self.xyxy)
+ xyxy = self.xyxy
+
+ def coordinates(
+ x: npt.NDArray[np.number], y: npt.NDArray[np.number]
+ ) -> npt.NDArray[np.generic]:
+ return cast(npt.NDArray[np.generic], np.array([x, y]).transpose())
+
if anchor == Position.CENTER:
- return np.array(
- [
- (xyxy[:, 0] + xyxy[:, 2]) / 2,
- (xyxy[:, 1] + xyxy[:, 3]) / 2,
- ]
- ).transpose()
+ return coordinates(
+ (xyxy[:, 0] + xyxy[:, 2]) / 2,
+ (xyxy[:, 1] + xyxy[:, 3]) / 2,
+ )
elif anchor == Position.CENTER_OF_MASS:
if self.mask is None:
raise ValueError(
@@ -2245,31 +2279,21 @@ class Detections:
)
return calculate_masks_centroids(masks=self.mask)
elif anchor == Position.CENTER_LEFT:
- return np.array(
- [
- xyxy[:, 0],
- (xyxy[:, 1] + xyxy[:, 3]) / 2,
- ]
- ).transpose()
+ return coordinates(xyxy[:, 0], (xyxy[:, 1] + xyxy[:, 3]) / 2)
elif anchor == Position.CENTER_RIGHT:
- return np.array(
- [
- xyxy[:, 2],
- (xyxy[:, 1] + xyxy[:, 3]) / 2,
- ]
- ).transpose()
+ return coordinates(xyxy[:, 2], (xyxy[:, 1] + xyxy[:, 3]) / 2)
elif anchor == Position.BOTTOM_CENTER:
- return np.array([(xyxy[:, 0] + xyxy[:, 2]) / 2, xyxy[:, 3]]).transpose()
+ return coordinates((xyxy[:, 0] + xyxy[:, 2]) / 2, xyxy[:, 3])
elif anchor == Position.BOTTOM_LEFT:
- return np.array([xyxy[:, 0], xyxy[:, 3]]).transpose()
+ return coordinates(xyxy[:, 0], xyxy[:, 3])
elif anchor == Position.BOTTOM_RIGHT:
- return np.array([xyxy[:, 2], xyxy[:, 3]]).transpose()
+ return coordinates(xyxy[:, 2], xyxy[:, 3])
elif anchor == Position.TOP_CENTER:
- return np.array([(xyxy[:, 0] + xyxy[:, 2]) / 2, xyxy[:, 1]]).transpose()
+ return coordinates((xyxy[:, 0] + xyxy[:, 2]) / 2, xyxy[:, 1])
elif anchor == Position.TOP_LEFT:
- return np.array([xyxy[:, 0], xyxy[:, 1]]).transpose()
+ return coordinates(xyxy[:, 0], xyxy[:, 1])
elif anchor == Position.TOP_RIGHT:
- return np.array([xyxy[:, 2], xyxy[:, 1]]).transpose()
+ return coordinates(xyxy[:, 2], xyxy[:, 1])
raise ValueError(f"{anchor} is not supported.")
@@ -2312,13 +2336,20 @@ class Detections:
return self
if isinstance(index, int):
index = [index]
+ array_index = cast(
+ slice | list[int] | npt.NDArray[np.integer | np.bool_], index
+ )
return Detections(
- xyxy=self.xyxy[index],
- mask=self.mask[index] if self.mask is not None else None,
- confidence=self.confidence[index] if self.confidence is not None else None,
- class_id=self.class_id[index] if self.class_id is not None else None,
- tracker_id=self.tracker_id[index] if self.tracker_id is not None else None,
- data=get_data_item(self.data, index),
+ xyxy=self.xyxy[array_index],
+ mask=self.mask[cast(Any, array_index)] if self.mask is not None else None,
+ confidence=(
+ self.confidence[array_index] if self.confidence is not None else None
+ ),
+ class_id=self.class_id[array_index] if self.class_id is not None else None,
+ tracker_id=(
+ self.tracker_id[array_index] if self.tracker_id is not None else None
+ ),
+ data=get_data_item(self.data, array_index),
metadata=self.metadata,
)
@@ -2402,7 +2433,9 @@ class Detections:
return self.mask.area
return np.array([np.sum(mask) for mask in self.mask])
if ORIENTED_BOX_COORDINATES in self.data:
- return obb_polygon_area(self.data[ORIENTED_BOX_COORDINATES])
+ return obb_polygon_area(
+ cast(npt.NDArray[np.number], self.data[ORIENTED_BOX_COORDINATES])
+ )
return self.box_area
@property
@@ -2492,18 +2525,24 @@ class Detections:
)
if class_agnostic:
- predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1)))
+ predictions = cast(
+ npt.NDArray[np.floating],
+ np.hstack((self.xyxy, self.confidence.reshape(-1, 1))),
+ )
else:
assert self.class_id is not None, (
"Detections class_id must be given for NMS to be executed. If you"
" intended to perform class agnostic NMS set class_agnostic=True."
)
- predictions = np.hstack(
- (
- self.xyxy,
- self.confidence.reshape(-1, 1),
- self.class_id.reshape(-1, 1),
- )
+ predictions = cast(
+ npt.NDArray[np.floating],
+ np.hstack(
+ (
+ self.xyxy,
+ self.confidence.reshape(-1, 1),
+ self.class_id.reshape(-1, 1),
+ )
+ ),
)
if self.mask is not None:
@@ -2581,18 +2620,24 @@ class Detections:
)
if class_agnostic:
- predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1)))
+ predictions = cast(
+ npt.NDArray[np.floating],
+ np.hstack((self.xyxy, self.confidence.reshape(-1, 1))),
+ )
else:
assert self.class_id is not None, (
"Detections class_id must be given for NMM to be executed. If you"
" intended to perform class agnostic NMM set class_agnostic=True."
)
- predictions = np.hstack(
- (
- self.xyxy,
- self.confidence.reshape(-1, 1),
- self.class_id.reshape(-1, 1),
- )
+ predictions = cast(
+ npt.NDArray[np.floating],
+ np.hstack(
+ (
+ self.xyxy,
+ self.confidence.reshape(-1, 1),
+ self.class_id.reshape(-1, 1),
+ )
+ ),
)
if self.mask is not None:
@@ -2697,6 +2742,7 @@ def _merge_detection_group(detections: list[Detections]) -> Detections:
all_xyxy = np.array([d.xyxy[0] for d in detections], dtype=np.float32)
areas = (all_xyxy[:, 2] - all_xyxy[:, 0]) * (all_xyxy[:, 3] - all_xyxy[:, 1])
+ confidence: npt.NDArray[np.floating] | None
if winner.confidence is not None:
total_area = float(areas.sum())
if total_area > 0:
@@ -2828,7 +2874,10 @@ def merge_inner_detection_object_pair(
if detections_1.mask is None and detections_2.mask is None:
merged_mask = None
else:
- merged_mask = np.logical_or(detections_1.mask, detections_2.mask)
+ merged_mask = np.logical_or(
+ cast(npt.NDArray[Any], detections_1.mask),
+ cast(npt.NDArray[Any], detections_2.mask),
+ )
if detections_1.confidence is None or detections_2.confidence is None:
winning_detection = detections_1
@@ -2871,7 +2920,11 @@ def merge_inner_detections_objects(
0
]
else:
- iou = box_iou_batch(detections_1.xyxy, detections_2.xyxy, overlap_metric)[0]
+ iou = box_iou_batch(
+ detections_1.xyxy,
+ detections_2.xyxy,
+ overlap_metric,
+ )[0]
if iou < threshold:
break
detections_1 = merge_inner_detection_object_pair(detections_1, detections_2)
diff --git a/src/supervision/detection/line_zone.py b/src/supervision/detection/line_zone.py
index 3178fca2..8a726e7b 100644
--- a/src/supervision/detection/line_zone.py
+++ b/src/supervision/detection/line_zone.py
@@ -1,11 +1,9 @@
-from __future__ import annotations
-
import math
import warnings
from collections import Counter, defaultdict, deque
from collections.abc import Iterable
from functools import lru_cache
-from typing import Any, Literal, cast
+from typing import Literal
import cv2
import numpy as np
@@ -99,7 +97,7 @@ class LineZone:
Position.BOTTOM_RIGHT,
),
minimum_crossing_threshold: int = 1,
- ):
+ ) -> None:
"""
Args:
start: The starting point of the line.
@@ -344,7 +342,7 @@ class LineZoneAnnotator:
display_text_box: bool = True,
text_orient_to_line: bool = False,
text_centered: bool = True,
- ):
+ ) -> None:
"""
A class for drawing the `LineZone` and its detected object count
on an image.
@@ -673,29 +671,33 @@ class LineZoneAnnotator:
annotation_shape = (annotation_dim, annotation_dim)
annotation_center = Point(annotation_dim // 2, annotation_dim // 2)
- annotation = np.zeros((*annotation_shape, 3), dtype=np.uint8)
- annotation_alpha = np.zeros((*annotation_shape, 1), dtype=np.uint8)
-
- text_args: dict[str, Any] = dict(
+ annotation: npt.NDArray[np.uint8] = np.zeros(
+ (*annotation_shape, 3), dtype=np.uint8
+ )
+ annotation_alpha: npt.NDArray[np.uint8] = np.zeros(
+ (*annotation_shape, 1), dtype=np.uint8
+ )
+ draw_text(
+ scene=annotation,
text=text,
text_anchor=annotation_center,
text_scale=text_scale,
text_thickness=text_thickness,
text_padding=text_padding,
- )
- draw_text(
- scene=annotation,
text_color=text_color,
background_color=text_box_color if text_box_show else None,
- **text_args,
)
draw_text(
scene=annotation_alpha,
+ text=text,
+ text_anchor=annotation_center,
+ text_scale=text_scale,
+ text_thickness=text_thickness,
+ text_padding=text_padding,
text_color=Color.WHITE,
background_color=Color.WHITE if text_box_show else None,
- **text_args,
)
- annotation = np.dstack((annotation, annotation_alpha))
+ annotation = np.dstack((annotation, annotation_alpha)).astype(np.uint8)
# Make sure text is displayed upright
if 90 < line_angle_degrees % 360 < 270:
@@ -705,9 +707,11 @@ class LineZoneAnnotator:
rotation_matrix = cv2.getRotationMatrix2D(
annotation_center.as_xy_float_tuple(), rotation_angle, scale=1
)
- annotation = cv2.warpAffine(annotation, rotation_matrix, annotation_shape)
+ annotation = cv2.warpAffine(
+ annotation, rotation_matrix, annotation_shape
+ ).astype(np.uint8)
- return cast(npt.NDArray[np.uint8], annotation)
+ return annotation
class LineZoneAnnotatorMulticlass:
@@ -728,7 +732,7 @@ class LineZoneAnnotatorMulticlass:
text_scale: float = 0.75,
text_thickness: int = 1,
force_draw_class_ids: bool = False,
- ):
+ ) -> None:
"""
Draw a table showing how many items of each class crossed each line.
diff --git a/src/supervision/detection/tools/inference_slicer.py b/src/supervision/detection/tools/inference_slicer.py
index 897403aa..ee40d4ab 100644
--- a/src/supervision/detection/tools/inference_slicer.py
+++ b/src/supervision/detection/tools/inference_slicer.py
@@ -4,10 +4,10 @@ import threading
import warnings
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor, as_completed
-from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
+from typing import TYPE_CHECKING, Any, Protocol, cast, runtime_checkable
if TYPE_CHECKING:
- from typing_extensions import TypeGuard
+ from typing import TypeGuard
import numpy as np
import numpy.typing as npt
@@ -74,7 +74,10 @@ def move_detections(
detections.xyxy = move_boxes(xyxy=detections.xyxy, offset=offset)
if ORIENTED_BOX_COORDINATES in detections.data:
detections.data[ORIENTED_BOX_COORDINATES] = move_oriented_boxes(
- xyxyxyxy=detections.data[ORIENTED_BOX_COORDINATES], offset=offset
+ xyxyxyxy=cast(
+ npt.NDArray[np.number], detections.data[ORIENTED_BOX_COORDINATES]
+ ),
+ offset=offset,
)
if detections.mask is not None:
if resolution_wh is None:
@@ -564,7 +567,10 @@ class InferenceSlicer:
slices = [crop_image(image=image, xyxy=offset) for offset in offsets]
resolution_wh = get_image_resolution_wh(image)
- detections_in_slices = self.callback(slices)
+ batch_callback = cast(
+ Callable[[list[npt.NDArray[Any]]], list[Detections]], self.callback
+ )
+ detections_in_slices = batch_callback(slices)
if not isinstance(detections_in_slices, list):
raise ValueError(
"Callback must return `list[Detections]` when `batch_size > 1`. "
diff --git a/src/supervision/detection/tools/polygon_zone.py b/src/supervision/detection/tools/polygon_zone.py
index acb95522..0beb4b62 100644
--- a/src/supervision/detection/tools/polygon_zone.py
+++ b/src/supervision/detection/tools/polygon_zone.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
from collections.abc import Iterable
from typing import Any, cast
@@ -57,7 +55,7 @@ class PolygonZone:
self,
polygon: npt.NDArray[np.int64],
triggering_anchors: Iterable[Position] = (Position.BOTTOM_CENTER,),
- ):
+ ) -> None:
self.polygon = polygon.astype(int)
self.triggering_anchors = triggering_anchors
if not list(self.triggering_anchors):
@@ -89,7 +87,7 @@ class PolygonZone:
"""
if len(detections) == 0:
self.current_count = 0
- return np.array([], dtype=bool)
+ return cast(npt.NDArray[np.bool_], np.array([], dtype=bool))
all_anchors = np.array(
[
@@ -105,7 +103,7 @@ class PolygonZone:
y_safe = np.clip(y, 0, mask_h - 1)
is_in_zone = np.all(in_bounds & self.mask[y_safe, x_safe], axis=0)
self.current_count = int(np.sum(is_in_zone))
- return is_in_zone.astype(bool)
+ return cast(npt.NDArray[np.bool_], is_in_zone.astype(bool))
class PolygonZoneAnnotator:
@@ -139,7 +137,7 @@ class PolygonZoneAnnotator:
text_padding: int = 10,
display_in_zone_count: bool = True,
opacity: float = 0,
- ):
+ ) -> None:
self.zone = zone
self.color = color
self.thickness = thickness
diff --git a/src/supervision/detection/tools/smoother.py b/src/supervision/detection/tools/smoother.py
index 9ebfd054..77305e29 100644
--- a/src/supervision/detection/tools/smoother.py
+++ b/src/supervision/detection/tools/smoother.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import warnings
from collections import defaultdict, deque
from copy import deepcopy
diff --git a/src/supervision/detection/tools/transformers.py b/src/supervision/detection/tools/transformers.py
index f535217a..6056a35e 100644
--- a/src/supervision/detection/tools/transformers.py
+++ b/src/supervision/detection/tools/transformers.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import io
from typing import Any, cast
diff --git a/src/supervision/detection/utils/_typing.py b/src/supervision/detection/utils/_typing.py
new file mode 100644
index 00000000..7b903ab3
--- /dev/null
+++ b/src/supervision/detection/utils/_typing.py
@@ -0,0 +1,8 @@
+from typing import Any, TypeAlias
+
+import numpy as np
+import numpy.typing as npt
+
+_DetectionDataValueType: TypeAlias = npt.NDArray[np.generic] | list[Any]
+_DetectionDataType: TypeAlias = dict[str, _DetectionDataValueType]
+_MetadataType: TypeAlias = dict[str, Any]
diff --git a/src/supervision/detection/utils/boxes.py b/src/supervision/detection/utils/boxes.py
index 3aa2498e..4f18699e 100644
--- a/src/supervision/detection/utils/boxes.py
+++ b/src/supervision/detection/utils/boxes.py
@@ -1,4 +1,4 @@
-from __future__ import annotations
+from typing import Any, cast
import numpy as np
import numpy.typing as npt
@@ -92,11 +92,11 @@ def pad_boxes(
if py is None:
py = px
- result = xyxy.copy()
+ result = cast(npt.NDArray[Any], xyxy.copy())
result[:, [0, 1]] -= [px, py]
result[:, [2, 3]] += [px, py]
- return result
+ return cast(npt.NDArray[np.number], result)
@deprecated( # type: ignore[untyped-decorator]
@@ -156,17 +156,17 @@ def denormalize_boxes(
```
"""
width, height = resolution_wh
- result = xyxy.copy()
+ result = cast(npt.NDArray[Any], xyxy.copy())
result[:, [0, 2]] = (result[:, [0, 2]] * width) / normalization_factor
result[:, [1, 3]] = (result[:, [1, 3]] * height) / normalization_factor
- return result
+ return cast(npt.NDArray[np.number], result)
def move_boxes(
- xyxy: npt.NDArray[np.float64], offset: npt.NDArray[np.int32]
-) -> npt.NDArray[np.float64]:
+ xyxy: npt.NDArray[np.number], offset: npt.NDArray[np.integer]
+) -> npt.NDArray[np.number]:
"""
Args:
xyxy: An array of shape `(n, 4)` containing the
@@ -196,8 +196,8 @@ def move_boxes(
def move_oriented_boxes(
- xyxyxyxy: npt.NDArray[np.float64], offset: npt.NDArray[np.int32]
-) -> npt.NDArray[np.float64]:
+ xyxyxyxy: npt.NDArray[np.number], offset: npt.NDArray[np.integer]
+) -> npt.NDArray[np.number]:
"""
Args:
xyxyxyxy: An array of shape `(n, 4, 2)` containing the
@@ -244,7 +244,7 @@ def move_oriented_boxes(
return xyxyxyxy + offset
-def obb_polygon_area(corners: npt.NDArray) -> npt.NDArray[np.float64]:
+def obb_polygon_area(corners: npt.NDArray[np.number]) -> npt.NDArray[np.float64]:
"""Compute the area of N oriented bounding boxes using the shoelace formula.
Args:
@@ -263,13 +263,13 @@ def obb_polygon_area(corners: npt.NDArray) -> npt.NDArray[np.float64]:
>>> obb_polygon_area(corners)
array([50.])
"""
- corners = np.asarray(corners)
+ corners = cast(npt.NDArray[np.number], np.asarray(corners))
if corners.ndim != 3 or corners.shape[-2:] != (4, 2):
raise ValueError(f"corners must have shape (N, 4, 2); got {corners.shape}")
x = corners[..., 0].astype(np.float64, copy=False)
y = corners[..., 1].astype(np.float64, copy=False)
cross = x * np.roll(y, -1, axis=-1) - y * np.roll(x, -1, axis=-1)
- return 0.5 * np.abs(np.sum(cross, axis=-1))
+ return cast(npt.NDArray[np.float64], 0.5 * np.abs(np.sum(cross, axis=-1)))
def xyxyxyxy_to_xyxy(
@@ -302,14 +302,14 @@ def xyxyxyxy_to_xyxy(
```
"""
- xyxyxyxy = np.asarray(xyxyxyxy)
+ xyxyxyxy = cast(npt.NDArray[np.number], np.asarray(xyxyxyxy))
if xyxyxyxy.ndim != 3 or xyxyxyxy.shape[-2:] != (4, 2):
raise ValueError(f"xyxyxyxy must have shape (N, 4, 2); got {xyxyxyxy.shape}")
x_min = xyxyxyxy[..., 0].min(axis=-1)
y_min = xyxyxyxy[..., 1].min(axis=-1)
x_max = xyxyxyxy[..., 0].max(axis=-1)
y_max = xyxyxyxy[..., 1].max(axis=-1)
- return np.stack([x_min, y_min, x_max, y_max], axis=-1)
+ return cast(npt.NDArray[np.number], np.stack([x_min, y_min, x_max, y_max], axis=-1))
def scale_boxes(
@@ -378,7 +378,7 @@ def spread_out_boxes(
if len(xyxy) == 0:
return xyxy
- xyxy_padded = pad_boxes(xyxy, px=1)
+ xyxy_padded = cast(npt.NDArray[Any], pad_boxes(xyxy, px=1))
for _ in range(max_iterations):
# NxN
iou = box_iou_batch(xyxy_padded, xyxy_padded)
diff --git a/src/supervision/detection/utils/converters.py b/src/supervision/detection/utils/converters.py
index a1652e47..764b9678 100644
--- a/src/supervision/detection/utils/converters.py
+++ b/src/supervision/detection/utils/converters.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
from typing import Any, cast
import cv2
@@ -79,7 +77,7 @@ def xywh_to_xyxy(xywh: npt.NDArray[np.number]) -> npt.NDArray[np.number]:
xyxy = xywh.copy()
xyxy[:, 2] = xywh[:, 0] + xywh[:, 2]
xyxy[:, 3] = xywh[:, 1] + xywh[:, 3]
- return xyxy
+ return cast(npt.NDArray[np.number], np.asarray(xyxy))
def xyxy_to_xywh(xyxy: npt.NDArray[np.number]) -> npt.NDArray[np.number]:
@@ -113,7 +111,7 @@ def xyxy_to_xywh(xyxy: npt.NDArray[np.number]) -> npt.NDArray[np.number]:
xywh = xyxy.copy()
xywh[:, 2] = xyxy[:, 2] - xyxy[:, 0]
xywh[:, 3] = xyxy[:, 3] - xyxy[:, 1]
- return xywh
+ return cast(npt.NDArray[np.number], np.asarray(xywh))
def xcycwh_to_xyxy(xcycwh: npt.NDArray[np.number]) -> npt.NDArray[np.number]:
@@ -149,7 +147,7 @@ def xcycwh_to_xyxy(xcycwh: npt.NDArray[np.number]) -> npt.NDArray[np.number]:
xyxy[:, 1] = xcycwh[:, 1] - xcycwh[:, 3] / 2
xyxy[:, 2] = xcycwh[:, 0] + xcycwh[:, 2] / 2
xyxy[:, 3] = xcycwh[:, 1] + xcycwh[:, 3] / 2
- return xyxy
+ return cast(npt.NDArray[np.number], np.asarray(xyxy))
def xyxy_to_xcycarh(xyxy: npt.NDArray[np.number]) -> npt.NDArray[np.floating]:
diff --git a/src/supervision/detection/utils/internal.py b/src/supervision/detection/utils/internal.py
index c5d09216..467c84b4 100644
--- a/src/supervision/detection/utils/internal.py
+++ b/src/supervision/detection/utils/internal.py
@@ -1,14 +1,13 @@
-from __future__ import annotations
-
import logging
from itertools import chain
-from typing import Any, Union, cast
+from typing import Any, cast
import cv2
import numpy as np
import numpy.typing as npt
from supervision.config import CLASS_NAME_DATA_FIELD
+from supervision.detection.utils._typing import _DetectionDataType, _MetadataType
from supervision.detection.utils.converters import polygon_to_mask, rle_to_mask
from supervision.geometry.core import Vector
@@ -59,7 +58,7 @@ def process_roboflow_result(
npt.NDArray[np.integer],
npt.NDArray[np.bool_] | None,
npt.NDArray[np.integer] | None,
- dict[str, npt.NDArray[np.generic]],
+ _DetectionDataType,
]:
"""Parse a Roboflow API or Inference package result into detection arrays.
@@ -194,7 +193,7 @@ def process_roboflow_result(
if tracker_ids and None not in tracker_ids
else None
)
- data: dict[str, npt.NDArray[np.generic]] = {CLASS_NAME_DATA_FIELD: class_name_arr}
+ data: _DetectionDataType = {CLASS_NAME_DATA_FIELD: class_name_arr}
return (
xyxy_arr,
@@ -207,8 +206,8 @@ def process_roboflow_result(
def is_data_equal(
- data_a: dict[str, npt.NDArray[np.generic] | list[Any]],
- data_b: dict[str, npt.NDArray[np.generic] | list[Any]],
+ data_a: _DetectionDataType,
+ data_b: _DetectionDataType,
) -> bool:
"""
Compares the data payloads of two Detections instances.
@@ -224,7 +223,7 @@ def is_data_equal(
)
-def is_metadata_equal(metadata_a: dict[str, Any], metadata_b: dict[str, Any]) -> bool:
+def is_metadata_equal(metadata_a: _MetadataType, metadata_b: _MetadataType) -> bool:
"""
Compares the metadata payloads of two Detections instances.
@@ -246,8 +245,8 @@ def is_metadata_equal(metadata_a: dict[str, Any], metadata_b: dict[str, Any]) ->
def merge_data(
- data_list: list[dict[str, npt.NDArray[np.generic] | list[Any]]],
-) -> dict[str, npt.NDArray[np.generic] | list[Any]]:
+ data_list: list[_DetectionDataType],
+) -> _DetectionDataType:
"""
Merges the data payloads of a list of Detections instances.
@@ -303,10 +302,10 @@ def merge_data(
f"types are allowed."
)
- return cast(dict[str, Union[npt.NDArray[np.generic], list[Any]]], merged_data)
+ return cast(_DetectionDataType, merged_data)
-def merge_metadata(metadata_list: list[dict[str, Any]]) -> dict[str, Any]:
+def merge_metadata(metadata_list: list[_MetadataType]) -> _MetadataType:
"""
Merge metadata from a list of metadata dictionaries.
@@ -333,7 +332,7 @@ def merge_metadata(metadata_list: list[dict[str, Any]]) -> dict[str, Any]:
if not all(keys_set == all_keys_sets[0] for keys_set in all_keys_sets):
raise ValueError("All metadata dictionaries must have the same keys to merge.")
- merged_metadata: dict[str, Any] = {}
+ merged_metadata: _MetadataType = {}
for metadata in metadata_list:
for key, value in metadata.items():
if key not in merged_metadata:
@@ -361,9 +360,9 @@ def merge_metadata(metadata_list: list[dict[str, Any]]) -> dict[str, Any]:
def get_data_item(
- data: dict[str, npt.NDArray[np.generic] | list[Any]],
+ data: _DetectionDataType,
index: int | slice | list[int] | npt.NDArray[np.integer | np.bool_],
-) -> dict[str, npt.NDArray[np.generic] | list[Any]]:
+) -> _DetectionDataType:
"""
Retrieve a subset of the data dictionary based on the given index.
@@ -374,7 +373,7 @@ def get_data_item(
Returns:
A subset of the data dictionary corresponding to the specified index.
"""
- subset_data: dict[str, npt.NDArray[np.generic] | list[Any]] = {}
+ subset_data: _DetectionDataType = {}
for key, value in data.items():
if isinstance(value, np.ndarray):
subset_data[key] = value[index]
diff --git a/src/supervision/detection/utils/iou_and_nms.py b/src/supervision/detection/utils/iou_and_nms.py
index e32eb31e..f27092fe 100644
--- a/src/supervision/detection/utils/iou_and_nms.py
+++ b/src/supervision/detection/utils/iou_and_nms.py
@@ -518,8 +518,13 @@ def oriented_box_iou_batch(
# Capture identity before reshape: NMS / NMM pass the same array twice, so
# the matrix is symmetric and we can compute only its upper triangle.
is_self_comparison = boxes_true is boxes_detection
- boxes_true = boxes_true.reshape(-1, 4, 2).astype(np.float64)
- boxes_detection = boxes_detection.reshape(-1, 4, 2).astype(np.float64)
+ boxes_true = cast(
+ npt.NDArray[np.floating], boxes_true.reshape(-1, 4, 2).astype(np.float64)
+ )
+ boxes_detection = cast(
+ npt.NDArray[np.floating],
+ boxes_detection.reshape(-1, 4, 2).astype(np.float64),
+ )
n, m = len(boxes_true), len(boxes_detection)
if n == 0 or m == 0:
@@ -694,11 +699,15 @@ def _mask_iou_batch_split(
# ~4096x4096) we promote to float64 so the counts stay exact.
pixels = int(np.prod(masks_true.shape[1:]))
count_dtype = np.float32 if pixels <= 2**24 else np.float64
- true_flat = masks_true.reshape(masks_true.shape[0], pixels).astype(
- count_dtype, copy=False
+ true_flat = cast(
+ npt.NDArray[np.floating],
+ masks_true.reshape(masks_true.shape[0], pixels).astype(count_dtype, copy=False),
)
- detection_flat = masks_detection.reshape(masks_detection.shape[0], pixels).astype(
- count_dtype, copy=False
+ detection_flat = cast(
+ npt.NDArray[np.floating],
+ masks_detection.reshape(masks_detection.shape[0], pixels).astype(
+ count_dtype, copy=False
+ ),
)
with np.errstate(divide="ignore", over="ignore", invalid="ignore"):
intersection_area: npt.NDArray[np.floating[Any]] = true_flat @ detection_flat.T
@@ -734,8 +743,8 @@ def _mask_iou_batch_split(
def mask_iou_batch(
- masks_true: npt.NDArray[Any],
- masks_detection: npt.NDArray[Any],
+ masks_true: npt.NDArray[Any] | CompactMask,
+ masks_detection: npt.NDArray[Any] | CompactMask,
overlap_metric: OverlapMetric = OverlapMetric.IOU,
memory_limit: int = 1024 * 5,
) -> npt.NDArray[np.floating]:
@@ -831,7 +840,7 @@ def mask_iou_batch(
def mask_non_max_suppression(
predictions: npt.NDArray[np.floating],
- masks: npt.NDArray[Any],
+ masks: npt.NDArray[Any] | CompactMask,
iou_threshold: float = 0.5,
overlap_metric: OverlapMetric = OverlapMetric.IOU,
mask_dimension: int = 640,
@@ -893,7 +902,7 @@ def mask_non_max_suppression(
condition[row_idx + 1 :], False, keep[row_idx + 1 :]
)
- return cast(npt.NDArray[np.bool_], keep[sort_index.argsort()])
+ return keep[sort_index.argsort()]
def _prepare_predictions_for_nms(
@@ -967,12 +976,12 @@ def box_non_max_suppression(
sort_index, predictions, categories = _prepare_predictions_for_nms(predictions)
ious = box_iou_batch(predictions[:, :4], predictions[:, :4], overlap_metric)
keep = _nms_loop_from_iou_matrix(ious, categories, iou_threshold)
- return cast(npt.NDArray[np.bool_], keep[sort_index.argsort()])
+ return keep[sort_index.argsort()]
def _group_overlapping_masks(
- predictions: npt.NDArray[np.float64],
- masks: npt.NDArray[np.float64],
+ predictions: npt.NDArray[np.floating],
+ masks: npt.NDArray[np.bool_],
iou_threshold: float = 0.5,
overlap_metric: OverlapMetric = OverlapMetric.IOU,
) -> list[list[int]]:
@@ -1029,7 +1038,7 @@ def _group_overlapping_masks(
def mask_non_max_merge(
predictions: npt.NDArray[np.floating],
- masks: npt.NDArray[Any],
+ masks: npt.NDArray[Any] | CompactMask,
iou_threshold: float = 0.5,
mask_dimension: int = 640,
overlap_metric: OverlapMetric = OverlapMetric.IOU,
@@ -1103,7 +1112,7 @@ def mask_non_max_merge(
def _greedy_nmm_via_iou_callback(
- predictions: npt.NDArray[np.float64],
+ predictions: npt.NDArray[np.floating],
iou_against_candidate: Callable[
[npt.NDArray[np.int_], int], npt.NDArray[np.floating]
],
@@ -1134,7 +1143,7 @@ def _greedy_nmm_via_iou_callback(
def _non_max_merge_per_category(
- predictions: npt.NDArray[np.float64],
+ predictions: npt.NDArray[np.floating],
group_within: Callable[[npt.NDArray[np.int_]], list[list[int]]],
) -> list[list[int]]:
"""Dispatch NMM grouping per class, then translate local indices back to
@@ -1167,7 +1176,7 @@ def _non_max_merge_per_category(
def _group_overlapping_boxes(
- predictions: npt.NDArray[np.float64],
+ predictions: npt.NDArray[np.floating],
iou_threshold: float = 0.5,
overlap_metric: OverlapMetric = OverlapMetric.IOU,
) -> list[list[int]]:
@@ -1204,7 +1213,7 @@ def _group_overlapping_boxes(
def box_non_max_merge(
- predictions: npt.NDArray[np.float64],
+ predictions: npt.NDArray[np.floating],
iou_threshold: float = 0.5,
overlap_metric: OverlapMetric = OverlapMetric.IOU,
) -> list[list[int]]:
@@ -1327,7 +1336,7 @@ def oriented_box_non_max_suppression(
# same object intentional — triggers upper-triangle optimization
ious = oriented_box_iou_batch(oriented_boxes, oriented_boxes, overlap_metric)
keep = _nms_loop_from_iou_matrix(ious, categories, iou_threshold)
- return cast(npt.NDArray[np.bool_], keep[sort_index.argsort()])
+ return keep[sort_index.argsort()]
def _group_overlapping_oriented_boxes(
diff --git a/src/supervision/detection/utils/masks.py b/src/supervision/detection/utils/masks.py
index e8dd4322..c7a32e89 100644
--- a/src/supervision/detection/utils/masks.py
+++ b/src/supervision/detection/utils/masks.py
@@ -1,6 +1,4 @@
-from __future__ import annotations
-
-from typing import Any, Literal, cast
+from typing import Literal, cast
import cv2
import numpy as np
@@ -11,7 +9,7 @@ from supervision.detection.compact_mask import CompactMask
def move_masks(
masks: npt.NDArray[np.bool_],
- offset: npt.NDArray[np.int32],
+ offset: npt.NDArray[np.integer],
resolution_wh: tuple[int, int],
) -> npt.NDArray[np.bool_]:
"""
@@ -88,7 +86,7 @@ def move_masks(
def calculate_masks_centroids(
- masks: npt.NDArray[Any] | CompactMask,
+ masks: npt.NDArray[np.bool_] | CompactMask,
) -> npt.NDArray[np.int_]:
"""
Calculate the centroids of binary masks in a tensor.
@@ -260,7 +258,9 @@ def contains_multiple_segments(
return bool(number_of_labels > 2)
-def resize_masks(masks: npt.NDArray[Any], max_dimension: int = 640) -> npt.NDArray[Any]:
+def resize_masks(
+ masks: npt.NDArray[np.bool_], max_dimension: int = 640
+) -> npt.NDArray[np.bool_]:
"""
Resize all masks in the array to have a maximum dimension of max_dimension,
maintaining aspect ratio.
@@ -374,19 +374,17 @@ def filter_segments_by_distance(
height, width = mask.shape
if not np.any(mask):
- return mask.copy()
+ return cast(npt.NDArray[np.bool_], mask.copy())
- image = mask.astype(np.uint8)
- num_labels: int
- labels: npt.NDArray[np.int32]
- stats: npt.NDArray[np.int32]
- centroids: npt.NDArray[np.float64]
- num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(
- image, connectivity=connectivity
- )
+ image = cast(npt.NDArray[np.uint8], mask.astype(np.uint8))
+ components = cv2.connectedComponentsWithStats(image, connectivity=connectivity)
+ num_labels = int(components[0])
+ labels = cast(npt.NDArray[np.int32], components[1])
+ stats = cast(npt.NDArray[np.int32], components[2])
+ centroids = cast(npt.NDArray[np.float64], components[3])
if num_labels <= 1:
- return mask.copy()
+ return cast(npt.NDArray[np.bool_], mask.copy())
areas = stats[1:, cv2.CC_STAT_AREA]
main_label = 1 + int(np.argmax(areas))
diff --git a/src/supervision/detection/utils/polygons.py b/src/supervision/detection/utils/polygons.py
index 5d86acfe..4b749e90 100644
--- a/src/supervision/detection/utils/polygons.py
+++ b/src/supervision/detection/utils/polygons.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import cv2
import numpy as np
import numpy.typing as npt
diff --git a/src/supervision/detection/utils/vlms.py b/src/supervision/detection/utils/vlms.py
index 8a30abd7..24a784e5 100644
--- a/src/supervision/detection/utils/vlms.py
+++ b/src/supervision/detection/utils/vlms.py
@@ -1,6 +1,3 @@
-from __future__ import annotations
-
-
def edit_distance(string_1: str, string_2: str, case_sensitive: bool = True) -> int:
"""
Calculates the minimum number of single-character edits required
diff --git a/src/supervision/draw/base.py b/src/supervision/draw/base.py
index ce6e6f70..0f3d50c6 100644
--- a/src/supervision/draw/base.py
+++ b/src/supervision/draw/base.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
from typing import TypeVar
import numpy as np
diff --git a/src/supervision/draw/utils.py b/src/supervision/draw/utils.py
index 8d7bb679..057a39af 100644
--- a/src/supervision/draw/utils.py
+++ b/src/supervision/draw/utils.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import os
from typing import cast
diff --git a/src/supervision/geometry/utils.py b/src/supervision/geometry/utils.py
index 0c3da397..e9a95a31 100644
--- a/src/supervision/geometry/utils.py
+++ b/src/supervision/geometry/utils.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import numpy as np
import numpy.typing as npt
diff --git a/src/supervision/key_points/annotators.py b/src/supervision/key_points/annotators.py
index 02db1c9a..26e0daee 100644
--- a/src/supervision/key_points/annotators.py
+++ b/src/supervision/key_points/annotators.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
from abc import ABC, abstractmethod
from collections.abc import Sequence
from typing import cast
diff --git a/src/supervision/key_points/core.py b/src/supervision/key_points/core.py
index 02f73bb4..f0b10a0c 100644
--- a/src/supervision/key_points/core.py
+++ b/src/supervision/key_points/core.py
@@ -3,13 +3,14 @@ from __future__ import annotations
import logging
from collections.abc import Iterable, Iterator
from dataclasses import dataclass, field
-from typing import Any, Union, cast
+from typing import Any, cast
import numpy as np
import numpy.typing as npt
from supervision.config import CLASS_NAME_DATA_FIELD
from supervision.detection.core import Detections
+from supervision.detection.utils._typing import _DetectionDataType
from supervision.detection.utils.internal import get_data_item, is_data_equal
from supervision.detection.utils.iou_and_nms import (
OverlapMetric,
@@ -20,16 +21,12 @@ from supervision.validators import _validate_keypoints_fields
logger = logging.getLogger(__name__)
-Index1D = Union[
- int, slice, list[int], list[bool], npt.NDArray[np.int_], npt.NDArray[np.bool_]
-]
+Index1D = (
+ int | slice | list[int] | list[bool] | npt.NDArray[np.int_] | npt.NDArray[np.bool_]
+)
Index2D = tuple[Index1D, Index1D]
-_RowIndexInput = Union[int, np.integer[Any], npt.NDArray[np.generic], list[Any], slice]
-_NormalizedRowIndex = Union[
- npt.NDArray[np.generic],
- list[Any],
- slice,
-]
+_RowIndexInput = int | np.integer[Any] | npt.NDArray[np.generic] | list[Any] | slice
+_NormalizedRowIndex = npt.NDArray[np.generic] | list[Any] | slice
def _optional_array_equal(
@@ -233,7 +230,7 @@ class KeyPoints:
keypoint_confidence: npt.NDArray[np.float32] | None = None
detection_confidence: npt.NDArray[np.float32] | None = None
visible: npt.NDArray[np.bool_] | None = None
- data: dict[str, npt.NDArray[np.generic] | list[Any]] = field(default_factory=dict)
+ data: _DetectionDataType = field(default_factory=dict)
def __init__(
self,
@@ -242,7 +239,7 @@ class KeyPoints:
keypoint_confidence: npt.NDArray[np.float32] | None = None,
detection_confidence: npt.NDArray[np.float32] | None = None,
visible: npt.NDArray[np.bool_] | None = None,
- data: dict[str, npt.NDArray[np.generic] | list[Any]] | None = None,
+ data: _DetectionDataType | None = None,
*,
confidence: npt.NDArray[np.float32] | None = None,
) -> None:
@@ -342,7 +339,7 @@ class KeyPoints:
npt.NDArray[np.float32],
npt.NDArray[np.float32] | None,
npt.NDArray[np.int_] | None,
- dict[str, npt.NDArray[np.generic] | list[Any]],
+ _DetectionDataType,
]
]:
"""
@@ -450,9 +447,7 @@ class KeyPoints:
class_id.append(prediction["class_id"])
class_names.append(prediction["class"])
- data: dict[str, npt.NDArray[np.generic] | list[Any]] = {
- CLASS_NAME_DATA_FIELD: np.array(class_names)
- }
+ data: _DetectionDataType = {CLASS_NAME_DATA_FIELD: np.array(class_names)}
return cls(
xy=np.array(xy, dtype=np.float32),
@@ -621,9 +616,7 @@ class KeyPoints:
class_names = np.array([ultralytics_results.names[i] for i in class_id])
confidence = ultralytics_results.keypoints.conf.cpu().numpy()
- data: dict[str, npt.NDArray[np.generic] | list[Any]] = {
- CLASS_NAME_DATA_FIELD: class_names
- }
+ data: _DetectionDataType = {CLASS_NAME_DATA_FIELD: class_names}
return cls(xy=xy, class_id=class_id, keypoint_confidence=confidence, data=data)
@classmethod
@@ -669,7 +662,7 @@ class KeyPoints:
else:
class_id = None
- data: dict[str, npt.NDArray[np.generic] | list[Any]] = {}
+ data: _DetectionDataType = {}
if class_id is not None and yolo_nas_results.class_names is not None:
class_names = []
for c_id in class_id:
diff --git a/src/supervision/key_points/skeletons.py b/src/supervision/key_points/skeletons.py
index 414a8460..71edfd65 100644
--- a/src/supervision/key_points/skeletons.py
+++ b/src/supervision/key_points/skeletons.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
from enum import Enum
Edges = tuple[tuple[int, int], ...]
diff --git a/src/supervision/metrics/core.py b/src/supervision/metrics/core.py
index ad6a1781..347b61dd 100644
--- a/src/supervision/metrics/core.py
+++ b/src/supervision/metrics/core.py
@@ -2,16 +2,18 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum
-from typing import Any
+from typing import Any, Generic, TypeVar
+
+R = TypeVar("R")
-class Metric(ABC):
+class Metric(ABC, Generic[R]):
"""
The base class for all supervision metrics.
"""
@abstractmethod
- def update(self, *args: Any, **kwargs: Any) -> Metric:
+ def update(self, *args: Any, **kwargs: Any) -> Metric[R]:
"""
Add data to the metric, without computing the result.
Return the metric itself to allow method chaining.
@@ -26,7 +28,7 @@ class Metric(ABC):
raise NotImplementedError
@abstractmethod
- def compute(self, *args: Any, **kwargs: Any) -> Any:
+ def compute(self, *args: Any, **kwargs: Any) -> R:
"""
Compute the metric from the internal state and return the result.
"""
diff --git a/src/supervision/metrics/detection.py b/src/supervision/metrics/detection.py
index 15795e49..558ceecd 100644
--- a/src/supervision/metrics/detection.py
+++ b/src/supervision/metrics/detection.py
@@ -283,8 +283,8 @@ def _split_detections_by_outcome(
)
else:
iou_matrix = box_iou_batch(
- boxes_true=cast(npt.NDArray[np.number], targets.xyxy),
- boxes_detection=cast(npt.NDArray[np.number], filtered_predictions.xyxy),
+ boxes_true=targets.xyxy,
+ boxes_detection=filtered_predictions.xyxy,
)
target_candidate_indices, prediction_candidate_indices = np.where(
@@ -483,7 +483,7 @@ def _annotate_detection_panel(
title_thickness,
cv2.LINE_AA,
)
- return panel
+ return cast(npt.NDArray[np.uint8], panel)
def _save_detection_validation_visualization(
diff --git a/src/supervision/metrics/f1_score.py b/src/supervision/metrics/f1_score.py
index 8cb0f7fa..f2f5eab6 100644
--- a/src/supervision/metrics/f1_score.py
+++ b/src/supervision/metrics/f1_score.py
@@ -2,7 +2,7 @@ from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, cast
import numpy as np
import numpy.typing as npt
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
import pandas as pd
-class F1Score(Metric):
+class F1Score(Metric["F1ScoreResult"]):
"""
F1 Score is a metric used to evaluate object detection models. It is the harmonic
mean of precision and recall, calculated at different IoU thresholds.
@@ -160,7 +160,7 @@ class F1Score(Metric):
is ``zeros((0,))``.
- Targets present: IoU matching produces ``matches`` array.
"""
- iou_thresholds = np.linspace(0.5, 0.95, 10)
+ iou_thresholds = np.linspace(0.5, 0.95, 10, dtype=np.float32)
stats: list[Any] = []
for predictions, targets in zip(predictions_list, targets_list):
@@ -453,12 +453,10 @@ class F1Score(Metric):
def _detections_content(self, detections: Detections) -> npt.NDArray[Any]:
"""Return boxes, masks or oriented bounding boxes from detections."""
if self._metric_target == MetricTarget.BOXES:
- result_boxes: npt.NDArray[np.float32] = detections.xyxy
- return result_boxes
+ return cast(npt.NDArray[Any], detections.xyxy)
if self._metric_target == MetricTarget.MASKS:
if detections.mask is not None:
- result_masks: npt.NDArray[np.bool_] = detections.mask
- return result_masks
+ return cast(npt.NDArray[Any], detections.mask)
return self._make_empty_content()
if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
obb = detections.data.get(ORIENTED_BOX_COORDINATES)
diff --git a/src/supervision/metrics/mean_average_precision.py b/src/supervision/metrics/mean_average_precision.py
index 8d598da9..1b4e14a2 100644
--- a/src/supervision/metrics/mean_average_precision.py
+++ b/src/supervision/metrics/mean_average_precision.py
@@ -7,7 +7,7 @@ from collections import defaultdict
from copy import deepcopy
from dataclasses import dataclass
from enum import Enum
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, TypeAlias, TypedDict
import numpy as np
import numpy.typing as npt
@@ -26,6 +26,40 @@ if TYPE_CHECKING:
import pandas as pd
+class _TypeCocoDict(TypedDict, total=False):
+ id: int
+ image_id: int
+ category_id: int
+ bbox: list[float]
+ area: float
+ iscrowd: int
+ ignore: int
+ _ignore: int
+ score: float
+ segmentation: list[list[float]]
+ name: str
+ supercategory: str
+ caption: str
+ keypoints: list[float]
+
+
+_TypeCocoDataset: TypeAlias = dict[str, list[_TypeCocoDict]]
+
+
+class _TypeEvaluationImageResult(TypedDict):
+ image_id: int
+ category_id: int
+ area_range: list[float] | tuple[float, float]
+ max_det: int
+ dt_ids: list[int]
+ gt_ids: list[int]
+ dtMatches: npt.NDArray[np.int64]
+ gtMatches: npt.NDArray[np.int64]
+ dtScores: list[float]
+ gtIgnore: npt.NDArray[np.int64]
+ dtIgnore: npt.NDArray[np.bool_]
+
+
@dataclass
class MeanAveragePrecisionResult:
"""
@@ -152,7 +186,7 @@ class MeanAveragePrecisionResult:
ensure_pandas_installed()
import pandas as pd
- pandas_data = {
+ pandas_data: dict[str, object] = {
"mAP@50:95": self.map50_95,
"mAP@50": self.map50,
"mAP@75": self.map75,
@@ -252,7 +286,7 @@ class EvaluationDataset:
`COCOEvaluator` class.
"""
- def __init__(self, targets: dict[str, Any] | None = None):
+ def __init__(self, targets: _TypeCocoDataset | None = None) -> None:
"""
Constructor of EvaluationDataset object used to evaluate models with
Mean Average Precision.
@@ -263,11 +297,11 @@ class EvaluationDataset:
"""
# Initialize members
# Initialize members
- self.dataset: dict[str, Any] = dict()
- self.anns: dict[int, Any] = dict()
- self.cats: dict[int, Any] = dict()
- self.imgs: dict[int, Any] = dict()
- self.img_to_anns: dict[int, list[Any]] = defaultdict(list)
+ self.dataset: _TypeCocoDataset = {}
+ self.anns: dict[int, _TypeCocoDict] = {}
+ self.cats: dict[int, _TypeCocoDict] = {}
+ self.imgs: dict[int, _TypeCocoDict] = {}
+ self.img_to_anns: dict[int, list[_TypeCocoDict]] = defaultdict(list)
self.cat_to_imgs: dict[int, list[int]] = defaultdict(list)
if targets is None:
@@ -285,8 +319,11 @@ class EvaluationDataset:
"""
Create index elements for the dataset.
"""
- anns, cats, imgs = {}, {}, {}
- img_to_anns, cat_to_imgs = defaultdict(list), defaultdict(list)
+ anns: dict[int, _TypeCocoDict] = {}
+ cats: dict[int, _TypeCocoDict] = {}
+ imgs: dict[int, _TypeCocoDict] = {}
+ img_to_anns: dict[int, list[_TypeCocoDict]] = defaultdict(list)
+ cat_to_imgs: dict[int, list[int]] = defaultdict(list)
if "annotations" in self.dataset:
for ann in self.dataset["annotations"]:
img_to_anns[ann["image_id"]].append(ann)
@@ -442,7 +479,7 @@ class EvaluationDataset:
return list(ids_set)
- def get_annotations(self, ids: list[int] | None = None) -> list[dict[str, Any]]:
+ def get_annotations(self, ids: list[int] | None = None) -> list[_TypeCocoDict]:
"""
Get annotations with the specified ids.
@@ -456,7 +493,7 @@ class EvaluationDataset:
return []
return [self.anns[idx] for idx in ids]
- def load_predictions(self, predictions: list[dict[str, Any]]) -> EvaluationDataset:
+ def load_predictions(self, predictions: list[_TypeCocoDict]) -> EvaluationDataset:
"""
Load prediction result into an EvaluationDataset object.
@@ -468,7 +505,7 @@ class EvaluationDataset:
"""
# Create an empty EvaluationDataset object for the predictions
predictions_dataset = EvaluationDataset.empty()
- predictions_dataset.dataset["images"] = [img for img in self.dataset["images"]]
+ predictions_dataset.dataset["images"] = list(self.dataset["images"])
if not isinstance(predictions, list):
raise ValueError("results must be a list")
@@ -588,7 +625,7 @@ class COCOEvaluator:
def __init__(
self, coco_targets: EvaluationDataset, coco_predictions: EvaluationDataset
- ):
+ ) -> None:
"""
Constructor of COCOEvaluator object.
@@ -606,18 +643,22 @@ class COCOEvaluator:
# List of dictionaries containing the evaluation results
# len(eval_imgs) = (categories) * (area_ranges) * (images)
# For COCO 2017: len(eval_images) = 80 * 4 * 5000 = 1600000
- self.eval_imgs: Any = defaultdict(list)
+ self.eval_imgs: list[_TypeEvaluationImageResult | None] = []
# Dictionary of accumulated results
- self.results: dict[str, Any] = {}
+ self.results: dict[str, object] = {}
# Dictionary of targets for evaluation
- self._targets: defaultdict[tuple[int, int], list[Any]] = defaultdict(list)
- self._predictions: defaultdict[tuple[int, int], list[Any]] = defaultdict(list)
+ self._targets: defaultdict[tuple[int, int], list[_TypeCocoDict]] = defaultdict(
+ list
+ )
+ self._predictions: defaultdict[tuple[int, int], list[_TypeCocoDict]] = (
+ defaultdict(list)
+ )
# Parameters for evaluation
self.params = COCOEvaluatorParameters()
# List of results summarization
- self.stats: list[Any] = []
+ self.stats: list[object] = []
# Dictionary of IOUs between all targets and predictions
- self.ious: dict[tuple[int, int], Any] = {}
+ self.ious: dict[tuple[int, int], npt.NDArray[np.float32]] = {}
# Set image and category ids
self.params.img_ids = sorted(self.coco_targets.get_image_ids())
self.params.cat_ids = sorted(self.coco_targets.get_category_ids())
@@ -653,7 +694,7 @@ class COCOEvaluator:
self._predictions[dt["image_id"], dt["category_id"]].append(dt)
# Initialize evaluation results
- self.eval_imgs = defaultdict(list)
+ self.eval_imgs = []
self.results = {}
def _compute_iou(self, img_id: int, cat_id: int) -> npt.NDArray[np.float32]:
@@ -703,7 +744,7 @@ class COCOEvaluator:
cat_id: int,
area_range: list[float] | tuple[float, float],
max_det: int,
- ) -> dict[str, Any] | None:
+ ) -> _TypeEvaluationImageResult | None:
"""
Perform evaluation for single category and image.
Args:
@@ -716,8 +757,8 @@ class COCOEvaluator:
The evaluation results.
"""
# Get targets (gt) and predictions (dt) for the given image and category
- gt: list[dict[str, Any]] = self._targets[img_id, cat_id]
- dt: list[dict[str, Any]] = self._predictions[img_id, cat_id]
+ gt: list[_TypeCocoDict] = self._targets[img_id, cat_id]
+ dt: list[_TypeCocoDict] = self._predictions[img_id, cat_id]
# If there is nothing to evaluate
if len(gt) == 0 and len(dt) == 0:
@@ -754,11 +795,11 @@ class COCOEvaluator:
num_detections = len(dt)
# Initialize matches: 0 means no match
- gt_matches = np.zeros((num_thresholds, num_ground_truths))
- dt_matches = np.zeros((num_thresholds, num_detections))
+ gt_matches = np.zeros((num_thresholds, num_ground_truths), dtype=np.int64)
+ dt_matches = np.zeros((num_thresholds, num_detections), dtype=np.int64)
# Initialize ignore flags: 0 means no ignore
- gt_ignore = np.array([g["_ignore"] for g in gt])
- dt_ignore = np.zeros((num_thresholds, num_detections))
+ gt_ignore = np.array([g["_ignore"] for g in gt], dtype=np.int64)
+ dt_ignore = np.zeros((num_thresholds, num_detections), dtype=np.bool_)
if len(ious) != 0:
# Go through the iou thresholds
for tresh_idx, thresh in enumerate(self.params.iou_thrs):
@@ -901,10 +942,12 @@ class COCOEvaluator:
# Loop through max detections
for max_det_idx, max_det in enumerate(selected_max_detections):
- eval_img_data = [
+ eval_img_data_raw = [
self.eval_imgs[cat_offset + area_offset + i] for i in image_inds
]
- eval_img_data = [e for e in eval_img_data if e is not None]
+ eval_img_data: list[_TypeEvaluationImageResult] = [
+ e for e in eval_img_data_raw if e is not None
+ ]
# No image to evaluate
if len(eval_img_data) == 0:
@@ -1009,23 +1052,23 @@ class COCOEvaluator:
# Helper function to compute average precision while handling -1 sentinel values
def compute_average_precision(
precision_slice: npt.NDArray[np.float32],
- ) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]:
+ ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
"""Compute average precision while handling -1 sentinel values."""
valid_mask = precision_slice != -1
valid_precision = np.where(valid_mask, precision_slice, np.float32(0.0))
def mean_with_mask(
axis: int | tuple[int, ...],
- ) -> npt.NDArray[np.float32]:
+ ) -> npt.NDArray[np.float64]:
sums = valid_precision.sum(axis=axis, dtype=np.float64)
counts = valid_mask.sum(axis=axis)
- means = np.divide(
+ means: npt.NDArray[np.float64] = np.divide(
sums,
counts,
out=np.full(sums.shape, -1.0, dtype=np.float64),
where=counts > 0,
)
- return means.astype(np.float32)
+ return means
mAP_scores = mean_with_mask((1, 2))
ap_per_class = mean_with_mask(1).transpose(1, 0)
@@ -1119,7 +1162,9 @@ class COCOEvaluator:
if use_ap:
# Dimension of precision:
# threshold x recall x classes x areas x max detections
- s = self.results["precision"]
+ s: npt.NDArray[np.float32] = np.asarray(
+ self.results["precision"], dtype=np.float32
+ )
# IOU
if iou_thr is not None:
t = np.where(iou_thr == self.params.iou_thrs)[0]
@@ -1128,7 +1173,7 @@ class COCOEvaluator:
else:
# Dimension of recall:
# threshold x classes x areas x max detections
- s = self.results["recall"]
+ s = np.asarray(self.results["recall"], dtype=np.float32)
if iou_thr is not None:
t = np.where(iou_thr == self.params.iou_thrs)[0]
s = s[t]
@@ -1223,7 +1268,7 @@ class COCOEvaluator:
self._accumulate()
-class MeanAveragePrecision(Metric):
+class MeanAveragePrecision(Metric[MeanAveragePrecisionResult]):
"""
Mean Average Precision (mAP) is a metric used to evaluate object detection models.
It is the average of the precision-recall curves at different IoU thresholds.
@@ -1267,7 +1312,7 @@ class MeanAveragePrecision(Metric):
class_agnostic: bool = False,
class_mapping: dict[int, int] | None = None,
image_indices: list[int] | None = None,
- ):
+ ) -> None:
"""
Initialize the Mean Average Precision metric.
@@ -1336,13 +1381,13 @@ class MeanAveragePrecision(Metric):
def _prepare_targets(
self, targets: list[Detections]
- ) -> dict[str, list[dict[str, Any]]]:
+ ) -> dict[str, list[_TypeCocoDict]]:
"""Transform targets into a dictionary that can be used by the COCO evaluator"""
- images = [{"id": img_id} for img_id in range(len(targets))]
+ images: list[_TypeCocoDict] = [{"id": img_id} for img_id in range(len(targets))]
if self._image_indices is not None:
images = [{"id": self._image_indices[img["id"]]} for img in images]
# Annotations list
- annotations: list[dict[str, Any]] = []
+ annotations: list[_TypeCocoDict] = []
for image_id, image_targets in enumerate(targets):
if self._image_indices is not None:
image_id = self._image_indices[image_id]
@@ -1367,16 +1412,22 @@ class MeanAveragePrecision(Metric):
# Use area from data if available, otherwise calculate from bbox
area = None
if image_targets.data is not None and "area" in image_targets.data:
- area = float(image_targets.data["area"][target_idx])
+ area_data: npt.NDArray[np.float32] = np.asarray(
+ image_targets.data["area"], dtype=np.float32
+ )
+ area = float(area_data[target_idx])
if area is None:
area = xywh[2] * xywh[3]
iscrowd = 0
if image_targets.data is not None and "iscrowd" in image_targets.data:
- iscrowd = int(image_targets.data["iscrowd"][target_idx])
+ iscrowd_data: npt.NDArray[np.int64] = np.asarray(
+ image_targets.data["iscrowd"], dtype=np.int64
+ )
+ iscrowd = int(iscrowd_data[target_idx])
- dict_annotation = {
+ dict_annotation: _TypeCocoDict = {
"area": area,
"iscrowd": iscrowd,
"image_id": image_id,
@@ -1387,8 +1438,8 @@ class MeanAveragePrecision(Metric):
}
annotations.append(dict_annotation)
# Category list
- all_cat_ids = {annotation.get("category_id") for annotation in annotations}
- categories = [{"id": cat_id} for cat_id in all_cat_ids]
+ all_cat_ids = {annotation["category_id"] for annotation in annotations}
+ categories: list[_TypeCocoDict] = [{"id": cat_id} for cat_id in all_cat_ids]
# Create coco dictionary
return {
"images": images,
@@ -1398,10 +1449,10 @@ class MeanAveragePrecision(Metric):
def _prepare_predictions(
self, predictions: list[Detections]
- ) -> list[dict[str, Any]]:
+ ) -> list[_TypeCocoDict]:
"""Transform predictions into a list of predictions that can be used by the COCO
evaluator."""
- coco_predictions: list[dict[str, Any]] = []
+ coco_predictions: list[_TypeCocoDict] = []
for image_id, image_predictions in enumerate(predictions):
if self._image_indices is not None:
image_id = self._image_indices[image_id]
@@ -1431,12 +1482,15 @@ class MeanAveragePrecision(Metric):
image_predictions.data is not None
and "area" in image_predictions.data
):
- area = float(image_predictions.data["area"][pred_idx])
+ area_data: npt.NDArray[np.float32] = np.asarray(
+ image_predictions.data["area"], dtype=np.float32
+ )
+ area = float(area_data[pred_idx])
if area is None:
area = xywh[2] * xywh[3]
- dict_prediction = {
+ dict_prediction: _TypeCocoDict = {
"image_id": image_id,
"bbox": xywh,
"score": score,
@@ -1481,38 +1535,54 @@ class MeanAveragePrecision(Metric):
mAP_small = MeanAveragePrecisionResult(
metric_target=self._metric_target,
is_class_agnostic=self._class_agnostic,
- mAP_scores=cocoEval.results["mAP_scores_small"],
- ap_per_class=cocoEval.results["ap_per_class_small"],
- iou_thresholds=cocoEval.params.iou_thrs,
- matched_classes=np.array(cocoEval.params.cat_ids),
+ mAP_scores=np.asarray(
+ cocoEval.results["mAP_scores_small"], dtype=np.float64
+ ),
+ ap_per_class=np.asarray(
+ cocoEval.results["ap_per_class_small"], dtype=np.float64
+ ),
+ iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64),
+ matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32),
)
# Create MeanAveragePrecisionResult object for medium objects
mAP_medium = MeanAveragePrecisionResult(
metric_target=self._metric_target,
is_class_agnostic=self._class_agnostic,
- mAP_scores=cocoEval.results["mAP_scores_medium"],
- ap_per_class=cocoEval.results["ap_per_class_medium"],
- iou_thresholds=cocoEval.params.iou_thrs,
- matched_classes=np.array(cocoEval.params.cat_ids),
+ mAP_scores=np.asarray(
+ cocoEval.results["mAP_scores_medium"], dtype=np.float64
+ ),
+ ap_per_class=np.asarray(
+ cocoEval.results["ap_per_class_medium"], dtype=np.float64
+ ),
+ iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64),
+ matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32),
)
# Create MeanAveragePrecisionResult object for large objects
mAP_large = MeanAveragePrecisionResult(
metric_target=self._metric_target,
is_class_agnostic=self._class_agnostic,
- mAP_scores=cocoEval.results["mAP_scores_large"],
- ap_per_class=cocoEval.results["ap_per_class_large"],
- iou_thresholds=cocoEval.params.iou_thrs,
- matched_classes=np.array(cocoEval.params.cat_ids),
+ mAP_scores=np.asarray(
+ cocoEval.results["mAP_scores_large"], dtype=np.float64
+ ),
+ ap_per_class=np.asarray(
+ cocoEval.results["ap_per_class_large"], dtype=np.float64
+ ),
+ iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64),
+ matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32),
)
# Create the final MeanAveragePrecisionResult object
mAP_result = MeanAveragePrecisionResult(
metric_target=self._metric_target,
is_class_agnostic=self._class_agnostic,
- mAP_scores=cocoEval.results["mAP_scores_all_sizes"],
- ap_per_class=cocoEval.results["ap_per_class_all_sizes"],
- iou_thresholds=cocoEval.params.iou_thrs,
- matched_classes=np.array(cocoEval.params.cat_ids),
+ mAP_scores=np.asarray(
+ cocoEval.results["mAP_scores_all_sizes"], dtype=np.float64
+ ),
+ ap_per_class=np.asarray(
+ cocoEval.results["ap_per_class_all_sizes"], dtype=np.float64
+ ),
+ iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64),
+ matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32),
small_objects=mAP_small,
medium_objects=mAP_medium,
large_objects=mAP_large,
diff --git a/src/supervision/metrics/mean_average_recall.py b/src/supervision/metrics/mean_average_recall.py
index 4f81831b..98b5a4b3 100644
--- a/src/supervision/metrics/mean_average_recall.py
+++ b/src/supervision/metrics/mean_average_recall.py
@@ -2,7 +2,7 @@ from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, cast
import numpy as np
import numpy.typing as npt
@@ -256,7 +256,7 @@ class MeanAverageRecallResult:
plt.show()
-class MeanAverageRecall(Metric):
+class MeanAverageRecall(Metric["MeanAverageRecallResult"]):
"""
Mean Average Recall (mAR) measures how well the model detects
and retrieves relevant objects by averaging recall over multiple
@@ -381,7 +381,7 @@ class MeanAverageRecall(Metric):
def _compute(
self, predictions_list: list[Detections], targets_list: list[Detections]
) -> MeanAverageRecallResult:
- iou_thresholds = np.linspace(0.5, 0.95, 10)
+ iou_thresholds = np.linspace(0.5, 0.95, 10, dtype=np.float32)
stats: list[Any] = []
for predictions, targets in zip(predictions_list, targets_list):
@@ -490,7 +490,7 @@ class MeanAverageRecall(Metric):
]:
unique_classes, class_counts = np.unique(true_class_ids, return_counts=True)
- recalls_at_k = []
+ recalls_at_k: list[npt.NDArray[np.float64]] = []
for max_detections in self.max_detections:
# Shape: PxTh,P,C,C -> CxThx3
confusion_matrix = self._compute_confusion_matrix(
@@ -505,8 +505,8 @@ class MeanAverageRecall(Metric):
recalls_at_k.append(recall_per_class)
# Shape: KxCxTh -> KxC
- recalls_at_k = np.array(recalls_at_k)
- average_recall_per_class = np.mean(recalls_at_k, axis=2)
+ recalls_at_k_array = np.array(recalls_at_k)
+ average_recall_per_class = np.mean(recalls_at_k_array, axis=2)
# Shape: KxC -> K
recall_scores = np.mean(average_recall_per_class, axis=1)
@@ -548,8 +548,8 @@ class MeanAverageRecall(Metric):
def _compute_confusion_matrix(
sorted_matches: npt.NDArray[np.bool_],
sorted_prediction_class_ids: npt.NDArray[np.int32],
- unique_classes: npt.NDArray[np.int32],
- class_counts: npt.NDArray[np.int32],
+ unique_classes: npt.NDArray[np.integer],
+ class_counts: npt.NDArray[np.integer],
) -> npt.NDArray[np.float64]:
"""
Compute the confusion matrix for each class and IoU threshold.
@@ -638,12 +638,10 @@ class MeanAverageRecall(Metric):
def _detections_content(self, detections: Detections) -> npt.NDArray[Any]:
"""Return boxes, masks or oriented bounding boxes from detections."""
if self._metric_target == MetricTarget.BOXES:
- result_boxes: npt.NDArray[np.float32] = detections.xyxy
- return result_boxes
+ return cast(npt.NDArray[Any], detections.xyxy)
if self._metric_target == MetricTarget.MASKS:
if detections.mask is not None:
- result_masks: npt.NDArray[np.bool_] = detections.mask
- return result_masks
+ return cast(npt.NDArray[Any], detections.mask)
return self._make_empty_content()
if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
obb = detections.data.get(ORIENTED_BOX_COORDINATES)
diff --git a/src/supervision/metrics/precision.py b/src/supervision/metrics/precision.py
index 8e3ac5d7..77613b0e 100644
--- a/src/supervision/metrics/precision.py
+++ b/src/supervision/metrics/precision.py
@@ -2,7 +2,7 @@ from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, cast
import numpy as np
import numpy.typing as npt
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
import pandas as pd
-class Precision(Metric):
+class Precision(Metric["PrecisionResult"]):
"""
Precision is a metric used to evaluate object detection models. It is the ratio of
true positive detections to the total number of predicted detections. We calculate
@@ -163,7 +163,7 @@ class Precision(Metric):
is ``zeros((0,))``.
- Targets present: IoU matching produces ``matches`` array.
"""
- iou_thresholds = np.linspace(0.5, 0.95, 10)
+ iou_thresholds = np.linspace(0.5, 0.95, 10, dtype=np.float32)
stats: list[Any] = []
for predictions, targets in zip(predictions_list, targets_list):
@@ -459,12 +459,10 @@ class Precision(Metric):
def _detections_content(self, detections: Detections) -> npt.NDArray[Any]:
"""Return boxes, masks or oriented bounding boxes from detections."""
if self._metric_target == MetricTarget.BOXES:
- result_boxes: npt.NDArray[np.float32] = detections.xyxy
- return result_boxes
+ return cast(npt.NDArray[Any], detections.xyxy)
if self._metric_target == MetricTarget.MASKS:
if detections.mask is not None:
- result_masks: npt.NDArray[np.bool_] = detections.mask
- return result_masks
+ return cast(npt.NDArray[Any], detections.mask)
return self._make_empty_content()
if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
obb = detections.data.get(ORIENTED_BOX_COORDINATES)
diff --git a/src/supervision/metrics/recall.py b/src/supervision/metrics/recall.py
index f122fd8d..f34b05bd 100644
--- a/src/supervision/metrics/recall.py
+++ b/src/supervision/metrics/recall.py
@@ -2,7 +2,7 @@ from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, cast
import numpy as np
import numpy.typing as npt
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
import pandas as pd
-class Recall(Metric):
+class Recall(Metric["RecallResult"]):
"""
Recall is a metric used to evaluate object detection models. It is the ratio of
true positive detections to the total number of ground truth instances. We calculate
@@ -155,7 +155,7 @@ class Recall(Metric):
def _compute(
self, predictions_list: list[Detections], targets_list: list[Detections]
) -> RecallResult:
- iou_thresholds = np.linspace(0.5, 0.95, 10)
+ iou_thresholds = np.linspace(0.5, 0.95, 10, dtype=np.float32)
stats: list[Any] = []
for predictions, targets in zip(predictions_list, targets_list):
@@ -319,8 +319,8 @@ class Recall(Metric):
def _compute_confusion_matrix(
sorted_matches: npt.NDArray[np.bool_],
sorted_prediction_class_ids: npt.NDArray[np.int32],
- unique_classes: npt.NDArray[np.int32],
- class_counts: npt.NDArray[np.int32],
+ unique_classes: npt.NDArray[np.integer],
+ class_counts: npt.NDArray[np.integer],
) -> npt.NDArray[np.float64]:
"""
Compute the confusion matrix for each class and IoU threshold.
@@ -408,12 +408,10 @@ class Recall(Metric):
def _detections_content(self, detections: Detections) -> npt.NDArray[Any]:
"""Return boxes, masks or oriented bounding boxes from detections."""
if self._metric_target == MetricTarget.BOXES:
- result_boxes: npt.NDArray[np.float32] = detections.xyxy
- return result_boxes
+ return cast(npt.NDArray[Any], detections.xyxy)
if self._metric_target == MetricTarget.MASKS:
if detections.mask is not None:
- result_masks: npt.NDArray[np.bool_] = detections.mask
- return result_masks
+ return cast(npt.NDArray[Any], detections.mask)
return self._make_empty_content()
if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
obb = detections.data.get(ORIENTED_BOX_COORDINATES)
diff --git a/src/supervision/metrics/utils/object_size.py b/src/supervision/metrics/utils/object_size.py
index 03f005a1..131dd5e4 100644
--- a/src/supervision/metrics/utils/object_size.py
+++ b/src/supervision/metrics/utils/object_size.py
@@ -1,7 +1,7 @@
from __future__ import annotations
from enum import Enum
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, cast
import numpy as np
import numpy.typing as npt
@@ -44,7 +44,8 @@ class ObjectSizeCategory(Enum):
def get_object_size_category(
- data: npt.NDArray, metric_target: MetricTarget
+ data: npt.NDArray[np.number] | npt.NDArray[np.bool_],
+ metric_target: MetricTarget,
) -> npt.NDArray[np.int_]:
"""
Get the size category of an object. Distinguish based on the metric target.
@@ -74,15 +75,18 @@ def get_object_size_category(
```
"""
if metric_target == MetricTarget.BOXES:
- return get_bbox_size_category(data)
+ bbox_data = cast(npt.NDArray[np.number], data)
+ return get_bbox_size_category(bbox_data)
if metric_target == MetricTarget.MASKS:
- return get_mask_size_category(data)
+ mask_data = cast(npt.NDArray[np.bool_], data)
+ return get_mask_size_category(mask_data)
if metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
- return get_obb_size_category(data)
+ obb_data = cast(npt.NDArray[np.number], data)
+ return get_obb_size_category(obb_data)
raise ValueError("Invalid metric type")
-def get_bbox_size_category(xyxy: npt.NDArray[np.float32]) -> npt.NDArray[np.int_]:
+def get_bbox_size_category(xyxy: npt.NDArray[np.number]) -> npt.NDArray[np.int_]:
"""
Get the size category of a bounding boxes array.
@@ -165,7 +169,7 @@ def get_mask_size_category(
return result
-def get_obb_size_category(xyxyxyxy: npt.NDArray[np.float32]) -> npt.NDArray[np.int_]:
+def get_obb_size_category(xyxyxyxy: npt.NDArray[np.number]) -> npt.NDArray[np.int_]:
"""
Get the size category of a oriented bounding boxes array.
@@ -229,13 +233,18 @@ def get_detection_size_category(
if metric_target == MetricTarget.BOXES:
return get_bbox_size_category(detections.xyxy)
if metric_target == MetricTarget.MASKS:
- if detections.mask is None:
+ mask = detections.mask
+ if mask is None:
raise ValueError("Detections mask is not available")
- return get_mask_size_category(detections.mask)
+ return get_mask_size_category(mask)
if metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
- if detections.data.get(ORIENTED_BOX_COORDINATES) is None:
+ oriented_box_coordinates = detections.data.get(ORIENTED_BOX_COORDINATES)
+ if oriented_box_coordinates is None:
raise ValueError("Detections oriented bounding boxes are not available")
return get_obb_size_category(
- np.array(detections.data[ORIENTED_BOX_COORDINATES])
+ cast(
+ npt.NDArray[np.number],
+ np.asarray(oriented_box_coordinates, dtype=np.float32),
+ )
)
raise ValueError("Invalid metric type")
diff --git a/src/supervision/tracker/byte_tracker/core.py b/src/supervision/tracker/byte_tracker/core.py
index f83db914..5b174c73 100644
--- a/src/supervision/tracker/byte_tracker/core.py
+++ b/src/supervision/tracker/byte_tracker/core.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
from typing import cast
import numpy as np
@@ -63,7 +61,7 @@ class ByteTrack:
minimum_matching_threshold: float = 0.8,
frame_rate: float = 30,
minimum_consecutive_frames: int = 1,
- ):
+ ) -> None:
self.track_activation_threshold = track_activation_threshold
self.minimum_matching_threshold = minimum_matching_threshold
diff --git a/src/supervision/tracker/byte_tracker/kalman_filter.py b/src/supervision/tracker/byte_tracker/kalman_filter.py
index fbaf779c..0d5cbeb9 100644
--- a/src/supervision/tracker/byte_tracker/kalman_filter.py
+++ b/src/supervision/tracker/byte_tracker/kalman_filter.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import numpy as np
import numpy.typing as npt
import scipy.linalg
@@ -146,10 +144,10 @@ class KalmanFilter:
]
sqr = np.square(np.r_[std_pos, std_vel]).T
- motion_cov = []
+ motion_cov_list: list[npt.NDArray[np.float32]] = []
for i in range(len(mean)):
- motion_cov.append(np.diag(sqr[i]))
- motion_cov = np.asarray(motion_cov)
+ motion_cov_list.append(np.diag(sqr[i]))
+ motion_cov = np.asarray(motion_cov_list)
mean = np.dot(mean, self._motion_mat.T)
left = np.dot(self._motion_mat, covariance).transpose((1, 0, 2))
diff --git a/src/supervision/tracker/byte_tracker/matching.py b/src/supervision/tracker/byte_tracker/matching.py
index 9d2c7dc6..b792d9df 100644
--- a/src/supervision/tracker/byte_tracker/matching.py
+++ b/src/supervision/tracker/byte_tracker/matching.py
@@ -1,15 +1,11 @@
-from __future__ import annotations
-
-from typing import TYPE_CHECKING
+from typing import cast
import numpy as np
import numpy.typing as npt
from scipy.optimize import linear_sum_assignment
from supervision.detection.utils.iou_and_nms import box_iou_batch
-
-if TYPE_CHECKING:
- from supervision.tracker.byte_tracker.single_object_track import STrack
+from supervision.tracker.byte_tracker.single_object_track import STrack
def indices_to_matches(
@@ -48,17 +44,23 @@ def iou_distance(
if (len(atracks) > 0 and isinstance(atracks[0], np.ndarray)) or (
len(btracks) > 0 and isinstance(btracks[0], np.ndarray)
):
- atlbrs = atracks
- btlbrs = btracks
+ atlbrs = cast(list[npt.NDArray[np.float32]], atracks)
+ btlbrs = cast(list[npt.NDArray[np.float32]], btracks)
else:
- atlbrs = [track.tlbr for track in atracks]
- btlbrs = [track.tlbr for track in btracks]
+ atlbrs = [track.tlbr for track in cast(list[STrack], atracks)]
+ btlbrs = [track.tlbr for track in cast(list[STrack], btracks)]
- _ious = np.zeros((len(atlbrs), len(btlbrs)), dtype=np.float32)
- if _ious.size != 0:
- _ious = box_iou_batch(np.asarray(atlbrs), np.asarray(btlbrs))
- cost_matrix = 1 - _ious
+ if len(atlbrs) == 0 or len(btlbrs) == 0:
+ return cast(
+ npt.NDArray[np.float32],
+ np.empty((len(atlbrs), len(btlbrs)), dtype=np.float32),
+ )
+ ious = box_iou_batch(
+ np.asarray(atlbrs, dtype=np.float32),
+ np.asarray(btlbrs, dtype=np.float32),
+ )
+ cost_matrix = np.asarray(1 - ious, dtype=np.float32)
return cost_matrix
@@ -68,8 +70,8 @@ def fuse_score(
if cost_matrix.size == 0:
return cost_matrix
iou_sim = 1 - cost_matrix
- det_scores = np.array([strack.score for strack in stracks])
+ det_scores = np.array([strack.score for strack in stracks], dtype=np.float32)
det_scores = np.expand_dims(det_scores, axis=0).repeat(cost_matrix.shape[0], axis=0)
fuse_sim = iou_sim * det_scores
- fuse_cost = 1 - fuse_sim
+ fuse_cost = np.asarray(1 - fuse_sim, dtype=np.float32)
return fuse_cost
diff --git a/src/supervision/tracker/byte_tracker/single_object_track.py b/src/supervision/tracker/byte_tracker/single_object_track.py
index 01618b3c..5194b490 100644
--- a/src/supervision/tracker/byte_tracker/single_object_track.py
+++ b/src/supervision/tracker/byte_tracker/single_object_track.py
@@ -1,6 +1,7 @@
from __future__ import annotations
from enum import Enum
+from typing import cast
import numpy as np
import numpy.typing as npt
@@ -142,11 +143,11 @@ class STrack:
width, height)`.
"""
if self.mean is None:
- return self._tlwh.copy()
+ return cast(npt.NDArray[np.float32], self._tlwh.copy())
ret = self.mean[:4].copy()
ret[2] *= ret[3]
ret[:2] -= ret[2:] / 2
- return ret
+ return cast(npt.NDArray[np.float32], ret)
@property
def tlbr(self) -> npt.NDArray[np.float32]:
@@ -155,7 +156,7 @@ class STrack:
"""
ret = self.tlwh.copy()
ret[2:] += ret[:2]
- return ret
+ return cast(npt.NDArray[np.float32], ret)
@staticmethod
def tlwh_to_xyah(tlwh: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
@@ -165,7 +166,7 @@ class STrack:
ret = np.asarray(tlwh).copy()
ret[:2] += ret[2:] / 2
ret[2] /= ret[3]
- return ret
+ return cast(npt.NDArray[np.float32], ret)
def to_xyah(self) -> npt.NDArray[np.float32]:
return self.tlwh_to_xyah(self.tlwh)
@@ -174,13 +175,13 @@ class STrack:
def tlbr_to_tlwh(tlbr: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
ret = np.asarray(tlbr).copy()
ret[2:] -= ret[:2]
- return ret
+ return cast(npt.NDArray[np.float32], ret)
@staticmethod
def tlwh_to_tlbr(tlwh: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
ret = np.asarray(tlwh).copy()
ret[2:] += ret[:2]
- return ret
+ return cast(npt.NDArray[np.float32], ret)
def __repr__(self) -> str:
return f"OT_{self.internal_track_id}_({self.start_frame}-{self.frame_id})"
diff --git a/src/supervision/tracker/byte_tracker/utils.py b/src/supervision/tracker/byte_tracker/utils.py
index 3404f809..4e1c1c29 100644
--- a/src/supervision/tracker/byte_tracker/utils.py
+++ b/src/supervision/tracker/byte_tracker/utils.py
@@ -1,8 +1,5 @@
-from __future__ import annotations
-
-
class IdCounter:
- def __init__(self, start_id: int = 0):
+ def __init__(self, start_id: int = 0) -> None:
"""
Initialize the ID counter.
diff --git a/src/supervision/utils/conversion.py b/src/supervision/utils/conversion.py
index 5341a154..55a71b88 100644
--- a/src/supervision/utils/conversion.py
+++ b/src/supervision/utils/conversion.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import functools
from collections.abc import Callable
from typing import Any, TypeVar, cast
@@ -30,7 +28,7 @@ def ensure_cv2_image_for_class_method(
"""
@functools.wraps(annotate_func)
- def wrapper(self: Any, scene: ImageType, *args: Any, **kwargs: Any) -> ImageType:
+ def wrapper(self: Any, scene: ImageType, *args: Any, **kwargs: Any) -> Any:
if isinstance(scene, np.ndarray):
return annotate_func(self, scene, *args, **kwargs)
@@ -70,7 +68,7 @@ def ensure_cv2_image_for_standalone_function(
"""
@functools.wraps(image_processing_fun)
- def wrapper(image: ImageType, *args: Any, **kwargs: Any) -> ImageType:
+ def wrapper(image: ImageType, *args: Any, **kwargs: Any) -> Any:
if isinstance(image, np.ndarray):
return image_processing_fun(image, *args, **kwargs)
@@ -98,7 +96,7 @@ def ensure_pil_image_for_class_method(
"""
@functools.wraps(annotate_func)
- def wrapper(self: Any, scene: ImageType, *args: Any, **kwargs: Any) -> ImageType:
+ def wrapper(self: Any, scene: ImageType, *args: Any, **kwargs: Any) -> Any:
if isinstance(scene, np.ndarray):
scene_pil = cv2_to_pillow(scene)
annotated_pil = annotate_func(self, scene_pil, *args, **kwargs)
@@ -135,7 +133,9 @@ def ensure_cv2_image_for_processing(
return cast(F, void(image_processing_fun))
-def images_to_cv2(images: list[ImageType]) -> list[npt.NDArray[np.uint8]]:
+def images_to_cv2(
+ images: list[npt.NDArray[np.uint8] | Image.Image],
+) -> list[npt.NDArray[np.uint8]]:
"""
Converts images provided either as Pillow images or OpenCV
images into OpenCV format.
@@ -148,11 +148,12 @@ def images_to_cv2(images: list[ImageType]) -> list[npt.NDArray[np.uint8]]:
(with order preserved).
"""
- result = []
+ result: list[npt.NDArray[np.uint8]] = []
for image in images:
- if issubclass(type(image), Image.Image):
- image = pillow_to_cv2(image)
- result.append(image)
+ if isinstance(image, Image.Image):
+ result.append(pillow_to_cv2(image))
+ else:
+ result.append(image)
return result
@@ -171,7 +172,7 @@ def pillow_to_cv2(image: Image.Image) -> npt.NDArray[np.uint8]:
scene = cv2.cvtColor(scene, cv2.COLOR_RGB2BGR)
# cvtColor already returns uint8 here, so astype is a no-op other than the
# full-image copy it forces; copy=False keeps the dtype guard without it.
- return scene.astype(np.uint8, copy=False)
+ return cast(npt.NDArray[np.uint8], scene.astype(np.uint8, copy=False))
def cv2_to_pillow(image: npt.NDArray[np.uint8]) -> Image.Image:
@@ -185,5 +186,5 @@ def cv2_to_pillow(image: npt.NDArray[np.uint8]) -> Image.Image:
Returns:
Input image converted to Pillow format.
"""
- image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
- return Image.fromarray(image)
+ rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
+ return Image.fromarray(rgb_image)
diff --git a/src/supervision/utils/file.py b/src/supervision/utils/file.py
index 55716a08..00466e53 100644
--- a/src/supervision/utils/file.py
+++ b/src/supervision/utils/file.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import json
from pathlib import Path
from typing import Any
diff --git a/src/supervision/utils/image.py b/src/supervision/utils/image.py
index 22685c67..a7bd3605 100644
--- a/src/supervision/utils/image.py
+++ b/src/supervision/utils/image.py
@@ -6,7 +6,8 @@ import os
import shutil
from collections.abc import Callable
from functools import partial
-from typing import Any, Literal, cast
+from types import TracebackType
+from typing import Literal, cast
import cv2
import numpy as np
@@ -36,7 +37,7 @@ MAX_COLUMNS_FOR_SINGLE_ROW_GRID = 3
@ensure_cv2_image_for_standalone_function
def crop_image(
image: ImageType,
- xyxy: npt.NDArray[int] | list[int] | tuple[int, int, int, int],
+ xyxy: npt.NDArray[np.number] | list[int] | tuple[int, int, int, int],
) -> ImageType:
"""
Crop image based on bounding box coordinates.
@@ -77,17 +78,14 @@ def crop_image(
{ align=center width="1000" }
""" # noqa E501 // docs
- if isinstance(xyxy, (list, tuple)):
- xyxy = np.array(xyxy)
-
- xyxy = np.round(xyxy).astype(int)
- x_min, y_min, x_max, y_max = xyxy.flatten()
+ xyxy_arr = np.asarray(xyxy, dtype=np.float64).round().astype(np.int32)
+ x_min, y_min, x_max, y_max = xyxy_arr.flatten()
if isinstance(image, np.ndarray):
return image[y_min:y_max, x_min:x_max]
if isinstance(image, Image.Image):
- return image.crop((x_min, y_min, x_max, y_max))
+ return image.crop((float(x_min), float(y_min), float(x_max), float(y_max)))
raise TypeError(
f"`image` must be a numpy.ndarray or PIL.Image.Image. Received {type(image)}"
@@ -142,7 +140,10 @@ def scale_image(image: ImageType, scale_factor: float) -> ImageType:
width_old, height_old = image.shape[1], image.shape[0]
width_new = int(width_old * scale_factor)
height_new = int(height_old * scale_factor)
- return cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR)
+ return cast(
+ npt.NDArray[np.uint8],
+ cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR),
+ )
@ensure_cv2_image_for_standalone_function
@@ -208,7 +209,10 @@ def resize_image(
else:
width_new, height_new = resolution_wh
- return cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR)
+ return cast(
+ npt.NDArray[np.uint8],
+ cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR),
+ )
@ensure_cv2_image_for_standalone_function
@@ -269,14 +273,17 @@ def letterbox_image(
padding_bottom = resolution_wh[1] - height_new - padding_top
padding_left = (resolution_wh[0] - width_new) // 2
padding_right = resolution_wh[0] - width_new - padding_left
- image_with_borders = cv2.copyMakeBorder(
- resized_image,
- padding_top,
- padding_bottom,
- padding_left,
- padding_right,
- cv2.BORDER_CONSTANT,
- value=color,
+ image_with_borders = cast(
+ npt.NDArray[np.uint8],
+ cv2.copyMakeBorder(
+ resized_image,
+ padding_top,
+ padding_bottom,
+ padding_left,
+ padding_right,
+ cv2.BORDER_CONSTANT,
+ value=color,
+ ),
)
return image_with_borders
@@ -345,12 +352,12 @@ def overlay_image(
b, g, r, alpha = cv2.split(
overlay[crop_y_min:crop_y_max, crop_x_min:crop_x_max]
)
- alpha = alpha[:, :, None] / 255.0
- overlay_color = cv2.merge((b, g, r))
+ alpha_f32 = alpha[:, :, None].astype(np.float32) / 255.0
+ overlay_color = cv2.merge((b, g, r)).astype(np.float32)
- roi = image[y_min:y_max, x_min:x_max]
- roi[:] = roi * (1 - alpha) + overlay_color * alpha
- image[y_min:y_max, x_min:x_max] = roi
+ roi = image[y_min:y_max, x_min:x_max].astype(np.float32)
+ blended = roi * (1 - alpha_f32) + overlay_color * alpha_f32
+ image[y_min:y_max, x_min:x_max] = np.clip(blended, 0, 255).astype(np.uint8)
else:
image[y_min:y_max, x_min:x_max] = overlay[
crop_y_min:crop_y_max, crop_x_min:crop_x_max
@@ -434,8 +441,9 @@ def grayscale_image(image: ImageType) -> ImageType:
{ align=center width="1000" }
""" # noqa E501 // docs
- grayscaled = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
- return cv2.cvtColor(grayscaled, cv2.COLOR_GRAY2BGR)
+ assert isinstance(image, np.ndarray)
+ grayscaled = cast(npt.NDArray[np.uint8], cv2.cvtColor(image, cv2.COLOR_BGR2GRAY))
+ return cast(npt.NDArray[np.uint8], cv2.cvtColor(grayscaled, cv2.COLOR_GRAY2BGR))
def get_image_resolution_wh(image: ImageType) -> tuple[int, int]:
@@ -491,7 +499,7 @@ class ImageSink:
target_dir_path: str,
overwrite: bool = False,
image_name_pattern: str = "image_{:05d}.png",
- ):
+ ) -> None:
"""
Initialize context manager for saving images to directory.
@@ -559,7 +567,7 @@ class ImageSink:
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
- exc_traceback: Any,
+ exc_traceback: TracebackType | None,
) -> None:
pass
@@ -663,32 +671,32 @@ def create_tiles(
return_type = _negotiate_tiles_format(images=images)
tile_padding_color = unify_to_bgr(color=tile_padding_color)
tile_margin_color = unify_to_bgr(color=tile_margin_color)
- images = images_to_cv2(images=images)
+ images_cv2 = images_to_cv2(images=images)
if single_tile_size is None:
- single_tile_size = _aggregate_images_shape(images=images, mode=tile_scaling)
+ single_tile_size = _aggregate_images_shape(images=images_cv2, mode=tile_scaling)
resized_images = [
letterbox_image(
image=i, resolution_wh=single_tile_size, color=tile_padding_color
)
- for i in images
+ for i in images_cv2
]
- grid_size = _establish_grid_size(images=images, grid_size=grid_size)
- if len(images) > grid_size[0] * grid_size[1]:
+ grid_size = _establish_grid_size(images=images_cv2, grid_size=grid_size)
+ if len(images_cv2) > grid_size[0] * grid_size[1]:
raise ValueError(
- f"Could not place {len(images)} in grid with size: {grid_size}."
+ f"Could not place {len(images_cv2)} in grid with size: {grid_size}."
)
if titles is not None:
- titles = fill(sequence=titles, desired_size=len(images), content=None)
+ titles = fill(sequence=titles, desired_size=len(images_cv2), content=None)
if isinstance(titles_anchors, list):
titles_anchors_sequence = titles_anchors
else:
titles_anchors_sequence = [titles_anchors]
titles_anchors = fill(
- sequence=titles_anchors_sequence, desired_size=len(images), content=None
+ sequence=titles_anchors_sequence, desired_size=len(images_cv2), content=None
)
titles_color = unify_to_bgr(color=titles_color)
titles_background_color = unify_to_bgr(color=titles_background_color)
- tiles = _generate_tiles(
+ tiles_image = _generate_tiles(
images=resized_images,
grid_size=grid_size,
single_tile_size=single_tile_size,
@@ -706,8 +714,10 @@ def create_tiles(
default_title_placement=default_title_placement,
)
if return_type == "pillow":
- tiles = cv2_to_pillow(image=tiles)
- return cast(ImageType, tiles)
+ tiles_image_pillow: object = cv2_to_pillow(image=tiles_image)
+ return cast(ImageType, tiles_image_pillow)
+ tiles_image_cv2: object = tiles_image
+ return cast(ImageType, tiles_image_cv2)
def _negotiate_tiles_format(images: list[ImageType]) -> Literal["cv2", "pillow"]:
@@ -890,9 +900,10 @@ def _merge_tiles_elements(
tile_margin: int,
tile_margin_color: tuple[int, int, int],
) -> npt.NDArray[np.uint8]:
- vertical_padding: npt.NDArray[np.uint8] = (
- np.ones((single_tile_size[1], tile_margin, 3), dtype=np.uint8)
- * tile_margin_color
+ vertical_padding: npt.NDArray[np.uint8] = np.full(
+ (single_tile_size[1], tile_margin, 3),
+ tile_margin_color,
+ dtype=np.uint8,
)
merged_rows = [
np.concatenate(
@@ -906,26 +917,19 @@ def _merge_tiles_elements(
for row in tiles_elements
]
row_width = merged_rows[0].shape[1]
- horizontal_padding = (
- np.ones((tile_margin, row_width, 3), dtype=np.uint8) * tile_margin_color
+ horizontal_padding: npt.NDArray[np.uint8] = np.full(
+ (tile_margin, row_width, 3),
+ tile_margin_color,
+ dtype=np.uint8,
)
- rows_with_paddings = []
+ rows_with_paddings: list[npt.NDArray[np.uint8]] = []
for row in merged_rows:
rows_with_paddings.append(row)
rows_with_paddings.append(horizontal_padding)
- return cast(
- npt.NDArray[np.uint8],
- np.concatenate(
- rows_with_paddings[:-1],
- axis=0,
- ).astype(np.uint8),
- )
+ return np.concatenate(rows_with_paddings[:-1], axis=0).astype(np.uint8, copy=False)
def _generate_color_image(
shape: tuple[int, int], color: tuple[int, int, int]
) -> npt.NDArray[np.uint8]:
- return cast(
- npt.NDArray[np.uint8],
- np.ones((*shape[::-1], 3), dtype=np.uint8) * color,
- )
+ return np.full((*shape[::-1], 3), color, dtype=np.uint8)
diff --git a/src/supervision/utils/internal.py b/src/supervision/utils/internal.py
index 2ceddb47..8d6e2c00 100644
--- a/src/supervision/utils/internal.py
+++ b/src/supervision/utils/internal.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import functools
import inspect
import os
@@ -142,7 +140,7 @@ class classproperty(Generic[T]):
...
"""
- def __init__(self, fget: Callable[..., T]):
+ def __init__(self, fget: Callable[..., T]) -> None:
"""
Args:
The function that is called when the property is accessed.
diff --git a/src/supervision/utils/iterables.py b/src/supervision/utils/iterables.py
index 54646a94..9b30e6bb 100644
--- a/src/supervision/utils/iterables.py
+++ b/src/supervision/utils/iterables.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
from collections.abc import Generator, Iterable
from typing import TypeVar
diff --git a/src/supervision/utils/logger.py b/src/supervision/utils/logger.py
index f6ac66e8..2ed8c452 100644
--- a/src/supervision/utils/logger.py
+++ b/src/supervision/utils/logger.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import logging
import os
import sys
diff --git a/src/supervision/utils/notebook.py b/src/supervision/utils/notebook.py
index ed8f4f68..25d0f4b4 100644
--- a/src/supervision/utils/notebook.py
+++ b/src/supervision/utils/notebook.py
@@ -1,7 +1,7 @@
-from __future__ import annotations
-
import cv2
import matplotlib.pyplot as plt
+import numpy as np
+import numpy.typing as npt
from PIL import Image
from supervision.draw.base import ImageType
@@ -34,14 +34,16 @@ def plot_image(
```
"""
if isinstance(image, Image.Image):
- image = pillow_to_cv2(image)
+ image_np = pillow_to_cv2(image)
+ else:
+ image_np = image
plt.figure(figsize=size)
- if image.ndim == 2:
- plt.imshow(image, cmap=cmap)
+ if image_np.ndim == 2:
+ plt.imshow(image_np, cmap=cmap)
else:
- plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
+ plt.imshow(cv2.cvtColor(image_np, cv2.COLOR_BGR2RGB))
plt.axis("off")
plt.show()
@@ -91,11 +93,11 @@ def plot_images_grid(
"""
nrows, ncols = grid_size
- for idx, img in enumerate(images):
- if isinstance(img, Image.Image):
- images[idx] = pillow_to_cv2(img)
+ images_np: list[npt.NDArray[np.uint8]] = [
+ pillow_to_cv2(img) if isinstance(img, Image.Image) else img for img in images
+ ]
- if len(images) > nrows * ncols:
+ if len(images_np) > nrows * ncols:
raise ValueError(
"The number of images exceeds the grid size. Please increase the grid size"
" or reduce the number of images."
@@ -104,11 +106,11 @@ def plot_images_grid(
_fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=size)
for idx, ax in enumerate(axes.flat):
- if idx < len(images):
- if images[idx].ndim == 2:
- ax.imshow(images[idx], cmap=cmap)
+ if idx < len(images_np):
+ if images_np[idx].ndim == 2:
+ ax.imshow(images_np[idx], cmap=cmap)
else:
- ax.imshow(cv2.cvtColor(images[idx], cv2.COLOR_BGR2RGB))
+ ax.imshow(cv2.cvtColor(images_np[idx], cv2.COLOR_BGR2RGB))
if titles is not None and idx < len(titles):
ax.set_title(titles[idx])
diff --git a/src/supervision/utils/video.py b/src/supervision/utils/video.py
index 1d3fe2a4..51a2e16d 100644
--- a/src/supervision/utils/video.py
+++ b/src/supervision/utils/video.py
@@ -10,7 +10,8 @@ from collections import deque
from collections.abc import Callable, Generator
from dataclasses import dataclass
from queue import Empty, Full, Queue
-from typing import Any
+from types import TracebackType
+from typing import cast
import cv2
import numpy as np
@@ -96,18 +97,24 @@ class VideoSink:
```
""" # noqa: E501 // docs
- def __init__(self, target_path: str, video_info: VideoInfo, codec: str = "mp4v"):
+ def __init__(
+ self, target_path: str, video_info: VideoInfo, codec: str = "mp4v"
+ ) -> None:
self.target_path = target_path
self.video_info = video_info
self.__codec = codec
- self.__writer = None
+ self.__fourcc: int = 0
+ self.__writer: cv2.VideoWriter | None = None
def __enter__(self) -> VideoSink:
+ fourcc_fn = cast(
+ Callable[[str, str, str, str], int], getattr(cv2, "VideoWriter_fourcc")
+ )
try:
- self.__fourcc = cv2.VideoWriter_fourcc(*self.__codec)
+ self.__fourcc = int(fourcc_fn(*self.__codec))
except TypeError as e:
logger.warning("%s. Defaulting to mp4v...", str(e))
- self.__fourcc = cv2.VideoWriter_fourcc(*"mp4v")
+ self.__fourcc = int(fourcc_fn(*"mp4v"))
self.__writer = cv2.VideoWriter(
self.target_path,
self.__fourcc,
@@ -131,7 +138,7 @@ class VideoSink:
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
- exc_traceback: Any,
+ exc_traceback: TracebackType | None,
) -> None:
if self.__writer is not None:
self.__writer.release()
@@ -271,7 +278,7 @@ def get_video_frames_generator(
if not success or frame_position >= end:
break
if frame is not None:
- yield frame
+ yield cast(npt.NDArray[np.uint8], frame)
for _ in range(stride - 1):
success = video.grab()
if not success:
@@ -461,7 +468,7 @@ class FPSMonitor:
A class for monitoring frames per second (FPS) to benchmark latency.
"""
- def __init__(self, sample_size: int = 30):
+ def __init__(self, sample_size: int = 30) -> None:
"""
Args:
sample_size: The maximum number of observations for latency
diff --git a/src/supervision/validators/__init__.py b/src/supervision/validators/__init__.py
index 313cd412..b8cd58d3 100644
--- a/src/supervision/validators/__init__.py
+++ b/src/supervision/validators/__init__.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
from typing import Any
import numpy as np
diff --git a/tests/annotators/test_core.py b/tests/annotators/test_core.py
index 27180d4f..005f8166 100644
--- a/tests/annotators/test_core.py
+++ b/tests/annotators/test_core.py
@@ -2,8 +2,6 @@
Tests for supervision/annotators/core.py
"""
-from __future__ import annotations
-
import warnings
import numpy as np
diff --git a/tests/annotators/test_docs.py b/tests/annotators/test_docs.py
index b31b06e1..f9def6ca 100644
--- a/tests/annotators/test_docs.py
+++ b/tests/annotators/test_docs.py
@@ -1,7 +1,5 @@
"""Regression tests for docs/detection/annotators.md tab structure."""
-from __future__ import annotations
-
import ast
import re
from pathlib import Path
diff --git a/tests/annotators/test_utils.py b/tests/annotators/test_utils.py
index 51642cfe..21b19310 100644
--- a/tests/annotators/test_utils.py
+++ b/tests/annotators/test_utils.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
from contextlib import ExitStack as DoesNotRaise
import numpy as np
diff --git a/tests/assets/test_downloader.py b/tests/assets/test_downloader.py
index a7f16115..ca0c677d 100644
--- a/tests/assets/test_downloader.py
+++ b/tests/assets/test_downloader.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
from unittest.mock import MagicMock, mock_open, patch
import pytest
@@ -9,7 +7,7 @@ from supervision.assets.list import ImageAssets, VideoAssets
class TestMD5HashMatching:
- def test_file_exists_matching_hash(self):
+ def test_file_exists_matching_hash(self) -> None:
"""Test is_md5_hash_matching when file exists and hash matches."""
test_content = b"test content"
test_hash = "9473fdd0d880a43c21b7778d34872157" # MD5 of "test content"
@@ -20,7 +18,7 @@ class TestMD5HashMatching:
):
assert is_md5_hash_matching("dummy_file", test_hash)
- def test_file_exists_not_matching_hash(self):
+ def test_file_exists_not_matching_hash(self) -> None:
"""Test is_md5_hash_matching when file exists but hash doesn't match."""
test_content = b"test content"
wrong_hash = "wrong_hash"
@@ -31,7 +29,7 @@ class TestMD5HashMatching:
):
assert not is_md5_hash_matching("dummy_file", wrong_hash)
- def test_file_not_exists(self):
+ def test_file_not_exists(self) -> None:
"""Test is_md5_hash_matching when file doesn't exist."""
with patch("os.path.exists", return_value=False):
assert not is_md5_hash_matching("nonexistent_file", "some_hash")
@@ -41,7 +39,7 @@ class TestDownloadAssets:
@patch("supervision.assets.downloader.logger")
@patch("supervision.assets.downloader.is_md5_hash_matching", return_value=True)
@patch("pathlib.Path.exists", return_value=True)
- def test_already_exists_and_valid(self, mock_exists, mock_md5, mock_logger):
+ def test_already_exists_and_valid(self, mock_exists, mock_md5, mock_logger) -> None:
"""Test download_assets when file already exists and is valid."""
filename = "vehicles.mp4"
result = download_assets(filename)
@@ -57,7 +55,7 @@ class TestDownloadAssets:
@patch("pathlib.Path.exists", return_value=True)
def test_already_exists_but_corrupted(
self, mock_exists, mock_md5, mock_remove, mock_logger
- ):
+ ) -> None:
"""Test download_assets when file exists but is corrupted (re-downloads)."""
filename = "vehicles.mp4"
result = download_assets(filename)
@@ -81,7 +79,7 @@ class TestDownloadAssets:
mock_mkdir,
mock_open_file,
mock_logger,
- ):
+ ) -> None:
"""Test download_assets downloading a new file."""
filename = "vehicles.mp4"
@@ -104,7 +102,7 @@ class TestDownloadAssets:
mock_copyfileobj.assert_called_once()
@patch("pathlib.Path.exists", return_value=False)
- def test_invalid_asset(self, mock_exists):
+ def test_invalid_asset(self, mock_exists) -> None:
"""Test download_assets with invalid asset name."""
invalid_filename = "invalid.mp4"
@@ -115,7 +113,7 @@ class TestDownloadAssets:
assert "vehicles.mp4" in str(exc_info.value)
@patch("pathlib.Path.exists", return_value=True)
- def test_invalid_asset_when_file_exists(self, mock_exists):
+ def test_invalid_asset_when_file_exists(self, mock_exists) -> None:
"""Test download_assets with invalid asset name that already exists."""
invalid_filename = "invalid.mp4"
@@ -141,7 +139,7 @@ class TestDownloadAssets:
mock_mkdir,
mock_open_file,
mock_logger,
- ):
+ ) -> None:
"""Test download_assets with VideoAssets enum."""
asset = VideoAssets.VEHICLES
@@ -174,7 +172,7 @@ class TestDownloadAssets:
mock_mkdir,
mock_open_file,
mock_logger,
- ):
+ ) -> None:
"""Test download_assets with ImageAssets enum."""
asset = ImageAssets.SOCCER
diff --git a/tests/assets/test_list.py b/tests/assets/test_list.py
index 2574475c..db143ed2 100644
--- a/tests/assets/test_list.py
+++ b/tests/assets/test_list.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
from supervision.assets.list import (
BASE_IMAGE_URL,
BASE_VIDEO_URL,
@@ -9,7 +7,7 @@ from supervision.assets.list import (
)
-def test_video_assets_list():
+def test_video_assets_list() -> None:
"""Test that VideoAssets.list() returns all video filenames."""
expected_filenames = [
"vehicles.mp4",
@@ -26,7 +24,7 @@ def test_video_assets_list():
assert VideoAssets.list() == expected_filenames
-def test_image_assets_list():
+def test_image_assets_list() -> None:
"""Test that ImageAssets.list() returns all image filenames."""
expected_filenames = [
"people-walking.jpg",
@@ -35,21 +33,21 @@ def test_image_assets_list():
assert ImageAssets.list() == expected_filenames
-def test_video_assets_values():
+def test_video_assets_values() -> None:
"""Test that VideoAssets enum members have correct attributes."""
assert VideoAssets.VEHICLES.filename == "vehicles.mp4"
assert VideoAssets.VEHICLES.md5_hash == "8155ff4e4de08cfa25f39de96483f918"
assert VideoAssets.VEHICLES.value == "vehicles.mp4"
-def test_image_assets_values():
+def test_image_assets_values() -> None:
"""Test that ImageAssets enum members have correct attributes."""
assert ImageAssets.SOCCER.filename == "soccer.jpg"
assert ImageAssets.SOCCER.md5_hash == "0f5a4b98abf3e3973faf9e9260a7d876"
assert ImageAssets.SOCCER.value == "soccer.jpg"
-def test_media_assets_dict_keys():
+def test_media_assets_dict_keys() -> None:
"""Test that MEDIA_ASSETS has all VideoAssets and ImageAssets as keys."""
expected_keys = {asset.filename for asset in VideoAssets} | {
asset.filename for asset in ImageAssets
@@ -57,7 +55,7 @@ def test_media_assets_dict_keys():
assert set(MEDIA_ASSETS.keys()) == expected_keys
-def test_media_assets_dict_values():
+def test_media_assets_dict_values() -> None:
"""Test that MEDIA_ASSETS values are tuples of (url, md5_hash)."""
for filename, (url, md5_hash) in MEDIA_ASSETS.items():
assert isinstance(url, str)
diff --git a/tests/conftest.py b/tests/conftest.py
index d4f6294b..40990c61 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import matplotlib
import numpy as np
import pytest
diff --git a/tests/dataset/formats/test_coco.py b/tests/dataset/formats/test_coco.py
index 83a1f717..f4dcf360 100644
--- a/tests/dataset/formats/test_coco.py
+++ b/tests/dataset/formats/test_coco.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import json
from contextlib import ExitStack as DoesNotRaise
from pathlib import Path
diff --git a/tests/dataset/formats/test_createml.py b/tests/dataset/formats/test_createml.py
index ee258c92..0097c3c5 100644
--- a/tests/dataset/formats/test_createml.py
+++ b/tests/dataset/formats/test_createml.py
@@ -1,7 +1,5 @@
"""Tests for CreateML object-detection annotation load/save and conversion helpers."""
-from __future__ import annotations
-
import json
from collections.abc import Callable
from pathlib import Path
diff --git a/tests/dataset/formats/test_labelme.py b/tests/dataset/formats/test_labelme.py
index 96f1ab7c..a0e09ec7 100644
--- a/tests/dataset/formats/test_labelme.py
+++ b/tests/dataset/formats/test_labelme.py
@@ -1,7 +1,5 @@
"""Tests for the LabelMe dataset format loader and exporter."""
-from __future__ import annotations
-
import json
from pathlib import Path
diff --git a/tests/dataset/formats/test_pascal_voc.py b/tests/dataset/formats/test_pascal_voc.py
index 25fc56a4..26959337 100644
--- a/tests/dataset/formats/test_pascal_voc.py
+++ b/tests/dataset/formats/test_pascal_voc.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
from contextlib import ExitStack as DoesNotRaise
import numpy as np
@@ -15,7 +13,7 @@ from supervision.dataset.formats.pascal_voc import (
from tests.helpers import _create_detections
-def are_xml_elements_equal(elem1, elem2):
+def are_xml_elements_equal(elem1, elem2) -> bool:
if (
elem1.tag != elem2.tag
or elem1.attrib != elem2.attrib
@@ -67,7 +65,7 @@ def test_object_to_pascal_voc(
polygon: np.ndarray | None,
expected_result,
exception: Exception,
-):
+) -> None:
with exception:
result = object_to_pascal_voc(xyxy=xyxy, name=name, polygon=polygon)
assert are_xml_elements_equal(result, expected_result)
@@ -132,7 +130,7 @@ def test_parse_polygon_points(
polygon_element,
expected_result: list[list],
exception,
-):
+) -> None:
with exception:
result = parse_polygon_points(polygon_element)
assert np.array_equal(result, expected_result)
@@ -221,7 +219,7 @@ MIXED_POLYGON_AND_BOX = """