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:
parent
5a90113b15
commit
0a95bae8a8
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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+."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from inference import InferencePipeline
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
import cv2
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from hashlib import md5
|
||||
from pathlib import Path
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from xml.etree.ElementTree import Element, SubElement
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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`. "
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from collections import defaultdict, deque
|
||||
from copy import deepcopy
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from typing import Any, cast
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TypeVar
|
||||
|
||||
import numpy as np
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import cast
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Sequence
|
||||
from typing import cast
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
|
||||
Edges = tuple[tuple[int, int], ...]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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})"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import os
|
|||
import shutil
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
from typing import Any, Literal, cast
|
||||
from types import TracebackType
|
||||
from typing import Literal, cast
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
|
@ -36,7 +37,7 @@ MAX_COLUMNS_FOR_SINGLE_ROW_GRID = 3
|
|||
@ensure_cv2_image_for_standalone_function
|
||||
def crop_image(
|
||||
image: ImageType,
|
||||
xyxy: npt.NDArray[int] | list[int] | tuple[int, int, int, int],
|
||||
xyxy: npt.NDArray[np.number] | list[int] | tuple[int, int, int, int],
|
||||
) -> ImageType:
|
||||
"""
|
||||
Crop image based on bounding box coordinates.
|
||||
|
|
@ -77,17 +78,14 @@ def crop_image(
|
|||
|
||||
{ align=center width="1000" }
|
||||
""" # noqa E501 // docs
|
||||
if isinstance(xyxy, (list, tuple)):
|
||||
xyxy = np.array(xyxy)
|
||||
|
||||
xyxy = np.round(xyxy).astype(int)
|
||||
x_min, y_min, x_max, y_max = xyxy.flatten()
|
||||
xyxy_arr = np.asarray(xyxy, dtype=np.float64).round().astype(np.int32)
|
||||
x_min, y_min, x_max, y_max = xyxy_arr.flatten()
|
||||
|
||||
if isinstance(image, np.ndarray):
|
||||
return image[y_min:y_max, x_min:x_max]
|
||||
|
||||
if isinstance(image, Image.Image):
|
||||
return image.crop((x_min, y_min, x_max, y_max))
|
||||
return image.crop((float(x_min), float(y_min), float(x_max), float(y_max)))
|
||||
|
||||
raise TypeError(
|
||||
f"`image` must be a numpy.ndarray or PIL.Image.Image. Received {type(image)}"
|
||||
|
|
@ -142,7 +140,10 @@ def scale_image(image: ImageType, scale_factor: float) -> ImageType:
|
|||
width_old, height_old = image.shape[1], image.shape[0]
|
||||
width_new = int(width_old * scale_factor)
|
||||
height_new = int(height_old * scale_factor)
|
||||
return cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR)
|
||||
return cast(
|
||||
npt.NDArray[np.uint8],
|
||||
cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR),
|
||||
)
|
||||
|
||||
|
||||
@ensure_cv2_image_for_standalone_function
|
||||
|
|
@ -208,7 +209,10 @@ def resize_image(
|
|||
else:
|
||||
width_new, height_new = resolution_wh
|
||||
|
||||
return cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR)
|
||||
return cast(
|
||||
npt.NDArray[np.uint8],
|
||||
cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR),
|
||||
)
|
||||
|
||||
|
||||
@ensure_cv2_image_for_standalone_function
|
||||
|
|
@ -269,14 +273,17 @@ def letterbox_image(
|
|||
padding_bottom = resolution_wh[1] - height_new - padding_top
|
||||
padding_left = (resolution_wh[0] - width_new) // 2
|
||||
padding_right = resolution_wh[0] - width_new - padding_left
|
||||
image_with_borders = cv2.copyMakeBorder(
|
||||
resized_image,
|
||||
padding_top,
|
||||
padding_bottom,
|
||||
padding_left,
|
||||
padding_right,
|
||||
cv2.BORDER_CONSTANT,
|
||||
value=color,
|
||||
image_with_borders = cast(
|
||||
npt.NDArray[np.uint8],
|
||||
cv2.copyMakeBorder(
|
||||
resized_image,
|
||||
padding_top,
|
||||
padding_bottom,
|
||||
padding_left,
|
||||
padding_right,
|
||||
cv2.BORDER_CONSTANT,
|
||||
value=color,
|
||||
),
|
||||
)
|
||||
|
||||
return image_with_borders
|
||||
|
|
@ -345,12 +352,12 @@ def overlay_image(
|
|||
b, g, r, alpha = cv2.split(
|
||||
overlay[crop_y_min:crop_y_max, crop_x_min:crop_x_max]
|
||||
)
|
||||
alpha = alpha[:, :, None] / 255.0
|
||||
overlay_color = cv2.merge((b, g, r))
|
||||
alpha_f32 = alpha[:, :, None].astype(np.float32) / 255.0
|
||||
overlay_color = cv2.merge((b, g, r)).astype(np.float32)
|
||||
|
||||
roi = image[y_min:y_max, x_min:x_max]
|
||||
roi[:] = roi * (1 - alpha) + overlay_color * alpha
|
||||
image[y_min:y_max, x_min:x_max] = roi
|
||||
roi = image[y_min:y_max, x_min:x_max].astype(np.float32)
|
||||
blended = roi * (1 - alpha_f32) + overlay_color * alpha_f32
|
||||
image[y_min:y_max, x_min:x_max] = np.clip(blended, 0, 255).astype(np.uint8)
|
||||
else:
|
||||
image[y_min:y_max, x_min:x_max] = overlay[
|
||||
crop_y_min:crop_y_max, crop_x_min:crop_x_max
|
||||
|
|
@ -434,8 +441,9 @@ def grayscale_image(image: ImageType) -> ImageType:
|
|||
|
||||
{ align=center width="1000" }
|
||||
""" # noqa E501 // docs
|
||||
grayscaled = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||
return cv2.cvtColor(grayscaled, cv2.COLOR_GRAY2BGR)
|
||||
assert isinstance(image, np.ndarray)
|
||||
grayscaled = cast(npt.NDArray[np.uint8], cv2.cvtColor(image, cv2.COLOR_BGR2GRAY))
|
||||
return cast(npt.NDArray[np.uint8], cv2.cvtColor(grayscaled, cv2.COLOR_GRAY2BGR))
|
||||
|
||||
|
||||
def get_image_resolution_wh(image: ImageType) -> tuple[int, int]:
|
||||
|
|
@ -491,7 +499,7 @@ class ImageSink:
|
|||
target_dir_path: str,
|
||||
overwrite: bool = False,
|
||||
image_name_pattern: str = "image_{:05d}.png",
|
||||
):
|
||||
) -> None:
|
||||
"""
|
||||
Initialize context manager for saving images to directory.
|
||||
|
||||
|
|
@ -559,7 +567,7 @@ class ImageSink:
|
|||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
exc_traceback: Any,
|
||||
exc_traceback: TracebackType | None,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
|
|
@ -663,32 +671,32 @@ def create_tiles(
|
|||
return_type = _negotiate_tiles_format(images=images)
|
||||
tile_padding_color = unify_to_bgr(color=tile_padding_color)
|
||||
tile_margin_color = unify_to_bgr(color=tile_margin_color)
|
||||
images = images_to_cv2(images=images)
|
||||
images_cv2 = images_to_cv2(images=images)
|
||||
if single_tile_size is None:
|
||||
single_tile_size = _aggregate_images_shape(images=images, mode=tile_scaling)
|
||||
single_tile_size = _aggregate_images_shape(images=images_cv2, mode=tile_scaling)
|
||||
resized_images = [
|
||||
letterbox_image(
|
||||
image=i, resolution_wh=single_tile_size, color=tile_padding_color
|
||||
)
|
||||
for i in images
|
||||
for i in images_cv2
|
||||
]
|
||||
grid_size = _establish_grid_size(images=images, grid_size=grid_size)
|
||||
if len(images) > grid_size[0] * grid_size[1]:
|
||||
grid_size = _establish_grid_size(images=images_cv2, grid_size=grid_size)
|
||||
if len(images_cv2) > grid_size[0] * grid_size[1]:
|
||||
raise ValueError(
|
||||
f"Could not place {len(images)} in grid with size: {grid_size}."
|
||||
f"Could not place {len(images_cv2)} in grid with size: {grid_size}."
|
||||
)
|
||||
if titles is not None:
|
||||
titles = fill(sequence=titles, desired_size=len(images), content=None)
|
||||
titles = fill(sequence=titles, desired_size=len(images_cv2), content=None)
|
||||
if isinstance(titles_anchors, list):
|
||||
titles_anchors_sequence = titles_anchors
|
||||
else:
|
||||
titles_anchors_sequence = [titles_anchors]
|
||||
titles_anchors = fill(
|
||||
sequence=titles_anchors_sequence, desired_size=len(images), content=None
|
||||
sequence=titles_anchors_sequence, desired_size=len(images_cv2), content=None
|
||||
)
|
||||
titles_color = unify_to_bgr(color=titles_color)
|
||||
titles_background_color = unify_to_bgr(color=titles_background_color)
|
||||
tiles = _generate_tiles(
|
||||
tiles_image = _generate_tiles(
|
||||
images=resized_images,
|
||||
grid_size=grid_size,
|
||||
single_tile_size=single_tile_size,
|
||||
|
|
@ -706,8 +714,10 @@ def create_tiles(
|
|||
default_title_placement=default_title_placement,
|
||||
)
|
||||
if return_type == "pillow":
|
||||
tiles = cv2_to_pillow(image=tiles)
|
||||
return cast(ImageType, tiles)
|
||||
tiles_image_pillow: object = cv2_to_pillow(image=tiles_image)
|
||||
return cast(ImageType, tiles_image_pillow)
|
||||
tiles_image_cv2: object = tiles_image
|
||||
return cast(ImageType, tiles_image_cv2)
|
||||
|
||||
|
||||
def _negotiate_tiles_format(images: list[ImageType]) -> Literal["cv2", "pillow"]:
|
||||
|
|
@ -890,9 +900,10 @@ def _merge_tiles_elements(
|
|||
tile_margin: int,
|
||||
tile_margin_color: tuple[int, int, int],
|
||||
) -> npt.NDArray[np.uint8]:
|
||||
vertical_padding: npt.NDArray[np.uint8] = (
|
||||
np.ones((single_tile_size[1], tile_margin, 3), dtype=np.uint8)
|
||||
* tile_margin_color
|
||||
vertical_padding: npt.NDArray[np.uint8] = np.full(
|
||||
(single_tile_size[1], tile_margin, 3),
|
||||
tile_margin_color,
|
||||
dtype=np.uint8,
|
||||
)
|
||||
merged_rows = [
|
||||
np.concatenate(
|
||||
|
|
@ -906,26 +917,19 @@ def _merge_tiles_elements(
|
|||
for row in tiles_elements
|
||||
]
|
||||
row_width = merged_rows[0].shape[1]
|
||||
horizontal_padding = (
|
||||
np.ones((tile_margin, row_width, 3), dtype=np.uint8) * tile_margin_color
|
||||
horizontal_padding: npt.NDArray[np.uint8] = np.full(
|
||||
(tile_margin, row_width, 3),
|
||||
tile_margin_color,
|
||||
dtype=np.uint8,
|
||||
)
|
||||
rows_with_paddings = []
|
||||
rows_with_paddings: list[npt.NDArray[np.uint8]] = []
|
||||
for row in merged_rows:
|
||||
rows_with_paddings.append(row)
|
||||
rows_with_paddings.append(horizontal_padding)
|
||||
return cast(
|
||||
npt.NDArray[np.uint8],
|
||||
np.concatenate(
|
||||
rows_with_paddings[:-1],
|
||||
axis=0,
|
||||
).astype(np.uint8),
|
||||
)
|
||||
return np.concatenate(rows_with_paddings[:-1], axis=0).astype(np.uint8, copy=False)
|
||||
|
||||
|
||||
def _generate_color_image(
|
||||
shape: tuple[int, int], color: tuple[int, int, int]
|
||||
) -> npt.NDArray[np.uint8]:
|
||||
return cast(
|
||||
npt.NDArray[np.uint8],
|
||||
np.ones((*shape[::-1], 3), dtype=np.uint8) * color,
|
||||
)
|
||||
return np.full((*shape[::-1], 3), color, dtype=np.uint8)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator, Iterable
|
||||
from typing import TypeVar
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
Tests for supervision/annotators/core.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
|
||||
import numpy as np
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import matplotlib
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
from pathlib import Path
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
"""Tests for the LabelMe dataset format loader and exporter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
from pathlib import Path
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
from pathlib import Path
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
from typing import TypeVar
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
"""Integration tests: CompactMask <-> Detections, annotators, merge."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
|
||||
import numpy as np
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
|
||||
import numpy as np
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import warnings
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
|
||||
import numpy as np
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
|
||||
import numpy as np
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue