Compare commits
147 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
7d325d23e3 | |
|
|
fb22686a5e | |
|
|
6b143e4e41 | |
|
|
bc20dd19fb | |
|
|
e82349fabf | |
|
|
01df50b440 | |
|
|
a95d85ce07 | |
|
|
813c4295bf | |
|
|
7fc91180fe | |
|
|
f7f53d0a60 | |
|
|
f2efc328f7 | |
|
|
475d551908 | |
|
|
1a2b9b24db | |
|
|
3c19d176b3 | |
|
|
794971ba0f | |
|
|
541b0226cd | |
|
|
5212af7b70 | |
|
|
e138f6c544 | |
|
|
5f0654e5cc | |
|
|
caff4d1ec2 | |
|
|
ceddf331c6 | |
|
|
9c06bfeddb | |
|
|
fe77de5b9f | |
|
|
bcbe8de1e4 | |
|
|
156ee33740 | |
|
|
7d915a7357 | |
|
|
9837c17878 | |
|
|
b20d6eac46 | |
|
|
25e879ec05 | |
|
|
89d49c2e93 | |
|
|
140c05f273 | |
|
|
206adc083c | |
|
|
5fccf8a966 | |
|
|
8be58fa0aa | |
|
|
f1f8213c01 | |
|
|
937ed4af37 | |
|
|
60d748e57d | |
|
|
f7b63f149a | |
|
|
9aaf7fdbd6 | |
|
|
a4b9c4e097 | |
|
|
68e63f9e39 | |
|
|
bc7b9fc69e | |
|
|
1efa5b8eaa | |
|
|
20b7c085b7 | |
|
|
c3496134bc | |
|
|
39eb6571ce | |
|
|
7096ee911c | |
|
|
3d669f1ab4 | |
|
|
8ecd9a6680 | |
|
|
16814acff3 | |
|
|
94cfb7f290 | |
|
|
3e80c0ce3c | |
|
|
d5cadf526a | |
|
|
6306b7ca71 | |
|
|
14c3c86e22 | |
|
|
39c015c0ff | |
|
|
9897b0790f | |
|
|
287868e171 | |
|
|
5344cb99dd | |
|
|
8dedc3474d | |
|
|
1e4489f61b | |
|
|
75023c5f2f | |
|
|
23a2227ae7 | |
|
|
072f78471c | |
|
|
74db9e29ff | |
|
|
dde422703c | |
|
|
814a226eba | |
|
|
6a69197177 | |
|
|
5b4c8b6d0d | |
|
|
c3413a8f10 | |
|
|
beede8a638 | |
|
|
e13090f84b | |
|
|
8fd47a9ec6 | |
|
|
ced30b48f7 | |
|
|
bd0f44fcfd | |
|
|
78aec073c4 | |
|
|
eea04b3656 | |
|
|
afcf13a6f5 | |
|
|
0e1056df19 | |
|
|
f173905c8b | |
|
|
15dbbb5cb1 | |
|
|
0d4c3a4fcf | |
|
|
f196e15f26 | |
|
|
99049d84e1 | |
|
|
8692148c67 | |
|
|
058d8fd990 | |
|
|
8f576b02a8 | |
|
|
a32323d5bd | |
|
|
d590eb6658 | |
|
|
04858a2727 | |
|
|
3f816c0b6c | |
|
|
3ecd5d0744 | |
|
|
098f5315b5 | |
|
|
beb047095f | |
|
|
4624dde770 | |
|
|
97f5c0ca53 | |
|
|
e1b7a16101 | |
|
|
15f56dea30 | |
|
|
934da124f5 | |
|
|
ad2c75021f | |
|
|
0a95bae8a8 | |
|
|
5a90113b15 | |
|
|
58aacd3f74 | |
|
|
b5f0752f41 | |
|
|
939097bdce | |
|
|
46fdd5ceba | |
|
|
09b21992c5 | |
|
|
10b538373b | |
|
|
f34a940c0a | |
|
|
4b867f282b | |
|
|
57bb5e7e8b | |
|
|
2aa43bceab | |
|
|
7239af5048 | |
|
|
27ba0aa92f | |
|
|
25d98c85f3 | |
|
|
2169192492 | |
|
|
a3a860cf3f | |
|
|
9ff41b9706 | |
|
|
44b62cd164 | |
|
|
a179d9120f | |
|
|
14f6f245c8 | |
|
|
3e610a0558 | |
|
|
af6365acfb | |
|
|
49ecac0376 | |
|
|
9251893bd9 | |
|
|
ccd7098f2f | |
|
|
07d182be56 | |
|
|
cca3c14911 | |
|
|
44546a13f2 | |
|
|
4b60bbc9cc | |
|
|
6918d44190 | |
|
|
a48532f6f5 | |
|
|
11a586c133 | |
|
|
31e84f7909 | |
|
|
7e765eb054 | |
|
|
393ff52954 | |
|
|
c9962c9262 | |
|
|
04b6d45ea2 | |
|
|
2854932965 | |
|
|
bcc785fbb5 | |
|
|
c0df72b84a | |
|
|
15cf80abac | |
|
|
b9c24ddafd | |
|
|
c2490e6015 | |
|
|
a914a00ab8 | |
|
|
d3a0a84dd5 | |
|
|
1e551ec9c0 |
|
|
@ -11,14 +11,14 @@ Please read and adhere to our [Code of Conduct](https://supervision.roboflow.com
|
|||
## Table of Contents
|
||||
|
||||
- [Contribution Guidelines](#contribution-guidelines)
|
||||
- [Contributing Features](#contributing-features)
|
||||
- [API Design Principles](#api-design-principles)
|
||||
- [Contributing Features](#contributing-features)
|
||||
- [API Design Principles](#api-design-principles)
|
||||
- [How to Contribute Changes](#how-to-contribute-changes)
|
||||
- [Installation for Contributors](#installation-for-contributors)
|
||||
- [Code Style and Quality](#code-style-and-quality)
|
||||
- [Pre-commit tool](#pre-commit-tool)
|
||||
- [Docstrings](#docstrings)
|
||||
- [Type checking](#type-checking)
|
||||
- [Pre-commit tool](#pre-commit-tool)
|
||||
- [Docstrings](#docstrings)
|
||||
- [Type checking](#type-checking)
|
||||
- [Documentation](#documentation)
|
||||
- [Cookbooks](#cookbooks)
|
||||
- [Tests](#tests)
|
||||
|
|
@ -142,63 +142,63 @@ Before starting your work on the project, set up your development environment:
|
|||
|
||||
1. **Clone your fork of the project:**
|
||||
|
||||
**Option A: Recommended for most contributors (shallow clone of develop branch):**
|
||||
**Option A: Recommended for most contributors (shallow clone of develop branch):**
|
||||
|
||||
```bash
|
||||
git clone --depth 1 -b develop https://github.com/YOUR_USERNAME/supervision.git
|
||||
cd supervision
|
||||
```
|
||||
```bash
|
||||
git clone --depth 1 -b develop https://github.com/YOUR_USERNAME/supervision.git
|
||||
cd supervision
|
||||
```
|
||||
|
||||
Replace `YOUR_USERNAME` with your GitHub username.
|
||||
Replace `YOUR_USERNAME` with your GitHub username.
|
||||
|
||||
> **Note**: Using `--depth 1` creates a shallow clone with minimal history and `-b develop` ensures you start with the development branch. This significantly reduces download size while providing everything needed to contribute.
|
||||
> **Note**: Using `--depth 1` creates a shallow clone with minimal history and `-b develop` ensures you start with the development branch. This significantly reduces download size while providing everything needed to contribute.
|
||||
|
||||
**Option B: Full repository clone (if you need complete history):**
|
||||
**Option B: Full repository clone (if you need complete history):**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/supervision.git
|
||||
cd supervision
|
||||
git checkout develop
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/supervision.git
|
||||
cd supervision
|
||||
git checkout develop
|
||||
```
|
||||
|
||||
2. **Set up the upstream remote:**
|
||||
|
||||
```bash
|
||||
git remote add upstream https://github.com/roboflow/supervision.git
|
||||
git fetch upstream
|
||||
```
|
||||
```bash
|
||||
git remote add upstream https://github.com/roboflow/supervision.git
|
||||
git fetch upstream
|
||||
```
|
||||
|
||||
3. **Create and activate a virtual environment:**
|
||||
|
||||
**On Linux/macOS:**
|
||||
**On Linux/macOS:**
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
**On Windows:**
|
||||
**On Windows:**
|
||||
|
||||
```cmd
|
||||
python -m venv .venv
|
||||
.venv\Scripts\activate
|
||||
```
|
||||
```cmd
|
||||
python -m venv .venv
|
||||
.venv\Scripts\activate
|
||||
```
|
||||
|
||||
4. **Install `uv`:**
|
||||
|
||||
Follow the instructions on the [uv installation page](https://docs.astral.sh/uv/getting-started/installation/).
|
||||
Follow the instructions on the [uv installation page](https://docs.astral.sh/uv/getting-started/installation/).
|
||||
|
||||
5. **Install project dependencies:**
|
||||
|
||||
```bash
|
||||
uv pip install -r pyproject.toml --group dev --group docs --extra metrics
|
||||
```
|
||||
```bash
|
||||
uv pip install -r pyproject.toml --group dev --group docs --extra metrics
|
||||
```
|
||||
|
||||
6. **Verify the setup:**
|
||||
|
||||
```bash
|
||||
uv run pytest
|
||||
```
|
||||
```bash
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
## 🎨 Code Style and Quality
|
||||
|
||||
|
|
@ -212,27 +212,27 @@ To run the pre-commit tool, follow these steps:
|
|||
|
||||
1. **Install pre-commit** (already included if you followed the installation steps above):
|
||||
|
||||
```bash
|
||||
uv sync --group dev
|
||||
```
|
||||
```bash
|
||||
uv sync --group dev
|
||||
```
|
||||
|
||||
2. **Navigate to the project's root directory** (if not already there).
|
||||
|
||||
3. **Run pre-commit checks**:
|
||||
|
||||
```bash
|
||||
uv run pre-commit run --all-files
|
||||
```
|
||||
```bash
|
||||
uv run pre-commit run --all-files
|
||||
```
|
||||
|
||||
This will execute the pre-commit hooks configured for this project. If any issues are found, the pre-commit tool will provide feedback on how to resolve them. Make the necessary changes and re-run the command until all issues are resolved.
|
||||
This will execute the pre-commit hooks configured for this project. If any issues are found, the pre-commit tool will provide feedback on how to resolve them. Make the necessary changes and re-run the command until all issues are resolved.
|
||||
|
||||
4. **Install pre-commit as a git hook** (optional but recommended):
|
||||
|
||||
```bash
|
||||
uv run pre-commit install
|
||||
```
|
||||
```bash
|
||||
uv run pre-commit install
|
||||
```
|
||||
|
||||
This will automatically run pre-commit checks every time you make a `git commit`.
|
||||
This will automatically run pre-commit checks every time you make a `git commit`.
|
||||
|
||||
### Docstrings
|
||||
|
||||
|
|
@ -246,6 +246,10 @@ Every docstring should include a usage example. When the example only uses `supe
|
|||
|
||||
Type hints are required on all new code. mypy is enforced by the pre-commit hook configured in `.pre-commit-config.yaml` — your PR will fail CI if mypy reports errors.
|
||||
|
||||
### Readability
|
||||
|
||||
Avoid multi-branch conditional expressions inside function or constructor arguments. If an argument needs more than a simple `a if condition else b`, assign it to a named local variable before the call.
|
||||
|
||||
### Performance
|
||||
|
||||
- Avoid unnecessary copies of NumPy arrays.
|
||||
|
|
@ -280,15 +284,15 @@ To run the documentation locally:
|
|||
|
||||
1. **Install documentation dependencies** (if not already installed):
|
||||
|
||||
```bash
|
||||
uv sync --group docs
|
||||
```
|
||||
```bash
|
||||
uv sync --group docs
|
||||
```
|
||||
|
||||
2. **Start the documentation server**:
|
||||
|
||||
```bash
|
||||
uv run mkdocs serve
|
||||
```
|
||||
```bash
|
||||
uv run mkdocs serve
|
||||
```
|
||||
|
||||
3. **Access the documentation** at `http://127.0.0.1:8000` in your browser.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@
|
|||
|
||||
This file provides context-aware guidance for GitHub Copilot when working in the Supervision repository.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 📚 Repository Overview
|
||||
|
||||
**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
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 🏗️ Project Structure
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ supervision/
|
|||
└── examples/ # Usage examples
|
||||
```
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 🔧 Development Commands
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ uv run pytest --cov=supervision
|
|||
uv run mkdocs serve
|
||||
```
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 💻 Code Conventions
|
||||
|
||||
|
|
@ -77,8 +77,8 @@ uv run mkdocs serve
|
|||
- **Linting**: Enforced by `ruff-check` (pre-commit)
|
||||
- **Type Hints**: Required on all new code
|
||||
- **Docstrings**: Required using [Google Python style](https://google.github.io/styleguide/pyguide.html#383-functions-and-methods)
|
||||
- Must include usage examples with primitive values
|
||||
- Serve as runnable documentation
|
||||
- Must include usage examples with primitive values
|
||||
- Serve as runnable documentation
|
||||
|
||||
### Performance
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ uv run mkdocs serve
|
|||
- Maintain backward compatibility unless explicitly breaking
|
||||
- Prefer functional utilities over complex classes
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 🧪 Testing Requirements
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ All new features must include:
|
|||
- Clear test names describing what they validate
|
||||
- Proper assertions (not just "no exception raised")
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 📝 Documentation Requirements
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ For new public functions/classes:
|
|||
- Entry in appropriate `docs/*.md` file
|
||||
- Reference in `mkdocs.yml` navigation
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 🔍 Pull Request Reviews
|
||||
|
||||
|
|
@ -129,7 +129,7 @@ Quick checklist:
|
|||
- Score code quality, testing, docs (n/5 scale)
|
||||
- Use inline comments + GitHub suggestion format
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 🌿 Branching & Commits
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ Quick checklist:
|
|||
- Use **conventional commits**: `feat:`, `fix:`, `docs:`, `refactor:`, `perf:`, `test:`, `chore:`
|
||||
- All PRs target `develop` branch
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 🎯 Context-Aware Behavior
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ updates:
|
|||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: ⬆️
|
||||
target-branch: "develop"
|
||||
|
|
@ -17,6 +19,8 @@ updates:
|
|||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: ⬆️
|
||||
target-branch: "develop"
|
||||
|
|
|
|||
|
|
@ -9,9 +9,16 @@ accept = [
|
|||
200, # OK
|
||||
408, # Request Timeout
|
||||
# 429 means the server received the request and is actively rate-limiting — the URL is
|
||||
# reachable. Real dead links return 404, 410, 5xx, or fail to connect; none of those
|
||||
# produce a 429, so accepting it here does not hide broken links.
|
||||
# reachable. Real dead links return 404, 410, or fail to connect/resolve; none of
|
||||
# those produce a 429, so accepting it here does not hide broken links.
|
||||
429, # Too Many Requests (rate-limited but reachable; does not mask dead links)
|
||||
# CI regularly sees momentary 502/503/504 from large, healthy hosts (github.com,
|
||||
# supervision.roboflow.com), and in-run retries tend to land inside the same
|
||||
# incident window. Genuinely dead links surface as 404, 410, or connection/DNS
|
||||
# failures, which remain rejected.
|
||||
502, # Bad Gateway (transient upstream hiccup)
|
||||
503, # Service Unavailable (transient overload or maintenance)
|
||||
504, # Gateway Timeout (transient upstream hiccup)
|
||||
]
|
||||
|
||||
exclude = [
|
||||
|
|
@ -19,6 +26,7 @@ exclude = [
|
|||
"http://127.0.0.1:8000", # hint for local docs server
|
||||
"https://sam2.metademolab.com/", # returns 403 Forbidden
|
||||
"https://snyk.io/advisor/python/supervision/badge.svg", # badge URL
|
||||
"https://trendshift.io", # badge API times out in CI
|
||||
"https://universe.roboflow.com/",
|
||||
"https://universe.roboflow.com/model-examples/segmented-animals-basic",
|
||||
# fixme: this page returns 401 Unauthorized when accessed and 404 Not Found when accessed with browser,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Validate doctest prompt formatting in source docstrings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
DOCTEST_PROMPT_RE = re.compile(r"^\s*>>>")
|
||||
FENCE_RE = re.compile(r"^\s*```(?P<language>[A-Za-z0-9_-]*)\s*$")
|
||||
|
||||
|
||||
def _check_content(content: str, path: Path) -> list[str]:
|
||||
r"""Return doctest fence violations for file content.
|
||||
|
||||
Examples:
|
||||
```pycon
|
||||
>>> from pathlib import Path
|
||||
>>> _check_content('```pycon\n>>> len([1])\n1\n\n```\n', Path('src/a.py'))
|
||||
[]
|
||||
>>> _check_content('>>> len([1])\n1\n', Path('src/a.py'))
|
||||
['src/a.py:1: doctest prompt must be inside a ```pycon fenced block']
|
||||
>>> violations = _check_content(
|
||||
... '```pycon\n>>> len([1])\n1\n```\n', Path('src/a.py')
|
||||
... )
|
||||
>>> violations == [
|
||||
... 'src/a.py:4: pycon doctest block must include exactly one blank line '
|
||||
... 'before the closing fence'
|
||||
... ]
|
||||
True
|
||||
|
||||
```
|
||||
"""
|
||||
violations: list[str] = []
|
||||
active_fence_language: str | None = None
|
||||
in_invalid_doctest_block = False
|
||||
line_before_previous = ""
|
||||
previous_line = ""
|
||||
|
||||
for line_number, line in enumerate(content.splitlines(), start=1):
|
||||
fence_match = FENCE_RE.match(line)
|
||||
if fence_match is not None:
|
||||
in_invalid_doctest_block = False
|
||||
if active_fence_language is None:
|
||||
active_fence_language = fence_match.group("language")
|
||||
else:
|
||||
has_exactly_one_blank_line = (
|
||||
previous_line.strip() == "" and line_before_previous.strip() != ""
|
||||
)
|
||||
if active_fence_language == "pycon" and not has_exactly_one_blank_line:
|
||||
violations.append(
|
||||
f"{path}:{line_number}: pycon doctest block must include "
|
||||
"exactly one blank line before the closing fence"
|
||||
)
|
||||
active_fence_language = None
|
||||
line_before_previous = previous_line
|
||||
previous_line = line
|
||||
continue
|
||||
|
||||
if not line.strip():
|
||||
in_invalid_doctest_block = False
|
||||
|
||||
if (
|
||||
DOCTEST_PROMPT_RE.match(line)
|
||||
and active_fence_language != "pycon"
|
||||
and not in_invalid_doctest_block
|
||||
):
|
||||
violations.append(
|
||||
f"{path}:{line_number}: doctest prompt must be inside a "
|
||||
"```pycon fenced block"
|
||||
)
|
||||
in_invalid_doctest_block = True
|
||||
|
||||
line_before_previous = previous_line
|
||||
previous_line = line
|
||||
|
||||
return violations
|
||||
|
||||
|
||||
def check_file(path: Path) -> list[str]:
|
||||
"""Return doctest fence violations for a single source file."""
|
||||
if not path.is_file() or path.suffix != ".py" or "src" not in path.parts:
|
||||
return []
|
||||
|
||||
return _check_content(content=path.read_text(encoding="utf-8"), path=path)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Run the doctest fence check for pre-commit supplied files."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate doctest prompts in src/ are fenced as pycon blocks."
|
||||
)
|
||||
parser.add_argument("files", nargs="*", type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
violations = [
|
||||
violation for path in args.files for violation in check_file(path=path)
|
||||
]
|
||||
if violations:
|
||||
print("\n".join(violations))
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Verify a built Supervision wheel works without OpenCV."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import zipfile
|
||||
from email import message_from_bytes
|
||||
from email.message import Message
|
||||
from pathlib import Path
|
||||
|
||||
_MANIFEST_CHECKS = {
|
||||
"import-supervision",
|
||||
"no-cv2-module",
|
||||
"fallback-backend",
|
||||
"bgr-to-gray",
|
||||
"draw-rectangle",
|
||||
"required-pyav",
|
||||
}
|
||||
|
||||
|
||||
def _wheel_metadata(wheel: Path) -> Message:
|
||||
"""Read the core metadata embedded in a wheel archive."""
|
||||
with zipfile.ZipFile(wheel) as archive:
|
||||
metadata_paths = [
|
||||
name for name in archive.namelist() if name.endswith("/METADATA")
|
||||
]
|
||||
if len(metadata_paths) != 1:
|
||||
raise ValueError(
|
||||
f"expected one METADATA file in {wheel}, found {metadata_paths}"
|
||||
)
|
||||
return message_from_bytes(archive.read(metadata_paths[0]))
|
||||
|
||||
|
||||
def _validate_metadata(wheel: Path) -> None:
|
||||
"""Reject wheels that retain an OpenCV runtime requirement or extra."""
|
||||
metadata = _wheel_metadata(wheel)
|
||||
requirements = metadata.get_all("Requires-Dist", [])
|
||||
extras = metadata.get_all("Provides-Extra", [])
|
||||
if any("opencv" in requirement.lower() for requirement in requirements):
|
||||
raise ValueError(
|
||||
f"OpenCV runtime requirement remains in {wheel}: {requirements}"
|
||||
)
|
||||
if any("opencv" in extra.lower() for extra in extras):
|
||||
raise ValueError(f"OpenCV extra remains in {wheel}: {extras}")
|
||||
|
||||
|
||||
def _validate_manifest(manifest: Path) -> None:
|
||||
"""Keep the installed-wheel fallback smoke contract explicit and complete."""
|
||||
checks = {
|
||||
s
|
||||
for line in manifest.read_text(encoding="utf-8").splitlines()
|
||||
if (s := line.strip()) and not s.startswith("#")
|
||||
}
|
||||
if checks != _MANIFEST_CHECKS:
|
||||
raise ValueError(
|
||||
f"unexpected fallback manifest {checks}; expected {_MANIFEST_CHECKS}"
|
||||
)
|
||||
|
||||
|
||||
def _run_installed_wheel_probe(python: Path) -> None:
|
||||
"""Exercise the installed fallback without allowing the source tree on sys.path."""
|
||||
source = """
|
||||
import importlib.util
|
||||
from importlib import metadata
|
||||
from pathlib import Path
|
||||
|
||||
import av
|
||||
import numpy as np
|
||||
import supervision
|
||||
from supervision import _cv2
|
||||
|
||||
package_path = Path(supervision.__file__).resolve()
|
||||
if "site-packages" not in package_path.parts:
|
||||
raise AssertionError(
|
||||
f"supervision did not import from site-packages: {package_path}"
|
||||
)
|
||||
if importlib.util.find_spec("cv2") is not None:
|
||||
raise AssertionError("cv2 is installed in the clean-wheel environment")
|
||||
opencv_distributions = [
|
||||
distribution.metadata["Name"]
|
||||
for distribution in metadata.distributions()
|
||||
if "opencv" in distribution.metadata["Name"].lower()
|
||||
]
|
||||
if opencv_distributions:
|
||||
raise AssertionError(
|
||||
"OpenCV distributions remain in the clean-wheel environment: "
|
||||
f"{opencv_distributions}"
|
||||
)
|
||||
if _cv2.BACKEND_NAME != "fallback":
|
||||
raise AssertionError(f"expected fallback backend, got {_cv2.BACKEND_NAME!r}")
|
||||
|
||||
image = np.array([[[0, 0, 255]]], dtype=np.uint8)
|
||||
assert _cv2.cvtColor(image, _cv2.COLOR_BGR2GRAY).tolist() == [[76]]
|
||||
canvas = np.zeros((3, 3, 3), dtype=np.uint8)
|
||||
assert _cv2.rectangle(canvas, (0, 0), (2, 2), (1, 2, 3), -1) is canvas
|
||||
assert canvas.tolist() == [[[1, 2, 3]] * 3] * 3
|
||||
assert av.__version__
|
||||
"""
|
||||
subprocess.run( # noqa: S603 - the caller passes the clean CI interpreter explicitly.
|
||||
[str(python), "-c", source],
|
||||
check=True,
|
||||
cwd=Path.cwd().parent,
|
||||
)
|
||||
subprocess.run( # noqa: S603 - the caller passes the clean CI interpreter explicitly.
|
||||
[str(python), "-m", "pip", "check"], check=True
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Validate one wheel against a previously prepared clean environment."""
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--wheel", type=Path, required=True)
|
||||
parser.add_argument("--python", type=Path, required=True)
|
||||
parser.add_argument("--manifest", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
_validate_metadata(args.wheel)
|
||||
_validate_manifest(args.manifest)
|
||||
_run_installed_wheel_probe(args.python)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -20,10 +20,10 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ inputs.python-version }}
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
activate-environment: true
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ jobs:
|
|||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: 🐍 Install uv and set Python
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
python-version: "3.10"
|
||||
activate-environment: true
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: 🔗 Link Checker
|
||||
uses: lycheeverse/lychee-action@v2
|
||||
|
|
|
|||
|
|
@ -22,31 +22,57 @@ jobs:
|
|||
link-check: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
|
||||
run-tests:
|
||||
name: Import Test and Pytest Run
|
||||
name: Pytest Run
|
||||
# needs: build # todo: consider using this build package for testing
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
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"]
|
||||
cv2: ["none"]
|
||||
include:
|
||||
- { os: "ubuntu-latest", python-version: "3.13", cv2: "opencv-python" }
|
||||
- { os: "windows-latest", python-version: "3.13", cv2: "opencv-python" }
|
||||
- { os: "macos-latest", python-version: "3.13", cv2: "opencv-python" }
|
||||
- { os: "ubuntu-latest", python-version: "3.13", cv2: "opencv-python-headless" }
|
||||
- { os: "windows-latest", python-version: "3.13", cv2: "opencv-python-headless" }
|
||||
- { os: "macos-latest", python-version: "3.13", cv2: "opencv-python-headless" }
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
activate-environment: true
|
||||
|
||||
- name: 🚀 Install Packages
|
||||
run: uv sync --frozen --group dev --group docs --extra metrics
|
||||
run: uv sync --frozen --group dev --extra metrics
|
||||
|
||||
- name: 📷 Install selected OpenCV package
|
||||
if: matrix.cv2 != 'none'
|
||||
run: uv pip install ${{ matrix.cv2 }}
|
||||
|
||||
- name: 🧭 Confirm selected cv2 backend
|
||||
env:
|
||||
EXPECTED_BACKEND: ${{ matrix.cv2 == 'none' && 'fallback' || 'opencv' }}
|
||||
run: |
|
||||
import os
|
||||
from supervision import _cv2
|
||||
|
||||
expected = os.environ["EXPECTED_BACKEND"]
|
||||
assert _cv2.BACKEND_NAME == expected, f"expected {expected!r}, got {_cv2.BACKEND_NAME!r}"
|
||||
shell: python
|
||||
|
||||
- name: 📦 Run the Import test
|
||||
run: python -c "import supervision; from supervision import assets; from supervision import metrics; print(supervision.__version__)"
|
||||
|
||||
- name: 📋 Print installed packages
|
||||
run: uv pip list
|
||||
|
||||
- name: 🧪 Run the Test
|
||||
run: pytest src/ tests/ --cov=supervision --cov-report=xml
|
||||
|
||||
|
|
@ -68,23 +94,66 @@ jobs:
|
|||
- name: Minimize uv cache
|
||||
run: uv cache prune --ci
|
||||
|
||||
clean-wheel:
|
||||
name: Clean Wheel on ${{ matrix.os }} / Python ${{ matrix.python-version }}
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["ubuntu-latest", "windows-latest", "macos-latest"]
|
||||
python-version: ["3.10", "3.13"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
activate-environment: true
|
||||
|
||||
- name: 🏗️ Build the wheel
|
||||
run: |
|
||||
uv sync --frozen --group build
|
||||
uv build --wheel
|
||||
|
||||
- name: 🧼 Create an isolated wheel environment
|
||||
shell: bash
|
||||
run: |
|
||||
uv venv --clear --seed .clean-wheel
|
||||
if [ "$RUNNER_OS" = "Windows" ]; then
|
||||
echo "CLEAN_PYTHON=$PWD/.clean-wheel/Scripts/python.exe" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "CLEAN_PYTHON=$PWD/.clean-wheel/bin/python" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: 📦 Install and verify the wheel without OpenCV
|
||||
shell: bash
|
||||
run: |
|
||||
uv pip install --python "$CLEAN_PYTHON" --strict dist/*.whl
|
||||
"$CLEAN_PYTHON" .github/scripts/verify_clean_wheel.py \
|
||||
--wheel dist/*.whl \
|
||||
--python "$CLEAN_PYTHON" \
|
||||
--manifest tests/cv2/installed_wheel_fallback_manifest.txt
|
||||
|
||||
testing-guardian:
|
||||
runs-on: ubuntu-latest
|
||||
needs: run-tests
|
||||
needs: [run-tests, clean-wheel]
|
||||
if: always()
|
||||
steps:
|
||||
- name: 📋 Display test result
|
||||
run: echo "${{ needs.run-tests.result }}"
|
||||
run: echo "tests=${{ needs.run-tests.result }}, clean-wheel=${{ needs.clean-wheel.result }}"
|
||||
- name: ❌ Fail guardian on test failure
|
||||
if: needs.run-tests.result == 'failure'
|
||||
if: needs.run-tests.result == 'failure' || needs.clean-wheel.result == 'failure'
|
||||
run: exit 1
|
||||
# Ensure that cancelled or skipped test runs still cause this guardian job to fail,
|
||||
# using an explicit exit code instead of relying on timeout behavior.
|
||||
- name: ⚠️ cancelled or skipped...
|
||||
if: contains(fromJSON('["cancelled", "skipped"]'), needs.run-tests.result)
|
||||
if: contains(fromJSON('["cancelled", "skipped"]'), needs.run-tests.result) || contains(fromJSON('["cancelled", "skipped"]'), needs.clean-wheel.result)
|
||||
run: |
|
||||
echo "run-tests job result is '${{ needs.run-tests.result }}'; failing explicitly."
|
||||
exit 1
|
||||
- name: ✅ tests succeeded
|
||||
if: needs.run-tests.result == 'success'
|
||||
run: echo "All tests completed successfully in job 'run-tests'."
|
||||
run: echo "All tests completed successfully."
|
||||
|
|
|
|||
|
|
@ -32,12 +32,12 @@ jobs:
|
|||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🐍 Install uv and set Python
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
python-version: "3.10"
|
||||
activate-environment: true
|
||||
|
|
@ -62,6 +62,7 @@ jobs:
|
|||
env:
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if mike list | grep -Eq '^latest(\s|$)'; then mike delete latest; fi
|
||||
mike deploy --push latest
|
||||
|
||||
- name: 🏷️ Determine release deployment metadata
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ name: Publish Supervision Pre-Releases to PyPI
|
|||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]+a[0-9]"
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]+b[0-9]"
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]+rc[0-9]"
|
||||
- "[0-9]+.[0-9]+.[0-9]+a[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+b[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+.a[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+.b[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+.rc[0-9]+"
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
|
@ -43,6 +46,6 @@ jobs:
|
|||
|
||||
- name: 🚀 Publish to PyPi
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
|
||||
with:
|
||||
attestations: true
|
||||
|
|
|
|||
|
|
@ -48,6 +48,6 @@ jobs:
|
|||
- name: 🚀 Publish to PyPi
|
||||
# We only want to publish to PyPi if the event is a release and it's not a pre-release.
|
||||
if: (github.event_name == 'release' && github.event.release.prerelease != true) || github.event_name == 'workflow_dispatch'
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
|
||||
with:
|
||||
attestations: true
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ jobs:
|
|||
|
||||
- name: 🚀 Publish to Test-PyPi
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
attestations: true
|
||||
|
|
|
|||
|
|
@ -156,11 +156,27 @@ Desktop.ini
|
|||
|
||||
# local data
|
||||
data/
|
||||
examples/*/outputs/
|
||||
!src/supervision/_cv2/data/
|
||||
!src/supervision/_cv2/data/*
|
||||
*.mp4
|
||||
*.pt
|
||||
|
||||
# some artifacts
|
||||
/*.py
|
||||
/*.jpg
|
||||
/*.png
|
||||
/*.tif
|
||||
/*.json
|
||||
|
||||
|
||||
# Claude working scratchpad (plans, lessons, ephemeral artefacts)
|
||||
.claude/logs
|
||||
.claude/logs/
|
||||
.claude/state/
|
||||
.claude/worktrees/
|
||||
.developments/
|
||||
.plans/
|
||||
.notes/
|
||||
.reports/
|
||||
.temp/
|
||||
.tmp/
|
||||
|
|
@ -169,4 +185,7 @@ _resolutions/
|
|||
_reviews/
|
||||
tasks/
|
||||
*.local.md
|
||||
scripts/test_multi_skeleton.py
|
||||
|
||||
output/
|
||||
notebooks/
|
||||
releases/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
default_language_version:
|
||||
python: python3
|
||||
python: python3.10
|
||||
|
||||
ci:
|
||||
autofix_prs: true
|
||||
|
|
@ -8,6 +8,14 @@ ci:
|
|||
autoupdate_commit_msg: "chore(pre_commit): ⬆ pre_commit autoupdate"
|
||||
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: check-doctest-fences
|
||||
name: check doctest fences
|
||||
entry: python .github/scripts/check_doctest_fences.py
|
||||
language: python
|
||||
files: ^src/.*\.py$
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
|
|
@ -28,8 +36,8 @@ repos:
|
|||
- id: end-of-file-fixer
|
||||
- id: mixed-line-ending
|
||||
|
||||
- repo: https://github.com/JoC0de/pre-commit-prettier
|
||||
rev: v3.8.3 # using tag; previously pinned SHA when tags were not persistent
|
||||
- repo: https://github.com/rbubley/mirrors-prettier
|
||||
rev: v3.9.6
|
||||
hooks:
|
||||
- id: prettier
|
||||
files: \.(ya?ml|toml)$
|
||||
|
|
@ -37,9 +45,11 @@ repos:
|
|||
args: ["--print-width=120"]
|
||||
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: v2.21.1
|
||||
rev: v2.26.0
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
additional_dependencies:
|
||||
- "tomli>=2.0.1"
|
||||
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.25
|
||||
|
|
@ -47,7 +57,7 @@ repos:
|
|||
- id: validate-pyproject
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.12
|
||||
rev: v0.16.1
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args: ["--fix"]
|
||||
|
|
@ -58,24 +68,37 @@ repos:
|
|||
rev: 1.0.0
|
||||
hooks:
|
||||
- id: mdformat
|
||||
name: mdformat (gfm)
|
||||
exclude: ^docs/
|
||||
additional_dependencies:
|
||||
- "mdformat-frontmatter"
|
||||
- "mdformat-gfm"
|
||||
- "mdformat-ruff"
|
||||
args: ["--number", "--wrap=no"]
|
||||
- id: mdformat
|
||||
name: mdformat (mkdocs)
|
||||
files: ^docs/
|
||||
additional_dependencies:
|
||||
- "mdformat-frontmatter"
|
||||
- "mdformat-mkdocs[recommended]>=2.1.0"
|
||||
- "mdformat-ruff"
|
||||
args: ["--number", "--wrap=no"]
|
||||
exclude: ^(docs/changelog\.md|docs/deprecated\.md)$
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.20.2
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
language_version: python3.11
|
||||
additional_dependencies:
|
||||
- "numpy>=2.0"
|
||||
- "types-PyYAML"
|
||||
- "types-requests"
|
||||
- "types-tqdm"
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.4.2
|
||||
rev: v2.4.3
|
||||
hooks:
|
||||
- id: codespell
|
||||
exclude: ^src/supervision/_cv2/data/hershey_fonts\.json$
|
||||
additional_dependencies:
|
||||
- "tomli>=2.0.1"
|
||||
|
|
|
|||
25
AGENTS.md
25
AGENTS.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Behave like a senior contributor: precise, efficient, maintainable. When this file and [CONTRIBUTING.md](.github/CONTRIBUTING.md) conflict, **CONTRIBUTING.md wins**.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 1. Before You Code
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ Behave like a senior contributor: precise, efficient, maintainable. When this fi
|
|||
- Check whether the feature already exists under a different name.
|
||||
- Confirm alignment with `src/supervision/` architecture.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 2. Repository Architecture
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ src/supervision/
|
|||
- **Vectorized throughout** — NumPy arrays, no Python loops in hot paths. Never write `for det in detections`.
|
||||
- **Lazy-import heavy deps** — `torch`, `transformers`, `ultralytics` must be imported inside the function that needs them, never at module top level.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 3. Agent-Critical Rules
|
||||
|
||||
|
|
@ -54,6 +54,12 @@ These supplement [CONTRIBUTING.md](.github/CONTRIBUTING.md) — covering gaps or
|
|||
|
||||
**Type hints**: required on all new code. mypy is enforced by pre-commit (`.pre-commit-config.yaml`).
|
||||
|
||||
**Function docstrings**: every new or modified function, including private helpers and tests, must have a succinct docstring explaining its purpose. Put function-level why/what/how context inside the function docstring, not in a comment before the function. Public APIs still require the full Google-style structure described below.
|
||||
|
||||
**Readable argument lists**: do not put multi-branch conditional expressions inside function or constructor arguments. If an argument needs more than a simple `a if condition else b`, assign it to a named local variable before the call.
|
||||
|
||||
**Inline comments**: write code so the intent is clear from names, small helpers, and straightforward control flow. For non-trivial logic inside a function that still needs context, add concise inline comments explaining why the code exists, what invariant it protects, and how the tricky part works. Do not put comments before functions; use the function docstring instead. Do not comment obvious assignments, mechanical plumbing, lint-only changes, typing-only changes, or pure docs edits.
|
||||
|
||||
**Doctest determinism** — output must be reproducible across platforms:
|
||||
|
||||
- Use `# doctest: +ELLIPSIS` for floats that vary by platform.
|
||||
|
|
@ -65,13 +71,13 @@ These supplement [CONTRIBUTING.md](.github/CONTRIBUTING.md) — covering gaps or
|
|||
|
||||
For branching, commit, code style, and API design conventions see [CONTRIBUTING.md](.github/CONTRIBUTING.md).
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 4. Deprecated Module Aliases
|
||||
|
||||
`supervision.keypoint` deprecated since `0.27.0`, removed in `0.30.0`. Always import from `supervision.key_points`, not `supervision.keypoint`.
|
||||
`supervision.keypoint` deprecated since `0.27.0`, removed in `0.31.0`. Always import from `supervision.key_points`, not `supervision.keypoint`.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 5. Deprecating APIs
|
||||
|
||||
|
|
@ -87,13 +93,14 @@ Always name the version introduced and the removal version:
|
|||
warn_deprecated("'foo' deprecated in `0.29.0`, removed in `0.32.0`. Use 'bar'.")
|
||||
```
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 6. Implementing Features
|
||||
|
||||
- Minimal implementation; type hints and Google docstrings with usage examples.
|
||||
- Tests covering new functionality and edge cases (see [CONTRIBUTING.md §Tests](.github/CONTRIBUTING.md#-tests)).
|
||||
- Update docstrings and mkdocs entries as needed.
|
||||
- Update [docs/changelog.md](docs/changelog.md) for every functional change or bug fix, including user-visible behavior changes. Skip changelog entries for lint-only, type-only, formatting-only, and pure documentation-only changes.
|
||||
|
||||
**Extending `Detections`**: store metadata in `detections.data` as `np.ndarray` aligned with `xyxy`; define the key as a constant in `config.py` (e.g. `CLASS_NAME_DATA_FIELD`, `ORIENTED_BOX_COORDINATES`).
|
||||
|
||||
|
|
@ -115,7 +122,7 @@ def from_myframework(cls, result) -> "Detections":
|
|||
|
||||
VLM connectors go in `detection/vlm.py`, not `core.py`.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 7. Bugs & Refactoring
|
||||
|
||||
|
|
@ -123,7 +130,7 @@ VLM connectors go in `detection/vlm.py`, not `core.py`.
|
|||
|
||||
**Refactoring**: preserve behavior and API; reduce duplication; avoid sweeping changes unless requested; apply §5 deprecation when removing public API.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## 8. Before You Commit
|
||||
|
||||
|
|
|
|||
202
README.md
202
README.md
|
|
@ -1,6 +1,6 @@
|
|||
<div align="center">
|
||||
<p>
|
||||
<a align="center" href="" target="https://supervision.roboflow.com">
|
||||
<a align="center" href="https://supervision.roboflow.com" target="_blank">
|
||||
<img
|
||||
width="100%"
|
||||
src="https://media.roboflow.com/open-source/supervision/rf-supervision-banner.png?updatedAt=1678995927529"
|
||||
|
|
@ -24,13 +24,29 @@
|
|||
|
||||
</div>
|
||||
|
||||
## 👋 hello
|
||||
<details>
|
||||
<summary><strong>📑 Table of Contents</strong></summary>
|
||||
|
||||
- [👋 Hello](#-hello)
|
||||
- [💻 Install](#-install)
|
||||
- [🔥 Quickstart](#-quickstart)
|
||||
- [Models](#models)
|
||||
- [Annotators](#annotators)
|
||||
- [Datasets](#datasets)
|
||||
- [🎬 Tutorials](#-tutorials)
|
||||
- [💜 Built with Supervision](#-built-with-supervision)
|
||||
- [📚 Documentation](#-documentation)
|
||||
- [🏆 Contribution](#-contribution)
|
||||
|
||||
</details>
|
||||
|
||||
## 👋 Hello
|
||||
|
||||
**We are your essential toolkit for computer vision.** From data loading to real-time zone counting, we provide the building blocks so you can focus on building applications around your models. 🤝
|
||||
|
||||
## 💻 install
|
||||
## 💻 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
|
||||
|
|
@ -38,9 +54,9 @@ pip install supervision
|
|||
|
||||
Read more about conda, mamba, and installing from source in our [guide](https://roboflow.github.io/supervision/).
|
||||
|
||||
## 🔥 quickstart
|
||||
## 🔥 Quickstart
|
||||
|
||||
### models
|
||||
### Models
|
||||
|
||||
Supervision was designed to be model agnostic. Just plug in any classification, detection, or segmentation model. For your convenience, we have created [connectors](https://supervision.roboflow.com/latest/detection/core/#detections) for the most popular libraries like Ultralytics, Transformers, MMDetection, or Inference. Other integrations, like `rfdetr`, already return `sv.Detections` directly.
|
||||
|
||||
|
|
@ -51,7 +67,7 @@ import supervision as sv
|
|||
from PIL import Image
|
||||
from rfdetr import RFDETRSmall
|
||||
|
||||
image = Image.open(...)
|
||||
image = Image.open("path/to/image.jpg")
|
||||
model = RFDETRSmall()
|
||||
detections = model.predict(image, threshold=0.5)
|
||||
|
||||
|
|
@ -64,25 +80,25 @@ len(detections)
|
|||
|
||||
- inference
|
||||
|
||||
Running with [Inference](https://github.com/roboflow/inference) requires a [Roboflow API KEY](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key).
|
||||
Running with [Inference](https://github.com/roboflow/inference) requires a [Roboflow API KEY](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key).
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
from PIL import Image
|
||||
from inference import get_model
|
||||
```python
|
||||
import supervision as sv
|
||||
from PIL import Image
|
||||
from inference import get_model
|
||||
|
||||
image = Image.open(...)
|
||||
model = get_model(model_id="rfdetr-small", api_key="ROBOFLOW_API_KEY")
|
||||
result = model.infer(image)[0]
|
||||
detections = sv.Detections.from_inference(result)
|
||||
image = Image.open("path/to/image.jpg")
|
||||
model = get_model(model_id="rfdetr-small", api_key="ROBOFLOW_API_KEY")
|
||||
result = model.infer(image)[0]
|
||||
detections = sv.Detections.from_inference(result)
|
||||
|
||||
len(detections)
|
||||
# 5
|
||||
```
|
||||
len(detections)
|
||||
# 5
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### annotators
|
||||
### Annotators
|
||||
|
||||
Supervision offers a wide range of highly customizable [annotators](https://supervision.roboflow.com/latest/detection/annotators/), allowing you to compose the perfect visualization for your use case.
|
||||
|
||||
|
|
@ -90,7 +106,8 @@ Supervision offers a wide range of highly customizable [annotators](https://supe
|
|||
import cv2
|
||||
import supervision as sv
|
||||
|
||||
image = cv2.imread(...)
|
||||
image = cv2.imread("path/to/image.jpg")
|
||||
# Assuming detections are obtained from a model
|
||||
detections = sv.Detections(...)
|
||||
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
|
@ -99,7 +116,7 @@ annotated_frame = box_annotator.annotate(scene=image.copy(), detections=detectio
|
|||
|
||||
https://github.com/roboflow/supervision/assets/26109316/691e219c-0565-4403-9218-ab5644f39bce
|
||||
|
||||
### datasets
|
||||
### Datasets
|
||||
|
||||
Supervision provides a set of [utils](https://supervision.roboflow.com/latest/datasets/core/) that allow you to load, split, merge, and save datasets in one of the supported formats.
|
||||
|
||||
|
|
@ -123,97 +140,97 @@ for path, image, annotation in ds:
|
|||
pass
|
||||
```
|
||||
|
||||
<details close>
|
||||
<details>
|
||||
<summary>👉 more dataset utils</summary>
|
||||
|
||||
- load
|
||||
|
||||
```python
|
||||
dataset = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...,
|
||||
)
|
||||
```python
|
||||
dataset = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...,
|
||||
)
|
||||
|
||||
dataset = sv.DetectionDataset.from_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
)
|
||||
dataset = sv.DetectionDataset.from_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
)
|
||||
|
||||
dataset = sv.DetectionDataset.from_coco(
|
||||
images_directory_path=...,
|
||||
annotations_path=...,
|
||||
)
|
||||
```
|
||||
dataset = sv.DetectionDataset.from_coco(
|
||||
images_directory_path=...,
|
||||
annotations_path=...,
|
||||
)
|
||||
```
|
||||
|
||||
- split
|
||||
|
||||
```python
|
||||
train_dataset, test_dataset = dataset.split(split_ratio=0.7)
|
||||
test_dataset, valid_dataset = test_dataset.split(split_ratio=0.5)
|
||||
```python
|
||||
train_dataset, test_dataset = dataset.split(split_ratio=0.7)
|
||||
test_dataset, valid_dataset = test_dataset.split(split_ratio=0.5)
|
||||
|
||||
len(train_dataset), len(test_dataset), len(valid_dataset)
|
||||
# (700, 150, 150)
|
||||
```
|
||||
len(train_dataset), len(test_dataset), len(valid_dataset)
|
||||
# (700, 150, 150)
|
||||
```
|
||||
|
||||
- merge
|
||||
|
||||
```python
|
||||
ds_1 = sv.DetectionDataset(...)
|
||||
len(ds_1)
|
||||
# 100
|
||||
ds_1.classes
|
||||
# ['dog', 'person']
|
||||
```python
|
||||
ds_1 = sv.DetectionDataset(...)
|
||||
len(ds_1)
|
||||
# 100
|
||||
ds_1.classes
|
||||
# ['dog', 'person']
|
||||
|
||||
ds_2 = sv.DetectionDataset(...)
|
||||
len(ds_2)
|
||||
# 200
|
||||
ds_2.classes
|
||||
# ['cat']
|
||||
ds_2 = sv.DetectionDataset(...)
|
||||
len(ds_2)
|
||||
# 200
|
||||
ds_2.classes
|
||||
# ['cat']
|
||||
|
||||
ds_merged = sv.DetectionDataset.merge([ds_1, ds_2])
|
||||
len(ds_merged)
|
||||
# 300
|
||||
ds_merged.classes
|
||||
# ['cat', 'dog', 'person']
|
||||
```
|
||||
ds_merged = sv.DetectionDataset.merge([ds_1, ds_2])
|
||||
len(ds_merged)
|
||||
# 300
|
||||
ds_merged.classes
|
||||
# ['cat', 'dog', 'person']
|
||||
```
|
||||
|
||||
- save
|
||||
|
||||
```python
|
||||
dataset.as_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...,
|
||||
)
|
||||
```python
|
||||
dataset.as_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...,
|
||||
)
|
||||
|
||||
dataset.as_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
)
|
||||
dataset.as_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
)
|
||||
|
||||
dataset.as_coco(
|
||||
images_directory_path=...,
|
||||
annotations_path=...,
|
||||
)
|
||||
```
|
||||
dataset.as_coco(
|
||||
images_directory_path=...,
|
||||
annotations_path=...,
|
||||
)
|
||||
```
|
||||
|
||||
- convert
|
||||
|
||||
```python
|
||||
sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...,
|
||||
).as_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
)
|
||||
```
|
||||
```python
|
||||
sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...,
|
||||
).as_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 🎬 tutorials
|
||||
## 🎬 Tutorials
|
||||
|
||||
Want to learn how to use Supervision? Explore our [how-to guides](https://supervision.roboflow.com/develop/how_to/detect_and_annotate/), [end-to-end examples](./examples), [cheatsheet](https://roboflow.github.io/cheatsheet-supervision/), and [cookbooks](https://supervision.roboflow.com/develop/cookbooks/)!
|
||||
|
||||
|
|
@ -233,7 +250,7 @@ Want to learn how to use Supervision? Explore our [how-to guides](https://superv
|
|||
<div><strong>Created: 11 Jan 2024</strong></div>
|
||||
<br/>Learn how to track and estimate the speed of vehicles using YOLO, ByteTrack, and Roboflow Inference. This comprehensive tutorial covers object detection, multi-object tracking, filtering detections, perspective transformation, speed estimation, visualization improvements, and more.</p>
|
||||
|
||||
## 💜 built with supervision
|
||||
## 💜 Built with Supervision
|
||||
|
||||
Did you build something cool using supervision? [Let us know!](https://github.com/roboflow/supervision/discussions/categories/built-with-supervision)
|
||||
|
||||
|
|
@ -243,11 +260,11 @@ https://github.com/roboflow/supervision/assets/26109316/c9436828-9fbf-4c25-ae8c-
|
|||
|
||||
https://github.com/roboflow/supervision/assets/26109316/3ac6982f-4943-4108-9b7f-51787ef1a69f
|
||||
|
||||
## 📚 documentation
|
||||
## 📚 Documentation
|
||||
|
||||
Visit our [documentation](https://roboflow.github.io/supervision) page to learn how supervision can help you build computer vision applications faster and more reliably.
|
||||
|
||||
## 🏆 contribution
|
||||
## 🏆 Contribution
|
||||
|
||||
We love your input! Please see our [contributing guide](.github/CONTRIBUTING.md) to get started. Thank you 🙏 to all our contributors!
|
||||
|
||||
|
|
@ -259,8 +276,6 @@ We love your input! Please see our [contributing guide](.github/CONTRIBUTING.md)
|
|||
|
||||
<br>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<div align="center">
|
||||
<a href="https://youtube.com/roboflow">
|
||||
<img
|
||||
|
|
@ -295,6 +310,7 @@ We love your input! Please see our [contributing guide](.github/CONTRIBUTING.md)
|
|||
src="https://media.roboflow.com/notebooks/template/icons/purple/forum.png?ik-sdk-version=javascript-1.4.3&updatedAt=1672949633584"
|
||||
width="3%"
|
||||
/>
|
||||
</a>
|
||||
<img src="https://raw.githubusercontent.com/ultralytics/assets/main/social/logo-transparent.png" width="3%"/>
|
||||
<a href="https://blog.roboflow.com">
|
||||
<img
|
||||
|
|
@ -302,6 +318,4 @@ We love your input! Please see our [contributing guide](.github/CONTRIBUTING.md)
|
|||
width="3%"
|
||||
/>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
1474
docs/changelog.md
1474
docs/changelog.md
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
comments: true
|
||||
description: API reference for supervision's DetectionDataset and ClassificationDataset — load, merge, split, and convert datasets in YOLO, COCO, and VOC formats.
|
||||
description: API reference for supervision's DetectionDataset and ClassificationDataset — load, merge, split, and convert datasets in YOLO, COCO, VOC, CreateML, and LabelMe formats.
|
||||
---
|
||||
|
||||
# Datasets
|
||||
|
|
|
|||
|
|
@ -7,16 +7,17 @@ status: deprecated
|
|||
|
||||
These features are phased out due to better alternatives or potential issues in future versions. Deprecated functionalities are typically supported for multiple subsequent releases, providing time for users to transition to updated methods.
|
||||
|
||||
- [`sv.ByteTrack`](https://supervision.roboflow.com/latest/trackers/#supervision.tracker.byte_tracker.core.ByteTrack) is deprecated in favour of `ByteTrackTracker` from the external [`trackers`](https://pypi.org/project/trackers/) package (`pip install trackers`). The update method is renamed from `update_with_detections()` to `update()`. Removal planned for `supervision-0.30.0`.
|
||||
- `supervision.keypoint` module is deprecated; use `supervision.key_points` instead. Will be removed in `supervision-0.30.0`.
|
||||
- `create_tiles` in `supervision.utils.image` is deprecated. Will be removed in `supervision-0.31.0`.
|
||||
- `ensure_cv2_image_for_processing` in `supervision.utils.conversion` is deprecated. Will be removed in `supervision-0.31.0`.
|
||||
- Keypoint validation utilities in `supervision.validators` are deprecated. Will be removed in `supervision-0.31.0`.
|
||||
- `normalized_xyxy` argument in [`sv.denormalize_boxes`](https://supervision.roboflow.com/latest/detection/utils/boxes/#supervision.detection.utils.boxes.denormalize_boxes) is renamed to `xyxy`. Passing `normalized_xyxy=` emits a `FutureWarning`; support will be removed in `supervision-0.30.0`.
|
||||
- `supervision.dataset.utils` import path for [`sv.rle_to_mask`](https://supervision.roboflow.com/latest/detection/utils/converters/#supervision.detection.utils.converters.rle_to_mask) and [`sv.mask_to_rle`](https://supervision.roboflow.com/latest/detection/utils/converters/#supervision.detection.utils.converters.mask_to_rle) is deprecated. These functions moved to `supervision.detection.utils.converters`. Will be removed in `supervision-0.30.0`.
|
||||
- `sv.LMM` enum is deprecated and will be removed in `supervision-0.31.0`. Use `sv.VLM` instead.
|
||||
- [`sv.Detections.from_lmm`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_lmm) property is deprecated and will be removed in `supervision-0.31.0`. Use [`sv.Detections.from_vlm`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_vlm) instead.
|
||||
- Public `validate_*` helper functions are deprecated and will be removed in `supervision-0.31.0`. Supervision internals now use private `_validate_*` helpers.
|
||||
- [`sv.ByteTrack`](https://supervision.roboflow.com/latest/trackers/#supervision.tracker.byte_tracker.core.ByteTrack) is deprecated in `supervision-0.28.0` in favour of `ByteTrackTracker` from the external [`trackers`](https://pypi.org/project/trackers/) package (`pip install trackers`). The update method is renamed from `update_with_detections()` to `update()`. Removal is planned for `supervision-0.31.0`.
|
||||
- `supervision.keypoint` module is deprecated in `supervision-0.27.0`; use `supervision.key_points` instead. It will be removed in `supervision-0.31.0`.
|
||||
- `create_tiles` in `supervision.utils.image` is deprecated in `supervision-0.27.0`. It will be removed in `supervision-0.31.0`.
|
||||
- `ensure_cv2_image_for_processing` in `supervision.utils.conversion` is deprecated in `supervision-0.27.0`. It will be removed in `supervision-0.31.0`.
|
||||
- Keypoint validation utilities in `supervision.validators` are deprecated in `supervision-0.27.0`. They will be removed in `supervision-0.31.0`.
|
||||
- `normalized_xyxy` argument in [`sv.denormalize_boxes`](https://supervision.roboflow.com/latest/detection/utils/boxes/#supervision.detection.utils.boxes.denormalize_boxes) is deprecated in `supervision-0.27.0` and renamed to `xyxy`. Passing `normalized_xyxy=` emits a `FutureWarning`; support will be removed in `supervision-0.31.0`.
|
||||
- `supervision.dataset.utils` import path for [`sv.rle_to_mask`](https://supervision.roboflow.com/latest/detection/utils/converters/#supervision.detection.utils.converters.rle_to_mask) and [`sv.mask_to_rle`](https://supervision.roboflow.com/latest/detection/utils/converters/#supervision.detection.utils.converters.mask_to_rle) is deprecated in `supervision-0.28.0`. These functions moved to `supervision.detection.utils.converters` and will be removed from `supervision.dataset.utils` in `supervision-0.31.0`.
|
||||
- `sv.LMM` enum is deprecated in `supervision-0.27.0` and will be removed in `supervision-0.31.0`. Use `sv.VLM` instead.
|
||||
- [`sv.Detections.from_lmm`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_lmm) classmethod is deprecated in `supervision-0.26.0` and will be removed in `supervision-0.31.0`. Use [`sv.Detections.from_vlm`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_vlm) instead.
|
||||
- `KeyPoints.confidence` is deprecated in `supervision-0.29.0`. Use `KeyPoints.keypoint_confidence` instead. It will be removed in `supervision-0.32.0`.
|
||||
- Public `validate_*` helper functions are deprecated in `supervision-0.29.0` and will be removed in `supervision-0.32.0`. Supervision internals now use private `_validate_*` helpers.
|
||||
|
||||
# Removed
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ comments: true
|
|||
|
||||
Starting with `0.23.0`, a new metrics module is being introduced to supervision. Metrics here are part of the legacy evaluation API and will be deprecated in the future.
|
||||
|
||||
Install the metrics extra before using this page's APIs:
|
||||
|
||||
```bash
|
||||
pip install "supervision[metrics]"
|
||||
```
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.metrics.detection.ConfusionMatrix">ConfusionMatrix</a></h2>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,4 +4,51 @@ comments: true
|
|||
|
||||
# InferenceSlicer
|
||||
|
||||
## GeoTIFF Datasets
|
||||
|
||||
Install the optional GeoTIFF dependencies before running this example:
|
||||
|
||||
```bash
|
||||
pip install "supervision[geotiff]"
|
||||
wget -O RGB.byte.tif https://raw.githubusercontent.com/rasterio/rasterio/main/tests/data/RGB.byte.tif
|
||||
```
|
||||
|
||||
`InferenceSlicer` can read an open `rasterio` dataset window-by-window. This keeps large GeoTIFFs out of memory while passing each tile to the callback as an `(H, W, C)` NumPy array.
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import rasterio
|
||||
import supervision as sv
|
||||
|
||||
|
||||
def callback(tile: np.ndarray) -> sv.Detections:
|
||||
h, w = tile.shape[:2]
|
||||
return sv.Detections(
|
||||
xyxy=np.array([[w * 0.25, h * 0.25, w * 0.75, h * 0.75]], dtype=float),
|
||||
confidence=np.array([0.9]),
|
||||
class_id=np.array([0]),
|
||||
)
|
||||
|
||||
|
||||
slicer = sv.InferenceSlicer(
|
||||
callback=callback,
|
||||
slice_wh=(256, 256),
|
||||
overlap_wh=(64, 64),
|
||||
overlap_filter=sv.OverlapFilter.NONE,
|
||||
)
|
||||
|
||||
with rasterio.open("RGB.byte.tif") as dataset:
|
||||
detections = slicer(dataset)
|
||||
|
||||
print(len(detections))
|
||||
```
|
||||
|
||||
GeoTIFF inputs must use a projected coordinate reference system. Reproject geographic rasters before passing them to `InferenceSlicer`.
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.tools.inference_slicer.WindowedRasterDataset">WindowedRasterDataset</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.tools.inference_slicer.WindowedRasterDataset
|
||||
|
||||
:::supervision.detection.tools.inference_slicer.InferenceSlicer
|
||||
|
|
|
|||
|
|
@ -76,3 +76,9 @@ status: new
|
|||
</div>
|
||||
|
||||
:::supervision.detection.utils.converters.mask_to_rle
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.converters.is_compressed_rle">is_compressed_rle</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.utils.converters.is_compressed_rle
|
||||
|
|
|
|||
|
|
@ -52,12 +52,24 @@ comments: true
|
|||
|
||||
:::supervision.detection.utils.iou_and_nms.box_non_max_suppression
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.iou_and_nms.box_soft_non_max_suppression">box_soft_non_max_suppression</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.utils.iou_and_nms.box_soft_non_max_suppression
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.iou_and_nms.mask_non_max_suppression">mask_non_max_suppression</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.utils.iou_and_nms.mask_non_max_suppression
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.iou_and_nms.mask_soft_non_max_suppression">mask_soft_non_max_suppression</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.utils.iou_and_nms.mask_soft_non_max_suppression
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.iou_and_nms.box_non_max_merge">box_non_max_merge</a></h2>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ status: new
|
|||
|
||||
# Masks Utils
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.masks.mask_to_roi">mask_to_roi</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.utils.masks.mask_to_roi
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.masks.move_masks">move_masks</a></h2>
|
||||
</div>
|
||||
|
|
@ -28,3 +34,9 @@ status: new
|
|||
</div>
|
||||
|
||||
:::supervision.detection.utils.masks.filter_segments_by_distance
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.masks.calculate_masks_centroids">calculate_masks_centroids</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.utils.masks.calculate_masks_centroids
|
||||
|
|
|
|||
|
|
@ -3,7 +3,25 @@ comments: true
|
|||
status: new
|
||||
---
|
||||
|
||||
# VLMs Utils
|
||||
# VLM Utils
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.vlm.VLM">VLM</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.vlm.VLM
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.vlm.LMM">LMM</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.vlm.LMM
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.vlm.validate_vlm_parameters">validate_vlm_parameters</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.vlm.validate_vlm_parameters
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.vlms.edit_distance">edit_distance</a></h2>
|
||||
|
|
|
|||
29
docs/faq.md
29
docs/faq.md
|
|
@ -25,6 +25,8 @@ pip install "supervision[metrics]"
|
|||
|
||||
Sample asset utilities are part of the base package under `supervision.assets`.
|
||||
|
||||
Supervision does not install OpenCV. Its image, drawing, and file-video APIs use the included fallback when `cv2` is unavailable, and automatically use a compatible `cv2` already present in your environment. See the [OpenCV migration guide](how_to/opencv_migration.md) when upgrading an existing environment or choosing an OpenCV wheel yourself.
|
||||
|
||||
## Which object detection models work with Supervision?
|
||||
|
||||
Supervision is model agnostic. `sv.Detections` includes converters for Ultralytics YOLO, Roboflow Inference, Hugging Face Transformers outputs, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers including Florence-2, PaliGemma, Qwen VL, Gemini, DeepSeek VL 2, and Moondream. Keypoint outputs have separate `sv.KeyPoints` converters, including MediaPipe.
|
||||
|
|
@ -35,11 +37,11 @@ You can annotate images and video, filter detections, track objects, count objec
|
|||
|
||||
## How do I track objects across video frames?
|
||||
|
||||
Assign persistent tracker IDs before visualization. The built-in `sv.ByteTrack` wrapper accepts `Detections` through `update_with_detections()`. After tracking, combine the output with annotators such as `sv.TraceAnnotator`, `sv.BoxAnnotator`, and `sv.LabelAnnotator`.
|
||||
Assign persistent tracker IDs before visualization. The built-in `sv.ByteTrack` wrapper accepts `Detections` through `update_with_detections()`, but it is deprecated in favor of `ByteTrackTracker` from the external `trackers` package. After tracking, combine the output with annotators such as `sv.TraceAnnotator`, `sv.BoxAnnotator`, and `sv.LabelAnnotator`.
|
||||
|
||||
## What dataset formats does Supervision support?
|
||||
|
||||
For detection datasets, Supervision supports YOLO, COCO JSON, and Pascal VOC. Use `DetectionDataset.from_yolo()`, `DetectionDataset.from_coco()`, or `DetectionDataset.from_pascal_voc()` to load datasets, and the matching `as_*` methods to export them.
|
||||
For detection datasets, Supervision supports YOLO, COCO JSON, Pascal VOC, CreateML, and LabelMe. Use `DetectionDataset.from_yolo()`, `DetectionDataset.from_coco()`, `DetectionDataset.from_pascal_voc()`, `DetectionDataset.from_createml()`, or `DetectionDataset.from_labelme()` to load datasets, and the matching `as_*` methods to export them.
|
||||
|
||||
## How do I count objects in a zone?
|
||||
|
||||
|
|
@ -47,12 +49,33 @@ Use `sv.PolygonZone` for arbitrary polygon regions and `sv.LineZone` for line-cr
|
|||
|
||||
## How do I benchmark a model?
|
||||
|
||||
Use `supervision.metrics.mean_average_precision.MeanAveragePrecision` for mAP and `sv.ConfusionMatrix` for confusion matrices. Accumulate predictions and ground-truth `Detections`, then call `compute()` to calculate metrics.
|
||||
Install `supervision[metrics]`, then use `supervision.metrics.mean_average_precision.MeanAveragePrecision` for mAP and `sv.ConfusionMatrix` for confusion matrices. Accumulate predictions and ground-truth `Detections`, then call `compute()` to calculate metrics.
|
||||
|
||||
## Is Supervision free to use?
|
||||
|
||||
Yes. Supervision is free and open source under the MIT license.
|
||||
|
||||
## How do I process frames from a webcam with supervision?
|
||||
|
||||
Supervision does not support live camera capture. Manage the capture device yourself with `cv2.VideoCapture`, which works regardless of which OpenCV wheel (`opencv-python` or `opencv-python-headless`) is installed, and pass individual frames to supervision annotators:
|
||||
|
||||
```python
|
||||
import cv2 # requires: pip install opencv-python (or opencv-python-headless)
|
||||
import supervision as sv
|
||||
|
||||
cap = cv2.VideoCapture(0)
|
||||
annotator = sv.BoxAnnotator()
|
||||
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
# run your detector, then annotate:
|
||||
# annotated = annotator.annotate(frame, detections)
|
||||
|
||||
cap.release()
|
||||
```
|
||||
|
||||
## Where is the source code?
|
||||
|
||||
The source code is available at [github.com/roboflow/supervision](https://github.com/roboflow/supervision).
|
||||
|
|
|
|||
|
|
@ -42,14 +42,9 @@ We'll use the following libraries:
|
|||
- `supervision` to evaluate the model results
|
||||
|
||||
```bash
|
||||
pip install roboflow supervision
|
||||
pip install git+https://github.com/roboflow/inference.git@linas/allow-latest-rc-supervision
|
||||
pip install roboflow inference "supervision[metrics]"
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
We're updating `inference` at the moment. Please install it as shown above.
|
||||
|
||||
Here's how you can download a dataset:
|
||||
|
||||
```python
|
||||
|
|
@ -329,6 +324,20 @@ Here, predictions in purple are targets (ground truth), and predictions in teal
|
|||
|
||||
See [annotator documentation](https://supervision.roboflow.com/latest/detection/annotators/) for even more options.
|
||||
|
||||
## Visual Benchmarking
|
||||
|
||||
To inspect where a model succeeds and fails, pass `save_directory_path` to `sv.ConfusionMatrix.benchmark(...)`. For every dataset image it writes a 2x2 result grid — `Ground Truth`, `True Positives`, `False Positives`, and `False Negatives` panels — directly into that directory, reusing the original image filenames. This makes it easy to skim through per-image outcomes alongside the aggregate confusion matrix.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
confusion_matrix = sv.ConfusionMatrix.benchmark(
|
||||
dataset=test_set,
|
||||
callback=callback,
|
||||
save_directory_path="./results",
|
||||
)
|
||||
```
|
||||
|
||||
## Benchmarking Metrics
|
||||
|
||||
With multiple models, fine details matter. Visual inspection may not be enough. `supervision` provides a collection of metrics that help obtain precise numerical results of model performance.
|
||||
|
|
@ -462,7 +471,7 @@ Yes, if you want to evaluate their bounding boxes. Convert model outputs to `Det
|
|||
|
||||
### What is a ConfusionMatrix and how do I use it?
|
||||
|
||||
`sv.ConfusionMatrix` visualizes true positives, false positives, and false negatives per class. Create one with `sv.ConfusionMatrix.from_detections(predictions=predictions, targets=targets, classes=classes, conf_threshold=0.5, iou_threshold=0.5)`, then call `metric.plot()` to render a heatmap.
|
||||
`sv.ConfusionMatrix` visualizes true positives, false positives, and false negatives per class. Create one with `sv.ConfusionMatrix.from_detections(predictions=predictions, targets=targets, classes=classes, conf_threshold=0.5, iou_threshold=0.5)`, then call `confusion_matrix.plot()` to render a heatmap. If you want per-image validation visualizations saved to disk, pass `save_directory_path="./results"` to `sv.ConfusionMatrix.benchmark(...)`; it will write 2x2 result grids directly into that directory using the original image filenames, with `Ground Truth`, `True Positives`, `False Positives`, and `False Negatives` panels.
|
||||
|
||||
## Author
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ download_assets(VideoAssets.VEHICLES_2)
|
|||
|
||||
First, we need to initialize a model. Let's use a YOLOv8 model with the default COCO checkpoint. We also need to load a video on which to run inference.
|
||||
|
||||
Create a YOLO model instance and load the source video using supervision's `VideoInfo` helper. The model will process each frame during inference, while `VideoInfo` extracts resolution and frame-rate metadata needed by the polygon zone annotator. A shared color palette ensures consistent zone coloring throughout the output video.
|
||||
Create a YOLO model instance and download the source video. The model will process each frame during inference. A shared color palette ensures consistent zone coloring throughout the output video.
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
|
|
@ -32,13 +32,13 @@ import supervision as sv
|
|||
import cv2
|
||||
|
||||
from ultralytics import YOLO
|
||||
from supervision.assets import VideoAssets, download_assets
|
||||
|
||||
model = YOLO("yolov8s.pt")
|
||||
|
||||
VIDEO = str(VideoAssets.VEHICLES_2)
|
||||
VIDEO = download_assets(VideoAssets.VEHICLES_2)
|
||||
|
||||
colors = sv.ColorPalette.default()
|
||||
video_info = sv.VideoInfo.from_video_path(VIDEO)
|
||||
colors = sv.ColorPalette.DEFAULT
|
||||
```
|
||||
|
||||
## Calculate Coordinates
|
||||
|
|
@ -80,10 +80,7 @@ With the coordinates of the zones to draw ready, we can set up our zones:
|
|||
Instantiate a `PolygonZone` for each polygon array, pairing it with a `PolygonZoneAnnotator` for visual overlay and a `BoxAnnotator` for drawing detection boxes. Each zone will later trigger on incoming detections to determine which objects fall inside its boundaries, enabling per-zone counting in the inference callback.
|
||||
|
||||
```python
|
||||
zones = [
|
||||
sv.PolygonZone(polygon=polygon, frame_resolution_wh=video_info.resolution_wh)
|
||||
for polygon in polygons
|
||||
]
|
||||
zones = [sv.PolygonZone(polygon=polygon) for polygon in polygons]
|
||||
zone_annotators = [
|
||||
sv.PolygonZoneAnnotator(
|
||||
zone=zone,
|
||||
|
|
@ -98,8 +95,6 @@ box_annotators = [
|
|||
sv.BoxAnnotator(
|
||||
color=colors.by_idx(index),
|
||||
thickness=4,
|
||||
text_thickness=4,
|
||||
text_scale=2,
|
||||
)
|
||||
for index in range(len(polygons))
|
||||
]
|
||||
|
|
@ -121,9 +116,7 @@ def process_frame(frame: np.ndarray, i) -> np.ndarray:
|
|||
):
|
||||
mask = zone.trigger(detections=detections)
|
||||
detections_filtered = detections[mask]
|
||||
frame = box_annotator.annotate(
|
||||
scene=frame, detections=detections_filtered, skip_label=True
|
||||
)
|
||||
frame = box_annotator.annotate(scene=frame, detections=detections_filtered)
|
||||
frame = zone_annotator.annotate(scene=frame)
|
||||
|
||||
return frame
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@ Use NumPy-style boolean indexing: `detections[detections.class_id == 0]` for cla
|
|||
|
||||
### How do I filter by bounding box area?
|
||||
|
||||
`detections[detections.area > 1000]` filters by pixel area. If masks are present, `detections.area` uses mask area; otherwise it uses bounding box area from `xyxy`. Use `detections.box_area` when you specifically need bounding box area.
|
||||
`detections[detections.area > 1000]` filters by pixel area. If masks are present, `detections.area` uses mask area; otherwise, if oriented-box coordinates are present, it uses oriented polygon area; all remaining detections use bounding box area from `xyxy`. Use `detections.box_area` when you specifically need axis-aligned bounding box area.
|
||||
|
||||
### Can I filter by box aspect ratio or dimensions?
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
comments: true
|
||||
description: Migrate Supervision installations after OpenCV becomes an ambient optional backend: use the included fallback by default or select one compatible OpenCV wheel for your application.
|
||||
date_modified: 2026-07-17
|
||||
---
|
||||
|
||||
# Migrate to Supervision Without an OpenCV Dependency
|
||||
|
||||
Supervision no longer installs OpenCV or offers an OpenCV extra. A standard installation includes the NumPy, Pillow, SciPy, and PyAV fallback needed by Supervision's image, drawing, and file-video APIs. When a compatible `cv2` is already installed, Supervision selects it once when the process imports the package.
|
||||
|
||||
## Keep the default fallback
|
||||
|
||||
Install Supervision normally when your application does not otherwise require OpenCV:
|
||||
|
||||
```bash
|
||||
pip install supervision
|
||||
```
|
||||
|
||||
The fallback keeps Supervision's documented APIs operational. Some text and anti-aliased drawing pixels can differ from OpenCV, so use the same backend while validating image-level baselines.
|
||||
|
||||
## Prefer OpenCV behavior
|
||||
|
||||
Install exactly one OpenCV wheel family when your application relies on OpenCV outside Supervision or needs its native behavior:
|
||||
|
||||
```bash
|
||||
# Servers and containers without OpenCV GUI modules
|
||||
pip install opencv-python-headless supervision
|
||||
|
||||
# Desktop applications that need OpenCV GUI modules
|
||||
pip install opencv-python supervision
|
||||
```
|
||||
|
||||
Do not install both `opencv-python` and `opencv-python-headless`. If another dependency, such as a model runtime, already provides a compatible `cv2`, keep that installation instead of adding a second wheel family.
|
||||
|
||||
## Verify the selected backend
|
||||
|
||||
Backend selection happens at import time and lasts for the process lifetime. Run this command in a fresh Python process after changing dependencies:
|
||||
|
||||
```bash
|
||||
python -c "from supervision import _cv2; print(_cv2.BACKEND_NAME)"
|
||||
```
|
||||
|
||||
It prints `fallback` without OpenCV and the OpenCV backend name when `cv2` is available. `_cv2` is private; use this command only as an installation diagnostic, not as application API.
|
||||
|
||||
## Capture webcams yourself
|
||||
|
||||
Supervision's video helpers support file paths through either backend. Live camera capture remains application-owned, so install your chosen OpenCV wheel if you use `cv2.VideoCapture(0)`:
|
||||
|
||||
```python
|
||||
import cv2
|
||||
import supervision as sv
|
||||
|
||||
capture = cv2.VideoCapture(0)
|
||||
annotator = sv.BoxAnnotator()
|
||||
```
|
||||
|
||||
## Roll back an upgrade
|
||||
|
||||
If a downstream image baseline requires the pre-migration package behavior, pin Supervision below the first release that removes the OpenCV dependency, then plan a backend-specific migration separately:
|
||||
|
||||
```bash
|
||||
pip install "supervision<0.30.0"
|
||||
```
|
||||
|
|
@ -1,24 +1,24 @@
|
|||
---
|
||||
comments: true
|
||||
description: Load, split, merge, and convert computer vision datasets between YOLO, COCO, and Pascal VOC formats using supervision's DetectionDataset.
|
||||
description: Load, split, merge, and convert computer vision datasets between YOLO, COCO, Pascal VOC, CreateML, and LabelMe formats using supervision's DetectionDataset.
|
||||
authors:
|
||||
- name: Piotr Skalski
|
||||
role: Computer Vision Engineer, Roboflow
|
||||
github: https://github.com/SkalskiP
|
||||
date_modified: 2026-04-22
|
||||
date_modified: 2026-06-25
|
||||
---
|
||||
|
||||
With Supervision, you can load and manipulate classification, object detection, and segmentation datasets. This tutorial will walk you through how to load, split, merge, visualize, and augment datasets in Supervision.
|
||||
|
||||
## Download Dataset
|
||||
|
||||
In this tutorial, we will use a dataset from [Roboflow Universe](https://universe.roboflow.com/), a public repository of thousands of computer vision datasets. If you already have your dataset in [COCO](https://roboflow.com/formats/coco-json), [YOLO](https://roboflow.com/formats/yolov8-pytorch-txt), or [Pascal VOC](https://roboflow.com/formats/pascal-voc-xml) format, you can skip this section.
|
||||
In this tutorial, we will use a dataset from [Roboflow Universe](https://universe.roboflow.com/), a public repository of thousands of computer vision datasets. If you already have your dataset in [COCO](https://roboflow.com/formats/coco-json), [YOLO](https://roboflow.com/formats/yolov8-pytorch-txt), [Pascal VOC](https://roboflow.com/formats/pascal-voc-xml), [CreateML](https://roboflow.com/formats/createml-json), or [LabelMe](https://roboflow.com/formats/labelme-json) format, you can skip this section.
|
||||
|
||||
```bash
|
||||
pip install roboflow
|
||||
```
|
||||
|
||||
Next, log into your Roboflow account and download the dataset of your choice in the COCO, YOLO, or Pascal VOC format. You can customize the following code snippet with your workspace ID, project ID, and version number.
|
||||
Next, log into your Roboflow account and download the dataset of your choice. The following snippets show common COCO, YOLO, Pascal VOC, and CreateML exports; LabelMe datasets can also be loaded directly from per-image JSON files in the next section. You can customize the code with your workspace ID, project ID, and version number.
|
||||
|
||||
=== "COCO"
|
||||
|
||||
|
|
@ -56,6 +56,18 @@ Next, log into your Roboflow account and download the dataset of your choice in
|
|||
dataset = project.version("<PROJECT_VERSION>").download("voc")
|
||||
```
|
||||
|
||||
=== "CreateML"
|
||||
|
||||
```python
|
||||
import roboflow
|
||||
|
||||
roboflow.login()
|
||||
|
||||
rf = roboflow.Roboflow()
|
||||
project = rf.workspace("<WORKSPACE_ID>").project("<PROJECT_ID>")
|
||||
dataset = project.version("<PROJECT_VERSION>").download("createml")
|
||||
```
|
||||
|
||||
## Load Dataset
|
||||
|
||||
The Supervision library provides convenient functions to load datasets in various formats. If your dataset is already split into train, test, and valid subsets, you can load each of those as separate [`sv.DetectionDataset`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset) instances.
|
||||
|
|
@ -144,6 +156,60 @@ The Supervision library provides convenient functions to load datasets in variou
|
|||
# 800, 100, 100
|
||||
```
|
||||
|
||||
=== "CreateML"
|
||||
|
||||
We can do so using the [`sv.DetectionDataset.from_createml`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.from_createml) to load annotations in [CreateML](https://roboflow.com/formats/createml-json) format.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
ds_train = sv.DetectionDataset.from_createml(
|
||||
images_directory_path=f"{dataset.location}/train",
|
||||
annotations_path=f"{dataset.location}/train/_annotations.createml.json",
|
||||
)
|
||||
ds_valid = sv.DetectionDataset.from_createml(
|
||||
images_directory_path=f"{dataset.location}/valid",
|
||||
annotations_path=f"{dataset.location}/valid/_annotations.createml.json",
|
||||
)
|
||||
ds_test = sv.DetectionDataset.from_createml(
|
||||
images_directory_path=f"{dataset.location}/test",
|
||||
annotations_path=f"{dataset.location}/test/_annotations.createml.json",
|
||||
)
|
||||
|
||||
ds_train.classes
|
||||
# ['person', 'bicycle', 'car', ...]
|
||||
|
||||
len(ds_train), len(ds_valid), len(ds_test)
|
||||
# 800, 100, 100
|
||||
```
|
||||
|
||||
=== "LabelMe"
|
||||
|
||||
We can do so using the [`sv.DetectionDataset.from_labelme`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.from_labelme) to load annotations in [LabelMe](https://roboflow.com/formats/labelme-json) format. LabelMe `rectangle` shapes are loaded as bounding boxes and `polygon` shapes are loaded as masks with bounding boxes.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
ds_train = sv.DetectionDataset.from_labelme(
|
||||
images_directory_path="<TRAIN_IMAGES_DIRECTORY_PATH>",
|
||||
annotations_directory_path="<TRAIN_ANNOTATIONS_DIRECTORY_PATH>",
|
||||
)
|
||||
ds_valid = sv.DetectionDataset.from_labelme(
|
||||
images_directory_path="<VALID_IMAGES_DIRECTORY_PATH>",
|
||||
annotations_directory_path="<VALID_ANNOTATIONS_DIRECTORY_PATH>",
|
||||
)
|
||||
ds_test = sv.DetectionDataset.from_labelme(
|
||||
images_directory_path="<TEST_IMAGES_DIRECTORY_PATH>",
|
||||
annotations_directory_path="<TEST_ANNOTATIONS_DIRECTORY_PATH>",
|
||||
)
|
||||
|
||||
ds_train.classes
|
||||
# ['person', 'bicycle', 'car', ...]
|
||||
|
||||
len(ds_train), len(ds_valid), len(ds_test)
|
||||
# 800, 100, 100
|
||||
```
|
||||
|
||||
## Split Dataset
|
||||
|
||||
If your dataset is not already split into train, test, and valid subsets, you can easily do so using the [`sv.DetectionDataset.split`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.split) method. We can split it as follows, ensuring a random shuffle of the data.
|
||||
|
|
@ -269,6 +335,72 @@ If you have multiple datasets that you would like to merge, you can do so using
|
|||
# 1000
|
||||
```
|
||||
|
||||
=== "CreateML"
|
||||
|
||||
```{ .py hl_lines="22-28" }
|
||||
import supervision as sv
|
||||
|
||||
ds_train = sv.DetectionDataset.from_createml(
|
||||
images_directory_path=f'{dataset.location}/train',
|
||||
annotations_path=f'{dataset.location}/train/_annotations.createml.json',
|
||||
)
|
||||
ds_valid = sv.DetectionDataset.from_createml(
|
||||
images_directory_path=f'{dataset.location}/valid',
|
||||
annotations_path=f'{dataset.location}/valid/_annotations.createml.json',
|
||||
)
|
||||
ds_test = sv.DetectionDataset.from_createml(
|
||||
images_directory_path=f'{dataset.location}/test',
|
||||
annotations_path=f'{dataset.location}/test/_annotations.createml.json',
|
||||
)
|
||||
|
||||
ds_train.classes
|
||||
# ['person', 'bicycle', 'car', ...]
|
||||
|
||||
len(ds_train), len(ds_valid), len(ds_test)
|
||||
# 800, 100, 100
|
||||
|
||||
ds = sv.DetectionDataset.merge([ds_train, ds_valid, ds_test])
|
||||
|
||||
ds.classes
|
||||
# ['person', 'bicycle', 'car', ...]
|
||||
|
||||
len(ds)
|
||||
# 1000
|
||||
```
|
||||
|
||||
=== "LabelMe"
|
||||
|
||||
```{ .py hl_lines="22-28" }
|
||||
import supervision as sv
|
||||
|
||||
ds_train = sv.DetectionDataset.from_labelme(
|
||||
images_directory_path="<TRAIN_IMAGES_DIRECTORY_PATH>",
|
||||
annotations_directory_path="<TRAIN_ANNOTATIONS_DIRECTORY_PATH>",
|
||||
)
|
||||
ds_valid = sv.DetectionDataset.from_labelme(
|
||||
images_directory_path="<VALID_IMAGES_DIRECTORY_PATH>",
|
||||
annotations_directory_path="<VALID_ANNOTATIONS_DIRECTORY_PATH>",
|
||||
)
|
||||
ds_test = sv.DetectionDataset.from_labelme(
|
||||
images_directory_path="<TEST_IMAGES_DIRECTORY_PATH>",
|
||||
annotations_directory_path="<TEST_ANNOTATIONS_DIRECTORY_PATH>",
|
||||
)
|
||||
|
||||
ds_train.classes
|
||||
# ['person', 'bicycle', 'car', ...]
|
||||
|
||||
len(ds_train), len(ds_valid), len(ds_test)
|
||||
# 800, 100, 100
|
||||
|
||||
ds = sv.DetectionDataset.merge([ds_train, ds_valid, ds_test])
|
||||
|
||||
ds.classes
|
||||
# ['person', 'bicycle', 'car', ...]
|
||||
|
||||
len(ds)
|
||||
# 1000
|
||||
```
|
||||
|
||||
## Iterate over Dataset
|
||||
|
||||
There are two ways to loop over a `sv.DetectionDataset`: using a direct [for loop](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.__iter__) called on the `sv.DetectionDataset` instance or loading `sv.DetectionDataset` entries [by index](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.__getitem__).
|
||||
|
|
@ -367,6 +499,36 @@ sv.plot_images_grid(
|
|||
)
|
||||
```
|
||||
|
||||
=== "CreateML"
|
||||
|
||||
We can do so using the [`sv.DetectionDataset.as_createml`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.as_createml) method to save annotations in [CreateML](https://roboflow.com/formats/createml-json) format.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
ds = sv.DetectionDataset(...)
|
||||
|
||||
ds.as_createml(
|
||||
images_directory_path="<IMAGE_DIRECTORY_PATH>",
|
||||
annotations_path="<ANNOTATIONS_PATH>",
|
||||
)
|
||||
```
|
||||
|
||||
=== "LabelMe"
|
||||
|
||||
We can do so using the [`sv.DetectionDataset.as_labelme`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.as_labelme) method to save annotations in [LabelMe](https://roboflow.com/formats/labelme-json) format. Detections with masks are exported as `polygon` shapes; box-only detections are exported as `rectangle` shapes.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
ds = sv.DetectionDataset(...)
|
||||
|
||||
ds.as_labelme(
|
||||
images_directory_path="<IMAGE_DIRECTORY_PATH>",
|
||||
annotations_directory_path="<ANNOTATIONS_DIRECTORY_PATH>",
|
||||
)
|
||||
```
|
||||
|
||||
## Augment Dataset
|
||||
|
||||
In this section, we'll explore using Supervision in combination with Albumentations to augment our dataset. Data augmentation is a common technique in computer vision to increase the size and diversity of training datasets, leading to improved model performance and generalization.
|
||||
|
|
@ -424,7 +586,7 @@ augmented_annotations = replace(
|
|||
|
||||
### What dataset formats does supervision support?
|
||||
|
||||
For detection datasets, supervision supports YOLO, COCO JSON, and Pascal VOC. Use `DetectionDataset.from_yolo()`, `from_coco()`, or `from_pascal_voc()` to load, and `as_yolo()`, `as_coco()`, or `as_pascal_voc()` to save. Classification datasets use `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
|
||||
For detection datasets, supervision supports YOLO, COCO JSON, Pascal VOC, CreateML, and LabelMe. Use `DetectionDataset.from_yolo()`, `from_coco()`, `from_pascal_voc()`, `from_createml()`, or `from_labelme()` to load, and `as_yolo()`, `as_coco()`, `as_pascal_voc()`, `as_createml()`, or `as_labelme()` to save. Classification datasets use `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
|
||||
|
||||
### Can I split a dataset into train/val/test sets?
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,10 @@ We will define a `callback` function, which will process each frame of the video
|
|||
|
||||
After running inference and obtaining predictions, the next step is to track the detected objects throughout the video. Utilizing Supervision’s [`sv.ByteTrack`](https://supervision.roboflow.com/latest/trackers/#supervision.tracker.byte_tracker.core.ByteTrack) functionality, each detected object is assigned a unique tracker ID, enabling the continuous following of the object's motion path across different frames.
|
||||
|
||||
!!! warning "Deprecated tracker wrapper"
|
||||
|
||||
`sv.ByteTrack` is deprecated in favor of `ByteTrackTracker` from the external `trackers` package. The external tracker uses `update()` instead of `update_with_detections()`.
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="6 12" }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,193 @@
|
|||
---
|
||||
comments: true
|
||||
description: Use CompactMask for memory-efficient instance segmentation in supervision — ingest COCO RLE payloads, skip mask materialisation, and merge mixed dense and compact detections without allocating a full pixel stack.
|
||||
authors:
|
||||
- name: Borda
|
||||
role: Open Source Engineer, Roboflow
|
||||
github: https://github.com/borda
|
||||
date_modified: 2026-07-01
|
||||
---
|
||||
|
||||
# Use Compact Masks for Memory-Efficient Segmentation
|
||||
|
||||
[CompactMask][supervision.detection.compact_mask.CompactMask] stores each instance mask as a run-length encoding of its bounding-box **crop** rather than a full `(H, W)` boolean frame. For high-resolution images with many sparse masks this can reduce memory from tens of gigabytes to tens of megabytes, and eliminates full-frame decode work in annotators that only need the cropped region.
|
||||
|
||||
!!! Note
|
||||
|
||||
`sv.mask_to_xyxy` keeps supervision's inclusive max-coordinate convention for compatibility with `CompactMask` and current box-based adapters. Use `sv.mask_to_roi` when you need exclusive slice bounds for NumPy indexing or crop extraction.
|
||||
|
||||
This guide covers the four main integration points:
|
||||
|
||||
1. [Ingesting COCO RLE payloads directly as CompactMask](#ingest-coco-rle-payloads)
|
||||
2. [Parsing Roboflow Inference results without a dense stack](#parse-inference-results)
|
||||
3. [Skipping mask materialisation for box/label annotators](#skip-unnecessary-materialisation)
|
||||
4. [Merging mixed dense and compact detections](#merge-mixed-detections)
|
||||
|
||||
---
|
||||
|
||||
## Ingest COCO RLE Payloads
|
||||
|
||||
If your model or API returns masks in the COCO RLE format (`{"size": [H, W], "counts": "..."}`) you can convert them directly to `CompactMask` without allocating an `(N, H, W)` boolean array:
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from supervision.detection.compact_mask import CompactMask
|
||||
|
||||
# Example: two COCO RLE masks for a 720×1280 frame.
|
||||
# Replace the counts strings with actual compressed RLE payloads from your
|
||||
# model or API — e.g., from pycocotools mask.encode() or an Inference response.
|
||||
rles = [
|
||||
{"size": [720, 1280], "counts": "YOUR_RLE_COUNTS_STRING_HERE"},
|
||||
{"size": [720, 1280], "counts": "YOUR_RLE_COUNTS_STRING_HERE"},
|
||||
]
|
||||
xyxy = np.array(
|
||||
[
|
||||
[100.0, 50.0, 400.0, 300.0],
|
||||
[500.0, 200.0, 900.0, 600.0],
|
||||
]
|
||||
)
|
||||
|
||||
compact = CompactMask.from_coco_rle(rles, xyxy, image_shape=(720, 1280))
|
||||
|
||||
detections = sv.Detections(
|
||||
xyxy=xyxy,
|
||||
mask=compact,
|
||||
class_id=np.array([0, 1]),
|
||||
)
|
||||
```
|
||||
|
||||
`from_coco_rle` uses run-length arithmetic scoped to each bounding box so no dense pixel array is ever created. Uncompressed integer count lists are also accepted in place of compressed strings.
|
||||
|
||||
---
|
||||
|
||||
## Parse Inference Results
|
||||
|
||||
`Detections.from_inference` accepts a `compact_masks=True` flag that routes the Roboflow RLE payload through `CompactMask.from_coco_rle` instead of decoding to a dense stack:
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
# result: a Roboflow Inference v2 response dict with instance masks.
|
||||
detections = sv.Detections.from_inference(result, compact_masks=True)
|
||||
|
||||
from supervision.detection.compact_mask import CompactMask
|
||||
|
||||
assert isinstance(detections.mask, CompactMask)
|
||||
```
|
||||
|
||||
!!! Warning
|
||||
|
||||
`compact_masks=True` crops each mask to its detector bounding box. Pixels outside the box are silently dropped. For masks that extend meaningfully beyond the reported bounding box, use the default `compact_masks=False` (dense decode) to preserve all pixels.
|
||||
|
||||
To convert an existing dense-mask `Detections` to compact at any point:
|
||||
|
||||
```python
|
||||
detections_compact = detections.to_compact_masks()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Skip Unnecessary Materialisation
|
||||
|
||||
Annotators that do not draw masks (box, label, circle, ellipse, trace, keypoint) expose `requires_mask = False`. Integrations can branch on this flag to avoid decoding compact or RLE masks before annotation:
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
annotators = [
|
||||
sv.BoxAnnotator(),
|
||||
sv.LabelAnnotator(),
|
||||
sv.MaskAnnotator(), # requires_mask = True
|
||||
]
|
||||
|
||||
for ann in annotators:
|
||||
if ann.requires_mask:
|
||||
# Annotator reads mask pixels — CompactMask decodes lazily per crop.
|
||||
scene = ann.annotate(scene, detections)
|
||||
else:
|
||||
# Annotator ignores masks — strip mask field to eliminate any decode cost.
|
||||
det_no_mask = sv.Detections(
|
||||
xyxy=detections.xyxy,
|
||||
confidence=detections.confidence,
|
||||
class_id=detections.class_id,
|
||||
)
|
||||
scene = ann.annotate(scene, det_no_mask)
|
||||
```
|
||||
|
||||
Annotators that set `requires_mask = True`: [MaskAnnotator][supervision.annotators.core.MaskAnnotator], [PolygonAnnotator][supervision.annotators.core.PolygonAnnotator], [HaloAnnotator][supervision.annotators.core.HaloAnnotator].
|
||||
|
||||
All others default to `requires_mask = False`.
|
||||
|
||||
!!! Note
|
||||
|
||||
`PolygonAnnotator` and `MaskAnnotator` both operate directly on `CompactMask` without materialising the full `(N, H, W)` frame — passing compact detections to them is already efficient.
|
||||
|
||||
---
|
||||
|
||||
## Merge Mixed Detections
|
||||
|
||||
When merging `Detections` objects that mix dense `ndarray` masks and `CompactMask` instances, `Detections.merge` converts dense inputs to `CompactMask` automatically. No full `(N, H, W)` stack is allocated:
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from supervision.detection.compact_mask import CompactMask
|
||||
|
||||
H, W = 720, 1280
|
||||
|
||||
# Compact detections from an RLE-based source.
|
||||
# Replace the counts string with a real compressed RLE payload from your model or API.
|
||||
rles = [{"size": [H, W], "counts": "YOUR_RLE_COUNTS_STRING_HERE"}]
|
||||
xyxy_a = np.array([[100.0, 50.0, 400.0, 300.0]])
|
||||
cm = CompactMask.from_coco_rle(rles, xyxy_a, image_shape=(H, W))
|
||||
det_a = sv.Detections(xyxy=xyxy_a, mask=cm, class_id=np.array([0]))
|
||||
|
||||
# Dense detections from a different source.
|
||||
masks_b = np.zeros((1, H, W), dtype=bool)
|
||||
masks_b[0, 200:400, 500:800] = True
|
||||
xyxy_b = np.array([[500.0, 200.0, 799.0, 399.0]])
|
||||
det_b = sv.Detections(xyxy=xyxy_b, mask=masks_b, class_id=np.array([1]))
|
||||
|
||||
# Output is CompactMask regardless of input order.
|
||||
merged = sv.Detections.merge([det_a, det_b])
|
||||
assert isinstance(merged.mask, CompactMask)
|
||||
assert len(merged) == 2
|
||||
```
|
||||
|
||||
Merge rules:
|
||||
|
||||
| Inputs | Output mask type |
|
||||
| ------------------------------------- | ------------------------------- |
|
||||
| All `CompactMask` | `CompactMask` |
|
||||
| Mixed `CompactMask` + dense `ndarray` | `CompactMask` |
|
||||
| All dense `ndarray` | `ndarray` (backward compatible) |
|
||||
|
||||
All `CompactMask` inputs must share the same `image_shape`; mismatches raise `ValueError`.
|
||||
|
||||
---
|
||||
|
||||
## Performance Notes
|
||||
|
||||
These estimates apply to the **parsing and annotation stage**, not end-to-end pipeline FPS. Model inference typically dominates total runtime.
|
||||
|
||||
| Optimisation | Realistic gain | Applies when |
|
||||
| ---------------------------- | -------------------------- | ------------------------------------------------------------- |
|
||||
| `from_coco_rle` ingestion | 25–60% faster parse | Full-frame COCO RLE payload; current dense decode path |
|
||||
| `MaskAnnotator` ROI blending | 10–35% faster annotation | Many small, sparse masks on high-res frames |
|
||||
| `PolygonAnnotator` crop path | 15–45% faster polygon draw | Many compact masks; full-frame materialise was the bottleneck |
|
||||
| Mixed-mask merge | 5–20% faster merge | Mix of compact and dense sources (e.g. multi-camera stitch) |
|
||||
|
||||
Upper-end gains assume: ≥1080p frames, tens to hundreds of instances, masks covering less than ~20% of total pixels.
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
- [CompactMask][supervision.detection.compact_mask.CompactMask]
|
||||
- [CompactMask.from_coco_rle][supervision.detection.compact_mask.CompactMask.from_coco_rle]
|
||||
- [CompactMask.from_dense][supervision.detection.compact_mask.CompactMask.from_dense]
|
||||
- [Detections.from_inference][supervision.detection.core.Detections.from_inference]
|
||||
- [Detections.to_compact_masks][supervision.detection.core.Detections.to_compact_masks]
|
||||
- [Detections.merge][supervision.detection.core.Detections.merge]
|
||||
- [BaseAnnotator.requires_mask][supervision.annotators.base.BaseAnnotator]
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ comments: true
|
|||
)
|
||||
```
|
||||
|
||||
`sv.VertexEllipseAnnotator` is a compatibility alias for `sv.VertexEllipseAreaAnnotator`.
|
||||
|
||||
=== "VertexEllipseOutlineAnnotator"
|
||||
|
||||
```python
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ The built-in `sv.ByteTrack` wrapper assigns persistent IDs across video frames t
|
|||
|
||||
### Datasets
|
||||
|
||||
`sv.DetectionDataset` loads, merges, splits, and converts object detection datasets. Supported formats include YOLO, COCO JSON, and Pascal VOC. `sv.ClassificationDataset` supports folder-structured classification datasets.
|
||||
`sv.DetectionDataset` loads, merges, splits, and converts object detection datasets. Supported formats include YOLO, COCO JSON, Pascal VOC, and LabelMe. `sv.ClassificationDataset` supports folder-structured classification datasets.
|
||||
|
||||
### Metrics
|
||||
|
||||
|
|
@ -162,7 +162,7 @@ No. Supervision is model agnostic. It is designed to normalize model outputs int
|
|||
|
||||
### What dataset formats are supported?
|
||||
|
||||
For object detection datasets, Supervision supports YOLO, COCO JSON, and Pascal VOC import and export. For classification datasets, it supports folder-structure import and export.
|
||||
For object detection datasets, Supervision supports YOLO, COCO JSON, Pascal VOC, and LabelMe import and export. For classification datasets, it supports folder-structure import and export.
|
||||
|
||||
### How do I detect small objects?
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ Zone-based counting. `PolygonZone.trigger(detections)` returns a boolean mask fo
|
|||
|
||||
### sv.DetectionDataset and sv.ClassificationDataset
|
||||
|
||||
For detection datasets, load, merge, split, and convert between YOLO, COCO JSON, and Pascal VOC formats. Classification datasets use folder-structure import and export via `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
|
||||
For detection datasets, load, merge, split, and convert between YOLO, COCO JSON, Pascal VOC, and LabelMe formats. Classification datasets use folder-structure import and export via `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
|
||||
|
||||
### sv.InferenceSlicer
|
||||
|
||||
|
|
@ -129,11 +129,11 @@ 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?
|
||||
|
||||
Annotate images and video with bounding boxes, masks, and labels; track objects across frames with persistent IDs; count detections inside polygon zones or line crossings; filter and query detection results; load, split, and convert detection datasets between YOLO, COCO, and Pascal VOC formats; manage classification datasets with folder structures; and benchmark model performance with mAP and confusion matrices.
|
||||
Annotate images and video with bounding boxes, masks, and labels; track objects across frames with persistent IDs; count detections inside polygon zones or line crossings; filter and query detection results; load, split, and convert detection datasets between YOLO, COCO, Pascal VOC, and LabelMe formats; manage classification datasets with folder structures; and benchmark model performance with mAP and confusion matrices.
|
||||
|
||||
### Is supervision free to use?
|
||||
|
||||
|
|
@ -153,7 +153,7 @@ Use a tracker to assign persistent IDs. The built-in `sv.ByteTrack` wrapper acce
|
|||
|
||||
### What dataset formats does supervision support?
|
||||
|
||||
For detection datasets, supervision supports YOLO, COCO JSON, and Pascal VOC. Use `DetectionDataset.from_yolo()`, `from_coco()`, or `from_pascal_voc()` to load, and `as_yolo()`, `as_coco()`, or `as_pascal_voc()` to save. For classification datasets, use `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
|
||||
For detection datasets, supervision supports YOLO, COCO JSON, Pascal VOC, and LabelMe. Use `DetectionDataset.from_yolo()`, `from_coco()`, `from_pascal_voc()`, or `from_labelme()` to load, and `as_yolo()`, `as_coco()`, `as_pascal_voc()`, or `as_labelme()` to save. For classification datasets, use `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
|
||||
|
||||
### How do I count objects in a zone?
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ Object tracker wrapper that assigns persistent IDs across video frames. The buil
|
|||
Zone-based counting. `PolygonZone.trigger(detections)` returns a boolean mask for detections currently inside an arbitrary polygon. `LineZone.trigger(detections)` returns `(crossed_in, crossed_out)` arrays for line crossings and requires `detections.tracker_id` so objects can be matched across frames. Both are commonly paired with zone annotators for visualization.
|
||||
|
||||
### sv.DetectionDataset and sv.ClassificationDataset
|
||||
For detection datasets, load, merge, split, and convert between YOLO, COCO JSON, and Pascal VOC formats. Classification datasets use folder-structure import and export via `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
|
||||
For detection datasets, load, merge, split, and convert between YOLO, COCO JSON, Pascal VOC, CreateML, and LabelMe formats. Classification datasets use folder-structure import and export via `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
|
||||
|
||||
### sv.InferenceSlicer
|
||||
SAHI-style inference slicing: split high-resolution images into overlapping tiles, run detection on each tile, merge results with non-maximum suppression or non-maximum merge. Configure tile overlap in pixels with `overlap_wh`.
|
||||
|
|
@ -120,11 +120,11 @@ 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?
|
||||
|
||||
Annotate images and video with bounding boxes, masks, and labels; track objects across frames with persistent IDs; count detections inside polygon zones or line crossings; filter and query detection results; load, split, and convert detection datasets between YOLO, COCO, and Pascal VOC formats; manage classification datasets with folder structures; and benchmark model performance with mAP and confusion matrices.
|
||||
Annotate images and video with bounding boxes, masks, and labels; track objects across frames with persistent IDs; count detections inside polygon zones or line crossings; filter and query detection results; load, split, and convert detection datasets between YOLO, COCO, Pascal VOC, and LabelMe formats; manage classification datasets with folder structures; and benchmark model performance with mAP and confusion matrices.
|
||||
|
||||
### Is supervision free to use?
|
||||
|
||||
|
|
@ -144,7 +144,7 @@ Use a tracker to assign persistent IDs. The built-in `sv.ByteTrack` wrapper acce
|
|||
|
||||
### What dataset formats does supervision support?
|
||||
|
||||
For detection datasets, supervision supports YOLO, COCO JSON, and Pascal VOC. Use `DetectionDataset.from_yolo()`, `from_coco()`, or `from_pascal_voc()` to load, and `as_yolo()`, `as_coco()`, or `as_pascal_voc()` to save. For classification datasets, use `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
|
||||
For detection datasets, supervision supports YOLO, COCO JSON, Pascal VOC, CreateML, and LabelMe. Use `DetectionDataset.from_yolo()`, `from_coco()`, `from_pascal_voc()`, `from_createml()`, or `from_labelme()` to load, and `as_yolo()`, `as_coco()`, `as_pascal_voc()`, `as_createml()`, or `as_labelme()` to save. For classification datasets, use `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
|
||||
|
||||
### How do I count objects in a zone?
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ comments: true
|
|||
|
||||
This page contains supplementary values, types and enums that metrics use.
|
||||
|
||||
Install the metrics extra before using metrics APIs:
|
||||
|
||||
```bash
|
||||
pip install "supervision[metrics]"
|
||||
```
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.metrics.core.MetricTarget">MetricTarget</a></h2>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ comments: true
|
|||
|
||||
# F1 Score
|
||||
|
||||
Install the metrics extra before using this API:
|
||||
|
||||
```bash
|
||||
pip install "supervision[metrics]"
|
||||
```
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.metrics.f1_score.F1Score">F1Score</a></h2>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
---
|
||||
comments: true
|
||||
description: API reference for MeanAveragePrecision — compute mAP for object detection benchmarking with bounding boxes.
|
||||
description: API reference for MeanAveragePrecision — compute mAP for object detection benchmarking with boxes, masks, and oriented boxes.
|
||||
---
|
||||
|
||||
# Mean Average Precision
|
||||
|
||||
Install the metrics extra before using this API:
|
||||
|
||||
```bash
|
||||
pip install "supervision[metrics]"
|
||||
```
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.metrics.mean_average_precision.MeanAveragePrecision">MeanAveragePrecision</a></h2>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ comments: true
|
|||
|
||||
# Mean Average Recall
|
||||
|
||||
Install the metrics extra before using this API:
|
||||
|
||||
```bash
|
||||
pip install "supervision[metrics]"
|
||||
```
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.metrics.mean_average_recall.MeanAverageRecall">MeanAverageRecall</a></h2>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ comments: true
|
|||
|
||||
# Precision
|
||||
|
||||
Install the metrics extra before using this API:
|
||||
|
||||
```bash
|
||||
pip install "supervision[metrics]"
|
||||
```
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.metrics.precision.Precision">Precision</a></h2>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ comments: true
|
|||
|
||||
# Recall
|
||||
|
||||
Install the metrics extra before using this API:
|
||||
|
||||
```bash
|
||||
pip install "supervision[metrics]"
|
||||
```
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.metrics.recall.Recall">Recall</a></h2>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "jxcxFKy2hRnA"
|
||||
},
|
||||
"source": [
|
||||
"# Blurring Faces\n",
|
||||
"\n",
|
||||
"---\n",
|
||||
"\n",
|
||||
"[](https://colab.research.google.com/github/roboflow/supervision/blob/develop/docs/notebooks/blurring_faces.ipynb)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "FAxD-vAgkadG"
|
||||
},
|
||||
"source": [
|
||||
"Click the `Open in Colab` button to run the cookbook on Google Colab.\n",
|
||||
"\n",
|
||||
"## Introduction\n",
|
||||
"\n",
|
||||
"In this cookbook we'll use a frame from a video of someone in a supermarket. We'll download this video via the `supervision` assets module. We'll then run inference on this frame using the hosted Roboflow API to fetch detections of faces utilizing an open source face detection model on Roboflow Universe. Finally, we'll use supervision to blur the detected faces."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "o_F-fJGskuz9"
|
||||
},
|
||||
"source": [
|
||||
"## Install packages\n",
|
||||
"\n",
|
||||
"Let's quickly install the `supervision` package with the assets module, as well as the roboflow `inference_sdk` with pip. We'll also install `tqdm` to show a progress bar, but this is optional in production code."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "c2X8k1opItiO"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": "!pip3 install -q supervision inference tqdm \"Pillow<12\""
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "9xy1jXMrm5iU"
|
||||
},
|
||||
"source": [
|
||||
"## Download Video and Extract Frame\n",
|
||||
"\n",
|
||||
"In order to blur a face in a frame, we'll need a frame with a face in it. Let's download a video, and grab a frame in the middle of the video. I played around a little, and found that the 800th frame is great frame for us to test, since the customer is facing the camera. In this code, we're also using tqdm to display a progress bar of our script."
|
||||
]
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "code",
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"source": [
|
||||
"import supervision as sv\n",
|
||||
"\n",
|
||||
"video = sv.download_assets(sv.VideoAssets.GROCERY_STORE)\n",
|
||||
"\n",
|
||||
"# Seek directly to frame 800 using the start parameter (O(1) seek)\n",
|
||||
"frame = next(sv.get_video_frames_generator(video, start=800))\n",
|
||||
"\n",
|
||||
"sv.plot_image(frame)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "tgejF7Zlosyd"
|
||||
},
|
||||
"source": [
|
||||
"## Detecting Faces\n",
|
||||
"\n",
|
||||
"Now that we've got our image we'll need a good face detecting model. For this task, there are already an impressive amount of open source models available on [Roboflow Universe](https://universe.roboflow.com/). After a little digging, this [face detection model](https://universe.roboflow.com/mohamed-traore-2ekkp/face-detection-mik1i) has over 1300 images. Some models, including this one, require a Roboflow API key. You can [create a free account here](https://app.roboflow.com/login). From there, you can find the key under Settings > Workspaces > Roboflow API. Let's give it a try."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"id": "3MDjVxEzKWGn",
|
||||
"outputId": "8fee3446-cc81-4e3c-c4a0-b531d286f1a4"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"from inference_sdk import InferenceHTTPClient\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" from google.colab import userdata\n",
|
||||
" ROBOFLOW_API_KEY = userdata.get(\"ROBOFLOW_API_KEY\") or \"\"\n",
|
||||
"except ImportError:\n",
|
||||
" ROBOFLOW_API_KEY = os.environ.get(\"ROBOFLOW_API_KEY\", \"\")\n",
|
||||
"\n",
|
||||
"assert ROBOFLOW_API_KEY, \"Set ROBOFLOW_API_KEY in Colab secrets or as env var\"\n",
|
||||
"\n",
|
||||
"client = InferenceHTTPClient(\n",
|
||||
" api_url=\"https://detect.roboflow.com\",\n",
|
||||
" api_key=ROBOFLOW_API_KEY\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"results = client.infer(frame, model_id=\"face-detection-mik1i/18\")\n",
|
||||
"\n",
|
||||
"print(f\"Detected {len(results['predictions'])} face(s)\")\n",
|
||||
"print(results)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "dkPMOHa_univ"
|
||||
},
|
||||
"source": [
|
||||
"## Blurring the Face\n",
|
||||
"\n",
|
||||
"Now that we're detecting faces, bluring them is easy with supervision. Let's pass our results into a `Detections` object and annotate the frame with a `BlurAnnotator`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "code",
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"source": [
|
||||
"blur = sv.BlurAnnotator(kernel_size=100)\n",
|
||||
"\n",
|
||||
"detections = sv.Detections.from_inference(results)\n",
|
||||
"\n",
|
||||
"annotated_frame = blur.annotate(scene=frame.copy(), detections=detections)\n",
|
||||
"\n",
|
||||
"sv.plot_image(annotated_frame)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "YVNe8oe4vY4N"
|
||||
},
|
||||
"source": [
|
||||
"## Conclusion\n",
|
||||
"\n",
|
||||
"With supervision, inference, and Roboflow Universe we were able to blur faces in minutes with an open source model. There are many other impressive use cases out there, so feel free to share in your own cookbooks. Happy building!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"accelerator": "GPU",
|
||||
"colab": {
|
||||
"cell_execution_strategy": "setup",
|
||||
"gpuType": "T4",
|
||||
"provenance": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -34,6 +34,10 @@
|
|||
<p class="card repo-card" data-name="Object Tracking" data-labels="TRACKING, ANNOTATOR" data-version="v0.18.0"
|
||||
data-author="nickherrig"></p>
|
||||
</a>
|
||||
<a href="../notebooks/blurring-faces/">
|
||||
<p class="card repo-card" data-name="Blurring Faces" data-labels="ANNOTATOR,API,UNIVERSE"
|
||||
data-version="v0.18.0" data-author="nickherrig"></p>
|
||||
</a>
|
||||
<a href="../notebooks/occupancy_analytics/">
|
||||
<p class="card repo-card" data-name="Analyzing Zone Occupancy" data-labels="ANNOTATOR,DETECTION,ZONES"
|
||||
data-version="v0.26.0" data-author="stellasphere"></p>
|
||||
|
|
@ -62,6 +66,10 @@
|
|||
<p class="card repo-card" data-name="Memory-Efficient Instance Segmentation"
|
||||
data-labels="COMPACT MASK,SAM3,SEGMENTATION" data-version="v0.28.0" data-author="Borda"></p>
|
||||
</a>
|
||||
<a href="../notebooks/oriented-bounding-boxes/">
|
||||
<p class="card repo-card" data-name="Oriented Bounding Boxes for Densely Packed Objects"
|
||||
data-labels="OBB,DETECTIONS,NMS,DATASET" data-version="v0.29.0" data-author="kounelisagis"></p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -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+."
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
---
|
||||
comments: true
|
||||
description: API reference for supervision's object trackers — ByteTrack and SORT implementations that assign persistent IDs across video frames.
|
||||
description: API reference for supervision's deprecated ByteTrack tracker wrapper.
|
||||
---
|
||||
|
||||
# ByteTrack
|
||||
|
||||
!!! warning "Deprecated"
|
||||
|
||||
`sv.ByteTrack` is deprecated in `supervision-0.28.0` and will be removed in `supervision-0.31.0`. Install `trackers` and use `ByteTrackTracker` instead.
|
||||
|
||||
:::supervision.tracker.byte_tracker.core.ByteTrack
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Conversion Utils
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.utils.conversion.cv2_to_pillow">cv2_to_pillow</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.utils.conversion.cv2_to_pillow
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.utils.conversion.pillow_to_cv2">pillow_to_cv2</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.utils.conversion.pillow_to_cv2
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.utils.conversion.ensure_cv2_image_for_annotation">ensure_cv2_image_for_annotation</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.utils.conversion.ensure_cv2_image_for_annotation
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.utils.conversion.ensure_pil_image_for_annotation">ensure_pil_image_for_annotation</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.utils.conversion.ensure_pil_image_for_annotation
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.utils.conversion.images_to_cv2">images_to_cv2</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.utils.conversion.images_to_cv2
|
||||
|
|
@ -13,3 +13,21 @@ comments: true
|
|||
</div>
|
||||
|
||||
:::supervision.geometry.core.Position
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.geometry.core.Point">Point</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.geometry.core.Point
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.geometry.core.Rect">Rect</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.geometry.core.Rect
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.geometry.core.Vector">Vector</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.geometry.core.Vector
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@ status: new
|
|||
|
||||
:::supervision.utils.image.crop_image
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.utils.image.load_image_from_url">load_image_from_url</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.utils.image.load_image_from_url
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.utils.image.scale_image">scale_image</a></h2>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Image Window
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.utils.image_window.ImageWindow">ImageWindow</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.utils.image_window.ImageWindow
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
This example benchmarks `CompactMask`, a new mask representation introduced in `supervision` that replaces dense `(N, H, W)` boolean arrays with a crop-scoped Run-Length Encoding (RLE). The benchmark demonstrates full API compatibility, massive memory savings, and order-of-magnitude annotation speedups — with no change to your existing `Detections` code.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## The Problem
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ For a 4K image with 1 000 detected objects:
|
|||
|
||||
At this scale, typical pipelines crash with `MemoryError` before a single frame is annotated. Aerial imagery, satellite tiles, and high-density crowd scenes all hit this wall.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## The Solution — Crop-RLE Storage
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ Crop RLE's `.crop()` method powers the `MaskAnnotator` optimisation — it never
|
|||
|
||||
At N=1 000 with 1 % overlap, bbox pre-filter reduces 499 500 candidate pairs to ~5 000 overlapping pairs — a ~2 000x reduction in pixel-level work.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## Why Crop-RLE Was Chosen over Local Crop
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ Both formats compress extremely well; the deciding factors for Crop-RLE are:
|
|||
|
||||
The main trade-off: crop-only decode is O(A) rather than O(1). For the common solid-fill segmentation mask this is negligible (\<0.1 ms per mask).
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## Operation-by-Operation Speedup Analysis
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ This section walks through every `Detections` operation that touches masks and s
|
|||
|
||||
At 50% fill on an FHD image each mask's bounding box covers a large portion of the frame, producing many RLE runs per row.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
### Memory
|
||||
|
||||
|
|
@ -146,7 +146,7 @@ Scaled to N=200: 200 x 4.7 KB = ~933 KB of RLE data, plus `_crop_shapes` (1.6 KB
|
|||
|
||||
At 5% fill with 8-vertex polygons, the ratio reaches 10 000x–20 000x because crops are tiny and RLEs are extremely short. The benchmark's 4K-200-5%-v8 scenario measures 21 786x (theory) / ~6 000x (malloc). The SAT-200-5%-v8 scenario reaches 62 968x theoretical.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
### `.area`
|
||||
|
||||
|
|
@ -179,7 +179,7 @@ At FHD-200-50%-v600, dense `.area` takes 84.66 ms; compact takes 0.48 ms — a *
|
|||
| No (H, W) allocation per mask | latency |
|
||||
| **Combined** | **~1 000x** |
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
### `filter` / `__getitem__` (boolean index)
|
||||
|
||||
|
|
@ -212,7 +212,7 @@ At FHD-200-50%-v600, dense `filter` takes 14.56 ms; compact takes 0.03 ms — a
|
|||
| Allocation | new `(K, H, W)` array | new `CompactMask` shell (~trivial) |
|
||||
| **Speedup** | | **hundreds to tens of thousands x** |
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
### `annotate` (`MaskAnnotator`)
|
||||
|
||||
|
|
@ -246,7 +246,7 @@ colored_mask[y1 : y1 + crop_h, x1 : x1 + crop_w][crop_m] = color.as_bgr()
|
|||
| x N masks | compounds |
|
||||
| **Combined** | **~26 – 400x** |
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
### IoU (`mask_iou_batch` / `compact_mask_iou_batch`)
|
||||
|
||||
|
|
@ -315,7 +315,7 @@ At FHD-200-50%-v600, dense IoU takes 23 915 ms; compact takes 51.58 ms — a **4
|
|||
|
||||
At 20% fill the gaps close — more pairs overlap, larger crops — speedup drops toward the lower end of the range.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
### NMS (`mask_non_max_suppression`)
|
||||
|
||||
|
|
@ -339,7 +339,7 @@ All three IoU optimisations apply to the compact path:
|
|||
|
||||
At FHD-200-50%-v600, dense NMS takes 5 231 ms; compact takes 48.15 ms — a **481x speedup**. Dense IoU/NMS is skipped for scenarios above 1 GB (4K-200 and SAT-200 tiers); compact NMS still runs on those.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
### `merge` (`Detections.merge`)
|
||||
|
||||
|
|
@ -387,7 +387,7 @@ if len(self.xyxy) > 0:
|
|||
|
||||
This O(1) check avoids the O(N x H x W) dense materialisation that previously dominated compact merge time.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
### `offset` / `with_offset` (`InferenceSlicer` tile stitching)
|
||||
|
||||
|
|
@ -425,7 +425,7 @@ At FHD-200-50%-v600, dense offset takes 42.30 ms; compact takes 0.02 ms — a **
|
|||
|
||||
In the `InferenceSlicer` pipeline the canvas is always expanded by the tile offset, so no crop ever overflows — the fast path is always taken. Clipping only activates for objects that genuinely straddle the image boundary.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
### `centroids` (`calculate_masks_centroids`)
|
||||
|
||||
|
|
@ -460,7 +460,7 @@ At FHD-200-50%-v600, dense centroids takes 1 133.68 ms; compact takes 60.39 ms
|
|||
| No global `np.indices((H, W))` allocation | saves large float64 |
|
||||
| **Combined (N=200)** | **~19 – 1 000x** |
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
### Summary
|
||||
|
||||
|
|
@ -480,7 +480,7 @@ Measured speedups at the **FHD-200-50%-v600** operating point (dense fill, compl
|
|||
|
||||
All speedups are larger at sparser fill fractions and larger resolutions. At SAT-200-20%-v128, `.area` reaches 1 204x and `merge` reaches 89 046x. At the sparsest scenarios (5% fill, 8-vertex polygons), memory ratios exceed 60 000x.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## Drop-In Compatibility
|
||||
|
||||
|
|
@ -517,7 +517,7 @@ Supported indexing patterns:
|
|||
| `mask[slice]` | New `CompactMask` |
|
||||
| `np.asarray(mask)` | Dense `(N, H, W)` bool array |
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## Benchmark
|
||||
|
||||
|
|
@ -527,6 +527,69 @@ Run on any machine — no GPU or real model required:
|
|||
uv run python examples/compact_mask/benchmark.py
|
||||
```
|
||||
|
||||
For a focused benchmark of the Roboflow inference-result parser API, run:
|
||||
|
||||
```bash
|
||||
uv run python examples/compact_mask/bench_inference_api.py
|
||||
```
|
||||
|
||||
This script downloads all supervision image assets plus the middle frame from every supervision video asset by default, runs one real segmentation inference per source image, requests native RLE masks from Inference, freezes that result, and then compares parser performance:
|
||||
|
||||
```python
|
||||
sv.Detections.from_inference(result)
|
||||
sv.Detections.from_inference(result, compact_masks=True)
|
||||
```
|
||||
|
||||
Timing repetitions, warmups, confidence, IoU, response mask format, and the default model live as constants in `bench_inference_api.py`.
|
||||
|
||||
Inference runs and segmentation-derived box fields are outside the timed benchmark loop. By default the script uses `rfdetr-seg-large` with `response_mask_format="rle"`; set `BENCH_INFERENCE_MODEL_ID` to override the model. Set `ROBOFLOW_API_KEY` when your model requires authentication. Sources where the model returns no native RLE segmentation masks are skipped because there is no RLE parser work to benchmark. `rfdetr-large` is a valid local Inference model id, but it is object detection only; use an `rfdetr-seg-*` model for instance segmentation.
|
||||
|
||||
Run one specific supervision image or video asset with `--asset`:
|
||||
|
||||
```bash
|
||||
uv run python examples/compact_mask/bench_inference_api.py --asset people-walking
|
||||
uv run python examples/compact_mask/bench_inference_api.py --asset soccer
|
||||
uv run python examples/compact_mask/bench_inference_api.py --asset vehicles
|
||||
uv run python examples/compact_mask/bench_inference_api.py --asset people-walking-video
|
||||
```
|
||||
|
||||
The output reports image size, segmented objects, median parser time, peak traced allocations, mask storage, and parser speedup (`dense parser time / compact parser time`).
|
||||
|
||||
**Speedup column:** The `speedup` value reflects allocation savings — how much time is saved by skipping the dense `(N, H, W)` bool-stack allocation — not a faster RLE decode. Compact RLE arithmetic is typically slower than the dense NumPy path. The net result:
|
||||
|
||||
- **Compact is faster** only when the dense `(N, H, W)` bool-stack allocation dominates — large images with many sparse masks where avoiding that allocation outweighs the RLE arithmetic cost.
|
||||
- **Compact is slower** for small images or dense/overlapping masks, where Python RLE arithmetic dominates and the allocation cost is negligible.
|
||||
- **The primary guaranteed benefit is memory**: compact masks use roughly 99% less memory than dense stacks for typical segmentation output, regardless of which parse direction is faster.
|
||||
|
||||
The default run includes a `synthetic-dense-64` row (64×64 image, 4 fully-filled masks) to demonstrate the adversarial regime where compact is slower than dense. For each real source with segmentation masks, the script also writes a validation overlay to `examples/compact_mask/outputs/*_segmentations.jpg`.
|
||||
|
||||
### Sample results — inference API
|
||||
|
||||
Measured on macOS Apple M4 Max, 50 reps after 3 warmups, using `rfdetr-seg-large` via Roboflow Inference.
|
||||
|
||||
| src | res | seg | dense ms | CM ms | speedup | peak MB (dense/compact) | mask MB (dense/compact) | ok |
|
||||
| -------------------------- | --------- | --- | -------- | ----- | ------- | ----------------------- | ----------------------- | --- |
|
||||
| synthetic-dense-64 | 64×64 | 4 | 0.03 | 0.11 | 0.31× | 0.04 / 0.05 | 0.02 / 0.00 | ✓ |
|
||||
| people-walking.jpg | 1920×1080 | 53 | 85.56 | 12.55 | 6.82× | 219.86 / 0.11 | 109.90 / 0.02 | ✓ |
|
||||
| soccer.jpg | 398×224 | 21 | 1.36 | 1.07 | 1.27× | 3.77 / 0.05 | 1.87 / 0.00 | ✓ |
|
||||
| vehicles.mp4#269 | 3840×2160 | 7 | 46.03 | 2.60 | 18× | 116.13 / 0.07 | 58.06 / 0.00 | ✓ |
|
||||
| milk-bottling-plant.mp4#94 | 1920×1080 | 9 | 15.61 | 11.57 | 1.35× | 37.34 / 0.53 | 18.66 / 0.03 | ✓ |
|
||||
| vehicles-2.mp4#637 | 1920×1080 | 47 | 76.87 | 13.59 | 5.66× | 194.97 / 0.13 | 97.46 / 0.03 | ✓ |
|
||||
| grocery-store.mp4#501 | 3840×2160 | 4 | 27.20 | 4.36 | 6.24× | 66.36 / 0.22 | 33.18 / 0.01 | ✓ |
|
||||
| subway.mp4#649 | 2160×3840 | 42 | 325.71 | 32.21 | 10× | 696.78 / 0.80 | 348.36 / 0.09 | ✓ |
|
||||
| market-square.mp4#237 | 2160×3840 | 96 | 732.98 | 27.24 | 27× | 1592.61 / 0.22 | 796.26 / 0.05 | ✓ |
|
||||
| people-walking.mp4#170 | 1920×1080 | 60 | 100.99 | 12.69 | 7.96× | 248.89 / 0.12 | 124.42 / 0.02 | ✓ |
|
||||
| beach-1.mp4#223 | 3840×2160 | 33 | 223.50 | 13.39 | 17× | 547.47 / 0.12 | 273.72 / 0.02 | ✓ |
|
||||
| basketball-1.mp4#238 | 1920×1080 | 2 | 3.61 | 2.05 | 1.76× | 8.30 / 0.15 | 4.15 / 0.01 | ✓ |
|
||||
| skiing.mp4#176 | 1920×1080 | 11 | 16.47 | 3.07 | 5.37× | 45.63 / 0.08 | 22.81 / 0.01 | ✓ |
|
||||
|
||||
- **seg** — number of instance segmentations returned by the model
|
||||
- **dense ms / CM ms** — median parse time for `from_inference()` vs `from_inference(compact_masks=True)`
|
||||
- **speedup** — dense / compact parse time; values below 1× (e.g., synthetic-dense-64) indicate the adversarial regime where RLE arithmetic cost exceeds allocation savings
|
||||
- **peak MB** — peak traced allocations during parsing (dense / compact)
|
||||
- **mask MB** — mask storage only (dense / compact); compact is typically 100–5 000× smaller
|
||||
- **ok** — `compact.to_dense()` pixel-exactly matches dense masks
|
||||
|
||||
Six image tiers x three fill fractions (5 / 20 / 50 %) x three vertex counts (8 / 128 / 600):
|
||||
|
||||
| Tier | Resolution | Objects | Dense array | Notes |
|
||||
|
|
@ -563,7 +626,7 @@ Dense timing is skipped automatically when the dense IoU/NMS array would exceed
|
|||
|
||||
All non-skipped scenarios pass: pixel-perfect annotation, exact area, lossless `to_dense()` roundtrip.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## Use-Cases
|
||||
|
||||
|
|
@ -573,7 +636,7 @@ All non-skipped scenarios pass: pixel-perfect annotation, exact area, lossless `
|
|||
- **Long-running tracking** — accumulated `Detections` across many frames stay in kilobytes rather than gigabytes.
|
||||
- **`InferenceSlicer`** — `with_offset()` adjusts crop origins directly when stitching tile results; no dense materialisation needed.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## Limitations
|
||||
|
||||
|
|
@ -581,11 +644,12 @@ All non-skipped scenarios pass: pixel-perfect annotation, exact area, lossless `
|
|||
- RLE format is **column-major (F-order), crop-scoped** — pixel-scan order matches COCO / pycocotools, but crop scope differs from full-image scope. Use `.to_dense()` to materialize a full-image dense mask, then encode that mask to COCO RLE before passing it to pycocotools.
|
||||
- `from_dense()` requires the input `(N, H, W)` array to fit in memory. For truly OOM-scale data, build `CompactMask` per-detection directly from model output crops rather than from a pre-allocated dense stack.
|
||||
|
||||
---
|
||||
______________________________________________________________________
|
||||
|
||||
## Files
|
||||
|
||||
| File | Description |
|
||||
| -------------- | ------------------------------------------------ |
|
||||
| `benchmark.py` | Full benchmark across FHD / 4K / satellite tiers |
|
||||
| `README.md` | This file |
|
||||
| File | Description |
|
||||
| ------------------------ | --------------------------------------------------- |
|
||||
| `benchmark.py` | Full benchmark across FHD / 4K / satellite tiers |
|
||||
| `bench_inference_api.py` | Focused dense vs compact `from_inference` benchmark |
|
||||
| `README.md` | This file |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,505 @@
|
|||
"""Benchmark dense vs compact Roboflow RLE ingestion.
|
||||
|
||||
Run with:
|
||||
uv run python examples/compact_mask/bench_inference_api.py
|
||||
|
||||
The benchmark downloads supervision assets, runs one segmentation inference per
|
||||
source image, then times dense vs compact parsing of that fixed inference result.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import gc
|
||||
import os
|
||||
import statistics
|
||||
import time
|
||||
import tracemalloc
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
import supervision as sv
|
||||
from supervision.assets import ImageAssets, VideoAssets, download_assets
|
||||
from supervision.config import CLASS_NAME_DATA_FIELD
|
||||
from supervision.detection.compact_mask import CompactMask
|
||||
|
||||
console = Console(width=120, force_terminal=True)
|
||||
|
||||
# Default segmentation model; use an rfdetr-seg-* id so masks are returned.
|
||||
MODEL_ID = "rfdetr-seg-large"
|
||||
# Environment variable that can override MODEL_ID without adding CLI noise.
|
||||
MODEL_ID_ENV = "BENCH_INFERENCE_MODEL_ID"
|
||||
# Optional Roboflow API key for models that require authentication.
|
||||
API_KEY_ENV = "ROBOFLOW_API_KEY"
|
||||
# Model confidence threshold used only for the one inference call per source.
|
||||
CONFIDENCE = 0.2
|
||||
# Model IoU threshold used only for the one inference call per source.
|
||||
IOU = 0.5
|
||||
# Request native RLE masks so the benchmark measures RLE parser ingestion.
|
||||
RESPONSE_MASK_FORMAT = "rle"
|
||||
# Parser timing repetitions; inference itself is not repeated.
|
||||
REPETITIONS = 50
|
||||
# Untimed parser warmup calls before measurements.
|
||||
WARMUP = 3
|
||||
# Visual segmentation overlays for manual validation.
|
||||
ARTIFACT_DIR = Path("examples/compact_mask/outputs")
|
||||
|
||||
ASSETS = {Path(asset.filename).stem: asset for asset in ImageAssets}
|
||||
for video_asset in VideoAssets:
|
||||
key = Path(video_asset.filename).stem
|
||||
ASSETS[key if key not in ASSETS else f"{key}-video"] = video_asset
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApiBenchmarkResult:
|
||||
"""Result for one dense-vs-compact parser benchmark run."""
|
||||
|
||||
source: str
|
||||
resolution: str
|
||||
segmented_objects: int
|
||||
dense_s: float
|
||||
compact_s: float
|
||||
dense_peak_bytes: int
|
||||
compact_peak_bytes: int
|
||||
dense_mask_bytes: int
|
||||
compact_mask_bytes: int
|
||||
pixel_perfect: bool
|
||||
|
||||
|
||||
def load_image_from_asset(path: Path | None, asset: str) -> tuple[np.ndarray, str]:
|
||||
"""Return ``(image, label)`` for an image or video middle frame."""
|
||||
if path is not None:
|
||||
image = cv2.imread(str(path))
|
||||
if image is None:
|
||||
raise FileNotFoundError(f"Could not read image: {path}")
|
||||
return image, str(path)
|
||||
|
||||
asset_obj = ASSETS[asset]
|
||||
asset_path = Path(download_assets(asset_obj))
|
||||
if isinstance(asset_obj, ImageAssets):
|
||||
image = cv2.imread(str(asset_path))
|
||||
if image is None:
|
||||
raise FileNotFoundError(f"Could not read image: {asset_path}")
|
||||
return image, str(asset_path)
|
||||
|
||||
video = cv2.VideoCapture(str(asset_path))
|
||||
if not video.isOpened():
|
||||
raise FileNotFoundError(f"Could not read video: {asset_path}")
|
||||
frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
frame_index = max(0, frame_count // 2)
|
||||
if frame_index:
|
||||
video.set(cv2.CAP_PROP_POS_FRAMES, frame_index)
|
||||
ok, frame = video.read()
|
||||
video.release()
|
||||
if not ok or frame is None:
|
||||
raise FileNotFoundError(f"Could not read middle frame: {asset_path}")
|
||||
return frame, f"{asset_path}#{frame_index}"
|
||||
|
||||
|
||||
def freeze_result(inference_result: Any) -> dict[str, Any]:
|
||||
"""Convert one Inference result to a reusable dictionary."""
|
||||
if isinstance(inference_result, dict):
|
||||
return inference_result
|
||||
if hasattr(inference_result, "model_dump"):
|
||||
return inference_result.model_dump(exclude_none=True, by_alias=True)
|
||||
if hasattr(inference_result, "dict"):
|
||||
return inference_result.dict(exclude_none=True, by_alias=True)
|
||||
raise TypeError(
|
||||
f"Expected dict-like Inference result, got {type(inference_result).__name__}"
|
||||
)
|
||||
|
||||
|
||||
def count_rle_predictions(result: dict[str, Any]) -> int:
|
||||
"""Return the number of predictions carrying Roboflow RLE masks."""
|
||||
return sum(
|
||||
isinstance(prediction.get("rle") or prediction.get("rle_mask"), dict)
|
||||
for prediction in result.get("predictions", [])
|
||||
)
|
||||
|
||||
|
||||
def synthetic_dense_small_result() -> tuple[np.ndarray, str, dict[str, Any]]:
|
||||
"""Return a small dense-mask adversarial payload where compact parsing is slower.
|
||||
|
||||
Uses a 64x64 image with 4 fully-filled masks. At this scale the dense
|
||||
``(N, H, W)`` allocation cost is negligible; Python RLE arithmetic dominates,
|
||||
making compact ingestion slower than the dense NumPy path. Included as a
|
||||
clearly labeled adversarial row in the default benchmark run to show that
|
||||
the ``speedup`` column reflects allocation savings, not decode speed.
|
||||
"""
|
||||
height, width = 64, 64
|
||||
image = np.zeros((height, width, 3), dtype=np.uint8)
|
||||
predictions = [
|
||||
{
|
||||
"x": width / 2,
|
||||
"y": height / 2,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"confidence": 0.9,
|
||||
"class_id": index,
|
||||
"class": f"dense-{index}",
|
||||
"rle": {"size": [height, width], "counts": [0, height * width]},
|
||||
}
|
||||
for index in range(4)
|
||||
]
|
||||
return (
|
||||
image,
|
||||
"synthetic-dense-64",
|
||||
{
|
||||
"predictions": predictions,
|
||||
"image": {"width": width, "height": height},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def derive_boxes_from_rle_masks(result: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Set prediction boxes from native RLE segmentation masks."""
|
||||
predictions = []
|
||||
for prediction in result.get("predictions", []):
|
||||
rle = prediction.get("rle") or prediction.get("rle_mask")
|
||||
if not isinstance(rle, dict):
|
||||
predictions.append(prediction)
|
||||
continue
|
||||
|
||||
height, width = rle["size"]
|
||||
mask = sv.rle_to_mask(rle["counts"], resolution_wh=(int(width), int(height)))
|
||||
if not mask.any():
|
||||
predictions.append(prediction)
|
||||
continue
|
||||
|
||||
x1, y1, x2, y2 = sv.mask_to_xyxy(mask[np.newaxis, ...])[0]
|
||||
predictions.append(
|
||||
{
|
||||
**prediction,
|
||||
"x": float((x1 + x2) / 2),
|
||||
"y": float((y1 + y2) / 2),
|
||||
"width": float(x2 - x1),
|
||||
"height": float(y2 - y1),
|
||||
}
|
||||
)
|
||||
return {**result, "predictions": predictions}
|
||||
|
||||
|
||||
def artifact_path(source: str) -> Path:
|
||||
"""Return the segmentation validation artifact path for a source."""
|
||||
source_path, separator, frame = source.partition("#")
|
||||
stem = Path(source_path).stem
|
||||
suffix = f"_frame_{frame}" if separator else ""
|
||||
return ARTIFACT_DIR / f"{stem}{suffix}_segmentations.jpg"
|
||||
|
||||
|
||||
def detection_labels(detections: sv.Detections) -> list[str]:
|
||||
"""Return compact class/confidence labels for validation artifacts."""
|
||||
raw_class_names = detections.get_data(CLASS_NAME_DATA_FIELD)
|
||||
class_names = (
|
||||
raw_class_names.astype(str).tolist()
|
||||
if isinstance(raw_class_names, np.ndarray)
|
||||
else [""] * len(detections)
|
||||
)
|
||||
|
||||
labels = []
|
||||
for index in range(len(detections)):
|
||||
class_name = class_names[index] if index < len(class_names) else ""
|
||||
confidence = (
|
||||
""
|
||||
if detections.confidence is None
|
||||
else f" {detections.confidence[index]:.2f}"
|
||||
)
|
||||
labels.append(f"{class_name}{confidence}".strip() or str(index))
|
||||
return labels
|
||||
|
||||
|
||||
def save_segmentation_artifact(
|
||||
image: np.ndarray,
|
||||
result: dict[str, Any],
|
||||
source: str,
|
||||
) -> Path | None:
|
||||
"""Draw parsed segmentation masks and save a validation artifact."""
|
||||
detections = sv.Detections.from_inference(result)
|
||||
if detections.mask is None:
|
||||
return None
|
||||
|
||||
annotated = image.copy()
|
||||
annotated = sv.MaskAnnotator(
|
||||
color_lookup=sv.ColorLookup.INDEX,
|
||||
opacity=0.45,
|
||||
).annotate(scene=annotated, detections=detections)
|
||||
annotated = sv.LabelAnnotator(
|
||||
color_lookup=sv.ColorLookup.INDEX,
|
||||
text_scale=0.35,
|
||||
text_padding=4,
|
||||
).annotate(
|
||||
scene=annotated,
|
||||
detections=detections,
|
||||
labels=detection_labels(detections),
|
||||
)
|
||||
|
||||
path = artifact_path(source)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not cv2.imwrite(str(path), annotated):
|
||||
raise OSError(f"Could not write segmentation artifact: {path}")
|
||||
return path
|
||||
|
||||
|
||||
def load_inference_model(model_id: str, api_key: str | None) -> Any:
|
||||
"""Load the requested Inference model."""
|
||||
try:
|
||||
from inference import get_model
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Install the `inference` package to run this benchmark."
|
||||
) from exc
|
||||
|
||||
model_kwargs = {"api_key": api_key} if api_key is not None else {}
|
||||
return get_model(model_id=model_id, **model_kwargs)
|
||||
|
||||
|
||||
def run_inference_once(
|
||||
image: np.ndarray,
|
||||
model: Any,
|
||||
model_id: str,
|
||||
confidence: float,
|
||||
iou: float,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Run one real segmentation inference and return a frozen result."""
|
||||
# Inference still serializes instance segmentations with x/y/width/height.
|
||||
# Derive those fields from the RLE masks so the benchmark uses segmentations,
|
||||
# not the model-reported detector boxes, as the source of truth.
|
||||
result = derive_boxes_from_rle_masks(
|
||||
freeze_result(
|
||||
model.infer(
|
||||
image,
|
||||
confidence=confidence,
|
||||
iou=iou,
|
||||
response_mask_format=RESPONSE_MASK_FORMAT,
|
||||
)[0]
|
||||
)
|
||||
)
|
||||
rle_count = count_rle_predictions(result)
|
||||
if rle_count == 0:
|
||||
console.print(
|
||||
f"[yellow]skipped[/yellow] {model_id}: no native RLE segmentation "
|
||||
f"predictions for response_mask_format={RESPONSE_MASK_FORMAT!r}"
|
||||
)
|
||||
return None
|
||||
return result
|
||||
|
||||
|
||||
def median_seconds(fn: Callable[[], object], reps: int, warmup: int) -> float:
|
||||
"""Return median runtime for ``fn``."""
|
||||
for _ in range(warmup):
|
||||
fn()
|
||||
gc.collect()
|
||||
|
||||
timings = []
|
||||
for _ in range(reps):
|
||||
start = time.perf_counter()
|
||||
fn()
|
||||
timings.append(time.perf_counter() - start)
|
||||
return statistics.median(timings)
|
||||
|
||||
|
||||
def peak_bytes(fn: Callable[[], object]) -> int:
|
||||
"""Return peak traced allocations for one call."""
|
||||
gc.collect()
|
||||
tracemalloc.start()
|
||||
fn()
|
||||
_, peak = tracemalloc.get_traced_memory()
|
||||
tracemalloc.stop()
|
||||
return int(peak)
|
||||
|
||||
|
||||
def dense_mask_bytes(detections: sv.Detections) -> int:
|
||||
"""Return dense mask storage bytes."""
|
||||
return 0 if detections.mask is None else int(np.asarray(detections.mask).nbytes)
|
||||
|
||||
|
||||
def compact_mask_bytes(detections: sv.Detections) -> int:
|
||||
"""Return compact mask storage bytes."""
|
||||
if not isinstance(detections.mask, CompactMask):
|
||||
return 0
|
||||
return sum(rle.nbytes for rle in detections.mask._rles)
|
||||
|
||||
|
||||
def _fmt_ratio(ratio: float) -> str:
|
||||
"""Format a speedup/compression ratio with colour coding."""
|
||||
fmt = f"{ratio:.0f}x" if ratio >= 10 else f"{ratio:.2f}x"
|
||||
if ratio >= 10:
|
||||
return f"[green]{fmt}[/green]"
|
||||
elif ratio >= 1:
|
||||
return f"[yellow]{fmt}[/yellow]"
|
||||
else:
|
||||
return f"[red]{fmt}[/red]"
|
||||
|
||||
|
||||
def _fmt_mb(num_bytes: int) -> str:
|
||||
"""Format bytes as compact megabytes."""
|
||||
return f"{num_bytes / 1e6:.2f}"
|
||||
|
||||
|
||||
def run_benchmark(
|
||||
source: str,
|
||||
image: np.ndarray,
|
||||
result: dict[str, Any],
|
||||
reps: int,
|
||||
warmup: int,
|
||||
) -> ApiBenchmarkResult:
|
||||
"""Run one dense-vs-compact parser benchmark."""
|
||||
|
||||
# Benchmark the public Roboflow/Inference adapter; RLE masks enter through
|
||||
# the result payload and should stay compact when compact_masks=True.
|
||||
def dense() -> sv.Detections:
|
||||
return sv.Detections.from_inference(result)
|
||||
|
||||
def compact() -> sv.Detections:
|
||||
return sv.Detections.from_inference(result, compact_masks=True)
|
||||
|
||||
dense_once = dense()
|
||||
compact_once = compact()
|
||||
if not isinstance(dense_once.mask, np.ndarray):
|
||||
raise TypeError(f"Expected dense ndarray mask, got {type(dense_once.mask)}")
|
||||
if not isinstance(compact_once.mask, CompactMask):
|
||||
raise TypeError(f"Expected CompactMask, got {type(compact_once.mask)}")
|
||||
np.testing.assert_array_equal(compact_once.mask.to_dense(), dense_once.mask)
|
||||
|
||||
dense_s = median_seconds(dense, reps, warmup)
|
||||
compact_s = median_seconds(compact, reps, warmup)
|
||||
dense_peak = peak_bytes(dense)
|
||||
compact_peak = peak_bytes(compact)
|
||||
|
||||
return ApiBenchmarkResult(
|
||||
source=source,
|
||||
resolution=f"{image.shape[1]}x{image.shape[0]}",
|
||||
segmented_objects=len(dense_once),
|
||||
dense_s=dense_s,
|
||||
compact_s=compact_s,
|
||||
dense_peak_bytes=dense_peak,
|
||||
compact_peak_bytes=compact_peak,
|
||||
dense_mask_bytes=dense_mask_bytes(dense_once),
|
||||
compact_mask_bytes=compact_mask_bytes(compact_once),
|
||||
pixel_perfect=True,
|
||||
)
|
||||
|
||||
|
||||
def print_summary(results: list[ApiBenchmarkResult], reps: int, warmup: int) -> None:
|
||||
"""Print a Rich summary table matching the compact mask benchmark style."""
|
||||
table = Table(
|
||||
title="CompactMask from_inference",
|
||||
box=box.ROUNDED,
|
||||
show_lines=False,
|
||||
header_style="bold cyan",
|
||||
)
|
||||
table.add_column("src", style="bold", no_wrap=True)
|
||||
table.add_column("res", no_wrap=True)
|
||||
table.add_column("seg", justify="right")
|
||||
table.add_column("dense ms", justify="right")
|
||||
table.add_column("CM ms", justify="right", style="green")
|
||||
table.add_column("speedup", justify="right")
|
||||
table.add_column("peak MB", justify="right", style="cyan")
|
||||
table.add_column("mask MB", justify="right")
|
||||
table.add_column("ok", justify="center")
|
||||
|
||||
for result in results:
|
||||
speedup = result.dense_s / max(result.compact_s, 1e-9)
|
||||
table.add_row(
|
||||
result.source,
|
||||
result.resolution,
|
||||
str(result.segmented_objects),
|
||||
f"{result.dense_s * 1e3:.2f}",
|
||||
f"{result.compact_s * 1e3:.2f}",
|
||||
_fmt_ratio(speedup),
|
||||
f"{_fmt_mb(result.dense_peak_bytes)}/{_fmt_mb(result.compact_peak_bytes)}",
|
||||
f"{_fmt_mb(result.dense_mask_bytes)}/{_fmt_mb(result.compact_mask_bytes)}",
|
||||
"[green]✓[/green]" if result.pixel_perfect else "[red]✗[/red]",
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
console.print(
|
||||
"[dim]"
|
||||
+ " · ".join(
|
||||
[
|
||||
f"timings are median of {reps} reps after {warmup} warmups",
|
||||
"peak MB and mask MB are dense/compact",
|
||||
"speedup = dense / compact parse time; gains are allocation-driven"
|
||||
" (avoiding the dense (N,H,W) bool-stack), not faster RLE decode",
|
||||
"compact RLE arithmetic is typically slower than the dense NumPy path"
|
||||
" — synthetic-dense-64 shows this adversarial regime (speedup < 1x)",
|
||||
"OK means compact.to_dense() exactly matches dense masks",
|
||||
]
|
||||
)
|
||||
+ "[/dim]"
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the benchmark."""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--asset", choices=ASSETS.keys(), default=None)
|
||||
parser.add_argument("--image", type=Path, default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
assets = [args.asset] if args.asset is not None else list(ASSETS)
|
||||
if args.image is not None:
|
||||
assets = ["custom"]
|
||||
|
||||
results = []
|
||||
if args.asset is None and args.image is None:
|
||||
image, source, inference_result = synthetic_dense_small_result()
|
||||
console.rule(f"[bold]{source}[/bold] | {image.shape[1]}x{image.shape[0]}")
|
||||
results.append(
|
||||
run_benchmark(
|
||||
source=source,
|
||||
image=image,
|
||||
result=inference_result,
|
||||
reps=REPETITIONS,
|
||||
warmup=WARMUP,
|
||||
)
|
||||
)
|
||||
model_id = os.getenv(MODEL_ID_ENV, MODEL_ID)
|
||||
model = load_inference_model(model_id=model_id, api_key=os.getenv(API_KEY_ENV))
|
||||
for asset in assets:
|
||||
image, source = load_image_from_asset(args.image, asset)
|
||||
console.rule(f"[bold]{source}[/bold] | {image.shape[1]}x{image.shape[0]}")
|
||||
inference_result = run_inference_once(
|
||||
image=image,
|
||||
model=model,
|
||||
model_id=model_id,
|
||||
confidence=CONFIDENCE,
|
||||
iou=IOU,
|
||||
)
|
||||
if inference_result is None:
|
||||
continue
|
||||
console.print(
|
||||
f"[dim]captured {count_rle_predictions(inference_result)} RLE masks "
|
||||
f"from {model_id}[/dim]"
|
||||
)
|
||||
artifact = save_segmentation_artifact(
|
||||
image=image,
|
||||
result=inference_result,
|
||||
source=source,
|
||||
)
|
||||
if artifact is not None:
|
||||
console.print(f"[dim]saved segmentation artifact: {artifact}[/dim]")
|
||||
results.append(
|
||||
run_benchmark(
|
||||
source=source,
|
||||
image=image,
|
||||
result=inference_result,
|
||||
reps=REPETITIONS,
|
||||
warmup=WARMUP,
|
||||
)
|
||||
)
|
||||
if not results:
|
||||
raise ValueError(f"Model {model_id!r} returned no segmentation masks.")
|
||||
print_summary(results, reps=REPETITIONS, warmup=WARMUP)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
Demonstrates that ``CompactMask`` is a drop-in replacement for dense
|
||||
``(N, H, W)`` bool arrays in ``supervision.Detections``, while using
|
||||
significantly less memory and enabling faster annotation.
|
||||
significantly less memory and enabling faster annotation. The annotation
|
||||
timing reports frame size, detection count, mask area ratio, and
|
||||
``MaskAnnotator`` speedup from ROI-only blending.
|
||||
|
||||
Run with:
|
||||
uv run python examples/compact_mask/benchmark.py
|
||||
|
|
@ -12,19 +14,17 @@ 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
|
||||
import math
|
||||
import time
|
||||
import tracemalloc
|
||||
from collections.abc import Callable
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
|
@ -74,7 +74,7 @@ class ScenarioResult:
|
|||
name: str
|
||||
resolution: str # e.g. "1920x1080"
|
||||
num_objects: int
|
||||
fill_name: str # e.g. "5%"
|
||||
fill_name: str # mask area ratio, e.g. "5%"
|
||||
num_vertices: int # polygon vertex count — complexity proxy
|
||||
# memory (theoretical: raw numpy nbytes)
|
||||
dense_bytes: int
|
||||
|
|
@ -956,7 +956,7 @@ def print_summary(results: list[ScenarioResult]) -> None:
|
|||
table.add_column("Scenario", style="bold", min_width=22)
|
||||
table.add_column("Objects", justify="right", min_width=7)
|
||||
table.add_column("Resolution", min_width=12, no_wrap=True)
|
||||
table.add_column("Fill", justify="right", min_width=5, no_wrap=True)
|
||||
table.add_column("Mask\narea", justify="right", min_width=5, no_wrap=True)
|
||||
table.add_column("Vertices", justify="right", min_width=8, no_wrap=True)
|
||||
table.add_column("Dense\ntheory", justify="right", min_width=10)
|
||||
table.add_column("Compact\ntheory", justify="right", style="green", min_width=9)
|
||||
|
|
@ -1037,7 +1037,8 @@ def print_summary(results: list[ScenarioResult]) -> None:
|
|||
"Decode ms/mask — to_dense() / N (compact→dense overhead per mask)",
|
||||
"Area x — .area speedup (RLE sum, no materialisation)",
|
||||
"Filter x — boolean-index speedup",
|
||||
"Annot x — MaskAnnotator speedup (crop-paint vs full-frame alloc)",
|
||||
"Annot x — MaskAnnotator speedup "
|
||||
"(ROI-only blend vs full-frame overlay)",
|
||||
f"IoU x — pairwise self-IoU speedup "
|
||||
f"(dense skipped >{IOU_DENSE_SKIP_GB:.0f} GB)",
|
||||
"NMS x — mask_non_max_suppression speedup",
|
||||
|
|
|
|||
|
|
@ -12,61 +12,61 @@ https://github.com/roboflow/supervision/assets/26109316/f84db7b5-79e2-4142-a1da-
|
|||
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/count_people_in_zone
|
||||
```
|
||||
```bash
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/count_people_in_zone
|
||||
```
|
||||
|
||||
- setup python environment and activate it [optional]
|
||||
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
- install required dependencies
|
||||
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
|
||||
- download `traffic_analysis.pt` and `traffic_analysis.mov` files
|
||||
|
||||
```bash
|
||||
./setup.sh
|
||||
```
|
||||
```bash
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
## 🛠️ script arguments
|
||||
|
||||
- ultralytics
|
||||
|
||||
- `--source_weights_path` (optional): The path to the YOLO model's weights file. Defaults to `"yolov8x.pt"` if not specified.
|
||||
- `--source_weights_path` (optional): The path to the YOLO model's weights file. Defaults to `"yolov8x.pt"` if not specified.
|
||||
|
||||
- `--zone_configuration_path`: Specifies the path to the JSON file containing zone configurations. This file defines the polygonal areas in the video where objects will be counted.
|
||||
- `--zone_configuration_path`: Specifies the path to the JSON file containing zone configurations. This file defines the polygonal areas in the video where objects will be counted.
|
||||
|
||||
- `--source_video_path`: The path to the source video file that will be analyzed.
|
||||
- `--source_video_path`: The path to the source video file that will be analyzed.
|
||||
|
||||
- `--target_video_path` (optional): The path to save the output video with annotations. If not provided, the processed video will be displayed in real-time.
|
||||
- `--target_video_path` (optional): The path to save the output video with annotations. If not provided, the processed video will be displayed in real-time.
|
||||
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO model to filter detections. Default is `0.3`.
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO model to filter detections. Default is `0.3`.
|
||||
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold for the model. Default is `0.7`.
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold for the model. Default is `0.7`.
|
||||
|
||||
- inference
|
||||
|
||||
- `--roboflow_api_key` (optional): The API key for Roboflow services. If not provided directly, the script tries to fetch it from the `ROBOFLOW_API_KEY` environment variable. Follow [this guide](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key) to acquire your `API KEY`.
|
||||
- `--roboflow_api_key` (optional): The API key for Roboflow services. If not provided directly, the script tries to fetch it from the `ROBOFLOW_API_KEY` environment variable. Follow [this guide](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key) to acquire your `API KEY`.
|
||||
|
||||
- `--model_id` (optional): Designates the Roboflow model ID to be used. The default value is `"yolov8x-1280"`.
|
||||
- `--model_id` (optional): Designates the Roboflow model ID to be used. The default value is `"yolov8x-1280"`.
|
||||
|
||||
- `--zone_configuration_path`: Specifies the path to the JSON file containing zone configurations. This file defines the polygonal areas in the video where objects will be counted.
|
||||
- `--zone_configuration_path`: Specifies the path to the JSON file containing zone configurations. This file defines the polygonal areas in the video where objects will be counted.
|
||||
|
||||
- `--source_video_path`: The path to the source video file that will be analyzed.
|
||||
- `--source_video_path`: The path to the source video file that will be analyzed.
|
||||
|
||||
- `--target_video_path` (optional): The path to save the output video with annotations. If not provided, the processed video will be displayed in real-time.
|
||||
- `--target_video_path` (optional): The path to save the output video with annotations. If not provided, the processed video will be displayed in real-time.
|
||||
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO model to filter detections. Default is `0.3`.
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO model to filter detections. Default is `0.3`.
|
||||
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold for the model. Default is `0.7`.
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold for the model. Default is `0.7`.
|
||||
|
||||
## 📌 zone configuration
|
||||
|
||||
|
|
@ -79,24 +79,24 @@ https://github.com/roboflow/supervision/assets/26109316/f84db7b5-79e2-4142-a1da-
|
|||
|
||||
- ultralytics
|
||||
|
||||
```bash
|
||||
python ultralytics_example.py \
|
||||
--zone_configuration_path data/multi-zone-config.json \
|
||||
--source_video_path data/market-square.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
```bash
|
||||
python ultralytics_example.py \
|
||||
--zone_configuration_path data/multi-zone-config.json \
|
||||
--source_video_path data/market-square.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
|
||||
- inference
|
||||
|
||||
```bash
|
||||
python inference_example.py \
|
||||
--roboflow_api_key "ROBOFLOW_API_KEY" \
|
||||
--zone_configuration_path data/multi-zone-config.json \
|
||||
--source_video_path data/market-square.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
```bash
|
||||
python inference_example.py \
|
||||
--roboflow_api_key "ROBOFLOW_API_KEY" \
|
||||
--zone_configuration_path data/multi-zone-config.json \
|
||||
--source_video_path data/market-square.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
|
||||
## © license
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import json
|
||||
import os
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from inference.core.models.roboflow import RoboflowInferenceModel
|
||||
from inference.models.utils import get_roboflow_model
|
||||
|
|
@ -117,7 +116,7 @@ def annotate(
|
|||
"""
|
||||
annotated_frame = frame.copy()
|
||||
for zone, zone_annotator, box_annotator in zip(
|
||||
zones, zone_annotators, box_annotators
|
||||
zones, zone_annotators, box_annotators, strict=True
|
||||
):
|
||||
detections_in_zone = detections[zone.trigger(detections=detections)]
|
||||
annotated_frame = zone_annotator.annotate(scene=annotated_frame)
|
||||
|
|
@ -135,7 +134,7 @@ def main(
|
|||
target_video_path: str | None = None,
|
||||
confidence_threshold: float = 0.3,
|
||||
iou_threshold: float = 0.7,
|
||||
):
|
||||
) -> None:
|
||||
"""
|
||||
Counting people in zones with Inference and Supervision.
|
||||
|
||||
|
|
@ -179,6 +178,7 @@ def main(
|
|||
)
|
||||
sink.write_frame(annotated_frame)
|
||||
else:
|
||||
window = sv.ImageWindow("Processed Video")
|
||||
for frame in tqdm(frames_generator, total=video_info.total_frames):
|
||||
detections = detect(frame, model, confidence_threshold, iou_threshold)
|
||||
annotated_frame = annotate(
|
||||
|
|
@ -188,11 +188,12 @@ def main(
|
|||
box_annotators=box_annotators,
|
||||
detections=detections,
|
||||
)
|
||||
cv2.imshow("Processed Video", annotated_frame)
|
||||
if cv2.waitKey(1) & 0xFF == ord("q"):
|
||||
window.show(annotated_frame)
|
||||
key = window.wait_key(1)
|
||||
if not window.is_open or key == "q":
|
||||
break
|
||||
|
||||
cv2.destroyAllWindows()
|
||||
window.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import json
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
from ultralytics import YOLO
|
||||
|
|
@ -116,7 +115,7 @@ def annotate(
|
|||
"""
|
||||
annotated_frame = frame.copy()
|
||||
for zone, zone_annotator, box_annotator in zip(
|
||||
zones, zone_annotators, box_annotators
|
||||
zones, zone_annotators, box_annotators, strict=True
|
||||
):
|
||||
detections_in_zone = detections[zone.trigger(detections=detections)]
|
||||
annotated_frame = zone_annotator.annotate(scene=annotated_frame)
|
||||
|
|
@ -133,7 +132,7 @@ def main(
|
|||
target_video_path: str | None = None,
|
||||
confidence_threshold: float = 0.3,
|
||||
iou_threshold: float = 0.7,
|
||||
):
|
||||
) -> None:
|
||||
"""
|
||||
Counting people in zones with YOLO and Supervision.
|
||||
|
||||
|
|
@ -167,6 +166,7 @@ def main(
|
|||
)
|
||||
sink.write_frame(annotated_frame)
|
||||
else:
|
||||
window = sv.ImageWindow("Processed Video")
|
||||
for frame in tqdm(frames_generator, total=video_info.total_frames):
|
||||
detections = detect(frame, model, confidence_threshold, iou_threshold)
|
||||
annotated_frame = annotate(
|
||||
|
|
@ -176,11 +176,12 @@ def main(
|
|||
box_annotators=box_annotators,
|
||||
detections=detections,
|
||||
)
|
||||
cv2.imshow("Processed Video", annotated_frame)
|
||||
if cv2.waitKey(1) & 0xFF == ord("q"):
|
||||
window.show(annotated_frame)
|
||||
key = window.wait_key(1)
|
||||
if not window.is_open or key == "q":
|
||||
break
|
||||
|
||||
cv2.destroyAllWindows()
|
||||
window.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -8,23 +8,23 @@ This script performs heatmap and tracking analysis using YOLOv8, an object-detec
|
|||
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/heatmap_and_track
|
||||
```
|
||||
```bash
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/heatmap_and_track
|
||||
```
|
||||
|
||||
- setup python environment and activate it [optional]
|
||||
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
- install required dependencies
|
||||
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 🛠️ script arguments
|
||||
|
||||
|
|
|
|||
|
|
@ -1,123 +1,121 @@
|
|||
from typing import Optional
|
||||
|
||||
import cv2
|
||||
from ultralytics import YOLO
|
||||
|
||||
import supervision as sv
|
||||
from supervision.assets import VideoAssets, download_assets
|
||||
|
||||
|
||||
def download_video() -> str:
|
||||
download_assets(VideoAssets.PEOPLE_WALKING)
|
||||
return VideoAssets.PEOPLE_WALKING.value
|
||||
|
||||
|
||||
def main(
|
||||
source_weights_path: str,
|
||||
source_video_path: Optional[str] = None,
|
||||
target_video_path: str = "output.mp4",
|
||||
confidence_threshold: float = 0.35,
|
||||
iou_threshold: float = 0.5,
|
||||
heatmap_alpha: float = 0.5,
|
||||
radius: int = 25,
|
||||
track_activation_threshold: float = 0.35,
|
||||
track_seconds: int = 5,
|
||||
minimum_matching_threshold: float = 0.99,
|
||||
) -> None:
|
||||
"""
|
||||
Heatmap and Tracking with Supervision.
|
||||
|
||||
Args:
|
||||
source_weights_path: Path to the source weights file
|
||||
source_video_path: Path to the source video file
|
||||
target_video_path: Path to the target video file
|
||||
confidence_threshold: Confidence threshold for the model
|
||||
iou_threshold: IOU threshold for the model
|
||||
heatmap_alpha: Opacity of the overlay mask, between 0 and 1
|
||||
radius: Radius of the heat circle
|
||||
track_activation_threshold: Detection confidence threshold for track activation
|
||||
track_seconds: Number of seconds to buffer when a track is lost
|
||||
minimum_matching_threshold: Threshold for matching tracks with detections
|
||||
"""
|
||||
### instantiate model
|
||||
model = YOLO(source_weights_path)
|
||||
source_video_path = source_video_path or download_video()
|
||||
|
||||
### heatmap config
|
||||
heat_map_annotator = sv.HeatMapAnnotator(
|
||||
position=sv.Position.BOTTOM_CENTER,
|
||||
opacity=heatmap_alpha,
|
||||
radius=radius,
|
||||
kernel_size=25,
|
||||
top_hue=0,
|
||||
low_hue=125,
|
||||
)
|
||||
|
||||
### annotation config
|
||||
label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER)
|
||||
|
||||
### get the video fps
|
||||
cap = cv2.VideoCapture(source_video_path)
|
||||
fps = int(cap.get(cv2.CAP_PROP_FPS))
|
||||
cap.release()
|
||||
|
||||
### tracker config
|
||||
byte_tracker = sv.ByteTrack(
|
||||
track_activation_threshold=track_activation_threshold,
|
||||
lost_track_buffer=track_seconds * fps,
|
||||
minimum_matching_threshold=minimum_matching_threshold,
|
||||
frame_rate=fps,
|
||||
)
|
||||
|
||||
### video config
|
||||
video_info = sv.VideoInfo.from_video_path(video_path=source_video_path)
|
||||
frames_generator = sv.get_video_frames_generator(
|
||||
source_path=source_video_path, stride=1
|
||||
)
|
||||
|
||||
### Detect, track, annotate, save
|
||||
with sv.VideoSink(target_path=target_video_path, video_info=video_info) as sink:
|
||||
for frame in frames_generator:
|
||||
result = model(
|
||||
source=frame,
|
||||
classes=[0], # only person class
|
||||
conf=confidence_threshold,
|
||||
iou=iou_threshold,
|
||||
# show_conf = True,
|
||||
# save_txt = True,
|
||||
# save_conf = True,
|
||||
# save = True,
|
||||
device=None, # use None = CPU, 0 = single GPU, or [0,1] = dual GPU
|
||||
)[0]
|
||||
|
||||
detections = sv.Detections.from_ultralytics(result) # get detections
|
||||
|
||||
detections = byte_tracker.update_with_detections(
|
||||
detections
|
||||
) # update tracker
|
||||
|
||||
### draw heatmap
|
||||
annotated_frame = heat_map_annotator.annotate(
|
||||
scene=frame.copy(), detections=detections
|
||||
)
|
||||
|
||||
### draw other attributes from `detections` object
|
||||
labels = [
|
||||
f"#{tracker_id}"
|
||||
for class_id, tracker_id in zip(
|
||||
detections.class_id, detections.tracker_id
|
||||
)
|
||||
]
|
||||
|
||||
label_annotator.annotate(
|
||||
scene=annotated_frame, detections=detections, labels=labels
|
||||
)
|
||||
|
||||
sink.write_frame(frame=annotated_frame)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from jsonargparse import auto_cli, set_parsing_settings
|
||||
|
||||
set_parsing_settings(parse_optionals_as_positionals=True)
|
||||
auto_cli(main, as_positional=False)
|
||||
import cv2
|
||||
from ultralytics import YOLO
|
||||
|
||||
import supervision as sv
|
||||
from supervision.assets import VideoAssets, download_assets
|
||||
|
||||
|
||||
def download_video() -> str:
|
||||
download_assets(VideoAssets.PEOPLE_WALKING)
|
||||
return VideoAssets.PEOPLE_WALKING.value
|
||||
|
||||
|
||||
def main(
|
||||
source_weights_path: str,
|
||||
source_video_path: str | None = None,
|
||||
target_video_path: str = "output.mp4",
|
||||
confidence_threshold: float = 0.35,
|
||||
iou_threshold: float = 0.5,
|
||||
heatmap_alpha: float = 0.5,
|
||||
radius: int = 25,
|
||||
track_activation_threshold: float = 0.35,
|
||||
track_seconds: int = 5,
|
||||
minimum_matching_threshold: float = 0.99,
|
||||
) -> None:
|
||||
"""
|
||||
Heatmap and Tracking with Supervision.
|
||||
|
||||
Args:
|
||||
source_weights_path: Path to the source weights file
|
||||
source_video_path: Path to the source video file
|
||||
target_video_path: Path to the target video file
|
||||
confidence_threshold: Confidence threshold for the model
|
||||
iou_threshold: IOU threshold for the model
|
||||
heatmap_alpha: Opacity of the overlay mask, between 0 and 1
|
||||
radius: Radius of the heat circle
|
||||
track_activation_threshold: Detection confidence threshold for track activation
|
||||
track_seconds: Number of seconds to buffer when a track is lost
|
||||
minimum_matching_threshold: Threshold for matching tracks with detections
|
||||
"""
|
||||
### instantiate model
|
||||
model = YOLO(source_weights_path)
|
||||
source_video_path = source_video_path or download_video()
|
||||
|
||||
### heatmap config
|
||||
heat_map_annotator = sv.HeatMapAnnotator(
|
||||
position=sv.Position.BOTTOM_CENTER,
|
||||
opacity=heatmap_alpha,
|
||||
radius=radius,
|
||||
kernel_size=25,
|
||||
top_hue=0,
|
||||
low_hue=125,
|
||||
)
|
||||
|
||||
### annotation config
|
||||
label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER)
|
||||
|
||||
### get the video fps
|
||||
cap = cv2.VideoCapture(source_video_path)
|
||||
fps = int(cap.get(cv2.CAP_PROP_FPS))
|
||||
cap.release()
|
||||
|
||||
### tracker config
|
||||
byte_tracker = sv.ByteTrack(
|
||||
track_activation_threshold=track_activation_threshold,
|
||||
lost_track_buffer=track_seconds * fps,
|
||||
minimum_matching_threshold=minimum_matching_threshold,
|
||||
frame_rate=fps,
|
||||
)
|
||||
|
||||
### video config
|
||||
video_info = sv.VideoInfo.from_video_path(video_path=source_video_path)
|
||||
frames_generator = sv.get_video_frames_generator(
|
||||
source_path=source_video_path, stride=1
|
||||
)
|
||||
|
||||
### Detect, track, annotate, save
|
||||
with sv.VideoSink(target_path=target_video_path, video_info=video_info) as sink:
|
||||
for frame in frames_generator:
|
||||
result = model(
|
||||
source=frame,
|
||||
classes=[0], # only person class
|
||||
conf=confidence_threshold,
|
||||
iou=iou_threshold,
|
||||
# show_conf = True,
|
||||
# save_txt = True,
|
||||
# save_conf = True,
|
||||
# save = True,
|
||||
device=None, # use None = CPU, 0 = single GPU, or [0,1] = dual GPU
|
||||
)[0]
|
||||
|
||||
detections = sv.Detections.from_ultralytics(result) # get detections
|
||||
|
||||
detections = byte_tracker.update_with_detections(
|
||||
detections
|
||||
) # update tracker
|
||||
|
||||
### draw heatmap
|
||||
annotated_frame = heat_map_annotator.annotate(
|
||||
scene=frame.copy(), detections=detections
|
||||
)
|
||||
|
||||
### draw other attributes from `detections` object
|
||||
labels = [
|
||||
f"#{tracker_id}"
|
||||
for class_id, tracker_id in zip(
|
||||
detections.class_id, detections.tracker_id
|
||||
)
|
||||
]
|
||||
|
||||
label_annotator.annotate(
|
||||
scene=annotated_frame, detections=detections, labels=labels
|
||||
)
|
||||
|
||||
sink.write_frame(frame=annotated_frame)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from jsonargparse import auto_cli, set_parsing_settings
|
||||
|
||||
set_parsing_settings(parse_optionals_as_positionals=True)
|
||||
auto_cli(main, as_positional=False)
|
||||
|
|
|
|||
|
|
@ -14,29 +14,29 @@ https://github.com/roboflow/supervision/assets/26109316/d50118c1-2ae4-458d-915a-
|
|||
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/speed_estimation
|
||||
```
|
||||
```bash
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/speed_estimation
|
||||
```
|
||||
|
||||
- setup python environment and activate it [optional]
|
||||
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
- install required dependencies
|
||||
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
|
||||
- download `vehicles.mp4` file
|
||||
|
||||
```bash
|
||||
python video_downloader.py
|
||||
```
|
||||
```bash
|
||||
python video_downloader.py
|
||||
```
|
||||
|
||||
## 🛠️ script arguments
|
||||
|
||||
|
|
@ -58,34 +58,34 @@ https://github.com/roboflow/supervision/assets/26109316/d50118c1-2ae4-458d-915a-
|
|||
|
||||
- yolo-nas
|
||||
|
||||
```bash
|
||||
python yolo_nas_example.py \
|
||||
--source_video_path data/vehicles.mp4 \
|
||||
--target_video_path data/vehicles-result.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
```bash
|
||||
python yolo_nas_example.py \
|
||||
--source_video_path data/vehicles.mp4 \
|
||||
--target_video_path data/vehicles-result.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
|
||||
- inference
|
||||
|
||||
```bash
|
||||
python inference_example.py \
|
||||
--roboflow_api_key "ROBOFLOW_API_KEY" \
|
||||
--source_video_path data/vehicles.mp4 \
|
||||
--target_video_path data/vehicles-result.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
```bash
|
||||
python inference_example.py \
|
||||
--roboflow_api_key "ROBOFLOW_API_KEY" \
|
||||
--source_video_path data/vehicles.mp4 \
|
||||
--target_video_path data/vehicles-result.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
|
||||
- ultralytics
|
||||
|
||||
```bash
|
||||
python ultralytics_example.py \
|
||||
--source_video_path data/vehicles.mp4 \
|
||||
--target_video_path data/vehicles-result.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
```bash
|
||||
python ultralytics_example.py \
|
||||
--source_video_path data/vehicles.mp4 \
|
||||
--target_video_path data/vehicles-result.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
|
||||
## © license
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ def main(
|
|||
roboflow_api_key: str | None = None,
|
||||
confidence_threshold: float = 0.3,
|
||||
iou_threshold: float = 0.7,
|
||||
):
|
||||
) -> None:
|
||||
"""
|
||||
Vehicle Speed Estimation using Inference and Supervision.
|
||||
|
||||
|
|
@ -96,6 +96,7 @@ def main(
|
|||
coordinates = defaultdict(lambda: deque(maxlen=int(video_info.fps)))
|
||||
|
||||
with sv.VideoSink(target_video_path, video_info) as sink:
|
||||
window = sv.ImageWindow("frame")
|
||||
for frame in frame_generator:
|
||||
results = model.infer(
|
||||
frame, confidence=confidence_threshold, iou=iou_threshold
|
||||
|
|
@ -109,7 +110,7 @@ def main(
|
|||
)
|
||||
points = view_transformer.transform_points(points=points).astype(int)
|
||||
|
||||
for tracker_id, [_, y] in zip(detections.tracker_id, points):
|
||||
for tracker_id, [_, y] in zip(detections.tracker_id, points, strict=True):
|
||||
coordinates[tracker_id].append(y)
|
||||
|
||||
labels = []
|
||||
|
|
@ -136,10 +137,11 @@ def main(
|
|||
)
|
||||
|
||||
sink.write_frame(annotated_frame)
|
||||
cv2.imshow("frame", annotated_frame)
|
||||
if cv2.waitKey(1) & 0xFF == ord("q"):
|
||||
window.show(annotated_frame)
|
||||
key = window.wait_key(1)
|
||||
if not window.is_open or key == "q":
|
||||
break
|
||||
cv2.destroyAllWindows()
|
||||
window.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ def main(
|
|||
target_video_path: str,
|
||||
confidence_threshold: float = 0.3,
|
||||
iou_threshold: float = 0.7,
|
||||
):
|
||||
) -> None:
|
||||
"""
|
||||
Vehicle Speed Estimation using Ultralytics and Supervision.
|
||||
|
||||
|
|
@ -82,6 +82,7 @@ def main(
|
|||
coordinates = defaultdict(lambda: deque(maxlen=int(video_info.fps)))
|
||||
|
||||
with sv.VideoSink(target_video_path, video_info) as sink:
|
||||
window = sv.ImageWindow("frame")
|
||||
for frame in frame_generator:
|
||||
result = model(frame, conf=confidence_threshold, iou=iou_threshold)[0]
|
||||
detections = sv.Detections.from_ultralytics(result)
|
||||
|
|
@ -93,7 +94,7 @@ def main(
|
|||
)
|
||||
points = view_transformer.transform_points(points=points).astype(int)
|
||||
|
||||
for tracker_id, [_, y] in zip(detections.tracker_id, points):
|
||||
for tracker_id, [_, y] in zip(detections.tracker_id, points, strict=True):
|
||||
coordinates[tracker_id].append(y)
|
||||
|
||||
labels = []
|
||||
|
|
@ -120,10 +121,11 @@ def main(
|
|||
)
|
||||
|
||||
sink.write_frame(annotated_frame)
|
||||
cv2.imshow("frame", annotated_frame)
|
||||
if cv2.waitKey(1) & 0xFF == ord("q"):
|
||||
window.show(annotated_frame)
|
||||
key = window.wait_key(1)
|
||||
if not window.is_open or key == "q":
|
||||
break
|
||||
cv2.destroyAllWindows()
|
||||
window.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ def main(
|
|||
target_video_path: str,
|
||||
confidence_threshold: float = 0.3,
|
||||
iou_threshold: float = 0.7,
|
||||
):
|
||||
) -> None:
|
||||
"""
|
||||
Vehicle Speed Estimation using YOLO-NAS and Supervision.
|
||||
|
||||
|
|
@ -83,6 +83,7 @@ def main(
|
|||
coordinates = defaultdict(lambda: deque(maxlen=int(video_info.fps)))
|
||||
|
||||
with sv.VideoSink(target_video_path, video_info) as sink:
|
||||
window = sv.ImageWindow("frame")
|
||||
for frame in frame_generator:
|
||||
result = model.predict(frame, conf=confidence_threshold, iou=iou_threshold)[
|
||||
0
|
||||
|
|
@ -123,10 +124,11 @@ def main(
|
|||
)
|
||||
|
||||
sink.write_frame(annotated_frame)
|
||||
cv2.imshow("frame", annotated_frame)
|
||||
if cv2.waitKey(1) & 0xFF == ord("q"):
|
||||
window.show(annotated_frame)
|
||||
key = window.wait_key(1)
|
||||
if not window.is_open or key == "q":
|
||||
break
|
||||
cv2.destroyAllWindows()
|
||||
window.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -12,23 +12,25 @@ https://github.com/roboflow/supervision/assets/26109316/d051cc8a-dd15-41d4-aa36-
|
|||
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/time_in_zone
|
||||
```
|
||||
```bash
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/time_in_zone
|
||||
```
|
||||
|
||||
- setup python environment and activate it [optional]
|
||||
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
- install required dependencies
|
||||
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
|
||||
The three RTSP `*_stream_example.py` scripts display frames from an `InferencePipeline` callback running on a worker thread, so they use OpenCV HighGUI instead of `sv.ImageWindow`. Install `opencv-python` and keep only one OpenCV wheel installed to run those scripts. The file and naive-stream examples use `sv.ImageWindow`, which works regardless of which OpenCV wheel (or none) is installed.
|
||||
|
||||
## 🛠 scripts
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import cv2
|
||||
import numpy as np
|
||||
from inference import get_model
|
||||
from utils.general import find_in_list, load_zones_config
|
||||
|
|
@ -49,6 +48,7 @@ def main(
|
|||
]
|
||||
timers = [FPSBasedTimer(video_info.fps) for _ in zones]
|
||||
|
||||
window = sv.ImageWindow("Processed Video")
|
||||
for frame in frames_generator:
|
||||
results = model.infer(
|
||||
frame, confidence=confidence_threshold, iou_threshold=iou_threshold
|
||||
|
|
@ -84,10 +84,11 @@ def main(
|
|||
custom_color_lookup=custom_color_lookup,
|
||||
)
|
||||
|
||||
cv2.imshow("Processed Video", annotated_frame)
|
||||
if cv2.waitKey(1) & 0xFF == ord("q"):
|
||||
window.show(annotated_frame)
|
||||
key = window.wait_key(1)
|
||||
if not window.is_open or key == "q":
|
||||
break
|
||||
cv2.destroyAllWindows()
|
||||
window.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import cv2
|
||||
import numpy as np
|
||||
from inference import get_model
|
||||
from utils.general import find_in_list, get_stream_frames_generator, load_zones_config
|
||||
|
|
@ -49,6 +48,7 @@ def main(
|
|||
]
|
||||
timers = [ClockBasedTimer() for _ in zones]
|
||||
|
||||
window = sv.ImageWindow("Processed Video")
|
||||
for frame in frames_generator:
|
||||
fps_monitor.tick()
|
||||
fps = fps_monitor.fps
|
||||
|
|
@ -94,10 +94,11 @@ def main(
|
|||
custom_color_lookup=custom_color_lookup,
|
||||
)
|
||||
|
||||
cv2.imshow("Processed Video", annotated_frame)
|
||||
if cv2.waitKey(1) & 0xFF == ord("q"):
|
||||
window.show(annotated_frame)
|
||||
key = window.wait_key(1)
|
||||
if not window.is_open or key == "q":
|
||||
break
|
||||
cv2.destroyAllWindows()
|
||||
window.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ LABEL_ANNOTATOR = sv.LabelAnnotator(
|
|||
|
||||
|
||||
class CustomSink:
|
||||
def __init__(self, zone_configuration_path: str, classes: list[int]):
|
||||
def __init__(self, zone_configuration_path: str, classes: list[int]) -> None:
|
||||
self.classes = classes
|
||||
self.tracker = sv.ByteTrack(minimum_matching_threshold=0.5)
|
||||
self.fps_monitor = sv.FPSMonitor()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ from __future__ import annotations
|
|||
|
||||
from enum import Enum
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from rfdetr import RFDETRBase, RFDETRLarge, RFDETRMedium, RFDETRNano, RFDETRSmall
|
||||
from utils.general import find_in_list, load_zones_config
|
||||
|
|
@ -25,7 +24,7 @@ class ModelSize(Enum):
|
|||
LARGE = "large"
|
||||
|
||||
@classmethod
|
||||
def list(cls):
|
||||
def list(cls) -> list[str]:
|
||||
return list(map(lambda c: c.value, cls))
|
||||
|
||||
@classmethod
|
||||
|
|
@ -44,7 +43,9 @@ class ModelSize(Enum):
|
|||
)
|
||||
|
||||
|
||||
def load_model(checkpoint: ModelSize | str, device: str, resolution: int):
|
||||
def load_model(
|
||||
checkpoint: ModelSize | str, device: str, resolution: int
|
||||
) -> RFDETRBase | RFDETRLarge | RFDETRMedium | RFDETRNano | RFDETRSmall:
|
||||
checkpoint = ModelSize.from_value(checkpoint)
|
||||
|
||||
if checkpoint == ModelSize.NANO:
|
||||
|
|
@ -126,6 +127,7 @@ def main(
|
|||
]
|
||||
timers = [FPSBasedTimer(video_info.fps) for _ in zones]
|
||||
|
||||
window = sv.ImageWindow("Processed Video")
|
||||
for frame in frames_generator:
|
||||
detections = model.predict(frame, threshold=confidence_threshold)
|
||||
detections = detections[find_in_list(detections.class_id, classes)]
|
||||
|
|
@ -159,10 +161,11 @@ def main(
|
|||
custom_color_lookup=custom_color_lookup,
|
||||
)
|
||||
|
||||
cv2.imshow("Processed Video", annotated_frame)
|
||||
if cv2.waitKey(1) & 0xFF == ord("q"):
|
||||
window.show(annotated_frame)
|
||||
key = window.wait_key(1)
|
||||
if not window.is_open or key == "q":
|
||||
break
|
||||
cv2.destroyAllWindows()
|
||||
window.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ from __future__ import annotations
|
|||
|
||||
from enum import Enum
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from rfdetr import RFDETRBase, RFDETRLarge, RFDETRMedium, RFDETRNano, RFDETRSmall
|
||||
from utils.general import find_in_list, get_stream_frames_generator, load_zones_config
|
||||
|
|
@ -25,7 +24,7 @@ class ModelSize(Enum):
|
|||
LARGE = "large"
|
||||
|
||||
@classmethod
|
||||
def list(cls):
|
||||
def list(cls) -> list[str]:
|
||||
return list(map(lambda c: c.value, cls))
|
||||
|
||||
@classmethod
|
||||
|
|
@ -44,7 +43,9 @@ class ModelSize(Enum):
|
|||
)
|
||||
|
||||
|
||||
def load_model(checkpoint: ModelSize | str, device: str, resolution: int):
|
||||
def load_model(
|
||||
checkpoint: ModelSize | str, device: str, resolution: int
|
||||
) -> RFDETRBase | RFDETRLarge | RFDETRMedium | RFDETRNano | RFDETRSmall:
|
||||
checkpoint = ModelSize.from_value(checkpoint)
|
||||
|
||||
if checkpoint == ModelSize.NANO:
|
||||
|
|
@ -126,6 +127,7 @@ def main(
|
|||
]
|
||||
timers = [ClockBasedTimer() for _ in zones]
|
||||
|
||||
window = sv.ImageWindow("Processed Video")
|
||||
for frame in frames_generator:
|
||||
fps_monitor.tick()
|
||||
fps = fps_monitor.fps
|
||||
|
|
@ -169,11 +171,12 @@ def main(
|
|||
custom_color_lookup=custom_color_lookup,
|
||||
)
|
||||
|
||||
cv2.imshow("Processed Video", annotated_frame)
|
||||
if cv2.waitKey(1) & 0xFF == ord("q"):
|
||||
window.show(annotated_frame)
|
||||
key = window.wait_key(1)
|
||||
if not window.is_open or key == "q":
|
||||
break
|
||||
|
||||
cv2.destroyAllWindows()
|
||||
window.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class ModelSize(Enum):
|
|||
LARGE = "large"
|
||||
|
||||
@classmethod
|
||||
def list(cls):
|
||||
def list(cls) -> list[str]:
|
||||
return [c.value for c in cls]
|
||||
|
||||
@classmethod
|
||||
|
|
@ -41,7 +41,9 @@ class ModelSize(Enum):
|
|||
)
|
||||
|
||||
|
||||
def load_model(checkpoint: ModelSize | str, device: str, resolution: int):
|
||||
def load_model(
|
||||
checkpoint: ModelSize | str, device: str, resolution: int
|
||||
) -> RFDETRBase | RFDETRLarge | RFDETRMedium | RFDETRNano | RFDETRSmall:
|
||||
checkpoint = ModelSize.from_value(checkpoint)
|
||||
if checkpoint == ModelSize.NANO:
|
||||
return RFDETRNano(device=device, resolution=resolution)
|
||||
|
|
@ -77,7 +79,7 @@ LABEL_ANNOTATOR = sv.LabelAnnotator(
|
|||
|
||||
|
||||
class CustomSink:
|
||||
def __init__(self, zone_configuration_path: str, classes: list[int]):
|
||||
def __init__(self, zone_configuration_path: str, classes: list[int]) -> None:
|
||||
self.classes = classes
|
||||
self.tracker = sv.ByteTrack(minimum_matching_threshold=0.8)
|
||||
self.fps_monitor = sv.FPSMonitor()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
|
@ -10,11 +7,10 @@ from jsonargparse import auto_cli
|
|||
|
||||
import supervision as sv
|
||||
|
||||
KEY_ENTER = 13
|
||||
KEY_NEWLINE = 10
|
||||
KEY_ESCAPE = 27
|
||||
KEY_QUIT = ord("q")
|
||||
KEY_SAVE = ord("s")
|
||||
KEY_ENTER = {"Return", "KP_Enter"}
|
||||
KEY_ESCAPE = "Escape"
|
||||
KEY_QUIT = "q"
|
||||
KEY_SAVE = "s"
|
||||
|
||||
THICKNESS = 2
|
||||
COLORS = sv.ColorPalette.DEFAULT
|
||||
|
|
@ -37,15 +33,17 @@ def resolve_source(source_path: str) -> np.ndarray | None:
|
|||
return frame
|
||||
|
||||
|
||||
def mouse_event(event: int, x: int, y: int, flags: int, param: Any) -> None:
|
||||
def mouse_event(x: int, y: int, event_type: str) -> None:
|
||||
global current_mouse_position
|
||||
if event == cv2.EVENT_MOUSEMOVE:
|
||||
if event_type == "move":
|
||||
current_mouse_position = (x, y)
|
||||
elif event == cv2.EVENT_LBUTTONDOWN:
|
||||
elif event_type == "down":
|
||||
POLYGONS[-1].append((x, y))
|
||||
|
||||
|
||||
def redraw(image: np.ndarray, original_image: np.ndarray) -> None:
|
||||
def redraw(
|
||||
image: np.ndarray, original_image: np.ndarray, window: sv.ImageWindow
|
||||
) -> None:
|
||||
global POLYGONS, current_mouse_position
|
||||
image[:] = original_image.copy()
|
||||
for idx, polygon in enumerate(POLYGONS):
|
||||
|
|
@ -80,10 +78,12 @@ def redraw(image: np.ndarray, original_image: np.ndarray) -> None:
|
|||
color=color,
|
||||
thickness=THICKNESS,
|
||||
)
|
||||
cv2.imshow(WINDOW_NAME, image)
|
||||
window.show(image)
|
||||
|
||||
|
||||
def close_and_finalize_polygon(image: np.ndarray, original_image: np.ndarray) -> None:
|
||||
def close_and_finalize_polygon(
|
||||
image: np.ndarray, original_image: np.ndarray, window: sv.ImageWindow
|
||||
) -> None:
|
||||
if len(POLYGONS[-1]) > 2:
|
||||
cv2.line(
|
||||
img=image,
|
||||
|
|
@ -95,7 +95,7 @@ def close_and_finalize_polygon(image: np.ndarray, original_image: np.ndarray) ->
|
|||
POLYGONS.append([])
|
||||
image[:] = original_image.copy()
|
||||
redraw_polygons(image)
|
||||
cv2.imshow(WINDOW_NAME, image)
|
||||
window.show(image)
|
||||
|
||||
|
||||
def redraw_polygons(image: np.ndarray) -> None:
|
||||
|
|
@ -119,7 +119,9 @@ def redraw_polygons(image: np.ndarray) -> None:
|
|||
)
|
||||
|
||||
|
||||
def save_polygons_to_json(polygons, target_path):
|
||||
def save_polygons_to_json(
|
||||
polygons: list[list[tuple[int, int]]], target_path: str | os.PathLike[str]
|
||||
) -> None:
|
||||
data_to_save = polygons if polygons[-1] else polygons[:-1]
|
||||
with open(target_path, "w") as f:
|
||||
json.dump(data_to_save, f)
|
||||
|
|
@ -140,13 +142,16 @@ def main(source_path: str, zone_configuration_path: str) -> None:
|
|||
return
|
||||
|
||||
image = original_image.copy()
|
||||
cv2.imshow(WINDOW_NAME, image)
|
||||
cv2.setMouseCallback(WINDOW_NAME, mouse_event, image)
|
||||
window = sv.ImageWindow(WINDOW_NAME)
|
||||
window.set_mouse_callback(mouse_event)
|
||||
window.show(image)
|
||||
|
||||
while True:
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key == KEY_ENTER or key == KEY_NEWLINE:
|
||||
close_and_finalize_polygon(image, original_image)
|
||||
key = window.wait_key(1)
|
||||
if not window.is_open:
|
||||
break
|
||||
if key in KEY_ENTER:
|
||||
close_and_finalize_polygon(image, original_image, window)
|
||||
elif key == KEY_ESCAPE:
|
||||
POLYGONS[-1] = []
|
||||
current_mouse_position = None
|
||||
|
|
@ -154,11 +159,11 @@ def main(source_path: str, zone_configuration_path: str) -> None:
|
|||
save_polygons_to_json(POLYGONS, zone_configuration_path)
|
||||
print(f"Polygons saved to {zone_configuration_path}")
|
||||
break
|
||||
redraw(image, original_image)
|
||||
redraw(image, original_image, window)
|
||||
if key == KEY_QUIT:
|
||||
break
|
||||
|
||||
cv2.destroyAllWindows()
|
||||
window.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import cv2
|
||||
import numpy as np
|
||||
from ultralytics import YOLO
|
||||
from utils.general import find_in_list, load_zones_config
|
||||
|
|
@ -49,6 +48,7 @@ def main(
|
|||
]
|
||||
timers = [FPSBasedTimer(video_info.fps) for _ in zones]
|
||||
|
||||
window = sv.ImageWindow("Processed Video")
|
||||
for frame in frames_generator:
|
||||
results = model(
|
||||
frame,
|
||||
|
|
@ -88,10 +88,11 @@ def main(
|
|||
custom_color_lookup=custom_color_lookup,
|
||||
)
|
||||
|
||||
cv2.imshow("Processed Video", annotated_frame)
|
||||
if cv2.waitKey(1) & 0xFF == ord("q"):
|
||||
window.show(annotated_frame)
|
||||
key = window.wait_key(1)
|
||||
if not window.is_open or key == "q":
|
||||
break
|
||||
cv2.destroyAllWindows()
|
||||
window.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import cv2
|
||||
import numpy as np
|
||||
from ultralytics import YOLO
|
||||
from utils.general import find_in_list, get_stream_frames_generator, load_zones_config
|
||||
|
|
@ -49,6 +48,7 @@ def main(
|
|||
]
|
||||
timers = [ClockBasedTimer() for _ in zones]
|
||||
|
||||
window = sv.ImageWindow("Processed Video")
|
||||
for frame in frames_generator:
|
||||
fps_monitor.tick()
|
||||
fps = fps_monitor.fps
|
||||
|
|
@ -98,10 +98,11 @@ def main(
|
|||
custom_color_lookup=custom_color_lookup,
|
||||
)
|
||||
|
||||
cv2.imshow("Processed Video", annotated_frame)
|
||||
if cv2.waitKey(1) & 0xFF == ord("q"):
|
||||
window.show(annotated_frame)
|
||||
key = window.wait_key(1)
|
||||
if not window.is_open or key == "q":
|
||||
break
|
||||
cv2.destroyAllWindows()
|
||||
window.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from inference import InferencePipeline
|
||||
|
|
@ -18,7 +16,7 @@ LABEL_ANNOTATOR = sv.LabelAnnotator(
|
|||
|
||||
|
||||
class CustomSink:
|
||||
def __init__(self, zone_configuration_path: str, classes: list[int]):
|
||||
def __init__(self, zone_configuration_path: str, classes: list[int]) -> None:
|
||||
self.classes = classes
|
||||
self.tracker = sv.ByteTrack(minimum_matching_threshold=0.8)
|
||||
self.fps_monitor = sv.FPSMonitor()
|
||||
|
|
|
|||
|
|
@ -8,71 +8,71 @@ This script provides functionality for processing videos using YOLOv8 for object
|
|||
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/tracking
|
||||
```
|
||||
```bash
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/tracking
|
||||
```
|
||||
|
||||
- setup python environment and activate it [optional]
|
||||
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
- install required dependencies
|
||||
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 🛠️ script arguments
|
||||
|
||||
- ultralytics
|
||||
|
||||
- `--source_weights_path`: Required. Specifies the path to the YOLO model's weights file, which is essential for the object detection process. This file contains the data that the model uses to identify objects in the video.
|
||||
- `--source_weights_path`: Required. Specifies the path to the YOLO model's weights file, which is essential for the object detection process. This file contains the data that the model uses to identify objects in the video.
|
||||
|
||||
- `--source_video_path`: Required. The path to the source video file to be processed. This is the video on which object detection and annotation will be performed.
|
||||
- `--source_video_path`: Required. The path to the source video file to be processed. This is the video on which object detection and annotation will be performed.
|
||||
|
||||
- `--target_video_path`: Required. The path where the processed video, with annotations added, will be saved. This is your output video file.
|
||||
- `--target_video_path`: Required. The path where the processed video, with annotations added, will be saved. This is your output video file.
|
||||
|
||||
- `--confidence_threshold` (optional): Sets the confidence level at which the model identifies objects in the video. Default is `0.3`. A higher threshold makes the model more selective, while a lower threshold makes it more inclusive in identifying objects.
|
||||
- `--confidence_threshold` (optional): Sets the confidence level at which the model identifies objects in the video. Default is `0.3`. A higher threshold makes the model more selective, while a lower threshold makes it more inclusive in identifying objects.
|
||||
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold for the model, defaulting to `0.7`. This parameter helps in differentiating between distinct objects, especially in crowded scenes.
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold for the model, defaulting to `0.7`. This parameter helps in differentiating between distinct objects, especially in crowded scenes.
|
||||
|
||||
- inference
|
||||
|
||||
- `--roboflow_api_key` (optional): The API key for Roboflow services. If not provided directly, the script tries to fetch it from the `ROBOFLOW_API_KEY` environment variable. Follow [this guide](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key) to acquire your `API KEY`.
|
||||
- `--roboflow_api_key` (optional): The API key for Roboflow services. If not provided directly, the script tries to fetch it from the `ROBOFLOW_API_KEY` environment variable. Follow [this guide](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key) to acquire your `API KEY`.
|
||||
|
||||
- `--model_id` (optional): Designates the Roboflow model ID to be used. The default value is `"yolov8x-1280"`.
|
||||
- `--model_id` (optional): Designates the Roboflow model ID to be used. The default value is `"yolov8x-1280"`.
|
||||
|
||||
- `--source_video_path`: Required. The path to the source video file to be processed. This is the video on which object detection and annotation will be performed.
|
||||
- `--source_video_path`: Required. The path to the source video file to be processed. This is the video on which object detection and annotation will be performed.
|
||||
|
||||
- `--target_video_path`: Required. The path where the processed video, with annotations added, will be saved. This is your output video file.
|
||||
- `--target_video_path`: Required. The path where the processed video, with annotations added, will be saved. This is your output video file.
|
||||
|
||||
- `--confidence_threshold` (optional): Sets the confidence level at which the model identifies objects in the video. Default is `0.3`. A higher threshold makes the model more selective, while a lower threshold makes it more inclusive in identifying objects.
|
||||
- `--confidence_threshold` (optional): Sets the confidence level at which the model identifies objects in the video. Default is `0.3`. A higher threshold makes the model more selective, while a lower threshold makes it more inclusive in identifying objects.
|
||||
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold for the model, defaulting to `0.7`. This parameter helps in differentiating between distinct objects, especially in crowded scenes.
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold for the model, defaulting to `0.7`. This parameter helps in differentiating between distinct objects, especially in crowded scenes.
|
||||
|
||||
## ⚙️ run
|
||||
|
||||
- inference
|
||||
|
||||
```bash
|
||||
python inference_example.py \
|
||||
--roboflow_api_key "ROBOFLOW_API_KEY" \
|
||||
--source_video_path input.mp4 \
|
||||
--target_video_path tracking_result.mp4
|
||||
```
|
||||
```bash
|
||||
python inference_example.py \
|
||||
--roboflow_api_key "ROBOFLOW_API_KEY" \
|
||||
--source_video_path input.mp4 \
|
||||
--target_video_path tracking_result.mp4
|
||||
```
|
||||
|
||||
- ultralytics
|
||||
|
||||
```bash
|
||||
python ultralytics_example.py \
|
||||
--source_weights_path yolov8s.pt \
|
||||
--source_video_path input.mp4 \
|
||||
--target_video_path tracking_result.mp4
|
||||
```
|
||||
```bash
|
||||
python ultralytics_example.py \
|
||||
--source_weights_path yolov8s.pt \
|
||||
--source_video_path input.mp4 \
|
||||
--target_video_path tracking_result.mp4
|
||||
```
|
||||
|
||||
## © license
|
||||
|
||||
|
|
|
|||
|
|
@ -10,81 +10,81 @@ https://github.com/roboflow/supervision/assets/26109316/c9436828-9fbf-4c25-ae8c-
|
|||
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/traffic_analysis
|
||||
```
|
||||
```bash
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/traffic_analysis
|
||||
```
|
||||
|
||||
- setup python environment and activate it [optional]
|
||||
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
- install required dependencies
|
||||
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
```bash
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
|
||||
- download `traffic_analysis.pt` and `traffic_analysis.mov` files
|
||||
|
||||
```bash
|
||||
./setup.sh
|
||||
```
|
||||
```bash
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
## 🛠️ script arguments
|
||||
|
||||
- ultralytics
|
||||
|
||||
- `--source_weights_path`: Required. Specifies the path to the YOLO model's weights file, which is essential for the object detection process. This file contains the data that the model uses to identify objects in the video.
|
||||
- `--source_weights_path`: Required. Specifies the path to the YOLO model's weights file, which is essential for the object detection process. This file contains the data that the model uses to identify objects in the video.
|
||||
|
||||
- `--source_video_path`: Required. The path to the source video file that will be analyzed. This is the input video on which traffic flow analysis will be performed.
|
||||
- `--source_video_path`: Required. The path to the source video file that will be analyzed. This is the input video on which traffic flow analysis will be performed.
|
||||
|
||||
- `--target_video_path` (optional): The path to save the output video with annotations. If not specified, the processed video will be displayed in real-time without being saved.
|
||||
- `--target_video_path` (optional): The path to save the output video with annotations. If not specified, the processed video will be displayed in real-time without being saved.
|
||||
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO model to filter detections. Default is `0.3`. This determines how confident the model should be to recognize an object in the video.
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO model to filter detections. Default is `0.3`. This determines how confident the model should be to recognize an object in the video.
|
||||
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold for the model. Default is 0.7. This value is used to manage object detection accuracy, particularly in distinguishing between different objects.
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold for the model. Default is 0.7. This value is used to manage object detection accuracy, particularly in distinguishing between different objects.
|
||||
|
||||
- inference
|
||||
|
||||
- `--roboflow_api_key` (optional): The API key for Roboflow services. If not provided directly, the script tries to fetch it from the `ROBOFLOW_API_KEY` environment variable. Follow [this guide](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key) to acquire your `API KEY`.
|
||||
- `--roboflow_api_key` (optional): The API key for Roboflow services. If not provided directly, the script tries to fetch it from the `ROBOFLOW_API_KEY` environment variable. Follow [this guide](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key) to acquire your `API KEY`.
|
||||
|
||||
- `--model_id` (optional): Designates the Roboflow model ID to be used. The default value is `"vehicle-count-in-drone-video/6"`.
|
||||
- `--model_id` (optional): Designates the Roboflow model ID to be used. The default value is `"vehicle-count-in-drone-video/6"`.
|
||||
|
||||
- `--source_video_path`: Required. The path to the source video file that will be analyzed. This is the input video on which traffic flow analysis will be performed.
|
||||
- `--source_video_path`: Required. The path to the source video file that will be analyzed. This is the input video on which traffic flow analysis will be performed.
|
||||
|
||||
- `--target_video_path` (optional): The path to save the output video with annotations. If not specified, the processed video will be displayed in real-time without being saved.
|
||||
- `--target_video_path` (optional): The path to save the output video with annotations. If not specified, the processed video will be displayed in real-time without being saved.
|
||||
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO model to filter detections. Default is `0.3`. This determines how confident the model should be to recognize an object in the video.
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO model to filter detections. Default is `0.3`. This determines how confident the model should be to recognize an object in the video.
|
||||
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold for the model. Default is 0.7. This value is used to manage object detection accuracy, particularly in distinguishing between different objects.
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold for the model. Default is 0.7. This value is used to manage object detection accuracy, particularly in distinguishing between different objects.
|
||||
|
||||
## ⚙️ run
|
||||
|
||||
- ultralytics
|
||||
|
||||
```bash
|
||||
python ultralytics_example.py \
|
||||
--source_weights_path data/traffic_analysis.pt \
|
||||
--source_video_path data/traffic_analysis.mov \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5 \
|
||||
--target_video_path data/traffic_analysis_result.mov
|
||||
```
|
||||
```bash
|
||||
python ultralytics_example.py \
|
||||
--source_weights_path data/traffic_analysis.pt \
|
||||
--source_video_path data/traffic_analysis.mov \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5 \
|
||||
--target_video_path data/traffic_analysis_result.mov
|
||||
```
|
||||
|
||||
- inference
|
||||
|
||||
```bash
|
||||
python inference_example.py \
|
||||
--roboflow_api_key "ROBOFLOW_API_KEY" \
|
||||
--source_video_path data/traffic_analysis.mov \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5 \
|
||||
--target_video_path data/traffic_analysis_result.mov
|
||||
```
|
||||
```bash
|
||||
python inference_example.py \
|
||||
--roboflow_api_key "ROBOFLOW_API_KEY" \
|
||||
--source_video_path data/traffic_analysis.mov \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5 \
|
||||
--target_video_path data/traffic_analysis_result.mov
|
||||
```
|
||||
|
||||
## © license
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from inference.models.utils import get_roboflow_model
|
||||
from tqdm import tqdm
|
||||
|
|
@ -103,7 +100,7 @@ class VideoProcessor:
|
|||
)
|
||||
self.detections_manager = DetectionsManager()
|
||||
|
||||
def process_video(self):
|
||||
def process_video(self) -> None:
|
||||
frame_generator = sv.get_video_frames_generator(
|
||||
source_path=self.source_video_path
|
||||
)
|
||||
|
|
@ -114,12 +111,14 @@ class VideoProcessor:
|
|||
annotated_frame = self.process_frame(frame)
|
||||
sink.write_frame(annotated_frame)
|
||||
else:
|
||||
window = sv.ImageWindow("Processed Video")
|
||||
for frame in tqdm(frame_generator, total=self.video_info.total_frames):
|
||||
annotated_frame = self.process_frame(frame)
|
||||
cv2.imshow("Processed Video", annotated_frame)
|
||||
if cv2.waitKey(1) & 0xFF == ord("q"):
|
||||
window.show(annotated_frame)
|
||||
key = window.wait_key(1)
|
||||
if not window.is_open or key == "q":
|
||||
break
|
||||
cv2.destroyAllWindows()
|
||||
window.close()
|
||||
|
||||
def annotate_frame(
|
||||
self, frame: np.ndarray, detections: sv.Detections
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
from ultralytics import YOLO
|
||||
|
|
@ -100,7 +97,7 @@ class VideoProcessor:
|
|||
)
|
||||
self.detections_manager = DetectionsManager()
|
||||
|
||||
def process_video(self):
|
||||
def process_video(self) -> None:
|
||||
frame_generator = sv.get_video_frames_generator(
|
||||
source_path=self.source_video_path
|
||||
)
|
||||
|
|
@ -111,12 +108,14 @@ class VideoProcessor:
|
|||
annotated_frame = self.process_frame(frame)
|
||||
sink.write_frame(annotated_frame)
|
||||
else:
|
||||
window = sv.ImageWindow("Processed Video")
|
||||
for frame in tqdm(frame_generator, total=self.video_info.total_frames):
|
||||
annotated_frame = self.process_frame(frame)
|
||||
cv2.imshow("Processed Video", annotated_frame)
|
||||
if cv2.waitKey(1) & 0xFF == ord("q"):
|
||||
window.show(annotated_frame)
|
||||
key = window.wait_key(1)
|
||||
if not window.is_open or key == "q":
|
||||
break
|
||||
cv2.destroyAllWindows()
|
||||
window.close()
|
||||
|
||||
def annotate_frame(
|
||||
self, frame: np.ndarray, detections: sv.Detections
|
||||
|
|
|
|||
15
mkdocs.yml
15
mkdocs.yml
|
|
@ -20,7 +20,7 @@ extra:
|
|||
link: https://discord.gg/GbfgXGJ8Bk
|
||||
analytics:
|
||||
provider: google
|
||||
property: G-P7ZG0Y19G5
|
||||
property: G-SEKT4K1EWR
|
||||
version:
|
||||
provider: mike
|
||||
|
||||
|
|
@ -42,6 +42,8 @@ nav:
|
|||
- Process Datasets: how_to/process_datasets.md
|
||||
- Benchmark a Model: how_to/benchmark_a_model.md
|
||||
- Count in Zone: how_to/count_in_zone.md
|
||||
- Use Compact Masks: how_to/use_compact_masks.md
|
||||
- OpenCV Migration: how_to/opencv_migration.md
|
||||
- Reference:
|
||||
- Detection and Segmentation:
|
||||
- Core: detection/core.md
|
||||
|
|
@ -52,7 +54,7 @@ nav:
|
|||
- Boxes: detection/utils/boxes.md
|
||||
- Masks: detection/utils/masks.md
|
||||
- Polygons: detection/utils/polygons.md
|
||||
- VLMs: detection/utils/vlms.md
|
||||
- VLM Utils: detection/utils/vlms.md
|
||||
- Keypoint Detection:
|
||||
- Core: keypoint/core.md
|
||||
- Annotators: keypoint/annotators.md
|
||||
|
|
@ -76,8 +78,10 @@ nav:
|
|||
- Common Values: metrics/common_values.md
|
||||
- Legacy Metrics: detection/metrics.md
|
||||
- Utils:
|
||||
- Conversion: utils/conversion.md
|
||||
- Video: utils/video.md
|
||||
- Image: utils/image.md
|
||||
- Image Window: utils/image_window.md
|
||||
- Iterables: utils/iterables.md
|
||||
- Notebook: utils/notebook.md
|
||||
- File: utils/file.md
|
||||
|
|
@ -85,6 +89,9 @@ nav:
|
|||
- Geometry: utils/geometry.md
|
||||
- Assets: assets.md
|
||||
- Cookbooks: cookbooks.md
|
||||
- Contributing: contributing.md
|
||||
- Code of Conduct: code_of_conduct.md
|
||||
- License: license.md
|
||||
- Changelog:
|
||||
- Changelog: changelog.md
|
||||
- Deprecated: deprecated.md
|
||||
|
|
@ -132,10 +139,10 @@ plugins:
|
|||
default_handler: python
|
||||
handlers:
|
||||
python:
|
||||
paths: [supervision]
|
||||
load_external_modules: true
|
||||
options:
|
||||
parameter_headings: true
|
||||
paths: [supervision]
|
||||
load_external_modules: true
|
||||
allow_inspection: true
|
||||
show_bases: true
|
||||
group_by_category: true
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ requires = [ "setuptools>=61" ]
|
|||
|
||||
[project]
|
||||
name = "supervision"
|
||||
version = "0.29.0rc1"
|
||||
version = "0.31.0.dev0"
|
||||
description = "A set of easy-to-use utils that will come in handy in any Computer Vision project"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
|
|
@ -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",
|
||||
|
|
@ -48,17 +47,20 @@ classifiers = [
|
|||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"av>=14.2",
|
||||
"defusedxml>=0.7.1",
|
||||
"matplotlib>=3.6",
|
||||
"numpy>=1.21.2",
|
||||
"opencv-python>=4.5.5.64",
|
||||
"pillow>=9.4",
|
||||
"pydeprecate>=0.9,<0.10",
|
||||
"pydeprecate>=0.9,<0.12",
|
||||
"pyyaml>=5.3",
|
||||
"requests>=2.26",
|
||||
"scipy>=1.10",
|
||||
"tqdm>=4.62.3"
|
||||
]
|
||||
optional-dependencies.geotiff = [
|
||||
"rasterio>=1.3", # 1.3 introduced stable window-read API and CRS.is_projected
|
||||
]
|
||||
optional-dependencies.metrics = [
|
||||
"pandas>=2",
|
||||
]
|
||||
|
|
@ -74,34 +76,36 @@ dev = [
|
|||
"nbconvert>=7.14.2",
|
||||
"notebook>=6.5.3,<8",
|
||||
"pre-commit>=3.8",
|
||||
"pytest>=7.2.2,<9",
|
||||
"pytest>=7.2.2,<10",
|
||||
"pytest-cov>=4,<8",
|
||||
"scikit-learn>=1.7",
|
||||
"tox>=4.11.4",
|
||||
"types-tqdm",
|
||||
]
|
||||
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",
|
||||
"mkdocstrings>=0.25.2,<0.31",
|
||||
"mkdocstrings-python>=1.10.9,<2", # todo: breaking changes in 2.x
|
||||
"mkdocstrings>=1,<1.1",
|
||||
"mkdocstrings-python>=2,<3",
|
||||
]
|
||||
build = [
|
||||
"build>=0.10,<1.5",
|
||||
"build>=1,<1.6",
|
||||
"twine>=5.1.1,<7",
|
||||
"wheel>=0.40,<0.48",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
include-package-data = false
|
||||
package-data.supervision = [ "py.typed" ]
|
||||
packages.find.where = [ "src" ]
|
||||
packages.find.include = [ "supervision*" ]
|
||||
|
||||
include-package-data = false
|
||||
package-data.supervision = [ "py.typed" ]
|
||||
# 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.
|
||||
|
|
@ -163,6 +167,7 @@ lint.per-file-ignores."src/**" = [
|
|||
]
|
||||
lint.per-file-ignores."tests/**" = [
|
||||
"S101", # Use of `assert` detected
|
||||
"S603", # `subprocess` call: subprocess with hardcoded args in test utilities is safe
|
||||
]
|
||||
lint.unfixable = []
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
|
|
@ -178,37 +183,34 @@ lint.pydocstyle.convention = "google"
|
|||
lint.pylint.max-args = 20
|
||||
|
||||
[tool.codespell]
|
||||
ignore-words-list = "STrack,sTrack,strack"
|
||||
skip = "*.ipynb"
|
||||
count = true
|
||||
quiet-level = 3
|
||||
ignore-words-list = "STrack,sTrack,strack"
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.9"
|
||||
ignore_missing_imports = false
|
||||
explicit_package_bases = true
|
||||
strict = true
|
||||
mypy_path = "src"
|
||||
explicit_package_bases = true
|
||||
ignore_missing_imports = false
|
||||
python_version = "3.10"
|
||||
warn_unused_ignores = true
|
||||
strict = true
|
||||
overrides = [
|
||||
# exclude = [
|
||||
# "docs",
|
||||
# "test",
|
||||
# "examples",
|
||||
# "setup.py",
|
||||
# ]
|
||||
{ module = [
|
||||
"tests.*",
|
||||
"examples.*",
|
||||
], ignore_errors = true },
|
||||
{ module = [ "examples.*", "tests.*" ], ignore_errors = true },
|
||||
{ module = [ "supervision._cv2" ], warn_unused_ignores = false },
|
||||
]
|
||||
|
||||
[tool.pytest]
|
||||
ini_options.testpaths = [ "src", "tests" ]
|
||||
ini_options.norecursedirs = [ ".git", ".venv", "build", "dist", "docs", "examples", "notebooks" ]
|
||||
ini_options.addopts = [
|
||||
"--doctest-modules",
|
||||
"--color=yes",
|
||||
]
|
||||
ini_options.filterwarnings = [
|
||||
"error::DeprecationWarning",
|
||||
]
|
||||
ini_options.doctest_optionflags = "ELLIPSIS NORMALIZE_WHITESPACE"
|
||||
ini_options.norecursedirs = [ "examples", "docs", "notebooks", ".venv", ".git", "dist", "build" ]
|
||||
|
||||
[tool.autoflake]
|
||||
check = true
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import importlib.metadata as importlib_metadata
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
try:
|
||||
# This will read version from pyproject.toml
|
||||
|
|
@ -52,7 +53,10 @@ from supervision.detection.line_zone import (
|
|||
LineZoneAnnotatorMulticlass,
|
||||
)
|
||||
from supervision.detection.tools.csv_sink import CSVSink
|
||||
from supervision.detection.tools.inference_slicer import InferenceSlicer
|
||||
from supervision.detection.tools.inference_slicer import (
|
||||
InferenceSlicer,
|
||||
WindowedRasterDataset,
|
||||
)
|
||||
from supervision.detection.tools.json_sink import JSONSink
|
||||
from supervision.detection.tools.polygon_zone import PolygonZone, PolygonZoneAnnotator
|
||||
from supervision.detection.tools.smoother import DetectionsSmoother
|
||||
|
|
@ -87,9 +91,11 @@ from supervision.detection.utils.iou_and_nms import (
|
|||
box_iou_batch_with_jaccard,
|
||||
box_non_max_merge,
|
||||
box_non_max_suppression,
|
||||
box_soft_non_max_suppression,
|
||||
mask_iou_batch,
|
||||
mask_non_max_merge,
|
||||
mask_non_max_suppression,
|
||||
mask_soft_non_max_suppression,
|
||||
oriented_box_iou_batch,
|
||||
oriented_box_non_max_merge,
|
||||
oriented_box_non_max_suppression,
|
||||
|
|
@ -99,6 +105,7 @@ from supervision.detection.utils.masks import (
|
|||
contains_holes,
|
||||
contains_multiple_segments,
|
||||
filter_segments_by_distance,
|
||||
mask_to_roi,
|
||||
move_masks,
|
||||
)
|
||||
from supervision.detection.utils.polygons import (
|
||||
|
|
@ -132,7 +139,6 @@ from supervision.key_points.annotators import (
|
|||
)
|
||||
from supervision.key_points.core import KeyPoints
|
||||
from supervision.metrics.detection import ConfusionMatrix, MeanAveragePrecision
|
||||
from supervision.tracker.byte_tracker.core import ByteTrack
|
||||
from supervision.utils.conversion import cv2_to_pillow, pillow_to_cv2
|
||||
from supervision.utils.file import list_files_with_extensions
|
||||
from supervision.utils.image import (
|
||||
|
|
@ -141,11 +147,13 @@ from supervision.utils.image import (
|
|||
get_image_resolution_wh,
|
||||
grayscale_image,
|
||||
letterbox_image,
|
||||
load_image_from_url,
|
||||
overlay_image,
|
||||
resize_image,
|
||||
scale_image,
|
||||
tint_image,
|
||||
)
|
||||
from supervision.utils.image_window import ImageWindow
|
||||
from supervision.utils.notebook import plot_image, plot_images_grid
|
||||
from supervision.utils.video import (
|
||||
FPSMonitor,
|
||||
|
|
@ -155,8 +163,12 @@ from supervision.utils.video import (
|
|||
process_video,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from supervision.tracker.byte_tracker.core import ByteTrack
|
||||
|
||||
__all__ = [
|
||||
"LMM",
|
||||
"VLM",
|
||||
"BackgroundOverlayAnnotator",
|
||||
"BaseDataset",
|
||||
"BlurAnnotator",
|
||||
|
|
@ -186,6 +198,7 @@ __all__ = [
|
|||
"HeatMapAnnotator",
|
||||
"IconAnnotator",
|
||||
"ImageSink",
|
||||
"ImageWindow",
|
||||
"InferenceSlicer",
|
||||
"JSONSink",
|
||||
"KeyPoints",
|
||||
|
|
@ -218,12 +231,14 @@ __all__ = [
|
|||
"VertexLabelAnnotator",
|
||||
"VideoInfo",
|
||||
"VideoSink",
|
||||
"WindowedRasterDataset",
|
||||
"approximate_polygon",
|
||||
"box_iou",
|
||||
"box_iou_batch",
|
||||
"box_iou_batch_with_jaccard",
|
||||
"box_non_max_merge",
|
||||
"box_non_max_suppression",
|
||||
"box_soft_non_max_suppression",
|
||||
"calculate_masks_centroids",
|
||||
"calculate_optimal_line_thickness",
|
||||
"calculate_optimal_text_scale",
|
||||
|
|
@ -232,6 +247,7 @@ __all__ = [
|
|||
"contains_multiple_segments",
|
||||
"crop_image",
|
||||
"cv2_to_pillow",
|
||||
"denormalize_boxes",
|
||||
"draw_filled_polygon",
|
||||
"draw_filled_rectangle",
|
||||
"draw_image",
|
||||
|
|
@ -253,11 +269,14 @@ __all__ = [
|
|||
"is_valid_hex",
|
||||
"letterbox_image",
|
||||
"list_files_with_extensions",
|
||||
"load_image_from_url",
|
||||
"mask_iou_batch",
|
||||
"mask_non_max_merge",
|
||||
"mask_non_max_suppression",
|
||||
"mask_soft_non_max_suppression",
|
||||
"mask_to_polygons",
|
||||
"mask_to_rle",
|
||||
"mask_to_roi",
|
||||
"mask_to_xyxy",
|
||||
"move_boxes",
|
||||
"move_masks",
|
||||
|
|
@ -284,4 +303,15 @@ __all__ = [
|
|||
"xyxy_to_polygons",
|
||||
"xyxy_to_xcycarh",
|
||||
"xyxy_to_xywh",
|
||||
"xyxyxyxy_to_xyxy",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
"""Lazily resolve deprecated compatibility exports."""
|
||||
if name == "ByteTrack":
|
||||
from supervision.tracker.byte_tracker.core import ByteTrack as byte_track
|
||||
|
||||
globals()[name] = byte_track
|
||||
return byte_track
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,302 @@
|
|||
"""Private OpenCV compatibility surface used by Supervision."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from typing import Any
|
||||
|
||||
import numpy.typing as npt
|
||||
|
||||
from supervision._cv2._color import _cvt_color, _merge, _split
|
||||
from supervision._cv2._common import BackendUnavailableError
|
||||
from supervision._cv2._components import (
|
||||
_connected_components,
|
||||
_connected_components_with_stats,
|
||||
)
|
||||
from supervision._cv2._contours import _find_contours
|
||||
from supervision._cv2._drawing import (
|
||||
_circle,
|
||||
_draw_contours,
|
||||
_ellipse,
|
||||
_fill_poly,
|
||||
_line,
|
||||
_polylines,
|
||||
_rectangle,
|
||||
)
|
||||
from supervision._cv2._geometry import (
|
||||
_approx_poly_dp,
|
||||
_contour_area,
|
||||
_intersect_convex_convex,
|
||||
)
|
||||
from supervision._cv2._image import (
|
||||
_add_weighted,
|
||||
_convert_scale_abs,
|
||||
_copy_make_border,
|
||||
_flip,
|
||||
_imdecode,
|
||||
_imencode,
|
||||
_imread,
|
||||
_imwrite,
|
||||
_mean,
|
||||
_resize,
|
||||
)
|
||||
from supervision._cv2._text import _get_text_size, _put_text
|
||||
from supervision._cv2._transform import _blur
|
||||
from supervision._cv2._video import (
|
||||
_video_writer_fourcc,
|
||||
_VideoCapture,
|
||||
_VideoWriter,
|
||||
)
|
||||
from supervision._cv2.constants import (
|
||||
_BORDER_CONSTANT,
|
||||
_CAP_PROP_FPS,
|
||||
_CAP_PROP_FRAME_COUNT,
|
||||
_CAP_PROP_FRAME_HEIGHT,
|
||||
_CAP_PROP_FRAME_WIDTH,
|
||||
_CAP_PROP_POS_FRAMES,
|
||||
_CC_STAT_AREA,
|
||||
_CHAIN_APPROX_SIMPLE,
|
||||
_COLOR_BGR2GRAY,
|
||||
_COLOR_BGR2RGB,
|
||||
_COLOR_GRAY2BGR,
|
||||
_COLOR_HSV2BGR,
|
||||
_COLOR_RGB2BGR,
|
||||
_FONT_HERSHEY_COMPLEX,
|
||||
_FONT_HERSHEY_COMPLEX_SMALL,
|
||||
_FONT_HERSHEY_DUPLEX,
|
||||
_FONT_HERSHEY_PLAIN,
|
||||
_FONT_HERSHEY_SCRIPT_COMPLEX,
|
||||
_FONT_HERSHEY_SCRIPT_SIMPLEX,
|
||||
_FONT_HERSHEY_SIMPLEX,
|
||||
_FONT_HERSHEY_TRIPLEX,
|
||||
_FONT_ITALIC,
|
||||
_IMREAD_COLOR,
|
||||
_IMREAD_UNCHANGED,
|
||||
_INTER_LINEAR,
|
||||
_INTER_NEAREST,
|
||||
_LINE_4,
|
||||
_LINE_8,
|
||||
_LINE_AA,
|
||||
_RETR_TREE,
|
||||
)
|
||||
|
||||
try:
|
||||
import cv2
|
||||
except (ImportError, OSError):
|
||||
_IS_CV2_AVAILABLE = False
|
||||
else:
|
||||
_IS_CV2_AVAILABLE = True
|
||||
|
||||
if _IS_CV2_AVAILABLE:
|
||||
from cv2 import ( # type: ignore[attr-defined]
|
||||
BORDER_CONSTANT,
|
||||
CAP_PROP_FPS,
|
||||
CAP_PROP_FRAME_COUNT,
|
||||
CAP_PROP_FRAME_HEIGHT,
|
||||
CAP_PROP_FRAME_WIDTH,
|
||||
CAP_PROP_POS_FRAMES,
|
||||
CC_STAT_AREA,
|
||||
COLOR_BGR2GRAY,
|
||||
COLOR_BGR2RGB,
|
||||
COLOR_GRAY2BGR,
|
||||
COLOR_HSV2BGR,
|
||||
COLOR_RGB2BGR,
|
||||
FONT_HERSHEY_COMPLEX,
|
||||
FONT_HERSHEY_COMPLEX_SMALL,
|
||||
FONT_HERSHEY_DUPLEX,
|
||||
FONT_HERSHEY_PLAIN,
|
||||
FONT_HERSHEY_SCRIPT_COMPLEX,
|
||||
FONT_HERSHEY_SCRIPT_SIMPLEX,
|
||||
FONT_HERSHEY_SIMPLEX,
|
||||
FONT_HERSHEY_TRIPLEX,
|
||||
FONT_ITALIC,
|
||||
IMREAD_COLOR,
|
||||
IMREAD_UNCHANGED,
|
||||
INTER_LINEAR,
|
||||
INTER_NEAREST,
|
||||
LINE_4,
|
||||
LINE_8,
|
||||
LINE_AA,
|
||||
VideoCapture,
|
||||
VideoWriter,
|
||||
VideoWriter_fourcc, # type: ignore[attr-defined]
|
||||
addWeighted,
|
||||
approxPolyDP,
|
||||
blur,
|
||||
circle,
|
||||
connectedComponents,
|
||||
connectedComponentsWithStats,
|
||||
contourArea,
|
||||
convertScaleAbs,
|
||||
copyMakeBorder,
|
||||
cvtColor,
|
||||
drawContours,
|
||||
ellipse,
|
||||
fillPoly,
|
||||
flip,
|
||||
getTextSize,
|
||||
imdecode,
|
||||
imencode,
|
||||
imread,
|
||||
imwrite,
|
||||
intersectConvexConvex,
|
||||
line,
|
||||
mean,
|
||||
merge,
|
||||
polylines,
|
||||
putText,
|
||||
rectangle,
|
||||
resize,
|
||||
split,
|
||||
)
|
||||
from cv2 import (
|
||||
findContours as _find_contours_impl,
|
||||
)
|
||||
|
||||
BACKEND_NAME = "opencv"
|
||||
else:
|
||||
BACKEND_NAME = "fallback"
|
||||
|
||||
warnings.warn(
|
||||
"OpenCV (`opencv-python`) is not installed; supervision is using its "
|
||||
"pure NumPy fallback backend instead. Some operations may be slower "
|
||||
"or behave slightly differently. Install `opencv-python` for full "
|
||||
"performance and compatibility.",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
BORDER_CONSTANT = _BORDER_CONSTANT
|
||||
CAP_PROP_FPS = _CAP_PROP_FPS
|
||||
CAP_PROP_FRAME_COUNT = _CAP_PROP_FRAME_COUNT
|
||||
CAP_PROP_FRAME_HEIGHT = _CAP_PROP_FRAME_HEIGHT
|
||||
CAP_PROP_FRAME_WIDTH = _CAP_PROP_FRAME_WIDTH
|
||||
CAP_PROP_POS_FRAMES = _CAP_PROP_POS_FRAMES
|
||||
CC_STAT_AREA = _CC_STAT_AREA
|
||||
COLOR_BGR2GRAY = _COLOR_BGR2GRAY
|
||||
COLOR_BGR2RGB = _COLOR_BGR2RGB
|
||||
COLOR_GRAY2BGR = _COLOR_GRAY2BGR
|
||||
COLOR_HSV2BGR = _COLOR_HSV2BGR
|
||||
COLOR_RGB2BGR = _COLOR_RGB2BGR
|
||||
FONT_HERSHEY_COMPLEX = _FONT_HERSHEY_COMPLEX
|
||||
FONT_HERSHEY_COMPLEX_SMALL = _FONT_HERSHEY_COMPLEX_SMALL
|
||||
FONT_HERSHEY_DUPLEX = _FONT_HERSHEY_DUPLEX
|
||||
FONT_HERSHEY_PLAIN = _FONT_HERSHEY_PLAIN
|
||||
FONT_HERSHEY_SCRIPT_COMPLEX = _FONT_HERSHEY_SCRIPT_COMPLEX
|
||||
FONT_HERSHEY_SCRIPT_SIMPLEX = _FONT_HERSHEY_SCRIPT_SIMPLEX
|
||||
FONT_HERSHEY_SIMPLEX = _FONT_HERSHEY_SIMPLEX
|
||||
FONT_HERSHEY_TRIPLEX = _FONT_HERSHEY_TRIPLEX
|
||||
FONT_ITALIC = _FONT_ITALIC
|
||||
IMREAD_COLOR = _IMREAD_COLOR
|
||||
IMREAD_UNCHANGED = _IMREAD_UNCHANGED
|
||||
INTER_LINEAR = _INTER_LINEAR
|
||||
INTER_NEAREST = _INTER_NEAREST
|
||||
LINE_4 = _LINE_4
|
||||
LINE_8 = _LINE_8
|
||||
LINE_AA = _LINE_AA
|
||||
|
||||
# Fallback implementations when cv2 is not available. Suppress type errors because
|
||||
# fallback types differ from cv2 types, but are functionally equivalent.
|
||||
VideoCapture = _VideoCapture # type: ignore[assignment,misc]
|
||||
VideoWriter = _VideoWriter # type: ignore[assignment,misc]
|
||||
VideoWriter_fourcc = _video_writer_fourcc # type: ignore[assignment]
|
||||
addWeighted = _add_weighted # type: ignore[assignment]
|
||||
approxPolyDP = _approx_poly_dp # type: ignore[assignment]
|
||||
blur = _blur # type: ignore[assignment]
|
||||
circle = _circle # type: ignore[assignment]
|
||||
connectedComponents = _connected_components # type: ignore[assignment]
|
||||
connectedComponentsWithStats = _connected_components_with_stats # type: ignore[assignment]
|
||||
contourArea = _contour_area # type: ignore[assignment]
|
||||
convertScaleAbs = _convert_scale_abs # type: ignore[assignment]
|
||||
copyMakeBorder = _copy_make_border # type: ignore[assignment]
|
||||
cvtColor = _cvt_color # type: ignore[assignment]
|
||||
drawContours = _draw_contours # type: ignore[assignment]
|
||||
ellipse = _ellipse # type: ignore[assignment]
|
||||
fillPoly = _fill_poly # type: ignore[assignment]
|
||||
_find_contours_impl = _find_contours
|
||||
flip = _flip # type: ignore[assignment]
|
||||
getTextSize = _get_text_size # type: ignore[assignment]
|
||||
imdecode = _imdecode # type: ignore[assignment]
|
||||
imencode = _imencode # type: ignore[assignment]
|
||||
imread = _imread # type: ignore[assignment]
|
||||
imwrite = _imwrite # type: ignore[assignment]
|
||||
intersectConvexConvex = _intersect_convex_convex # type: ignore[assignment]
|
||||
line = _line # type: ignore[assignment]
|
||||
mean = _mean # type: ignore[assignment]
|
||||
merge = _merge # type: ignore[assignment]
|
||||
polylines = _polylines # type: ignore[assignment]
|
||||
putText = _put_text # type: ignore[assignment]
|
||||
rectangle = _rectangle # type: ignore[assignment]
|
||||
resize = _resize # type: ignore[assignment]
|
||||
split = _split # type: ignore[assignment]
|
||||
|
||||
|
||||
def find_contours(image: npt.NDArray[Any]) -> list[npt.NDArray[Any]]:
|
||||
"""Return the contour geometry required by mask-to-polygon conversion."""
|
||||
contours, _ = _find_contours_impl(image, _RETR_TREE, _CHAIN_APPROX_SIMPLE)
|
||||
return list(contours)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BACKEND_NAME",
|
||||
"BORDER_CONSTANT",
|
||||
"CAP_PROP_FPS",
|
||||
"CAP_PROP_FRAME_COUNT",
|
||||
"CAP_PROP_FRAME_HEIGHT",
|
||||
"CAP_PROP_FRAME_WIDTH",
|
||||
"CAP_PROP_POS_FRAMES",
|
||||
"CC_STAT_AREA",
|
||||
"COLOR_BGR2GRAY",
|
||||
"COLOR_BGR2RGB",
|
||||
"COLOR_GRAY2BGR",
|
||||
"COLOR_HSV2BGR",
|
||||
"COLOR_RGB2BGR",
|
||||
"FONT_HERSHEY_COMPLEX",
|
||||
"FONT_HERSHEY_COMPLEX_SMALL",
|
||||
"FONT_HERSHEY_DUPLEX",
|
||||
"FONT_HERSHEY_PLAIN",
|
||||
"FONT_HERSHEY_SCRIPT_COMPLEX",
|
||||
"FONT_HERSHEY_SCRIPT_SIMPLEX",
|
||||
"FONT_HERSHEY_SIMPLEX",
|
||||
"FONT_HERSHEY_TRIPLEX",
|
||||
"FONT_ITALIC",
|
||||
"IMREAD_COLOR",
|
||||
"IMREAD_UNCHANGED",
|
||||
"INTER_LINEAR",
|
||||
"INTER_NEAREST",
|
||||
"LINE_4",
|
||||
"LINE_8",
|
||||
"LINE_AA",
|
||||
"BackendUnavailableError",
|
||||
"VideoCapture",
|
||||
"VideoWriter",
|
||||
"VideoWriter_fourcc",
|
||||
"addWeighted",
|
||||
"approxPolyDP",
|
||||
"blur",
|
||||
"circle",
|
||||
"connectedComponents",
|
||||
"connectedComponentsWithStats",
|
||||
"contourArea",
|
||||
"convertScaleAbs",
|
||||
"copyMakeBorder",
|
||||
"cvtColor",
|
||||
"drawContours",
|
||||
"ellipse",
|
||||
"fillPoly",
|
||||
"find_contours",
|
||||
"flip",
|
||||
"getTextSize",
|
||||
"imdecode",
|
||||
"imencode",
|
||||
"imread",
|
||||
"imwrite",
|
||||
"intersectConvexConvex",
|
||||
"line",
|
||||
"mean",
|
||||
"merge",
|
||||
"polylines",
|
||||
"putText",
|
||||
"rectangle",
|
||||
"resize",
|
||||
"split",
|
||||
]
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
"""Private color and channel-operation fallbacks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from supervision._cv2._common import _cast_array_like_opencv
|
||||
from supervision._cv2.constants import (
|
||||
_COLOR_BGR2GRAY,
|
||||
_COLOR_BGR2RGB,
|
||||
_COLOR_GRAY2BGR,
|
||||
_COLOR_HSV2BGR,
|
||||
_COLOR_RGB2BGR,
|
||||
)
|
||||
|
||||
|
||||
def _cvt_color(image: npt.NDArray[Any], code: int) -> npt.NDArray[Any]:
|
||||
"""Convert the BGR, RGB, grayscale, and 8-bit HSV formats used by Supervision."""
|
||||
if code in (_COLOR_BGR2RGB, _COLOR_RGB2BGR):
|
||||
if image.ndim != 3 or image.shape[2] != 3:
|
||||
raise ValueError("BGR/RGB conversion requires a three-channel image")
|
||||
return np.ascontiguousarray(image[..., ::-1])
|
||||
|
||||
if code == _COLOR_GRAY2BGR:
|
||||
if image.ndim != 2:
|
||||
raise ValueError("GRAY2BGR conversion requires a two-dimensional image")
|
||||
return np.repeat(image[..., np.newaxis], 3, axis=2)
|
||||
|
||||
if code == _COLOR_BGR2GRAY:
|
||||
if image.ndim != 3 or image.shape[2] != 3:
|
||||
raise ValueError("BGR2GRAY conversion requires a three-channel image")
|
||||
if image.dtype == np.uint8:
|
||||
values = image.astype(np.uint32)
|
||||
weighted = (
|
||||
values[..., 0] * 3735
|
||||
+ values[..., 1] * 19235
|
||||
+ values[..., 2] * 9798
|
||||
+ (1 << 14)
|
||||
) >> 15
|
||||
return weighted.astype(np.uint8)
|
||||
float_values = (
|
||||
image[..., 0].astype(np.float64) * 0.114
|
||||
+ image[..., 1].astype(np.float64) * 0.587
|
||||
+ image[..., 2].astype(np.float64) * 0.299
|
||||
)
|
||||
return _cast_array_like_opencv(float_values, image.dtype)
|
||||
|
||||
if code == _COLOR_HSV2BGR:
|
||||
if image.ndim != 3 or image.shape[2] != 3:
|
||||
raise ValueError("HSV2BGR conversion requires a three-channel image")
|
||||
return _hsv_to_bgr(image)
|
||||
|
||||
raise ValueError(f"Unsupported color conversion code: {code}")
|
||||
|
||||
|
||||
def _hsv_to_bgr(image: npt.NDArray[Any]) -> npt.NDArray[Any]:
|
||||
"""Convert OpenCV's 8-bit HSV representation to BGR."""
|
||||
values = image.astype(np.float64)
|
||||
hue = values[..., 0] / 30.0
|
||||
saturation = values[..., 1] / 255.0
|
||||
value = values[..., 2] / 255.0
|
||||
|
||||
chroma = value * saturation
|
||||
sector_index = np.floor(hue).astype(np.int64) % 6
|
||||
sector = hue - np.floor(hue)
|
||||
x = chroma * (1 - np.abs(((sector_index + sector) % 2) - 1))
|
||||
match = value - chroma
|
||||
zeros = np.zeros_like(chroma)
|
||||
|
||||
red = np.choose(sector_index, (chroma, x, zeros, zeros, x, chroma))
|
||||
green = np.choose(sector_index, (x, chroma, chroma, x, zeros, zeros))
|
||||
blue = np.choose(sector_index, (zeros, zeros, x, chroma, chroma, x))
|
||||
bgr = np.stack((blue + match, green + match, red + match), axis=-1) * 255
|
||||
return _cast_array_like_opencv(bgr, image.dtype)
|
||||
|
||||
|
||||
def _split(image: npt.NDArray[Any]) -> tuple[npt.NDArray[Any], ...]:
|
||||
"""Split an image into contiguous single-channel arrays."""
|
||||
if image.ndim == 2:
|
||||
return (np.ascontiguousarray(image),)
|
||||
return tuple(
|
||||
np.ascontiguousarray(image[..., index]) for index in range(image.shape[2])
|
||||
)
|
||||
|
||||
|
||||
def _merge(channels: Sequence[npt.NDArray[Any]]) -> npt.NDArray[Any]:
|
||||
"""Merge single-channel arrays along their final axis."""
|
||||
if not channels:
|
||||
raise ValueError("At least one channel is required")
|
||||
return np.ascontiguousarray(np.stack(channels, axis=-1))
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
"""Private helpers shared by OpenCV fallback implementations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
|
||||
class BackendUnavailableError(RuntimeError):
|
||||
"""Raised when an OpenCV operation is used without an available backend."""
|
||||
|
||||
|
||||
def _cast_array_like_opencv(
|
||||
values: npt.NDArray[Any], dtype: np.dtype[Any]
|
||||
) -> npt.NDArray[Any]:
|
||||
"""Round integer results using OpenCV's saturating conversion convention."""
|
||||
if np.issubdtype(dtype, np.integer):
|
||||
info = np.iinfo(dtype)
|
||||
values = np.rint(values)
|
||||
values = np.clip(values, info.min, info.max)
|
||||
return values.astype(dtype, copy=False)
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
"""Private connected-component and mask-topology fallbacks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
|
||||
def _validate_binary_image(image: npt.NDArray[Any]) -> npt.NDArray[np.bool_]:
|
||||
"""Validate and normalize a two-dimensional component image."""
|
||||
values = np.asarray(image)
|
||||
if values.ndim != 2:
|
||||
raise ValueError("Connected-component input must be a two-dimensional image")
|
||||
return cast(npt.NDArray[np.bool_], values != 0)
|
||||
|
||||
|
||||
def _label(
|
||||
image: npt.NDArray[Any], connectivity: int
|
||||
) -> tuple[int, npt.NDArray[np.int32]]:
|
||||
"""Label foreground pixels with the requested four- or eight-way topology."""
|
||||
if connectivity not in (4, 8):
|
||||
raise ValueError("Only 4- and 8-connectivity are supported")
|
||||
|
||||
from scipy import ndimage
|
||||
|
||||
structure = ndimage.generate_binary_structure(2, 1 if connectivity == 4 else 2)
|
||||
labels, count = ndimage.label(_validate_binary_image(image), structure=structure)
|
||||
return int(count), np.ascontiguousarray(labels, dtype=np.int32)
|
||||
|
||||
|
||||
def _connected_components(
|
||||
image: npt.NDArray[Any],
|
||||
labels: npt.NDArray[Any] | None = None,
|
||||
connectivity: int = 8,
|
||||
ltype: int = 4,
|
||||
) -> tuple[int, npt.NDArray[np.int32]]:
|
||||
"""Return OpenCV-shaped connected-component labels and their count."""
|
||||
del ltype
|
||||
count, result = _label(image, connectivity)
|
||||
if labels is not None and labels.shape == result.shape and labels.dtype == np.int32:
|
||||
labels[...] = result
|
||||
result = labels
|
||||
return count + 1, result
|
||||
|
||||
|
||||
def _connected_components_with_stats(
|
||||
image: npt.NDArray[Any], connectivity: int = 8, ltype: int = 4
|
||||
) -> tuple[
|
||||
int,
|
||||
npt.NDArray[np.int32],
|
||||
npt.NDArray[np.int32],
|
||||
npt.NDArray[np.float64],
|
||||
]:
|
||||
"""Return labels, bounding-box statistics, and centroids for components."""
|
||||
del ltype
|
||||
from scipy import ndimage
|
||||
|
||||
count, labels = _label(image, connectivity)
|
||||
component_count = count + 1
|
||||
flat_labels = labels.ravel()
|
||||
rows, columns = np.indices(labels.shape)
|
||||
areas = np.bincount(flat_labels, minlength=component_count)
|
||||
x_sums = np.bincount(
|
||||
flat_labels, weights=columns.ravel(), minlength=component_count
|
||||
)
|
||||
y_sums = np.bincount(flat_labels, weights=rows.ravel(), minlength=component_count)
|
||||
|
||||
centroids = np.zeros((component_count, 2), dtype=np.float64)
|
||||
populated = areas != 0
|
||||
centroids[populated, 0] = x_sums[populated] / areas[populated]
|
||||
centroids[populated, 1] = y_sums[populated] / areas[populated]
|
||||
|
||||
stats = np.zeros((component_count, 5), dtype=np.int32)
|
||||
stats[:, 4] = areas.astype(np.int32)
|
||||
objects = ndimage.find_objects(labels, max_label=count)
|
||||
for component, bounds in enumerate(objects, start=1):
|
||||
if bounds is None:
|
||||
continue
|
||||
row_slice, column_slice = bounds
|
||||
stats[component, :4] = (
|
||||
column_slice.start,
|
||||
row_slice.start,
|
||||
column_slice.stop - column_slice.start,
|
||||
row_slice.stop - row_slice.start,
|
||||
)
|
||||
|
||||
background_mask = labels == 0
|
||||
if np.any(background_mask):
|
||||
row_hits = np.any(background_mask, axis=1)
|
||||
col_hits = np.any(background_mask, axis=0)
|
||||
y_indices = np.flatnonzero(row_hits)
|
||||
x_indices = np.flatnonzero(col_hits)
|
||||
stats[0, :4] = (
|
||||
int(x_indices[0]),
|
||||
int(y_indices[0]),
|
||||
int(x_indices[-1] - x_indices[0] + 1),
|
||||
int(y_indices[-1] - y_indices[0] + 1),
|
||||
)
|
||||
return count + 1, labels, stats, centroids
|
||||
|
||||
|
||||
def _contains_holes(mask: npt.NDArray[Any]) -> bool:
|
||||
"""Return whether a mask has a background component detached from its border."""
|
||||
values = _validate_binary_image(mask)
|
||||
if values.size == 0 or np.all(values):
|
||||
return False
|
||||
|
||||
background_count, background_labels = _label(~values, connectivity=4)
|
||||
if background_count == 0:
|
||||
return False
|
||||
|
||||
border_labels = np.unique(
|
||||
np.concatenate(
|
||||
(
|
||||
background_labels[0],
|
||||
background_labels[-1],
|
||||
background_labels[:, 0],
|
||||
background_labels[:, -1],
|
||||
)
|
||||
)
|
||||
)
|
||||
return bool(
|
||||
np.any(
|
||||
~np.isin(np.arange(1, background_count + 1, dtype=np.int32), border_labels)
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
"""Private Suzuki-Abe contour fallback."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from supervision._cv2.constants import _CHAIN_APPROX_SIMPLE, _RETR_TREE
|
||||
|
||||
_NEIGHBORS = (
|
||||
(0, 1),
|
||||
(-1, 1),
|
||||
(-1, 0),
|
||||
(-1, -1),
|
||||
(0, -1),
|
||||
(1, -1),
|
||||
(1, 0),
|
||||
(1, 1),
|
||||
)
|
||||
|
||||
|
||||
def _follow_border(
|
||||
image: npt.NDArray[np.bool_],
|
||||
labels: npt.NDArray[np.int32],
|
||||
start: tuple[int, int],
|
||||
previous: tuple[int, int],
|
||||
border_number: int,
|
||||
) -> list[tuple[int, int]]:
|
||||
"""Trace one border using the Suzuki-Abe neighborhood walk."""
|
||||
rows, columns = image.shape
|
||||
|
||||
def is_foreground(row: int, column: int) -> bool:
|
||||
"""Check a pixel while treating the image boundary as background."""
|
||||
return 0 <= row < rows and 0 <= column < columns and bool(image[row, column])
|
||||
|
||||
direction = _NEIGHBORS.index((previous[0] - start[0], previous[1] - start[1]))
|
||||
first_direction = -1
|
||||
for offset in range(1, 9):
|
||||
candidate_direction = (direction + offset) % 8
|
||||
delta_row, delta_column = _NEIGHBORS[candidate_direction]
|
||||
if is_foreground(start[0] + delta_row, start[1] + delta_column):
|
||||
first_direction = candidate_direction
|
||||
break
|
||||
if first_direction < 0:
|
||||
labels[start] = -border_number
|
||||
return [start]
|
||||
|
||||
first_neighbor = (
|
||||
start[0] + _NEIGHBORS[first_direction][0],
|
||||
start[1] + _NEIGHBORS[first_direction][1],
|
||||
)
|
||||
contour: list[tuple[int, int]] = []
|
||||
previous_point, current = first_neighbor, start
|
||||
while True:
|
||||
direction = _NEIGHBORS.index(
|
||||
(previous_point[0] - current[0], previous_point[1] - current[1])
|
||||
)
|
||||
east_zero = False
|
||||
next_point: tuple[int, int] | None = None
|
||||
for offset in range(1, 9):
|
||||
candidate_direction = (direction - offset) % 8
|
||||
delta_row, delta_column = _NEIGHBORS[candidate_direction]
|
||||
row = current[0] + delta_row
|
||||
column = current[1] + delta_column
|
||||
if is_foreground(row, column):
|
||||
next_point = (row, column)
|
||||
break
|
||||
if candidate_direction == 0:
|
||||
east_zero = True
|
||||
|
||||
if east_zero:
|
||||
labels[current] = -border_number
|
||||
elif labels[current] == 0:
|
||||
labels[current] = border_number
|
||||
contour.append(current)
|
||||
|
||||
if next_point == start and current == first_neighbor and len(contour) > 1:
|
||||
return contour
|
||||
if next_point is None:
|
||||
return contour
|
||||
previous_point, current = current, next_point
|
||||
if len(contour) > 4 * image.size:
|
||||
raise RuntimeError("Contour border tracing did not converge")
|
||||
|
||||
|
||||
def _trace_borders(mask: npt.NDArray[np.bool_]) -> list[np.ndarray]:
|
||||
"""Trace all foreground and hole borders in raster candidate order."""
|
||||
image = np.ascontiguousarray(mask, dtype=bool)
|
||||
labels = np.zeros(image.shape, dtype=np.int32)
|
||||
left_zero = image & ~np.pad(image[:, :-1], ((0, 0), (1, 0)))
|
||||
right_zero = image & ~np.pad(image[:, 1:], ((0, 0), (0, 1)))
|
||||
candidates = np.argwhere(left_zero | right_zero)
|
||||
borders: list[np.ndarray] = []
|
||||
border_number = 1
|
||||
for row, column in candidates:
|
||||
row, column = int(row), int(column)
|
||||
if left_zero[row, column] and labels[row, column] == 0:
|
||||
border_number += 1
|
||||
border = _follow_border(
|
||||
image, labels, (row, column), (row, column - 1), border_number
|
||||
)
|
||||
borders.append(np.array([(column, row) for row, column in border]))
|
||||
elif right_zero[row, column] and labels[row, column] >= 0:
|
||||
border_number += 1
|
||||
border = _follow_border(
|
||||
image, labels, (row, column), (row, column + 1), border_number
|
||||
)
|
||||
borders.append(np.array([(column, row) for row, column in border]))
|
||||
return borders
|
||||
|
||||
|
||||
def _reverse_preserving_start(contour: np.ndarray) -> np.ndarray:
|
||||
"""Reverse a traced contour while retaining its Suzuki-Abe start pixel."""
|
||||
if len(contour) < 2:
|
||||
return contour
|
||||
return np.concatenate((contour[:1], contour[:0:-1]))
|
||||
|
||||
|
||||
def _compress_contour(contour: np.ndarray) -> np.ndarray:
|
||||
"""Apply OpenCV's collinear-run compression for SIMPLE contours."""
|
||||
contour = _reverse_preserving_start(contour)
|
||||
if len(contour) < 3:
|
||||
return contour
|
||||
keep: list[np.ndarray] = []
|
||||
for index, point in enumerate(contour):
|
||||
previous = point - contour[index - 1]
|
||||
following = contour[(index + 1) % len(contour)] - point
|
||||
if (
|
||||
np.any(previous)
|
||||
and np.any(following)
|
||||
and np.array_equal(np.sign(previous), np.sign(following))
|
||||
):
|
||||
continue
|
||||
keep.append(point)
|
||||
return np.asarray(keep, dtype=np.int32)
|
||||
|
||||
|
||||
def _find_contours(
|
||||
image: npt.NDArray[Any], mode: int, method: int
|
||||
) -> tuple[list[npt.NDArray[np.int32]], npt.NDArray[np.int32] | None]:
|
||||
"""Find contours for the supported tree and SIMPLE modes."""
|
||||
if mode != _RETR_TREE:
|
||||
raise ValueError("Only RETR_TREE is supported by the fallback")
|
||||
if method != _CHAIN_APPROX_SIMPLE:
|
||||
raise ValueError("Only CHAIN_APPROX_SIMPLE is supported by the fallback")
|
||||
values = np.asarray(image)
|
||||
if values.ndim != 2:
|
||||
raise ValueError("Contour input must be a two-dimensional image")
|
||||
traced = [_compress_contour(contour) for contour in _trace_borders(values != 0)]
|
||||
if not traced:
|
||||
return [], None
|
||||
return [contour.reshape(-1, 1, 2) for contour in traced], None
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
"""Private Pillow-based drawing fallbacks for the OpenCV facade."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
_ImageArray = npt.NDArray[Any]
|
||||
_Point = tuple[int, int]
|
||||
|
||||
|
||||
def _drawing_mask(
|
||||
image: _ImageArray, draw_operation: Callable[[Any], None]
|
||||
) -> npt.NDArray[np.bool_]:
|
||||
"""Render a shape into a clipped one-bit mask with Pillow."""
|
||||
height, width = image.shape[:2]
|
||||
mask = Image.new("1", (width, height))
|
||||
draw_operation(ImageDraw.Draw(mask))
|
||||
return np.asarray(mask, dtype=bool)
|
||||
|
||||
|
||||
def _color_for_image(image: _ImageArray, color: Any) -> Any:
|
||||
"""Normalize an OpenCV scalar color to the image channel count."""
|
||||
values = np.asarray(color).reshape(-1)
|
||||
if image.ndim == 2:
|
||||
return values[0] if values.size else 0
|
||||
channels = image.shape[2]
|
||||
if values.size == 0:
|
||||
return np.zeros(channels, dtype=image.dtype)
|
||||
if values.size < channels:
|
||||
return np.pad(values, (0, channels - values.size))
|
||||
return values[:channels]
|
||||
|
||||
|
||||
def _paint(image: _ImageArray, mask: npt.NDArray[np.bool_], color: Any) -> _ImageArray:
|
||||
"""Apply a scalar or multi-channel color to a drawing mask."""
|
||||
image[mask] = _color_for_image(image, color)
|
||||
return image
|
||||
|
||||
|
||||
def _point(point: Sequence[int | float]) -> _Point:
|
||||
"""Convert an OpenCV point to integer Pillow coordinates."""
|
||||
return round(point[0]), round(point[1])
|
||||
|
||||
|
||||
def _points(points: npt.NDArray[Any], offset: tuple[int, int] = (0, 0)) -> list[_Point]:
|
||||
"""Normalize OpenCV polygon shapes to integer Pillow coordinates."""
|
||||
values = np.asarray(points)
|
||||
if values.size == 0:
|
||||
return []
|
||||
if values.ndim not in (2, 3) or values.shape[-1] != 2:
|
||||
raise ValueError("Drawing points must have shape (N, 2) or (N, 1, 2)")
|
||||
normalized = np.rint(values.reshape(-1, 2)).astype(np.int64)
|
||||
normalized += np.asarray(offset, dtype=np.int64)
|
||||
return [(int(x), int(y)) for x, y in normalized]
|
||||
|
||||
|
||||
def _validate_shift(shift: int) -> None:
|
||||
"""Reject fixed-point coordinates not supported by the fallback."""
|
||||
if shift != 0:
|
||||
raise ValueError("Only unshifted drawing coordinates are supported")
|
||||
|
||||
|
||||
def _line(
|
||||
img: _ImageArray,
|
||||
pt1: Sequence[int | float],
|
||||
pt2: Sequence[int | float],
|
||||
color: Any,
|
||||
thickness: int = 1,
|
||||
lineType: int = 8,
|
||||
shift: int = 0,
|
||||
) -> _ImageArray:
|
||||
"""Draw a line in place using Pillow's integer rasterization."""
|
||||
del lineType
|
||||
_validate_shift(shift)
|
||||
width = max(1, thickness)
|
||||
mask = _drawing_mask(
|
||||
img,
|
||||
lambda draw: draw.line([_point(pt1), _point(pt2)], fill=1, width=width),
|
||||
)
|
||||
return _paint(img, mask, color)
|
||||
|
||||
|
||||
def _rectangle(
|
||||
img: _ImageArray,
|
||||
pt1: Sequence[int | float],
|
||||
pt2: Sequence[int | float],
|
||||
color: Any,
|
||||
thickness: int = 1,
|
||||
lineType: int = 8,
|
||||
shift: int = 0,
|
||||
) -> _ImageArray:
|
||||
"""Draw or fill an inclusive-axis-aligned rectangle in place."""
|
||||
del lineType
|
||||
_validate_shift(shift)
|
||||
first_point, second_point = _point(pt1), _point(pt2)
|
||||
first = tuple(min(left, right) for left, right in zip(first_point, second_point))
|
||||
second = tuple(max(left, right) for left, right in zip(first_point, second_point))
|
||||
if thickness < 0:
|
||||
mask = _drawing_mask(img, lambda draw: draw.rectangle([first, second], fill=1))
|
||||
else:
|
||||
width = max(1, thickness)
|
||||
mask = _drawing_mask(
|
||||
img,
|
||||
lambda draw: draw.rectangle([first, second], outline=1, width=width),
|
||||
)
|
||||
return _paint(img, mask, color)
|
||||
|
||||
|
||||
def _circle(
|
||||
img: _ImageArray,
|
||||
center: Sequence[int | float],
|
||||
radius: int,
|
||||
color: Any,
|
||||
thickness: int = 1,
|
||||
lineType: int = 8,
|
||||
shift: int = 0,
|
||||
) -> _ImageArray:
|
||||
"""Draw or fill a circle in place."""
|
||||
del lineType
|
||||
_validate_shift(shift)
|
||||
x, y = _point(center)
|
||||
bounds = [x - radius, y - radius, x + radius, y + radius]
|
||||
if thickness < 0:
|
||||
mask = _drawing_mask(img, lambda draw: draw.ellipse(bounds, fill=1))
|
||||
else:
|
||||
width = max(1, thickness)
|
||||
mask = _drawing_mask(
|
||||
img,
|
||||
lambda draw: draw.ellipse(bounds, outline=1, width=width),
|
||||
)
|
||||
return _paint(img, mask, color)
|
||||
|
||||
|
||||
def _ellipse_points(
|
||||
center: Sequence[int | float],
|
||||
axes: Sequence[int | float],
|
||||
angle: float,
|
||||
start_angle: float,
|
||||
end_angle: float,
|
||||
) -> list[_Point]:
|
||||
"""Sample an OpenCV ellipse arc as integer image coordinates."""
|
||||
center_x, center_y = center
|
||||
axis_x, axis_y = axes
|
||||
span = max(0.0, end_angle - start_angle)
|
||||
sample_count = max(2, int(np.ceil(span * max(axis_x, axis_y) * np.pi / 90)))
|
||||
angles = np.linspace(start_angle, end_angle, sample_count + 1)
|
||||
radians = np.deg2rad(angles)
|
||||
rotation = np.deg2rad(angle)
|
||||
cosine, sine = np.cos(rotation), np.sin(rotation)
|
||||
x = center_x + axis_x * np.cos(radians) * cosine - axis_y * np.sin(radians) * sine
|
||||
y = center_y + axis_x * np.cos(radians) * sine + axis_y * np.sin(radians) * cosine
|
||||
return [(round(x_value), round(y_value)) for x_value, y_value in zip(x, y)]
|
||||
|
||||
|
||||
def _ellipse(
|
||||
img: _ImageArray,
|
||||
center: Sequence[int | float],
|
||||
axes: Sequence[int | float],
|
||||
angle: float,
|
||||
startAngle: float,
|
||||
endAngle: float,
|
||||
color: Any,
|
||||
thickness: int = 1,
|
||||
lineType: int = 8,
|
||||
shift: int = 0,
|
||||
) -> _ImageArray:
|
||||
"""Draw or fill an optionally rotated ellipse arc in place."""
|
||||
del lineType
|
||||
_validate_shift(shift)
|
||||
ellipse_points = _ellipse_points(center, axes, angle, startAngle, endAngle)
|
||||
is_full = endAngle - startAngle >= 360
|
||||
|
||||
def draw_ellipse(draw: Any) -> None:
|
||||
if thickness < 0:
|
||||
if is_full:
|
||||
draw.polygon(ellipse_points, fill=1)
|
||||
else:
|
||||
draw.polygon([_point(center), *ellipse_points], fill=1)
|
||||
return
|
||||
width = max(1, thickness)
|
||||
draw.line(ellipse_points, fill=1, width=width, joint="curve")
|
||||
if is_full and len(ellipse_points) > 1:
|
||||
draw.line([ellipse_points[-1], ellipse_points[0]], fill=1, width=width)
|
||||
|
||||
return _paint(img, _drawing_mask(img, draw_ellipse), color)
|
||||
|
||||
|
||||
def _polylines(
|
||||
img: _ImageArray,
|
||||
pts: Sequence[npt.NDArray[Any]],
|
||||
isClosed: bool,
|
||||
color: Any,
|
||||
thickness: int = 1,
|
||||
lineType: int = 8,
|
||||
shift: int = 0,
|
||||
) -> _ImageArray:
|
||||
"""Draw one or more open or closed polylines in place."""
|
||||
del lineType
|
||||
_validate_shift(shift)
|
||||
width = max(1, thickness)
|
||||
|
||||
def draw_polylines(draw: Any) -> None:
|
||||
for polygon in pts:
|
||||
points = _points(polygon)
|
||||
if not points:
|
||||
continue
|
||||
points = [
|
||||
point
|
||||
for index, point in enumerate(points)
|
||||
if index == 0 or point != points[index - 1]
|
||||
]
|
||||
if len(points) == 1:
|
||||
draw.point(points[0], fill=1)
|
||||
continue
|
||||
if isClosed:
|
||||
points.append(points[0])
|
||||
draw.line(points, fill=1, width=width, joint="curve")
|
||||
|
||||
return _paint(img, _drawing_mask(img, draw_polylines), color)
|
||||
|
||||
|
||||
def _fill_poly(
|
||||
img: _ImageArray,
|
||||
pts: Sequence[npt.NDArray[Any]],
|
||||
color: Any,
|
||||
lineType: int = 8,
|
||||
shift: int = 0,
|
||||
offset: tuple[int, int] = (0, 0),
|
||||
) -> _ImageArray:
|
||||
"""Fill one or more polygons in place."""
|
||||
del lineType
|
||||
_validate_shift(shift)
|
||||
|
||||
def draw_polygons(draw: Any) -> None:
|
||||
for polygon in pts:
|
||||
points = _points(polygon, offset=offset)
|
||||
if len(points) == 1:
|
||||
draw.point(points[0], fill=1)
|
||||
elif len(points) == 2:
|
||||
draw.line(points, fill=1)
|
||||
elif len(points) >= 3:
|
||||
draw.polygon(points, fill=1)
|
||||
|
||||
return _paint(img, _drawing_mask(img, draw_polygons), color)
|
||||
|
||||
|
||||
def _draw_contours(
|
||||
image: _ImageArray,
|
||||
contours: Sequence[npt.NDArray[Any]],
|
||||
contourIdx: int,
|
||||
color: Any,
|
||||
thickness: int = 1,
|
||||
lineType: int = 8,
|
||||
hierarchy: npt.NDArray[Any] | None = None,
|
||||
maxLevel: int = 2**31 - 1,
|
||||
offset: tuple[int, int] = (0, 0),
|
||||
) -> _ImageArray:
|
||||
"""Draw selected contours with OpenCV-compatible in-place mutation."""
|
||||
del lineType, maxLevel
|
||||
# OpenCV walks the hierarchy tree to also draw a contour's nested descendants;
|
||||
# the fallback only does flat contourIdx selection, so reject a hierarchy
|
||||
# instead of silently diverging. maxLevel is a no-op without a hierarchy, which
|
||||
# matches OpenCV ignoring it whenever hierarchy is None.
|
||||
if hierarchy is not None:
|
||||
raise ValueError("Only None hierarchy is supported by the fallback")
|
||||
selected = contours if contourIdx < 0 else contours[contourIdx : contourIdx + 1]
|
||||
|
||||
def draw_selected(draw: Any) -> None:
|
||||
for contour in selected:
|
||||
points = _points(contour, offset=offset)
|
||||
if len(points) < 2:
|
||||
continue
|
||||
if thickness < 0:
|
||||
draw.polygon(points, fill=1)
|
||||
else:
|
||||
width = max(1, thickness)
|
||||
draw.line([*points, points[0]], fill=1, width=width, joint="curve")
|
||||
|
||||
return _paint(image, _drawing_mask(image, draw_selected), color)
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
"""Private polygon geometry fallbacks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
|
||||
def _as_points(contour: npt.NDArray[Any]) -> npt.NDArray[np.float64]:
|
||||
"""Normalize an OpenCV contour to an ``(N, 2)`` float64 array."""
|
||||
points = np.asarray(contour)
|
||||
if points.size == 0:
|
||||
return np.empty((0, 2), dtype=np.float64)
|
||||
if points.ndim not in (2, 3) or points.shape[-1] != 2:
|
||||
raise ValueError("Contours must have shape (N, 2) or (N, 1, 2)")
|
||||
return points.reshape(-1, 2).astype(np.float64, copy=False)
|
||||
|
||||
|
||||
def _contour_area(contour: npt.NDArray[Any], oriented: bool = False) -> float:
|
||||
"""Compute a contour's signed or absolute shoelace area."""
|
||||
points = _as_points(contour)
|
||||
if len(points) < 3:
|
||||
return 0.0
|
||||
x = points[:, 0]
|
||||
y = points[:, 1]
|
||||
area = 0.5 * float(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1)))
|
||||
return area if oriented else abs(area)
|
||||
|
||||
|
||||
def _simplify_slices(
|
||||
points: npt.NDArray[np.float64], epsilon_squared: float, closed: bool
|
||||
) -> npt.NDArray[np.float64]:
|
||||
"""Run OpenCV's stack-based Douglas-Peucker slice traversal."""
|
||||
count = len(points)
|
||||
stack: list[tuple[int, int]] = []
|
||||
output: list[npt.NDArray[np.float64]] = []
|
||||
|
||||
if closed or np.array_equal(points[0], points[-1]):
|
||||
closed = True
|
||||
position = 0
|
||||
right_start = 0
|
||||
start_point = points[0]
|
||||
within_epsilon = False
|
||||
for _ in range(3):
|
||||
position = (position + right_start) % count
|
||||
start_point = points[position]
|
||||
maximum_distance = 0.0
|
||||
right_start = 0
|
||||
for offset in range(1, count):
|
||||
point = points[(position + offset) % count]
|
||||
difference = point - start_point
|
||||
distance = float(np.dot(difference, difference))
|
||||
if distance > maximum_distance:
|
||||
maximum_distance = distance
|
||||
right_start = offset
|
||||
within_epsilon = maximum_distance <= epsilon_squared
|
||||
|
||||
if within_epsilon:
|
||||
output.append(start_point)
|
||||
else:
|
||||
split = (position + right_start) % count
|
||||
stack.extend(((split, position), (position, split)))
|
||||
else:
|
||||
stack.append((0, count - 1))
|
||||
|
||||
while stack:
|
||||
start, end = stack.pop()
|
||||
start_point = points[start]
|
||||
end_point = points[end]
|
||||
position = (start + 1) % count
|
||||
maximum_distance = 0.0
|
||||
split = start
|
||||
|
||||
if position != end:
|
||||
segment = end_point - start_point
|
||||
while position != end:
|
||||
point = points[position]
|
||||
distance = abs(
|
||||
float(
|
||||
(point[1] - start_point[1]) * segment[0]
|
||||
- (point[0] - start_point[0]) * segment[1]
|
||||
)
|
||||
)
|
||||
if distance > maximum_distance:
|
||||
maximum_distance = distance
|
||||
split = position
|
||||
position = (position + 1) % count
|
||||
segment_length_squared = float(np.dot(segment, segment))
|
||||
within_epsilon = (
|
||||
maximum_distance * maximum_distance
|
||||
<= epsilon_squared * segment_length_squared
|
||||
)
|
||||
else:
|
||||
within_epsilon = True
|
||||
|
||||
if within_epsilon:
|
||||
output.append(start_point)
|
||||
else:
|
||||
stack.extend(((split, end), (start, split)))
|
||||
|
||||
if not closed:
|
||||
output.append(points[-1])
|
||||
return np.asarray(output, dtype=np.float64)
|
||||
|
||||
|
||||
def _cleanup_approximation(
|
||||
points: npt.NDArray[np.float64], epsilon_squared: float, closed: bool
|
||||
) -> npt.NDArray[np.float64]:
|
||||
"""Remove OpenCV's final near-collinear points from an approximation."""
|
||||
count = len(points)
|
||||
if count <= 2:
|
||||
return points
|
||||
|
||||
destination = points.copy()
|
||||
new_count = count
|
||||
position = count - 1 if closed else 0
|
||||
start_point = destination[position]
|
||||
position = (position + 1) % count
|
||||
write_position = position
|
||||
point = destination[position]
|
||||
position = (position + 1) % count
|
||||
index = 0 if closed else 1
|
||||
stop = count if closed else count - 1
|
||||
|
||||
while index < stop and new_count > 2:
|
||||
end_point = destination[position]
|
||||
position = (position + 1) % count
|
||||
segment = end_point - start_point
|
||||
offset = point - start_point
|
||||
distance = abs(float(offset[0] * segment[1] - offset[1] * segment[0]))
|
||||
inner_product = float(np.dot(offset, end_point - point))
|
||||
removable = (
|
||||
distance * distance
|
||||
<= 0.5 * epsilon_squared * float(np.dot(segment, segment))
|
||||
and segment[0] != 0
|
||||
and segment[1] != 0
|
||||
and inner_product >= 0
|
||||
)
|
||||
if removable:
|
||||
new_count -= 1
|
||||
destination[write_position] = end_point
|
||||
start_point = end_point
|
||||
write_position = (write_position + 1) % count
|
||||
point = destination[position]
|
||||
position = (position + 1) % count
|
||||
index += 2
|
||||
continue
|
||||
|
||||
destination[write_position] = point
|
||||
start_point = point
|
||||
write_position = (write_position + 1) % count
|
||||
point = end_point
|
||||
index += 1
|
||||
|
||||
if not closed:
|
||||
destination[write_position] = point
|
||||
return np.asarray(destination[:new_count], dtype=np.float64)
|
||||
|
||||
|
||||
def _approx_poly_dp(
|
||||
contour: npt.NDArray[Any], epsilon: float, closed: bool
|
||||
) -> npt.NDArray[Any]:
|
||||
"""Approximate a contour with the supported OpenCV polygon contract."""
|
||||
if epsilon < 0:
|
||||
raise ValueError("epsilon must be non-negative")
|
||||
points = _as_points(contour)
|
||||
if len(points) == 0:
|
||||
dtype = np.asarray(contour).dtype
|
||||
return np.empty((0, 1, 2), dtype=dtype)
|
||||
|
||||
epsilon_squared = float(epsilon) ** 2
|
||||
simplified = _simplify_slices(points, epsilon_squared, closed)
|
||||
simplified = _cleanup_approximation(simplified, epsilon_squared, closed)
|
||||
|
||||
dtype = np.asarray(contour).dtype
|
||||
return simplified.astype(dtype, copy=False).reshape(-1, 1, 2)
|
||||
|
||||
|
||||
def _cross(edge: npt.NDArray[np.float64], point: npt.NDArray[np.float64]) -> float:
|
||||
"""Return the two-dimensional cross product of two vectors."""
|
||||
return float(edge[0] * point[1] - edge[1] * point[0])
|
||||
|
||||
|
||||
def _intersect_convex_convex(
|
||||
first: npt.NDArray[Any],
|
||||
second: npt.NDArray[Any],
|
||||
handle_nested: bool = True,
|
||||
) -> tuple[float, npt.NDArray[Any]]:
|
||||
"""Clip two convex polygons and return their intersection area and vertices."""
|
||||
del handle_nested
|
||||
subject = _as_points(first)
|
||||
clip = _as_points(second)
|
||||
output = subject
|
||||
if len(subject) < 3 or len(clip) < 3:
|
||||
return 0.0, np.empty((0, 1, 2), dtype=np.float32)
|
||||
|
||||
orientation = 1.0 if _contour_area(clip, oriented=True) >= 0 else -1.0
|
||||
for index, clip_start in enumerate(clip):
|
||||
clip_end = clip[(index + 1) % len(clip)]
|
||||
edge = clip_end - clip_start
|
||||
input_points = output
|
||||
if len(input_points) == 0:
|
||||
break
|
||||
output_points: list[npt.NDArray[np.float64]] = []
|
||||
previous = input_points[-1]
|
||||
previous_inside = orientation * _cross(edge, previous - clip_start) >= 0
|
||||
for current in input_points:
|
||||
current_inside = orientation * _cross(edge, current - clip_start) >= 0
|
||||
if current_inside != previous_inside:
|
||||
direction = current - previous
|
||||
denominator = _cross(edge, direction)
|
||||
if denominator != 0:
|
||||
factor = _cross(edge, clip_start - previous) / denominator
|
||||
output_points.append(previous + factor * direction)
|
||||
if current_inside:
|
||||
output_points.append(current)
|
||||
previous = current
|
||||
previous_inside = current_inside
|
||||
output = np.asarray(output_points, dtype=np.float64)
|
||||
|
||||
if len(output) == 0:
|
||||
dtype = np.asarray(first).dtype
|
||||
result_dtype = dtype if np.issubdtype(dtype, np.floating) else np.float32
|
||||
return 0.0, np.empty((0, 1, 2), dtype=result_dtype)
|
||||
|
||||
output = output[np.r_[True, np.any(np.diff(output, axis=0) != 0, axis=1)]]
|
||||
if len(output) > 1 and np.array_equal(output[0], output[-1]):
|
||||
output = output[:-1]
|
||||
area = _contour_area(output)
|
||||
dtype = np.asarray(first).dtype
|
||||
result_dtype = dtype if np.issubdtype(dtype, np.floating) else np.float32
|
||||
return area, output.astype(result_dtype, copy=False).reshape(-1, 1, 2)
|
||||
|
|
@ -0,0 +1,299 @@
|
|||
"""Private image-operation and image-I/O fallbacks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, cast
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from supervision._cv2._common import _cast_array_like_opencv
|
||||
from supervision._cv2.constants import (
|
||||
_BORDER_CONSTANT,
|
||||
_IMREAD_COLOR,
|
||||
_IMREAD_UNCHANGED,
|
||||
_INTER_LINEAR,
|
||||
_INTER_NEAREST,
|
||||
)
|
||||
|
||||
|
||||
def _flip(image: npt.NDArray[Any], flip_code: int) -> npt.NDArray[Any]:
|
||||
"""Flip an image vertically, horizontally, or along both axes."""
|
||||
if flip_code == 0:
|
||||
axes: tuple[int, ...] = (0,)
|
||||
elif flip_code == 1:
|
||||
axes = (1,)
|
||||
elif flip_code == -1:
|
||||
axes = (0, 1)
|
||||
else:
|
||||
raise ValueError(f"Unsupported flip code: {flip_code}")
|
||||
return np.ascontiguousarray(np.flip(image, axis=axes))
|
||||
|
||||
|
||||
def _copy_make_border(
|
||||
image: npt.NDArray[Any],
|
||||
top: int,
|
||||
bottom: int,
|
||||
left: int,
|
||||
right: int,
|
||||
border_type: int,
|
||||
value: int | float | Sequence[int | float] = 0,
|
||||
) -> npt.NDArray[Any]:
|
||||
"""Add a constant border around an image."""
|
||||
if border_type != _BORDER_CONSTANT:
|
||||
raise ValueError("Only BORDER_CONSTANT is supported by the fallback")
|
||||
if min(top, bottom, left, right) < 0:
|
||||
raise ValueError("Border sizes must be non-negative")
|
||||
|
||||
height, width = image.shape[:2]
|
||||
shape = (height + top + bottom, width + left + right, *image.shape[2:])
|
||||
|
||||
# OpenCV's Scalar(v) fills only channel 0 and zero-pads the rest for
|
||||
# multichannel images — a bare scalar is treated the same as a
|
||||
# length-1 sequence, not broadcast to every channel.
|
||||
sequence_value = value if isinstance(value, Sequence) else (value,)
|
||||
values = np.asarray(sequence_value, dtype=image.dtype).reshape(-1)
|
||||
if image.ndim == 2:
|
||||
fill_value: Any = values[0] if values.size else 0
|
||||
else:
|
||||
fill = np.zeros(image.shape[2], dtype=image.dtype)
|
||||
fill[: min(values.size, image.shape[2])] = values[: image.shape[2]]
|
||||
fill_value = fill.reshape((1, 1, -1))
|
||||
|
||||
result = np.full(shape, fill_value, dtype=image.dtype)
|
||||
result[top : top + height, left : left + width] = image
|
||||
return result
|
||||
|
||||
|
||||
def _add_weighted(
|
||||
src1: npt.NDArray[Any],
|
||||
alpha: float,
|
||||
src2: npt.NDArray[Any],
|
||||
beta: float,
|
||||
gamma: float,
|
||||
dst: npt.NDArray[Any] | None = None,
|
||||
dtype: int | None = None,
|
||||
) -> npt.NDArray[Any]:
|
||||
"""Blend two arrays with OpenCV-compatible saturation and optional mutation."""
|
||||
if dtype is not None and dtype != -1:
|
||||
raise ValueError(
|
||||
"addWeighted fallback only supports the default output depth; "
|
||||
f"unsupported dtype: {dtype}"
|
||||
)
|
||||
if src1.shape != src2.shape:
|
||||
raise ValueError("addWeighted inputs must have equal shapes")
|
||||
result = _cast_array_like_opencv(
|
||||
src1.astype(np.float64) * alpha + src2.astype(np.float64) * beta + gamma,
|
||||
src1.dtype,
|
||||
)
|
||||
if dst is not None:
|
||||
dst[...] = result
|
||||
return dst
|
||||
return result
|
||||
|
||||
|
||||
def _convert_scale_abs(
|
||||
image: npt.NDArray[Any], alpha: float = 1, beta: float = 0
|
||||
) -> npt.NDArray[np.uint8]:
|
||||
"""Scale, offset, take the absolute value, and saturate to uint8."""
|
||||
values = np.abs(image.astype(np.float64) * alpha + beta)
|
||||
return _cast_array_like_opencv(values, np.dtype(np.uint8))
|
||||
|
||||
|
||||
def _mean(
|
||||
image: npt.NDArray[Any], mask: npt.NDArray[Any] | None = None
|
||||
) -> tuple[float, float, float, float]:
|
||||
"""Return per-channel means using OpenCV's four-value result contract."""
|
||||
if mask is None:
|
||||
selected = (
|
||||
image.reshape(-1, 1)
|
||||
if image.ndim == 2
|
||||
else image.reshape(-1, image.shape[2])
|
||||
)
|
||||
else:
|
||||
if mask.shape != image.shape[:2]:
|
||||
raise ValueError("Mean mask must match the image height and width")
|
||||
selected = image[mask != 0]
|
||||
if image.ndim == 2:
|
||||
selected = selected.reshape(-1, 1)
|
||||
if selected.size == 0:
|
||||
means = np.zeros(4, dtype=np.float64)
|
||||
else:
|
||||
means = np.zeros(4, dtype=np.float64)
|
||||
means[: selected.shape[1]] = np.mean(selected, axis=0)
|
||||
return cast(
|
||||
tuple[float, float, float, float],
|
||||
tuple(float(value) for value in means),
|
||||
)
|
||||
|
||||
|
||||
def _resize(
|
||||
src: npt.NDArray[Any],
|
||||
dsize: tuple[int, int] | None,
|
||||
fx: float = 0,
|
||||
fy: float = 0,
|
||||
interpolation: int = _INTER_LINEAR,
|
||||
) -> npt.NDArray[Any]:
|
||||
"""Resize with exact nearest or OpenCV-compatible linear sampling."""
|
||||
source_height, source_width = src.shape[:2]
|
||||
width, height = dsize if dsize is not None else (0, 0)
|
||||
if width == 0 or height == 0:
|
||||
width = round(source_width * fx)
|
||||
height = round(source_height * fy)
|
||||
if min(width, height, source_width, source_height) <= 0:
|
||||
raise ValueError("Resize dimensions must be positive")
|
||||
|
||||
if interpolation == _INTER_NEAREST:
|
||||
y_indices = np.minimum(
|
||||
(np.arange(height) * source_height // height), source_height - 1
|
||||
)
|
||||
x_indices = np.minimum(
|
||||
(np.arange(width) * source_width // width), source_width - 1
|
||||
)
|
||||
return np.ascontiguousarray(src[y_indices[:, np.newaxis], x_indices])
|
||||
|
||||
if interpolation != _INTER_LINEAR:
|
||||
raise ValueError(f"Unsupported interpolation mode: {interpolation}")
|
||||
|
||||
if src.dtype == np.uint8 and (
|
||||
src.ndim == 2 or (src.ndim == 3 and src.shape[2] == 3)
|
||||
):
|
||||
from PIL import Image
|
||||
|
||||
size = (width, height)
|
||||
image = Image.fromarray(src)
|
||||
if width >= source_width and height >= source_height:
|
||||
resized = image.resize(size, resample=Image.Resampling.BILINEAR)
|
||||
else:
|
||||
# Affine sampling keeps Pillow from widening its bilinear kernel
|
||||
# during reduction and maps pixel centers like INTER_LINEAR.
|
||||
resized = image.transform(
|
||||
size,
|
||||
Image.Transform.AFFINE,
|
||||
(source_width / width, 0, 0, 0, source_height / height, 0),
|
||||
resample=Image.Resampling.BILINEAR,
|
||||
)
|
||||
return np.ascontiguousarray(np.asarray(resized))
|
||||
|
||||
y = (np.arange(height) + 0.5) * source_height / height - 0.5
|
||||
x = (np.arange(width) + 0.5) * source_width / width - 0.5
|
||||
y_floor = np.floor(y).astype(np.int64)
|
||||
x_floor = np.floor(x).astype(np.int64)
|
||||
y0 = np.clip(y_floor, 0, source_height - 1)
|
||||
y1 = np.clip(y_floor + 1, 0, source_height - 1)
|
||||
x0 = np.clip(x_floor, 0, source_width - 1)
|
||||
x1 = np.clip(x_floor + 1, 0, source_width - 1)
|
||||
wy = y - y_floor
|
||||
wx = x - x_floor
|
||||
|
||||
source = src.astype(np.float64)
|
||||
top_left = source[y0[:, np.newaxis], x0]
|
||||
top_right = source[y0[:, np.newaxis], x1]
|
||||
bottom_left = source[y1[:, np.newaxis], x0]
|
||||
bottom_right = source[y1[:, np.newaxis], x1]
|
||||
if src.ndim == 3:
|
||||
wy = wy[:, np.newaxis, np.newaxis]
|
||||
wx = wx[np.newaxis, :, np.newaxis]
|
||||
else:
|
||||
wy = wy[:, np.newaxis]
|
||||
wx = wx[np.newaxis, :]
|
||||
resized = (
|
||||
top_left * (1 - wx) * (1 - wy)
|
||||
+ top_right * wx * (1 - wy)
|
||||
+ bottom_left * (1 - wx) * wy
|
||||
+ bottom_right * wx * wy
|
||||
)
|
||||
return np.ascontiguousarray(_cast_array_like_opencv(resized, src.dtype))
|
||||
|
||||
|
||||
def _read_pil_source(source: Any, flags: int) -> npt.NDArray[Any] | None:
|
||||
"""Decode any source Pillow can open into BGR or BGRA arrays."""
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
with Image.open(source) as image:
|
||||
if flags == _IMREAD_UNCHANGED:
|
||||
if image.mode == "P":
|
||||
converted = image.convert(
|
||||
"RGBA" if "transparency" in image.info else "RGB"
|
||||
)
|
||||
values = np.asarray(converted)
|
||||
converted.close()
|
||||
else:
|
||||
values = np.asarray(image)
|
||||
elif image.mode in {"I", "I;16", "I;16B", "I;16L"}:
|
||||
values = np.asarray(image).astype(np.float64)
|
||||
values = np.clip(np.rint(values / 256), 0, 255).astype(np.uint8)
|
||||
if values.ndim == 2:
|
||||
values = np.repeat(values[..., np.newaxis], 3, axis=2)
|
||||
else:
|
||||
values = np.asarray(image.convert("RGB"))
|
||||
except (FileNotFoundError, OSError, ValueError):
|
||||
return None
|
||||
|
||||
if values.ndim == 3 and values.shape[2] == 3:
|
||||
values = values[..., ::-1]
|
||||
elif values.ndim == 3 and values.shape[2] == 4:
|
||||
values = values[..., [2, 1, 0, 3]]
|
||||
return np.ascontiguousarray(values)
|
||||
|
||||
|
||||
def _imread(filename: str, flags: int = _IMREAD_COLOR) -> npt.NDArray[Any] | None:
|
||||
"""Read an image with Pillow while returning BGR or BGRA arrays."""
|
||||
return _read_pil_source(filename, flags)
|
||||
|
||||
|
||||
def _imdecode(
|
||||
buf: npt.NDArray[Any], flags: int = _IMREAD_COLOR
|
||||
) -> npt.NDArray[Any] | None:
|
||||
"""Decode in-memory encoded image bytes, returning BGR or BGRA arrays."""
|
||||
import io
|
||||
|
||||
data = np.asarray(buf, dtype=np.uint8).tobytes()
|
||||
return _read_pil_source(io.BytesIO(data), flags)
|
||||
|
||||
|
||||
def _bgr_to_pil_values(image: npt.NDArray[Any]) -> npt.NDArray[Any]:
|
||||
"""Reorder BGR or BGRA channels into the RGB order Pillow expects."""
|
||||
values = np.asarray(image)
|
||||
if values.ndim == 3 and values.shape[2] == 3:
|
||||
values = values[..., ::-1]
|
||||
elif values.ndim == 3 and values.shape[2] == 4:
|
||||
values = values[..., [2, 1, 0, 3]]
|
||||
return np.ascontiguousarray(values)
|
||||
|
||||
|
||||
def _imwrite(
|
||||
filename: str, image: npt.NDArray[Any], params: Sequence[int] | None = None
|
||||
) -> bool:
|
||||
"""Write a BGR or BGRA array with Pillow and return OpenCV's boolean status."""
|
||||
from PIL import Image
|
||||
|
||||
del params
|
||||
try:
|
||||
Image.fromarray(_bgr_to_pil_values(image)).save(filename)
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _imencode(
|
||||
ext: str, image: npt.NDArray[Any], params: Sequence[int] | None = None
|
||||
) -> tuple[bool, npt.NDArray[np.uint8] | None]:
|
||||
"""Encode a BGR or BGRA array in memory, mirroring `cv2.imencode`'s return."""
|
||||
import io
|
||||
|
||||
from PIL import Image
|
||||
|
||||
del params
|
||||
# Pillow registers the JPEG codec as "JPEG", not the "jpg" file extension.
|
||||
image_format = ext.lstrip(".").upper()
|
||||
if image_format == "JPG":
|
||||
image_format = "JPEG"
|
||||
buffer = io.BytesIO()
|
||||
try:
|
||||
Image.fromarray(_bgr_to_pil_values(image)).save(buffer, format=image_format)
|
||||
except (KeyError, OSError, ValueError):
|
||||
return False, None
|
||||
return True, np.frombuffer(buffer.getvalue(), dtype=np.uint8)
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
"""Private Pillow-based text fallback for the OpenCV compatibility facade.
|
||||
|
||||
OpenCV renders text with built-in Hershey stroke fonts. The fallback instead
|
||||
draws a proportional TrueType face (DejaVu Sans, shipped with Matplotlib, an
|
||||
existing required dependency), so glyph shapes and text metrics differ from
|
||||
OpenCV within the documented visual-divergence tier. ``getTextSize`` derives
|
||||
its box from the same font ``putText`` renders with, so the reported rectangle
|
||||
always encloses the drawn text.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import cache
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from supervision._cv2._drawing import _paint
|
||||
from supervision._cv2.constants import _FONT_ITALIC, _LINE_8
|
||||
|
||||
_ImageArray = npt.NDArray[Any]
|
||||
|
||||
# OpenCV's font_scale is unit-relative rather than a pixel size. This factor
|
||||
# maps it to a Pillow point size whose cap height lands near OpenCV's Hershey
|
||||
# Simplex at the same scale; it is a readability choice, not an exact metric
|
||||
# match (which no proportional TrueType face can provide).
|
||||
_PIXELS_PER_SCALE = 32
|
||||
|
||||
|
||||
@cache
|
||||
def _font_path(italic: bool) -> str:
|
||||
"""Locate a bundled DejaVu Sans face through Matplotlib's font manager."""
|
||||
from matplotlib import font_manager
|
||||
|
||||
style = "italic" if italic else "normal"
|
||||
properties = font_manager.FontProperties(family="DejaVu Sans", style=style)
|
||||
return str(font_manager.findfont(properties))
|
||||
|
||||
|
||||
@cache
|
||||
def _load_font(font_face: int, font_scale: float) -> Any:
|
||||
"""Return a cached Pillow font sized for an OpenCV font scale."""
|
||||
from PIL import ImageFont
|
||||
|
||||
size = max(1, round(font_scale * _PIXELS_PER_SCALE))
|
||||
return ImageFont.truetype(_font_path(bool(font_face & _FONT_ITALIC)), size)
|
||||
|
||||
|
||||
def _stroke_width(thickness: int) -> int:
|
||||
"""Map an OpenCV stroke thickness to the Pillow stroke_width it renders with."""
|
||||
return max(0, thickness - 1)
|
||||
|
||||
|
||||
def _get_text_size(
|
||||
text: str, fontFace: int, fontScale: float, thickness: int
|
||||
) -> tuple[tuple[int, int], int]:
|
||||
"""Return an OpenCV-shaped ``((width, height), baseline)`` for the face.
|
||||
|
||||
Height is the font ascent and baseline the descent, both string-independent
|
||||
like OpenCV's contract, so consumers get stable row heights. Padding uses
|
||||
the same stroke_width _put_text renders with: Pillow's stroke dilates the
|
||||
glyph outline by stroke_width pixels on every side, so width grows by
|
||||
twice that (left and right) while height and baseline each grow by one
|
||||
stroke_width (top and bottom).
|
||||
"""
|
||||
font = _load_font(fontFace, fontScale)
|
||||
ascent, descent = font.getmetrics()
|
||||
stroke_width = _stroke_width(thickness)
|
||||
width = round(font.getlength(text)) + 2 * stroke_width
|
||||
height = ascent + stroke_width
|
||||
baseline = descent + stroke_width
|
||||
return (width, height), baseline
|
||||
|
||||
|
||||
def _put_text(
|
||||
img: _ImageArray,
|
||||
text: str,
|
||||
org: tuple[int, int],
|
||||
fontFace: int,
|
||||
fontScale: float,
|
||||
color: Any,
|
||||
thickness: int = 1,
|
||||
lineType: int = _LINE_8,
|
||||
bottomLeftOrigin: bool = False,
|
||||
) -> _ImageArray:
|
||||
"""Render text with a Pillow face, anchored at OpenCV's baseline origin.
|
||||
|
||||
Thickness maps to a Pillow stroke width to emulate OpenCV's bolder strokes.
|
||||
``bottomLeftOrigin`` (an inverted-axis mode no Supervision caller uses) is
|
||||
rejected rather than silently ignored.
|
||||
"""
|
||||
del lineType
|
||||
if bottomLeftOrigin:
|
||||
raise ValueError("bottomLeftOrigin is not supported by the fallback")
|
||||
if not text:
|
||||
return img
|
||||
|
||||
font = _load_font(fontFace, fontScale)
|
||||
stroke_width = _stroke_width(thickness)
|
||||
x, y = round(org[0]), round(org[1])
|
||||
left, top, right, bottom = font.getbbox(
|
||||
text, anchor="ls", stroke_width=stroke_width
|
||||
)
|
||||
width, height = right - left, bottom - top
|
||||
if width <= 0 or height <= 0:
|
||||
return img
|
||||
|
||||
mask_image = Image.new("1", (width, height))
|
||||
ImageDraw.Draw(mask_image).text(
|
||||
(-left, -top),
|
||||
text,
|
||||
fill=1,
|
||||
font=font,
|
||||
anchor="ls",
|
||||
stroke_width=stroke_width,
|
||||
)
|
||||
mask = np.asarray(mask_image, dtype=bool)
|
||||
|
||||
image_height, image_width = img.shape[:2]
|
||||
x_start, y_start = max(0, x + left), max(0, y + top)
|
||||
x_stop, y_stop = min(image_width, x + right), min(image_height, y + bottom)
|
||||
if x_start >= x_stop or y_start >= y_stop:
|
||||
return img
|
||||
mask_x = x_start - (x + left)
|
||||
mask_y = y_start - (y + top)
|
||||
clipped_mask = mask[
|
||||
mask_y : mask_y + (y_stop - y_start),
|
||||
mask_x : mask_x + (x_stop - x_start),
|
||||
]
|
||||
_paint(img[y_start:y_stop, x_start:x_stop], clipped_mask, color)
|
||||
return img
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
"""Private transform and filter fallbacks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from supervision._cv2._common import _cast_array_like_opencv
|
||||
|
||||
|
||||
def _blur(
|
||||
image: npt.NDArray[Any], ksize: tuple[int, int], border_type: int = 4
|
||||
) -> npt.NDArray[Any]:
|
||||
"""Apply a box filter with OpenCV's default reflect-101 boundary behavior."""
|
||||
if min(ksize) <= 0:
|
||||
raise ValueError("Blur kernel dimensions must be positive")
|
||||
if border_type != 4:
|
||||
raise ValueError("Only OpenCV's default blur border is supported")
|
||||
|
||||
from scipy import ndimage
|
||||
|
||||
size = (*ksize[::-1], 1) if image.ndim == 3 else ksize[::-1]
|
||||
values = ndimage.uniform_filter(image.astype(np.float64), size=size, mode="mirror")
|
||||
return np.ascontiguousarray(_cast_array_like_opencv(values, image.dtype))
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
"""Private PyAV-backed video and audio fallbacks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
from collections.abc import Callable, Iterator
|
||||
from fractions import Fraction
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import av
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from supervision._cv2._common import BackendUnavailableError
|
||||
from supervision._cv2.constants import (
|
||||
_CAP_PROP_FPS,
|
||||
_CAP_PROP_FRAME_COUNT,
|
||||
_CAP_PROP_FRAME_HEIGHT,
|
||||
_CAP_PROP_FRAME_WIDTH,
|
||||
_CAP_PROP_POS_FRAMES,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_CODECS = {
|
||||
"mp4v": ("mpeg4", "yuv420p"),
|
||||
"xvid": ("mpeg4", "yuv420p"),
|
||||
"avc1": ("libx264", "yuv420p"),
|
||||
"h264": ("libx264", "yuv420p"),
|
||||
"mjpg": ("mjpeg", "yuvj420p"),
|
||||
"vp09": ("libvpx-vp9", "yuv420p"),
|
||||
}
|
||||
|
||||
|
||||
def _video_writer_fourcc(*chars: str) -> int:
|
||||
"""Encode four single-character strings using OpenCV's integer layout."""
|
||||
if len(chars) != 4 or any(len(char) != 1 for char in chars):
|
||||
raise TypeError("VideoWriter_fourcc requires exactly four characters")
|
||||
return sum(ord(char) << (8 * index) for index, char in enumerate(chars))
|
||||
|
||||
|
||||
def _decode_fourcc(fourcc: int) -> str:
|
||||
"""Decode a fourcc integer into its four-character representation."""
|
||||
return "".join(chr((fourcc >> (8 * index)) & 0xFF) for index in range(4))
|
||||
|
||||
|
||||
def _codec_details(fourcc: int) -> tuple[str, str]:
|
||||
"""Return the PyAV codec and pixel format for a supported fourcc."""
|
||||
code = _decode_fourcc(fourcc).lower()
|
||||
try:
|
||||
return _CODECS[code]
|
||||
except KeyError as exc:
|
||||
raise ValueError(f"Unsupported video codec: {code!r}") from exc
|
||||
|
||||
|
||||
class _VideoCapture:
|
||||
"""Expose OpenCV-shaped file capture backed by PyAV decoding."""
|
||||
|
||||
def __init__(self, source: str | os.PathLike[str] | int) -> None:
|
||||
"""Open a file source and retain a lazy PyAV frame iterator."""
|
||||
self._container: Any = None
|
||||
self._stream: Any = None
|
||||
self._frames: Iterator[Any] | None = None
|
||||
self._source = source
|
||||
self._position = 0
|
||||
self._frame_count_cache: int | None = None
|
||||
self._opened = False
|
||||
self._error: Exception | None = None
|
||||
|
||||
try:
|
||||
if isinstance(source, int):
|
||||
raise BackendUnavailableError(
|
||||
"PyAV fallback supports file paths, not webcam device indexes."
|
||||
)
|
||||
self._container = av.open(str(source), mode="r")
|
||||
if not self._container.streams.video:
|
||||
raise ValueError(f"Video source has no video stream: {source}")
|
||||
self._stream = self._container.streams.video[0]
|
||||
self._frames = iter(self._container.decode(video=self._stream.index))
|
||||
self._opened = True
|
||||
except Exception as exc:
|
||||
self._error = exc
|
||||
logger.warning("Failed to open video source %r: %s", source, exc)
|
||||
self.release()
|
||||
|
||||
def isOpened(self) -> bool:
|
||||
"""Return whether the underlying video file is open for reading."""
|
||||
return self._opened
|
||||
|
||||
def _frame_count(self) -> int:
|
||||
"""Return the stream count, decoding a second handle if metadata lacks it."""
|
||||
if self._frame_count_cache is not None:
|
||||
return self._frame_count_cache
|
||||
|
||||
count = int(getattr(self._stream, "frames", 0) or 0)
|
||||
if count <= 0 and not isinstance(self._source, int):
|
||||
container = av.open(str(self._source), mode="r")
|
||||
try:
|
||||
count = sum(1 for _ in container.decode(video=self._stream.index))
|
||||
finally:
|
||||
container.close()
|
||||
self._frame_count_cache = count
|
||||
return count
|
||||
|
||||
def get(self, property_id: int) -> float:
|
||||
"""Return the supported OpenCV capture property as a float."""
|
||||
if not self._opened:
|
||||
return 0.0
|
||||
if property_id == _CAP_PROP_FRAME_WIDTH:
|
||||
return float(self._stream.width)
|
||||
if property_id == _CAP_PROP_FRAME_HEIGHT:
|
||||
return float(self._stream.height)
|
||||
if property_id == _CAP_PROP_FPS:
|
||||
rate = getattr(self._stream, "average_rate", None) or getattr(
|
||||
self._stream, "base_rate", None
|
||||
)
|
||||
return float(rate) if rate is not None else 0.0
|
||||
if property_id == _CAP_PROP_FRAME_COUNT:
|
||||
return float(self._frame_count())
|
||||
if property_id == _CAP_PROP_POS_FRAMES:
|
||||
return float(self._position)
|
||||
return 0.0
|
||||
|
||||
def _reset(self) -> None:
|
||||
"""Seek the decoder to the first frame and reset the logical position."""
|
||||
self._container.seek(0, stream=self._stream, backward=True)
|
||||
self._frames = iter(self._container.decode(video=self._stream.index))
|
||||
self._position = 0
|
||||
|
||||
def set(self, property_id: int, value: float) -> bool:
|
||||
"""Set the supported frame-position property using exact frame decoding."""
|
||||
if not self._opened or property_id != _CAP_PROP_POS_FRAMES:
|
||||
return False
|
||||
target = max(0, round(value))
|
||||
if target > self._frame_count():
|
||||
return False
|
||||
if target < self._position:
|
||||
self._reset()
|
||||
while self._position < target:
|
||||
success, _ = self.read()
|
||||
if not success:
|
||||
return False
|
||||
return True
|
||||
|
||||
def read(self) -> tuple[bool, npt.NDArray[np.uint8] | None]:
|
||||
"""Decode and return the next frame in OpenCV's BGR array format."""
|
||||
if not self._opened or self._frames is None:
|
||||
return False, None
|
||||
try:
|
||||
frame = next(self._frames)
|
||||
except StopIteration:
|
||||
return False, None
|
||||
except Exception as exc:
|
||||
self._error = exc
|
||||
return False, None
|
||||
|
||||
self._position += 1
|
||||
return True, frame.to_ndarray(format="bgr24")
|
||||
|
||||
def grab(self) -> bool:
|
||||
"""Decode and discard one frame."""
|
||||
success, _ = self.read()
|
||||
return success
|
||||
|
||||
def release(self) -> None:
|
||||
"""Close the PyAV container and make subsequent reads return false."""
|
||||
container = self._container
|
||||
self._container = None
|
||||
self._stream = None
|
||||
self._frames = None
|
||||
self._opened = False
|
||||
if container is not None:
|
||||
container.close()
|
||||
|
||||
|
||||
class _VideoWriter:
|
||||
"""Expose OpenCV-shaped video writing backed by PyAV encoding."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
filename: str | os.PathLike[str],
|
||||
fourcc: int,
|
||||
fps: float,
|
||||
frame_size: tuple[int, int],
|
||||
is_color: bool = True,
|
||||
) -> None:
|
||||
"""Open a PyAV writer for the requested codec and frame dimensions.
|
||||
|
||||
The PyAV fallback always encodes 3-channel BGR frames, so grayscale
|
||||
output is unsupported. ``is_color=False`` is rejected up front rather
|
||||
than silently ignored, keeping the OpenCV-shaped contract honest for
|
||||
callers that would otherwise expect single-channel writes.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: If ``is_color`` is ``False``; grayscale
|
||||
writing is not supported by the PyAV fallback.
|
||||
"""
|
||||
if not is_color:
|
||||
raise NotImplementedError(
|
||||
"PyAV video fallback only supports color (3-channel BGR) frames; "
|
||||
"is_color=False is not implemented."
|
||||
)
|
||||
self._container: Any = None
|
||||
self._stream: Any = None
|
||||
self._width, self._height = frame_size
|
||||
self._opened = False
|
||||
self._error: Exception | None = None
|
||||
|
||||
try:
|
||||
codec, pixel_format = _codec_details(fourcc)
|
||||
self._container = av.open(str(filename), mode="w")
|
||||
rate = Fraction(str(fps)).limit_denominator(100_000)
|
||||
self._stream = self._container.add_stream(codec, rate=rate)
|
||||
self._stream.width = self._width
|
||||
self._stream.height = self._height
|
||||
self._stream.pix_fmt = pixel_format
|
||||
self._opened = True
|
||||
except Exception as exc:
|
||||
self._error = exc
|
||||
self.release()
|
||||
|
||||
def isOpened(self) -> bool:
|
||||
"""Return whether the writer initialized successfully."""
|
||||
return self._opened
|
||||
|
||||
def write(self, frame: npt.NDArray[np.uint8]) -> None:
|
||||
"""Encode one BGR frame and mux all packets produced by the encoder."""
|
||||
if not self._opened or self._container is None or self._stream is None:
|
||||
raise RuntimeError("Video writer is not open") from self._error
|
||||
if frame.shape != (self._height, self._width, 3):
|
||||
raise ValueError(
|
||||
"Video frame must have shape "
|
||||
f"({self._height}, {self._width}, 3), got {frame.shape}"
|
||||
)
|
||||
if frame.dtype != np.uint8:
|
||||
raise ValueError("Video frames must use uint8 dtype")
|
||||
|
||||
video_frame = av.VideoFrame.from_ndarray(
|
||||
np.ascontiguousarray(frame), format="bgr24"
|
||||
)
|
||||
for packet in self._stream.encode(video_frame):
|
||||
self._container.mux(packet)
|
||||
|
||||
def release(self) -> None:
|
||||
"""Flush delayed encoder packets and close the output container."""
|
||||
container = self._container
|
||||
stream = self._stream
|
||||
self._container = None
|
||||
self._stream = None
|
||||
self._opened = False
|
||||
if container is None:
|
||||
return
|
||||
try:
|
||||
if stream is not None:
|
||||
for packet in stream.encode():
|
||||
container.mux(packet)
|
||||
finally:
|
||||
container.close()
|
||||
|
||||
|
||||
def _timestamp_seconds(timestamp: int | None, time_base: Any) -> float | None:
|
||||
"""Convert a stream timestamp to seconds while preserving missing values."""
|
||||
return None if timestamp is None else float(timestamp * time_base)
|
||||
|
||||
|
||||
def _best_effort_cleanup(action: Callable[[], None], description: str) -> None:
|
||||
"""Run a cleanup action, logging and suppressing any failure.
|
||||
|
||||
Cleanup steps in a ``finally`` block must never raise, otherwise a failing
|
||||
``container.close()`` (or file removal) would mask or replace the primary
|
||||
result or the original exception that sent control into ``finally``.
|
||||
"""
|
||||
try:
|
||||
action()
|
||||
except Exception as exc:
|
||||
logger.debug("Cleanup step failed (%s): %s", description, exc)
|
||||
|
||||
|
||||
def _mux_audio(source_path: str, video_path: str) -> None:
|
||||
"""Remux the source's first audio stream into the processed video with PyAV."""
|
||||
source_container: Any = None
|
||||
video_container: Any = None
|
||||
output_container: Any = None
|
||||
temporary_path: str | None = None
|
||||
try:
|
||||
source_container = av.open(source_path, mode="r")
|
||||
video_container = av.open(video_path, mode="r")
|
||||
if not source_container.streams.audio or not video_container.streams.video:
|
||||
logger.info("No audio or video stream available; leaving output unchanged")
|
||||
return
|
||||
|
||||
source_audio = source_container.streams.audio[0]
|
||||
target_video = video_container.streams.video[0]
|
||||
suffix = Path(video_path).suffix
|
||||
with tempfile.NamedTemporaryFile(
|
||||
suffix=suffix, dir=str(Path(video_path).absolute().parent), delete=False
|
||||
) as temporary_file:
|
||||
temporary_path = temporary_file.name
|
||||
|
||||
output_container = av.open(temporary_path, mode="w")
|
||||
output_video = output_container.add_stream_from_template(target_video)
|
||||
output_audio = output_container.add_stream_from_template(source_audio)
|
||||
|
||||
# Copy encoded packets to avoid a lossy decode/re-encode cycle for both streams.
|
||||
video_base: int | None = None
|
||||
video_duration: float | None = None
|
||||
for packet in video_container.demux(target_video):
|
||||
if packet.pts is None and packet.dts is None:
|
||||
continue
|
||||
if video_base is None:
|
||||
video_base = packet.pts if packet.pts is not None else packet.dts
|
||||
if video_base is None:
|
||||
continue
|
||||
packet.pts = None if packet.pts is None else packet.pts - video_base
|
||||
packet.dts = None if packet.dts is None else packet.dts - video_base
|
||||
packet.stream = output_video
|
||||
output_container.mux(packet)
|
||||
packet_end = packet.pts
|
||||
if packet_end is not None:
|
||||
packet_end += packet.duration or 0
|
||||
packet_seconds = _timestamp_seconds(packet_end, target_video.time_base)
|
||||
if packet_seconds is not None:
|
||||
video_duration = max(video_duration or 0.0, packet_seconds)
|
||||
|
||||
# Rebase audio independently and stop at the processed video duration, matching
|
||||
# ffmpeg's `-shortest` behavior without requiring a system executable.
|
||||
audio_base: int | None = None
|
||||
for packet in source_container.demux(source_audio):
|
||||
if packet.pts is None and packet.dts is None:
|
||||
continue
|
||||
if audio_base is None:
|
||||
audio_base = packet.pts if packet.pts is not None else packet.dts
|
||||
if audio_base is None:
|
||||
continue
|
||||
relative_pts = None if packet.pts is None else packet.pts - audio_base
|
||||
relative_seconds = _timestamp_seconds(relative_pts, source_audio.time_base)
|
||||
if (
|
||||
video_duration is not None
|
||||
and relative_seconds is not None
|
||||
and relative_seconds > video_duration
|
||||
):
|
||||
break
|
||||
packet.pts = relative_pts
|
||||
packet.dts = None if packet.dts is None else packet.dts - audio_base
|
||||
packet.stream = output_audio
|
||||
output_container.mux(packet)
|
||||
|
||||
output_container.close()
|
||||
output_container = None
|
||||
video_container.close()
|
||||
video_container = None
|
||||
source_container.close()
|
||||
source_container = None
|
||||
os.replace(temporary_path, video_path)
|
||||
temporary_path = None
|
||||
except Exception as exc:
|
||||
logger.warning("Audio remuxing failed: %s. Output video has no audio.", exc)
|
||||
finally:
|
||||
# Cleanup runs best-effort: a failing close/remove here must not mask the
|
||||
# primary result or replace the original exception handled above.
|
||||
if output_container is not None:
|
||||
_best_effort_cleanup(output_container.close, "closing output container")
|
||||
if video_container is not None:
|
||||
_best_effort_cleanup(video_container.close, "closing video container")
|
||||
if source_container is not None:
|
||||
_best_effort_cleanup(source_container.close, "closing source container")
|
||||
if temporary_path is not None and os.path.exists(temporary_path):
|
||||
leftover_path = temporary_path
|
||||
_best_effort_cleanup(
|
||||
lambda: os.remove(leftover_path), "removing temporary file"
|
||||
)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
"""Private numeric constants used by the OpenCV compatibility modules."""
|
||||
|
||||
_BORDER_CONSTANT = 0
|
||||
_CAP_PROP_FPS = 5
|
||||
_CAP_PROP_FRAME_COUNT = 7
|
||||
_CAP_PROP_FRAME_HEIGHT = 4
|
||||
_CAP_PROP_FRAME_WIDTH = 3
|
||||
_CAP_PROP_POS_FRAMES = 1
|
||||
_CC_STAT_AREA = 4
|
||||
_CHAIN_APPROX_SIMPLE = 2
|
||||
_COLOR_BGR2GRAY = 6
|
||||
_COLOR_BGR2RGB = 4
|
||||
_COLOR_GRAY2BGR = 8
|
||||
_COLOR_HSV2BGR = 54
|
||||
_COLOR_RGB2BGR = 4
|
||||
_FONT_HERSHEY_SIMPLEX = 0
|
||||
_FONT_HERSHEY_PLAIN = 1
|
||||
_FONT_HERSHEY_DUPLEX = 2
|
||||
_FONT_HERSHEY_COMPLEX = 3
|
||||
_FONT_HERSHEY_TRIPLEX = 4
|
||||
_FONT_HERSHEY_COMPLEX_SMALL = 5
|
||||
_FONT_HERSHEY_SCRIPT_SIMPLEX = 6
|
||||
_FONT_HERSHEY_SCRIPT_COMPLEX = 7
|
||||
_FONT_ITALIC = 16
|
||||
_IMREAD_COLOR = 1
|
||||
_IMREAD_UNCHANGED = -1
|
||||
_INTER_LINEAR = 1
|
||||
_INTER_NEAREST = 0
|
||||
_LINE_4 = 4
|
||||
_LINE_8 = 8
|
||||
_LINE_AA = 16
|
||||
_RETR_TREE = 3
|
||||
|
|
@ -5,6 +5,15 @@ from supervision.detection.core import Detections
|
|||
|
||||
|
||||
class BaseAnnotator(ABC):
|
||||
"""Base class for annotators that consume :class:`Detections`.
|
||||
|
||||
Attributes:
|
||||
requires_mask: Whether integrations must provide ``Detections.mask`` for
|
||||
this annotator. Check this before materializing expensive mask payloads.
|
||||
"""
|
||||
|
||||
requires_mask: bool = False
|
||||
|
||||
@abstractmethod
|
||||
def annotate(
|
||||
self, scene: Any, detections: Detections, *args: Any, **kwargs: Any
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from functools import lru_cache
|
||||
from math import sqrt
|
||||
from typing import Any, cast, overload
|
||||
from typing import Any, ClassVar, cast
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from deprecate import deprecated, void
|
||||
from deprecate import deprecated, void # type: ignore[import-untyped,unused-ignore]
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from scipy.interpolate import splev, splprep
|
||||
|
||||
from supervision import _cv2 as cv2
|
||||
from supervision.annotators.base import BaseAnnotator
|
||||
from supervision.annotators.utils import (
|
||||
PENDING_TRACK_ID,
|
||||
|
|
@ -35,6 +34,7 @@ from supervision.detection.utils.converters import (
|
|||
polygon_to_mask,
|
||||
xyxy_to_polygons,
|
||||
)
|
||||
from supervision.detection.utils.masks import _masks_to_roi
|
||||
from supervision.draw.base import ImageType
|
||||
from supervision.draw.color import Color, ColorPalette
|
||||
from supervision.draw.utils import draw_polygon, draw_rounded_rectangle, draw_text
|
||||
|
|
@ -44,9 +44,9 @@ from supervision.utils.conversion import (
|
|||
ensure_pil_image_for_class_method,
|
||||
)
|
||||
from supervision.utils.image import (
|
||||
_overlay_image,
|
||||
crop_image,
|
||||
letterbox_image,
|
||||
overlay_image,
|
||||
scale_image,
|
||||
)
|
||||
from supervision.utils.logger import _get_logger
|
||||
|
|
@ -54,14 +54,19 @@ 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: ...
|
||||
@lru_cache
|
||||
def _load_icon_from_path(
|
||||
icon_path: str, icon_resolution_wh: tuple[int, int]
|
||||
) -> npt.NDArray[np.uint8]:
|
||||
"""Load and resize an icon image through a cache shared by annotators."""
|
||||
icon = cv2.imread(icon_path, cv2.IMREAD_UNCHANGED)
|
||||
if icon is None:
|
||||
raise FileNotFoundError(f"Error: Couldn't load the icon image from {icon_path}")
|
||||
icon_array = cast(npt.NDArray[np.uint8], icon)
|
||||
result: npt.NDArray[np.uint8] = letterbox_image(
|
||||
image=icon_array, resolution_wh=icon_resolution_wh
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _normalize_color_input(color: Color | ColorPalette | str) -> Color | ColorPalette:
|
||||
|
|
@ -149,7 +154,7 @@ class _BaseLabelAnnotator(BaseAnnotator):
|
|||
resolution_wh: tuple[int, int],
|
||||
labels: list[str],
|
||||
label_properties: npt.NDArray[np.float32],
|
||||
) -> npt.NDArray[np.uint8]:
|
||||
) -> npt.NDArray[np.float32]:
|
||||
"""
|
||||
Adjusts the position of labels to ensure they stay within the frame boundaries.
|
||||
|
||||
|
|
@ -184,7 +189,10 @@ class _BaseLabelAnnotator(BaseAnnotator):
|
|||
adjusted_properties[:, :4], resolution_wh
|
||||
)
|
||||
|
||||
return adjusted_properties
|
||||
return cast(
|
||||
npt.NDArray[np.float32],
|
||||
np.asarray(adjusted_properties, dtype=np.float32),
|
||||
)
|
||||
|
||||
|
||||
class BoxAnnotator(BaseAnnotator):
|
||||
|
|
@ -322,7 +330,7 @@ class OrientedBoxAnnotator(BaseAnnotator):
|
|||
|
||||
Example:
|
||||
```python
|
||||
import cv2
|
||||
from supervision import _cv2 as cv2
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
|
||||
|
|
@ -361,6 +369,129 @@ class OrientedBoxAnnotator(BaseAnnotator):
|
|||
return scene
|
||||
|
||||
|
||||
# --- Shared mask-painting utilities ---
|
||||
def _iter_mask_crops(
|
||||
detections: Detections,
|
||||
) -> Iterator[tuple[int, npt.NDArray[np.bool_], npt.NDArray[np.int32] | None]]:
|
||||
"""Yield ``(detection_idx, mask_or_crop, offset_or_None)`` for each mask.
|
||||
|
||||
Encapsulates the ``CompactMask`` vs dense dispatch so individual annotators
|
||||
do not need inline ``isinstance`` checks. For ``CompactMask`` inputs yields
|
||||
the bbox crop and its ``(x1, y1)`` image-space origin; for dense masks
|
||||
yields the full-frame boolean slice with ``offset=None``.
|
||||
|
||||
Args:
|
||||
detections: Object detections whose masks to iterate.
|
||||
|
||||
Yields:
|
||||
Tuple of ``(detection_idx, mask_or_crop, offset_or_None)``.
|
||||
``mask_or_crop`` is boolean (crop-sized for ``CompactMask``, full-frame
|
||||
for dense). ``offset_or_None`` is an int32 ``(x1, y1)`` array for
|
||||
crop→image translation, or ``None`` for dense masks.
|
||||
"""
|
||||
masks = detections.mask
|
||||
if masks is None:
|
||||
return
|
||||
# TODO: replace isinstance dispatch with a MaskLike Protocol (separate PR)
|
||||
compact_mask = masks if isinstance(masks, CompactMask) else None
|
||||
for detection_idx in range(len(detections)):
|
||||
if compact_mask is None:
|
||||
yield (
|
||||
detection_idx,
|
||||
cast(npt.NDArray[np.bool_], masks[detection_idx]),
|
||||
None,
|
||||
)
|
||||
else:
|
||||
yield (
|
||||
detection_idx,
|
||||
compact_mask.crop(detection_idx),
|
||||
compact_mask.offsets[detection_idx],
|
||||
)
|
||||
|
||||
|
||||
def _paint_masks_by_area(
|
||||
canvas: npt.NDArray[np.uint8],
|
||||
detections: Detections,
|
||||
color: Color | ColorPalette,
|
||||
color_lookup: ColorLookup | npt.NDArray[np.int_],
|
||||
collect_union: bool = False,
|
||||
canvas_origin: tuple[int, int] = (0, 0),
|
||||
) -> npt.NDArray[np.bool_] | None:
|
||||
"""Paint each detection's mask into `canvas` in descending-area order.
|
||||
|
||||
Smaller masks are drawn on top of larger ones. `CompactMask` detections
|
||||
are painted into their bounding-box crop only, avoiding a full `(H, W)`
|
||||
allocation per mask; dense masks fall back to full-frame boolean indexing.
|
||||
|
||||
Args:
|
||||
canvas: BGR image array painted in place. Shape ``(H, W, 3)``.
|
||||
detections: Detections whose masks to paint. Returns immediately
|
||||
without modifying `canvas` when ``detections.mask`` is ``None``.
|
||||
color: Single color or palette used to resolve each detection's color.
|
||||
color_lookup: Strategy for mapping colors to detection indices.
|
||||
collect_union: When ``True``, allocate and return a ``(H, W)``
|
||||
boolean array that accumulates the union of all painted masks
|
||||
(useful for callers like `HaloAnnotator` that need the combined
|
||||
mask footprint). When ``False`` (default), returns ``None``.
|
||||
canvas_origin: Absolute ``(x, y)`` origin of `canvas` within the source
|
||||
image. Use the default for full-frame painting.
|
||||
|
||||
Returns:
|
||||
A boolean array matching the canvas dimensions when
|
||||
``collect_union=True``, otherwise ``None``. When called with an
|
||||
ROI sub-canvas, dimensions are the ROI size, not the full image.
|
||||
"""
|
||||
masks = detections.mask
|
||||
if masks is None:
|
||||
return None
|
||||
union: npt.NDArray[np.bool_] | None = (
|
||||
np.zeros(canvas.shape[:2], dtype=bool) if collect_union else None
|
||||
)
|
||||
compact_mask = masks if isinstance(masks, CompactMask) else None
|
||||
origin_x, origin_y = canvas_origin
|
||||
canvas_h, canvas_w = canvas.shape[:2]
|
||||
for detection_idx in np.flip(np.argsort(detections.area)):
|
||||
color_bgr = resolve_color(
|
||||
color=color,
|
||||
detections=detections,
|
||||
detection_idx=detection_idx,
|
||||
color_lookup=color_lookup,
|
||||
).as_bgr()
|
||||
if compact_mask is not None:
|
||||
x1 = int(compact_mask.offsets[detection_idx, 0])
|
||||
y1 = int(compact_mask.offsets[detection_idx, 1])
|
||||
crop_m = compact_mask.crop(detection_idx)
|
||||
crop_h, crop_w = crop_m.shape
|
||||
crop_x1 = max(0, origin_x - x1)
|
||||
crop_y1 = max(0, origin_y - y1)
|
||||
canvas_x1 = max(0, x1 - origin_x)
|
||||
canvas_y1 = max(0, y1 - origin_y)
|
||||
paint_w = min(crop_w - crop_x1, canvas_w - canvas_x1)
|
||||
paint_h = min(crop_h - crop_y1, canvas_h - canvas_y1)
|
||||
if paint_w <= 0 or paint_h <= 0:
|
||||
continue
|
||||
crop_slice = crop_m[
|
||||
crop_y1 : crop_y1 + paint_h, crop_x1 : crop_x1 + paint_w
|
||||
]
|
||||
canvas_slice = canvas[
|
||||
canvas_y1 : canvas_y1 + paint_h,
|
||||
canvas_x1 : canvas_x1 + paint_w,
|
||||
]
|
||||
canvas_slice[crop_slice] = color_bgr
|
||||
if union is not None:
|
||||
union[
|
||||
canvas_y1 : canvas_y1 + paint_h,
|
||||
canvas_x1 : canvas_x1 + paint_w,
|
||||
] |= crop_slice
|
||||
else:
|
||||
mask = np.asarray(masks[detection_idx], dtype=bool)
|
||||
mask = mask[origin_y : origin_y + canvas_h, origin_x : origin_x + canvas_w]
|
||||
canvas[mask] = color_bgr
|
||||
if union is not None:
|
||||
union |= mask
|
||||
return union
|
||||
|
||||
|
||||
class MaskAnnotator(BaseAnnotator):
|
||||
"""
|
||||
A class for drawing masks on an image using provided detections.
|
||||
|
|
@ -370,6 +501,8 @@ class MaskAnnotator(BaseAnnotator):
|
|||
This annotator uses `sv.Detections.mask`.
|
||||
"""
|
||||
|
||||
requires_mask = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
|
||||
|
|
@ -436,39 +569,35 @@ class MaskAnnotator(BaseAnnotator):
|
|||
if detections.mask is None:
|
||||
return scene
|
||||
|
||||
colored_mask = np.array(scene, copy=True, dtype=np.uint8)
|
||||
|
||||
compact_mask = (
|
||||
detections.mask if isinstance(detections.mask, CompactMask) else None
|
||||
image_shape = (int(scene.shape[0]), int(scene.shape[1]))
|
||||
effective_lookup = (
|
||||
self.color_lookup if custom_color_lookup is None else custom_color_lookup
|
||||
)
|
||||
for detection_idx in np.flip(np.argsort(detections.area)):
|
||||
color = resolve_color(
|
||||
if len(detections) > 0:
|
||||
resolve_color(
|
||||
color=self.color,
|
||||
detections=detections,
|
||||
detection_idx=detection_idx,
|
||||
color_lookup=self.color_lookup
|
||||
if custom_color_lookup is None
|
||||
else custom_color_lookup,
|
||||
detection_idx=0,
|
||||
color_lookup=effective_lookup,
|
||||
)
|
||||
if compact_mask is not None:
|
||||
# Paint only the bounding-box crop — avoids a full (H, W) alloc.
|
||||
x1 = int(compact_mask.offsets[detection_idx, 0])
|
||||
y1 = int(compact_mask.offsets[detection_idx, 1])
|
||||
crop_m = compact_mask.crop(detection_idx)
|
||||
crop_h, crop_w = crop_m.shape
|
||||
colored_mask[y1 : y1 + crop_h, x1 : x1 + crop_w][crop_m] = (
|
||||
color.as_bgr()
|
||||
)
|
||||
else:
|
||||
mask = np.asarray(
|
||||
detections.mask[detection_idx],
|
||||
dtype=bool,
|
||||
)
|
||||
colored_mask[mask] = color.as_bgr()
|
||||
roi = _masks_to_roi(detections.mask, image_shape, detections.xyxy)
|
||||
if roi is None:
|
||||
return scene
|
||||
|
||||
cv2.addWeighted(
|
||||
colored_mask, self.opacity, scene, 1 - self.opacity, 0, dst=scene
|
||||
x1, y1, x2, y2 = roi
|
||||
scene_roi = scene[y1:y2, x1:x2]
|
||||
colored_mask = np.array(scene_roi, copy=True, dtype=np.uint8)
|
||||
_paint_masks_by_area(
|
||||
colored_mask,
|
||||
detections,
|
||||
self.color,
|
||||
effective_lookup,
|
||||
canvas_origin=(x1, y1),
|
||||
)
|
||||
tmp = cv2.addWeighted(
|
||||
colored_mask, self.opacity, scene_roi.copy(), 1 - self.opacity, 0
|
||||
)
|
||||
scene_roi[:] = tmp
|
||||
return scene
|
||||
|
||||
|
||||
|
|
@ -481,6 +610,8 @@ class PolygonAnnotator(BaseAnnotator):
|
|||
This annotator uses `sv.Detections.mask`.
|
||||
"""
|
||||
|
||||
requires_mask = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
|
||||
|
|
@ -538,6 +669,14 @@ class PolygonAnnotator(BaseAnnotator):
|
|||
|
||||
```
|
||||
|
||||
Note:
|
||||
When `detections.mask` is a `CompactMask`, each detection's polygon
|
||||
is decoded from a bbox-sized crop (O(crop_area)) rather than a
|
||||
full-frame ``(H, W)`` allocation (O(H·W)). Polygon coordinates are
|
||||
shifted from crop-local space to image space via the stored
|
||||
``(x1, y1)`` bbox origin. Pixels outside the declared ``xyxy`` box
|
||||
are not represented in compact storage and will not be drawn.
|
||||
|
||||

|
||||
"""
|
||||
|
|
@ -546,8 +685,7 @@ class PolygonAnnotator(BaseAnnotator):
|
|||
if detections.mask is None:
|
||||
return scene
|
||||
|
||||
for detection_idx in range(len(detections)):
|
||||
mask = detections.mask[detection_idx]
|
||||
for detection_idx, mask, offset in _iter_mask_crops(detections):
|
||||
color = resolve_color(
|
||||
color=self.color,
|
||||
detections=detections,
|
||||
|
|
@ -557,9 +695,12 @@ class PolygonAnnotator(BaseAnnotator):
|
|||
else custom_color_lookup,
|
||||
)
|
||||
for polygon in mask_to_polygons(mask=mask):
|
||||
if offset is not None:
|
||||
# translate crop-local polygon to image space via (x1, y1) origin
|
||||
polygon = polygon + offset
|
||||
scene = draw_polygon(
|
||||
scene=scene,
|
||||
polygon=polygon,
|
||||
polygon=cast(npt.NDArray[np.int_], polygon),
|
||||
color=color,
|
||||
thickness=self.thickness,
|
||||
)
|
||||
|
|
@ -668,6 +809,8 @@ class HaloAnnotator(BaseAnnotator):
|
|||
This annotator uses `sv.Detections.mask`.
|
||||
"""
|
||||
|
||||
requires_mask = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
color: Color | ColorPalette | str = ColorPalette.DEFAULT,
|
||||
|
|
@ -701,7 +844,7 @@ class HaloAnnotator(BaseAnnotator):
|
|||
Annotates the given scene with halos based on the provided detections.
|
||||
|
||||
Args:
|
||||
scene: The image where masks will be drawn.
|
||||
scene: The image where the halo effect will be applied.
|
||||
`ImageType` is a flexible type, accepting either `numpy.ndarray`
|
||||
or `PIL.Image.Image`.
|
||||
detections: Object detections to annotate.
|
||||
|
|
@ -719,6 +862,7 @@ class HaloAnnotator(BaseAnnotator):
|
|||
>>> image = np.zeros((100, 100, 3), dtype=np.uint8)
|
||||
>>> detections = sv.Detections(
|
||||
... xyxy=np.array([[20, 20, 80, 80]]),
|
||||
... mask=np.zeros((1, 100, 100), dtype=bool),
|
||||
... class_id=np.array([0])
|
||||
... )
|
||||
>>> halo_annotator = sv.HaloAnnotator()
|
||||
|
|
@ -737,30 +881,34 @@ class HaloAnnotator(BaseAnnotator):
|
|||
if detections.mask is None:
|
||||
return scene
|
||||
colored_mask = np.zeros_like(scene, dtype=np.uint8)
|
||||
fmask = np.array([False] * scene.shape[0] * scene.shape[1]).reshape(
|
||||
scene.shape[0], scene.shape[1]
|
||||
fmask = _paint_masks_by_area(
|
||||
colored_mask,
|
||||
detections,
|
||||
self.color,
|
||||
self.color_lookup if custom_color_lookup is None else custom_color_lookup,
|
||||
collect_union=True,
|
||||
)
|
||||
assert fmask is not None # collect_union=True always returns an array
|
||||
|
||||
for detection_idx in np.flip(np.argsort(detections.area)):
|
||||
color = resolve_color(
|
||||
color=self.color,
|
||||
detections=detections,
|
||||
detection_idx=detection_idx,
|
||||
color_lookup=self.color_lookup
|
||||
if custom_color_lookup is None
|
||||
else custom_color_lookup,
|
||||
)
|
||||
mask = np.asarray(detections.mask[detection_idx], dtype=bool)
|
||||
fmask = np.logical_or(fmask, mask)
|
||||
color_bgr = color.as_bgr()
|
||||
colored_mask[mask] = color_bgr
|
||||
|
||||
colored_mask = cv2.blur(colored_mask, (self.kernel_size, self.kernel_size))
|
||||
colored_mask = cast(
|
||||
npt.NDArray[np.uint8],
|
||||
cv2.blur(colored_mask, (self.kernel_size, self.kernel_size)),
|
||||
)
|
||||
colored_mask[fmask] = [0, 0, 0]
|
||||
gray = cv2.cvtColor(colored_mask, cv2.COLOR_BGR2GRAY)
|
||||
alpha = self.opacity * gray / gray.max()
|
||||
gray_max = gray.max()
|
||||
if gray_max == 0:
|
||||
# no halo to draw (e.g. empty masks); leave the scene untouched
|
||||
return scene
|
||||
alpha = self.opacity * gray / gray_max
|
||||
alpha_mask = alpha[:, :, np.newaxis]
|
||||
blended_scene = np.uint8(scene * (1 - alpha_mask) + colored_mask * self.opacity)
|
||||
# Blend in float space so halo opacity cannot wrap around uint8 boundaries.
|
||||
blended_scene = np.clip(
|
||||
scene.astype(np.float32) * (1 - alpha_mask)
|
||||
+ colored_mask.astype(np.float32) * alpha_mask,
|
||||
0,
|
||||
255,
|
||||
).astype(np.uint8)
|
||||
np.copyto(scene, blended_scene)
|
||||
return scene
|
||||
|
||||
|
|
@ -1219,11 +1367,11 @@ class LabelAnnotator(_BaseLabelAnnotator):
|
|||
@ensure_cv2_image_for_class_method
|
||||
def annotate(
|
||||
self,
|
||||
scene: Image.Image,
|
||||
scene: ImageType,
|
||||
detections: Detections,
|
||||
labels: list[str] | None = None,
|
||||
custom_color_lookup: npt.NDArray[np.int_] | None = None,
|
||||
) -> Image.Image:
|
||||
) -> ImageType:
|
||||
"""
|
||||
Annotates the given scene with labels based on the provided detections.
|
||||
|
||||
|
|
@ -1278,10 +1426,6 @@ class LabelAnnotator(_BaseLabelAnnotator):
|
|||
)
|
||||
|
||||
if self.smart_position:
|
||||
xyxy = label_properties[:, :4]
|
||||
xyxy = spread_out_boxes(xyxy)
|
||||
label_properties[:, :4] = xyxy
|
||||
|
||||
label_properties = self._adjust_labels_in_frame(
|
||||
(scene.shape[1], scene.shape[0]),
|
||||
labels,
|
||||
|
|
@ -1441,12 +1585,51 @@ class LabelAnnotator(_BaseLabelAnnotator):
|
|||
color: tuple[int, int, int],
|
||||
border_radius: int,
|
||||
) -> npt.NDArray[np.uint8]:
|
||||
"""Draw a filled rectangle with optional rounded corners on an image.
|
||||
|
||||
Args:
|
||||
scene: BGR image array to draw on; modified in-place and returned.
|
||||
xyxy: Bounding box as (x1, y1, x2, y2) pixel coordinates.
|
||||
color: Fill color as a BGR tuple (e.g. ``(0, 0, 255)`` for red).
|
||||
border_radius: Corner rounding radius in pixels. Values <= 0
|
||||
(including values clamped to 0 by a degenerate box) draw a
|
||||
plain filled rectangle with square corners.
|
||||
|
||||
Returns:
|
||||
The annotated ``scene`` array.
|
||||
|
||||
Example:
|
||||
```python
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
|
||||
scene = np.zeros((200, 200, 3), dtype=np.uint8)
|
||||
scene = sv.LabelAnnotator.draw_rounded_rectangle(
|
||||
scene=scene,
|
||||
xyxy=(10, 10, 100, 50),
|
||||
color=(0, 255, 0),
|
||||
border_radius=0,
|
||||
)
|
||||
```
|
||||
"""
|
||||
x1, y1, x2, y2 = xyxy
|
||||
width = x2 - x1
|
||||
height = y2 - y1
|
||||
|
||||
border_radius = min(border_radius, min(width, height) // 2)
|
||||
|
||||
if border_radius <= 0:
|
||||
# square corners: a single fill rectangle (the common default), rather
|
||||
# than two rectangles plus four zero-radius corner circles
|
||||
cv2.rectangle(
|
||||
img=scene,
|
||||
pt1=(x1, y1),
|
||||
pt2=(x2, y2),
|
||||
color=color,
|
||||
thickness=-1,
|
||||
)
|
||||
return scene
|
||||
|
||||
rectangle_coordinates = [
|
||||
((x1 + border_radius, y1), (x2 - border_radius, y2)),
|
||||
((x1, y1 + border_radius), (x2, y2 - border_radius)),
|
||||
|
|
@ -1536,11 +1719,11 @@ class RichLabelAnnotator(_BaseLabelAnnotator):
|
|||
@ensure_pil_image_for_class_method
|
||||
def annotate(
|
||||
self,
|
||||
scene: Image.Image,
|
||||
scene: ImageType,
|
||||
detections: Detections,
|
||||
labels: list[str] | None = None,
|
||||
custom_color_lookup: npt.NDArray[np.int_] | None = None,
|
||||
) -> Image.Image:
|
||||
) -> ImageType:
|
||||
"""
|
||||
Annotates the given scene with labels based on the provided
|
||||
detections, with support for Unicode characters.
|
||||
|
|
@ -1584,7 +1767,6 @@ class RichLabelAnnotator(_BaseLabelAnnotator):
|
|||
|
||||
```
|
||||
"""
|
||||
assert isinstance(scene, Image.Image)
|
||||
_validate_labels(labels, detections)
|
||||
|
||||
draw = ImageDraw.Draw(scene)
|
||||
|
|
@ -1594,12 +1776,9 @@ class RichLabelAnnotator(_BaseLabelAnnotator):
|
|||
)
|
||||
|
||||
if self.smart_position:
|
||||
xyxy = label_properties[:, :4]
|
||||
xyxy = spread_out_boxes(xyxy)
|
||||
label_properties[:, :4] = xyxy
|
||||
|
||||
scene_pil = cast(Image.Image, scene)
|
||||
label_properties = self._adjust_labels_in_frame(
|
||||
(scene.width, scene.height),
|
||||
(scene_pil.width, scene_pil.height),
|
||||
labels,
|
||||
label_properties,
|
||||
)
|
||||
|
|
@ -1845,21 +2024,14 @@ class IconAnnotator(BaseAnnotator):
|
|||
x = int(xy[detection_idx, 0] - icon_w / 2 + self.offset_xy[0])
|
||||
y = int(xy[detection_idx, 1] - icon_h / 2 + self.offset_xy[1])
|
||||
|
||||
scene[:] = overlay_image(scene, icon, (x, y))
|
||||
scene[:] = _overlay_image(scene, icon, (x, y))
|
||||
return scene
|
||||
|
||||
@lru_cache
|
||||
def _load_icon(self, icon_path: str) -> npt.NDArray[np.uint8]:
|
||||
icon = cv2.imread(icon_path, cv2.IMREAD_UNCHANGED)
|
||||
if icon is None:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Couldn't load the icon image from {icon_path}"
|
||||
)
|
||||
icon = cast(
|
||||
npt.NDArray[np.uint8],
|
||||
letterbox_image(image=icon, resolution_wh=self.icon_resolution_wh),
|
||||
"""Load an icon through the module-level cache shared by annotators."""
|
||||
return _load_icon_from_path(
|
||||
icon_path=icon_path, icon_resolution_wh=self.icon_resolution_wh
|
||||
)
|
||||
return icon
|
||||
|
||||
|
||||
class BlurAnnotator(BaseAnnotator):
|
||||
|
|
@ -1921,7 +2093,8 @@ class BlurAnnotator(BaseAnnotator):
|
|||
return scene
|
||||
image_height, image_width = scene.shape[:2]
|
||||
clipped_xyxy: npt.NDArray[np.int32] = clip_boxes(
|
||||
xyxy=detections.xyxy, resolution_wh=(image_width, image_height)
|
||||
xyxy=detections.xyxy,
|
||||
resolution_wh=(image_width, image_height),
|
||||
).astype(int)
|
||||
|
||||
for x1, y1, x2, y2 in clipped_xyxy:
|
||||
|
|
@ -1933,7 +2106,7 @@ class BlurAnnotator(BaseAnnotator):
|
|||
if self.kernel_size is not None
|
||||
else calculate_dynamic_kernel_size(x1, y1, x2, y2)
|
||||
)
|
||||
roi = cv2.blur(roi, (kernel_size, kernel_size))
|
||||
roi = cast(npt.NDArray[np.uint8], cv2.blur(roi, (kernel_size, kernel_size)))
|
||||
scene[y1:y2, x1:x2] = roi
|
||||
|
||||
return scene
|
||||
|
|
@ -1981,6 +2154,36 @@ class TraceAnnotator(BaseAnnotator):
|
|||
self.smooth = smooth
|
||||
self.color_lookup: ColorLookup = color_lookup
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Clears the accumulated trace history so the annotator can be reused
|
||||
across independent streams without carrying over points from a
|
||||
previous stream.
|
||||
|
||||
Examples:
|
||||
```pycon
|
||||
>>> import numpy as np
|
||||
>>> import supervision as sv
|
||||
>>> image = np.zeros((20, 20, 3), dtype=np.uint8)
|
||||
>>> detections = sv.Detections(
|
||||
... xyxy=np.array([[1, 1, 10, 10]]),
|
||||
... class_id=np.array([0]),
|
||||
... tracker_id=np.array([1])
|
||||
... )
|
||||
>>> trace_annotator = sv.TraceAnnotator()
|
||||
>>> _ = trace_annotator.annotate(scene=image.copy(), detections=detections)
|
||||
>>> trace_annotator.trace.xy.shape
|
||||
(1, 2)
|
||||
>>> trace_annotator.reset()
|
||||
>>> trace_annotator.trace.current_frame_id
|
||||
0
|
||||
>>> trace_annotator.trace.xy.shape
|
||||
(0, 2)
|
||||
|
||||
```
|
||||
"""
|
||||
self.trace.reset()
|
||||
|
||||
@ensure_cv2_image_for_class_method
|
||||
def annotate(
|
||||
self,
|
||||
|
|
@ -2066,8 +2269,20 @@ class TraceAnnotator(BaseAnnotator):
|
|||
try:
|
||||
x, y = unique_xy[:, 0], unique_xy[:, 1]
|
||||
tck, _u = splprep([x, y], s=20)
|
||||
xy_new = splev(np.linspace(0, 1, 100), tck)
|
||||
spline_points = np.stack(xy_new, axis=1).astype(np.int32)
|
||||
x_new, y_new = splev(
|
||||
np.linspace(0, 1, 100),
|
||||
cast(
|
||||
tuple[
|
||||
npt.NDArray[np.float64],
|
||||
npt.NDArray[np.float64],
|
||||
int,
|
||||
],
|
||||
tck,
|
||||
),
|
||||
)
|
||||
spline_points = np.stack((x_new, y_new), axis=1).astype(
|
||||
np.int32
|
||||
)
|
||||
except ValueError:
|
||||
spline_points = unique_xy.astype(np.int32)
|
||||
else:
|
||||
|
|
@ -2119,6 +2334,34 @@ class HeatMapAnnotator(BaseAnnotator):
|
|||
self.low_hue = low_hue
|
||||
self.heat_mask: npt.NDArray[np.float32] | None = None
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Clears the accumulated heat so the annotator can be reused across
|
||||
independent streams. `annotate` already reinitializes the heat mask
|
||||
when the scene resolution changes; call this to discard heat from a
|
||||
previous stream that shares the same resolution.
|
||||
|
||||
Examples:
|
||||
```pycon
|
||||
>>> import numpy as np
|
||||
>>> import supervision as sv
|
||||
>>> image = np.zeros((40, 40, 3), dtype=np.uint8)
|
||||
>>> detections = sv.Detections(xyxy=np.array([[10, 10, 20, 20]]))
|
||||
>>> heat_map_annotator = sv.HeatMapAnnotator()
|
||||
>>> _ = heat_map_annotator.annotate(
|
||||
... scene=image.copy(),
|
||||
... detections=detections
|
||||
... )
|
||||
>>> bool(heat_map_annotator.heat_mask.sum() > 0)
|
||||
True
|
||||
>>> heat_map_annotator.reset()
|
||||
>>> heat_map_annotator.heat_mask is None
|
||||
True
|
||||
|
||||
```
|
||||
"""
|
||||
self.heat_mask = None
|
||||
|
||||
@ensure_cv2_image_for_class_method
|
||||
def annotate(self, scene: ImageType, detections: Detections) -> ImageType:
|
||||
"""
|
||||
|
|
@ -2165,10 +2408,10 @@ class HeatMapAnnotator(BaseAnnotator):
|
|||
"""
|
||||
if not isinstance(scene, np.ndarray):
|
||||
return scene
|
||||
if self.heat_mask is None:
|
||||
if self.heat_mask is None or self.heat_mask.shape != scene.shape[:2]:
|
||||
self.heat_mask = np.zeros(scene.shape[:2], dtype=np.float32)
|
||||
|
||||
mask = np.zeros(scene.shape[:2])
|
||||
mask: npt.NDArray[np.float32] = np.zeros(scene.shape[:2], dtype=np.float32)
|
||||
for xy in detections.get_anchors_coordinates(self.position):
|
||||
x, y = int(xy[0]), int(xy[1])
|
||||
cv2.circle(
|
||||
|
|
@ -2179,20 +2422,25 @@ class HeatMapAnnotator(BaseAnnotator):
|
|||
thickness=-1, # fill
|
||||
)
|
||||
self.heat_mask = mask + self.heat_mask
|
||||
temp = self.heat_mask.copy()
|
||||
max_val = temp.max()
|
||||
heat_mask = self.heat_mask
|
||||
heat_values = heat_mask.copy()
|
||||
max_val = heat_values.max()
|
||||
if max_val > 0:
|
||||
temp = self.low_hue - temp / max_val * (self.low_hue - self.top_hue)
|
||||
temp = temp.astype(np.uint8)
|
||||
heat_values = self.low_hue - heat_values / max_val * (
|
||||
self.low_hue - self.top_hue
|
||||
)
|
||||
heat_hue = heat_values.astype(np.uint8)
|
||||
if self.kernel_size is not None:
|
||||
temp = cv2.blur(temp, (self.kernel_size, self.kernel_size))
|
||||
heat_hue = cast(
|
||||
npt.NDArray[np.uint8],
|
||||
cv2.blur(heat_hue, (self.kernel_size, self.kernel_size)),
|
||||
)
|
||||
hsv = np.full(scene.shape, 255, dtype=np.uint8)
|
||||
hsv[..., 0] = temp
|
||||
temp = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
|
||||
mask = cv2.cvtColor(self.heat_mask.astype(np.uint8), cv2.COLOR_GRAY2BGR) > 0
|
||||
scene[mask] = cv2.addWeighted(temp, self.opacity, scene, 1 - self.opacity, 0)[
|
||||
mask
|
||||
]
|
||||
hsv[..., 0] = heat_hue
|
||||
heat_bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
|
||||
mask2d = heat_mask > 0
|
||||
blended = cv2.addWeighted(heat_bgr, self.opacity, scene, 1 - self.opacity, 0)
|
||||
scene[mask2d] = blended[mask2d]
|
||||
return scene
|
||||
|
||||
|
||||
|
|
@ -2255,7 +2503,8 @@ class PixelateAnnotator(BaseAnnotator):
|
|||
return scene
|
||||
image_height, image_width = scene.shape[:2]
|
||||
clipped_xyxy: npt.NDArray[np.int32] = clip_boxes(
|
||||
xyxy=detections.xyxy, resolution_wh=(image_width, image_height)
|
||||
xyxy=detections.xyxy,
|
||||
resolution_wh=(image_width, image_height),
|
||||
).astype(int)
|
||||
|
||||
for x1, y1, x2, y2 in clipped_xyxy:
|
||||
|
|
@ -2578,7 +2827,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
|
||||
|
||||
|
|
@ -2690,6 +2939,7 @@ class PercentageBarAnnotator(BaseAnnotator):
|
|||
def calculate_border_coordinates(
|
||||
anchor_xy: tuple[int, int], border_wh: tuple[int, int], position: Position
|
||||
) -> tuple[tuple[int, int], tuple[int, int]]:
|
||||
"""Compute the border corner coordinates for a given anchor position."""
|
||||
cx, cy = anchor_xy
|
||||
width, height = border_wh
|
||||
|
||||
|
|
@ -2714,6 +2964,7 @@ class PercentageBarAnnotator(BaseAnnotator):
|
|||
return (cx - width // 2, cy), (cx + width // 2, cy + height)
|
||||
elif position == Position.BOTTOM_RIGHT:
|
||||
return (cx, cy), (cx + width, cy + height)
|
||||
raise ValueError(f"Unsupported position: {position}")
|
||||
|
||||
@staticmethod
|
||||
def _validate_custom_values(
|
||||
|
|
@ -2812,6 +3063,12 @@ class CropAnnotator(BaseAnnotator):
|
|||
Returns:
|
||||
The annotated image.
|
||||
|
||||
Note:
|
||||
Detections whose bounding boxes extend partially outside `scene` are
|
||||
clipped to scene bounds before cropping. Detections fully outside the
|
||||
scene collapse to zero area after clipping and are skipped without
|
||||
raising an error.
|
||||
|
||||
Examples:
|
||||
```pycon
|
||||
>>> import numpy as np
|
||||
|
|
@ -2834,22 +3091,29 @@ class CropAnnotator(BaseAnnotator):
|
|||
"""
|
||||
if not isinstance(scene, np.ndarray):
|
||||
return scene
|
||||
crops = [
|
||||
crop_image(image=scene, xyxy=xyxy) for xyxy in detections.xyxy.astype(int)
|
||||
]
|
||||
resized_crops = [
|
||||
scale_image(image=crop, scale_factor=self.scale_factor) for crop in crops
|
||||
]
|
||||
image_height, image_width = scene.shape[:2]
|
||||
clipped_xyxy: npt.NDArray[np.int32] = clip_boxes(
|
||||
xyxy=detections.xyxy,
|
||||
resolution_wh=(image_width, image_height),
|
||||
).astype(np.int32)
|
||||
anchors: npt.NDArray[np.int32] = detections.get_anchors_coordinates(
|
||||
anchor=self.position
|
||||
).astype(int)
|
||||
).astype(np.int32)
|
||||
# Snapshot before the loop so later crops are taken from the original image,
|
||||
# not a scene already annotated by earlier iterations (overlapping-box case).
|
||||
source_scene = scene.copy()
|
||||
|
||||
for idx, (resized_crop, anchor) in enumerate(zip(resized_crops, anchors)):
|
||||
for idx, (xyxy, anchor) in enumerate(zip(clipped_xyxy, anchors)):
|
||||
crop_x1, crop_y1, crop_x2, crop_y2 = xyxy
|
||||
if crop_x2 <= crop_x1 or crop_y2 <= crop_y1:
|
||||
continue
|
||||
crop = crop_image(image=source_scene, xyxy=xyxy)
|
||||
resized_crop = scale_image(image=crop, scale_factor=self.scale_factor)
|
||||
crop_wh = resized_crop.shape[1], resized_crop.shape[0]
|
||||
(x1, y1), (x2, y2) = self.calculate_crop_coordinates(
|
||||
anchor=anchor, crop_wh=crop_wh, position=self.position
|
||||
)
|
||||
scene = overlay_image(image=scene, overlay=resized_crop, anchor=(x1, y1))
|
||||
scene = _overlay_image(image=scene, overlay=resized_crop, anchor=(x1, y1))
|
||||
color = resolve_color(
|
||||
color=self.border_color,
|
||||
detections=detections,
|
||||
|
|
@ -2872,6 +3136,7 @@ class CropAnnotator(BaseAnnotator):
|
|||
def calculate_crop_coordinates(
|
||||
anchor: tuple[int, int], crop_wh: tuple[int, int], position: Position
|
||||
) -> tuple[tuple[int, int], tuple[int, int]]:
|
||||
"""Compute the crop coordinates for a given anchor position."""
|
||||
anchor_x, anchor_y = anchor
|
||||
width, height = crop_wh
|
||||
|
||||
|
|
@ -2908,6 +3173,7 @@ class CropAnnotator(BaseAnnotator):
|
|||
)
|
||||
elif position == Position.BOTTOM_RIGHT:
|
||||
return (anchor_x, anchor_y), (anchor_x + width, anchor_y + height)
|
||||
raise ValueError(f"Unsupported position: {position}")
|
||||
|
||||
|
||||
class BackgroundOverlayAnnotator(BaseAnnotator):
|
||||
|
|
@ -2986,7 +3252,12 @@ class BackgroundOverlayAnnotator(BaseAnnotator):
|
|||
)
|
||||
|
||||
if detections.mask is None or self.force_box:
|
||||
for x1, y1, x2, y2 in detections.xyxy.astype(int):
|
||||
image_height, image_width = scene.shape[:2]
|
||||
clipped_xyxy: npt.NDArray[np.int32] = clip_boxes(
|
||||
xyxy=detections.xyxy,
|
||||
resolution_wh=(image_width, image_height),
|
||||
).astype(np.int32)
|
||||
for x1, y1, x2, y2 in clipped_xyxy:
|
||||
colored_mask[y1:y2, x1:x2] = scene[y1:y2, x1:x2]
|
||||
else:
|
||||
for mask in detections.mask:
|
||||
|
|
@ -3008,6 +3279,9 @@ class ComparisonAnnotator:
|
|||
Otherwise, uses the bounding box data.
|
||||
"""
|
||||
|
||||
# Not a BaseAnnotator subclass — duck-typing callers can still check requires_mask
|
||||
requires_mask: ClassVar[bool] = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
color_1: Color = Color.RED,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue