chore: bump minimum Python to 3.10 (#2260)

- Drop Python 3.9 from CI test matrix
- requires-python = ">=3.10" in pyproject.toml
- ruff target-version py39 → py310
- mypy python_version 3.9 → 3.10
- Remove Python 3.9 classifier

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Jirka Borovec 2026-06-29 14:45:30 +02:00 committed by GitHub
parent 5a90113b15
commit 0a95bae8a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
130 changed files with 879 additions and 797 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -5,6 +5,14 @@ date_modified: 2026-06-25
# Changelog
### Unreleased <small>upcoming</small>
!!! 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 <small>Jun 23, 2026</small>
- 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.

View File

@ -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"

View File

@ -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?

View File

@ -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?

View File

@ -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+."
}
},
{

View File

@ -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

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import os
import sys
from typing import Any

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import json
import os
from typing import Any

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import cv2
import numpy as np
from inference import InferencePipeline

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import os
from collections.abc import Iterable

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from collections.abc import Iterable
import cv2

View File

@ -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]

View File

@ -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)

View File

@ -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]:

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import os
from hashlib import md5
from pathlib import Path

View File

@ -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)

View File

@ -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:

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import os
from pathlib import Path
from xml.etree.ElementTree import Element, SubElement

View File

@ -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)

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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.

View File

@ -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`. "

View File

@ -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

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import warnings
from collections import defaultdict, deque
from copy import deepcopy

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import io
from typing import Any, cast

View File

@ -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]

View File

@ -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)

View File

@ -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]:

View File

@ -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]

View File

@ -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(

View File

@ -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))

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import cv2
import numpy as np
import numpy.typing as npt

View File

@ -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

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from typing import TypeVar
import numpy as np

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import os
from typing import cast

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import numpy as np
import numpy.typing as npt

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Sequence
from typing import cast

View File

@ -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:

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from enum import Enum
Edges = tuple[tuple[int, int], ...]

View File

@ -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.
"""

View File

@ -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(

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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")

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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})"

View File

@ -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.

View File

@ -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)

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any

View File

@ -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(
![crop-image](https://media.roboflow.com/supervision-docs/supervision-docs-crop-image-2.png){ 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:
![grayscale-image](https://media.roboflow.com/supervision-docs/supervision-docs-grayscale-image-2.png){ 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)

View File

@ -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.

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from collections.abc import Generator, Iterable
from typing import TypeVar

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import logging
import os
import sys

View File

@ -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])

View File

@ -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

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from typing import Any
import numpy as np

View File

@ -2,8 +2,6 @@
Tests for supervision/annotators/core.py
"""
from __future__ import annotations
import warnings
import numpy as np

View File

@ -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

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from contextlib import ExitStack as DoesNotRaise
import numpy as np

View File

@ -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

View File

@ -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)

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import matplotlib
import numpy as np
import pytest

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import json
from contextlib import ExitStack as DoesNotRaise
from pathlib import Path

View File

@ -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

View File

@ -1,7 +1,5 @@
"""Tests for the LabelMe dataset format loader and exporter."""
from __future__ import annotations
import json
from pathlib import Path

View File

@ -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 = """<annotation><object><name>test</name><bndbox>
)
def test_detections_from_xml_obj(
xml_string, classes, resolution_wh, force_masks, expected_result, exception
):
) -> None:
with exception:
root = ElementTree.fromstring(xml_string)
result, _ = detections_from_xml_obj(root, classes, resolution_wh, force_masks)

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from contextlib import ExitStack as DoesNotRaise
from pathlib import Path

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from contextlib import ExitStack as DoesNotRaise
from pathlib import Path

View File

@ -1,7 +1,5 @@
"""Tests for show_progress parameter on dataset load/save operations."""
from __future__ import annotations
import json
import os
from pathlib import Path

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from contextlib import ExitStack as DoesNotRaise
from typing import TypeVar

View File

@ -1,7 +1,5 @@
"""Unit tests for CompactMask and its private RLE helpers."""
from __future__ import annotations
from contextlib import ExitStack as DoesNotRaise
import numpy as np

View File

@ -1,7 +1,5 @@
"""Integration tests: CompactMask <-> Detections, annotators, merge."""
from __future__ import annotations
from contextlib import ExitStack as DoesNotRaise
import numpy as np

View File

@ -8,8 +8,6 @@ These tests verify that:
and produce the same keep-set as when given equivalent dense arrays.
"""
from __future__ import annotations
import numpy as np
import pytest

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import warnings
from contextlib import ExitStack as DoesNotRaise
@ -882,7 +880,7 @@ def test_merge_inner_detection_object_pair(
detection_2: Detections,
expected_result: Detections | None,
exception: Exception,
):
) -> None:
with exception:
result = merge_inner_detection_object_pair(detection_1, detection_2)
assert result == expected_result

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import csv
import os
from typing import Any
@ -515,7 +513,7 @@ def test_csv_sink_manual(
assert_csv_equal(file_name, expected_result)
def assert_csv_equal(file_name, expected_rows):
def assert_csv_equal(file_name, expected_rows) -> None:
with open(file_name, newline="") as file:
reader = csv.reader(file)
for i, row in enumerate(reader):

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import numpy as np
import pytest

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import numpy as np
import pytest

View File

@ -6,8 +6,6 @@ Verifies that with compact_masks=True:
- Final detections are pixel-identical to the compact_masks=False path.
"""
from __future__ import annotations
import numpy as np
import supervision as sv

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import json
import os
from typing import Any

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from contextlib import ExitStack as DoesNotRaise
import numpy as np

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import numpy as np
import pytest

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from contextlib import ExitStack as DoesNotRaise
import numpy as np
@ -42,7 +40,7 @@ class TestPolygonZoneInit:
),
],
)
def test_empty_anchors_raises(self, polygon, triggering_anchors, exception):
def test_empty_anchors_raises(self, polygon, triggering_anchors, exception) -> None:
with exception:
sv.PolygonZone(polygon, triggering_anchors=triggering_anchors)

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from contextlib import ExitStack as DoesNotRaise
from contextlib import nullcontext as does_not_raise
@ -1153,7 +1151,7 @@ def test_from_google_gemini_2_5(
classes: list[str] | None,
expected_results: None
| (tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]),
):
) -> None:
with exception:
(
xyxy,
@ -1274,7 +1272,7 @@ def test_from_deepseek_vl_2(
resolution_wh: tuple[int, int],
classes: list[str] | None,
expected_detections: Detections,
):
) -> None:
with exception:
detections = Detections.from_vlm(
vlm=VLM.DEEPSEEK_VL_2,

View File

@ -1,5 +1,3 @@
from __future__ import annotations
import threading
import warnings

View File

@ -1,10 +1,8 @@
"""Tests for windowed GeoTIFF reads in InferenceSlicer."""
from __future__ import annotations
import threading
import time
from typing import Callable
from collections.abc import Callable
import numpy as np
import pytest

View File

@ -1,7 +1,5 @@
"""Tests for DetectionsSmoother bounding-box and confidence smoothing."""
from __future__ import annotations
import numpy as np
import pytest
from numpy.testing import assert_allclose

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from contextlib import ExitStack as DoesNotRaise
import numpy as np

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from contextlib import ExitStack as DoesNotRaise
import numpy as np

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from contextlib import ExitStack as DoesNotRaise
from typing import Any
@ -750,7 +748,7 @@ def test_merge_data(
data_list: list[dict[str, Any]],
expected_result: dict[str, Any] | None,
exception: Exception,
):
) -> None:
with exception:
result = merge_data(data_list=data_list)
if expected_result is None:
@ -926,7 +924,7 @@ def test_get_data_item(
index: Any,
expected_result: dict[str, Any] | None,
exception: Exception,
):
) -> None:
with exception:
result = get_data_item(data=data, index=index)
for key in result:
@ -1103,7 +1101,7 @@ def test_get_data_item(
),
],
)
def test_merge_metadata(metadata_list, expected_result, exception):
def test_merge_metadata(metadata_list, expected_result, exception) -> None:
with exception:
result = merge_metadata(metadata_list)
if expected_result is None:

View File

@ -1,5 +1,3 @@
from __future__ import annotations
from contextlib import ExitStack as DoesNotRaise
import numpy as np

Some files were not shown because too many files have changed in this diff Show More