Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f79d29934 | ||
|
|
d86623600a | ||
|
|
475b2ceeea | ||
|
|
95661f3bb1 | ||
|
|
e6ab865a0c | ||
|
|
63669e587a | ||
|
|
2166bc23bc | ||
|
|
867dd78f58 | ||
|
|
255b34a183 | ||
|
|
f7b59870ed | ||
|
|
7948234f94 | ||
|
|
b5e83fa953 | ||
|
|
de536a3f8e | ||
|
|
6741ae642a | ||
|
|
3834e5cdf9 | ||
|
|
aa77c396f7 | ||
|
|
bee6dec22c | ||
|
|
db2eede55b | ||
|
|
ba880402ad | ||
|
|
62e097b559 | ||
|
|
451bb5e513 | ||
|
|
330167fc73 | ||
|
|
1c23e806ba | ||
|
|
2c31f8ec94 | ||
|
|
a6c73f70ec | ||
|
|
13ee396b20 | ||
|
|
0f222375e2 | ||
|
|
a63f65fff3 | ||
|
|
94f15a6e50 | ||
|
|
525e1b00ec | ||
|
|
7f4662cf3d | ||
|
|
3a31ea2986 | ||
|
|
e0ca4ca5e7 | ||
|
|
0fcc17ee11 | ||
|
|
52cbe684d4 | ||
|
|
fce6e660d9 | ||
|
|
c6f2bf27c1 | ||
|
|
523d76f86d | ||
|
|
1ba1b67b47 | ||
|
|
a45cc865dd | ||
|
|
4942d3d817 | ||
|
|
514f61b885 | ||
|
|
b40e2f228a | ||
|
|
da9d50b4e2 | ||
|
|
8f437107b8 | ||
|
|
2a9a1f05bb | ||
|
|
414e0327bc | ||
|
|
f2d2827eee | ||
|
|
8eaa42abc6 | ||
|
|
aa50052ff5 | ||
|
|
a59e4dfe73 | ||
|
|
d6d4760fce | ||
|
|
f2bbb7c38c | ||
|
|
fa44884ea7 | ||
|
|
6fdabf0a96 | ||
|
|
26289e3743 | ||
|
|
e2d1fe7695 | ||
|
|
36d3e94a76 | ||
|
|
c0c292ef82 | ||
|
|
9462a3058d | ||
|
|
77c2afe559 | ||
|
|
95c8576aaf | ||
|
|
12b3def032 | ||
|
|
e8f616ba2f | ||
|
|
f8005b268b | ||
|
|
e4ef57ec2d | ||
|
|
1642432541 | ||
|
|
1febd715e2 | ||
|
|
9a9d0c5259 | ||
|
|
446ae23d99 | ||
|
|
a181826527 | ||
|
|
0a1212f23d | ||
|
|
b0e0d074af | ||
|
|
27e8e2d91b | ||
|
|
f4c17f09c8 | ||
|
|
b219961f6e | ||
|
|
70fe357a4e | ||
|
|
4b621c3c8d | ||
|
|
277a9cb6b9 | ||
|
|
6dde70eb0c | ||
|
|
a91118322b | ||
|
|
86b33ed558 | ||
|
|
3723eae50c | ||
|
|
d7e08d42eb | ||
|
|
9fef53f8a3 | ||
|
|
8d8703a1c6 | ||
|
|
84a59a7e01 | ||
|
|
dec982200b | ||
|
|
da6a324c1f | ||
|
|
36cdcf1b12 | ||
|
|
7ec6bcd916 | ||
|
|
da13366d0a | ||
|
|
771a47e15d | ||
|
|
7067741310 | ||
|
|
f4dc515df1 | ||
|
|
0754cb75bd | ||
|
|
44a174f889 | ||
|
|
e9fbcf5dcb | ||
|
|
9b6b74dc77 | ||
|
|
e249ea8644 | ||
|
|
8662671e6b | ||
|
|
c7c8e67ffd | ||
|
|
b2b75f1105 | ||
|
|
2f726ca795 | ||
|
|
40a6fe1b5f | ||
|
|
0cfad8407c | ||
|
|
6587f39b0f | ||
|
|
0f95c5d8b3 | ||
|
|
800e39d64c | ||
|
|
926f935bad | ||
|
|
d3da6afa07 | ||
|
|
97ae155b1e | ||
|
|
24f013757f | ||
|
|
532245640b | ||
|
|
864764e665 | ||
|
|
ca348401d5 | ||
|
|
6def59444d | ||
|
|
bc0a38500d | ||
|
|
300ebea8ec | ||
|
|
5cc7da289e | ||
|
|
5edd58d6de | ||
|
|
5729f8ffc3 | ||
|
|
7a07517750 | ||
|
|
699fc01a0c | ||
|
|
43339e380a | ||
|
|
89a33cda8a | ||
|
|
e84302b43f | ||
|
|
10d3e3d48f | ||
|
|
3c59618152 | ||
|
|
e882c398b7 | ||
|
|
93ab21dda7 | ||
|
|
effa1415b5 | ||
|
|
e9e075acaf | ||
|
|
61e9584f5a | ||
|
|
2a640d3ab6 | ||
|
|
2f478f4d06 | ||
|
|
0c20830be5 | ||
|
|
6c51c4a1b4 | ||
|
|
d6883caadc | ||
|
|
f1f2728e0d | ||
|
|
b6b7663ea7 | ||
|
|
3d074fa213 | ||
|
|
9fd267acb2 | ||
|
|
727451b382 | ||
|
|
5f432fbc93 | ||
|
|
ea4638f5f5 | ||
|
|
4b38634c63 | ||
|
|
985608146c | ||
|
|
04b7badcbe | ||
|
|
54524a4a1f | ||
|
|
3f55e7a79a | ||
|
|
7a782b1ac3 | ||
|
|
6016338a4f | ||
|
|
8d131ad3e8 | ||
|
|
ae55e9c908 | ||
|
|
5cb3c603cd | ||
|
|
56e0b77211 | ||
|
|
bc9fe6f690 | ||
|
|
fb211a6160 | ||
|
|
37b7224380 | ||
|
|
314454c7a7 | ||
|
|
ac39bdd487 | ||
|
|
fc1e899575 | ||
|
|
fccbe6baa2 | ||
|
|
d21154e882 | ||
|
|
1b3af74756 | ||
|
|
dc1083711a | ||
|
|
8df5c5b0c4 | ||
|
|
abbc0e44b0 | ||
|
|
950744111a | ||
|
|
20954a2996 | ||
|
|
22d82c306f | ||
|
|
399121b65e | ||
|
|
1b9c65bdf8 | ||
|
|
3a8c4d63ea | ||
|
|
bfe058c307 | ||
|
|
12ce5feab7 | ||
|
|
186bfc44fa | ||
|
|
2c0905ba28 | ||
|
|
fd717ea749 | ||
|
|
40853350fd | ||
|
|
fdd8127293 | ||
|
|
21a480b483 | ||
|
|
1b013f85e5 | ||
|
|
36b48def89 | ||
|
|
b7f72b79de | ||
|
|
141428310a | ||
|
|
8f39df2fc0 | ||
|
|
5107dd359f | ||
|
|
d480ff5be2 | ||
|
|
2b876bdd64 | ||
|
|
476be527e5 | ||
|
|
f78ac90464 | ||
|
|
d4ea295f09 | ||
|
|
9c5c296fb6 | ||
|
|
85c4d5a644 | ||
|
|
ad06937653 | ||
|
|
e01e495c92 | ||
|
|
6784c78b4f | ||
|
|
4375e19245 | ||
|
|
a75a92708c | ||
|
|
c83e57c638 | ||
|
|
fcde1cea9c | ||
|
|
8614984f8f | ||
|
|
cc81ae723b | ||
|
|
f7a4a788ed | ||
|
|
35d9ee7bad | ||
|
|
abbde887dc | ||
|
|
4f5f87acb8 | ||
|
|
3f78150a1d | ||
|
|
183ba1e809 | ||
|
|
597c776c39 | ||
|
|
5291706bc1 | ||
|
|
a50e47e7c0 | ||
|
|
c9d85477f1 | ||
|
|
19271f362e | ||
|
|
9e1e9fc2fc | ||
|
|
629744dee1 | ||
|
|
c9d2b52900 | ||
|
|
43cca52a58 | ||
|
|
e1ee80cb37 | ||
|
|
6a8f3fc03b | ||
|
|
c4b33fe1d6 | ||
|
|
ebfaee22eb | ||
|
|
9c062aac29 |
@@ -5,14 +5,15 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- develop
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: pip install mkdocs-material
|
||||
- run: pip install "mkdocstrings[python]"
|
||||
- run: mkdocs gh-deploy --force
|
||||
- run: mkdocs gh-deploy --force
|
||||
|
||||
@@ -16,17 +16,32 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- name: 🐍 Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: 🦾 Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade virtualenv
|
||||
python -m pip install --upgrade pip
|
||||
pip install ".[dev]"
|
||||
- name: 🚀 Publish to PyPi
|
||||
env:
|
||||
PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
PYPI_TEST_PASSWORD: ${{ secrets.PYPI_TEST_PASSWORD }}
|
||||
virtualenv venv
|
||||
source venv/bin/activate
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --upgrade poetry
|
||||
poetry install
|
||||
|
||||
- name: 🏗️ Build source and wheel distributions
|
||||
run: |
|
||||
make publish -e PYPI_USERNAME=$PYPI_USERNAME -e PYPI_PASSWORD=$PYPI_PASSWORD -e PYPI_TEST_PASSWORD=$PYPI_TEST_PASSWORD
|
||||
python -m pip install --upgrade build twine
|
||||
python -m build
|
||||
twine check --strict dist/*
|
||||
- name: 🚀 Publish to PyPi
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: ${{ secrets.PYPI_USERNAME }}
|
||||
password: ${{ secrets.PYPI_PASSWORD }}
|
||||
- name: 🚀 Publish to Test-PyPi
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
user: ${{ secrets.PYPI_USERNAME }}
|
||||
password: ${{ secrets.PYPI_TEST_PASSWORD }}
|
||||
|
||||
@@ -2,21 +2,19 @@ name: Test WorkFlow
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
branches: [main, develop]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.7, 3.8, 3.9, '3.10']
|
||||
python-version: ["3.8", "3.9", "3.10","3.11"]
|
||||
steps:
|
||||
- name: 🛎️ Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- name: 🐍 Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: 🦾 Install dependencies
|
||||
|
||||
+3
-2
@@ -24,7 +24,7 @@ Before you contribute a new feature, consider submitting an Issue to discuss the
|
||||
|
||||
## How to Contribute Changes
|
||||
|
||||
First, fork this repository to your own GitHub account. Create a new branch that describes your changes (i.e. `line-counter-docs`). Push your changes to the branch on your fork and then submit a pull request to this repository.
|
||||
First, fork this repository to your own GitHub account. Create a new branch that describes your changes (i.e. `line-counter-docs`). Push your changes to the branch on your fork and then submit a pull request to `develop` branch of this repository.
|
||||
|
||||
When creating new functions, please ensure you have the following:
|
||||
|
||||
@@ -32,6 +32,7 @@ When creating new functions, please ensure you have the following:
|
||||
2. Unit tests for the function.
|
||||
3. Examples in the documentation for the function.
|
||||
4. Created an entry in our docs to autogenerate the documentation for the function.
|
||||
5. Please share google colab with minimal code to test new feature or reproduce PR whenever it is possible. Please ensure that google colab can be accessed without any issue.
|
||||
|
||||
All pull requests will be reviewed by the maintainers of the project. We will provide feedback and ask for changes if necessary.
|
||||
|
||||
@@ -48,4 +49,4 @@ So far, **there is no types checking with mypy**. See [issue](https://github.com
|
||||
|
||||
## 🧪 tests
|
||||
|
||||
[`pytests`](https://docs.pytest.org/en/7.1.x/) is used to run our tests.
|
||||
[`pytests`](https://docs.pytest.org/en/7.1.x/) is used to run our tests.
|
||||
|
||||
@@ -16,7 +16,7 @@ check_code_quality:
|
||||
flake8 $(check_dirs) --count --max-line-length=88 --exit-zero --ignore=D --extend-ignore=E203,E501,W503 --statistics
|
||||
|
||||
publish:
|
||||
python setup.py sdist bdist_wheel
|
||||
poetry build
|
||||
twine upload -r testpypi dist/* -u ${PYPI_USERNAME} -p ${PYPI_TEST_PASSWORD} --verbose
|
||||
twine check dist/*
|
||||
twine upload dist/* -u ${PYPI_USERNAME} -p ${PYPI_PASSWORD} --verbose
|
||||
@@ -25,29 +25,13 @@
|
||||
## 💻 install
|
||||
|
||||
Pip install the supervision package in a
|
||||
[**3.11>=Python>=3.7**](https://www.python.org/) environment.
|
||||
[**3.11>=Python>=3.8**](https://www.python.org/) environment.
|
||||
|
||||
```bash
|
||||
pip install supervision
|
||||
pip install supervision[desktop]
|
||||
```
|
||||
|
||||
<details close>
|
||||
<summary>👉 install from source</summary>
|
||||
|
||||
```bash
|
||||
# clone repository and navigate to root directory
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
cd supervision
|
||||
|
||||
# setup python environment and activate it
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# install
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
</details>
|
||||
Read more about desktop, headless, and local installation in our [guide](https://roboflow.github.io/supervision/).
|
||||
|
||||
## 🔥 quickstart
|
||||
|
||||
@@ -68,7 +52,7 @@ pip install -e ".[dev]"
|
||||
<details close>
|
||||
<summary>👉 more detections utils</summary>
|
||||
|
||||
- Easily switch inference pipeline between supported object detection / instance segmentation models
|
||||
- Easily switch inference pipeline between supported object detection/instance segmentation models
|
||||
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
@@ -123,7 +107,7 @@ pip install -e ".[dev]"
|
||||
<details close>
|
||||
<summary>👉 more dataset utils</summary>
|
||||
|
||||
- Load object detection / instance segmentation datasets in one of supported formats
|
||||
- Load object detection/instance segmentation datasets in one of the supported formats
|
||||
|
||||
```python
|
||||
>>> dataset = sv.DetectionDataset.from_yolo(
|
||||
@@ -154,7 +138,7 @@ pip install -e ".[dev]"
|
||||
[ 20.154999, 347.825 , 416.125 , 915.895 ]], dtype=float32)
|
||||
```
|
||||
|
||||
- Split dataset for training, testing and validation
|
||||
- Split dataset for training, testing, and validation
|
||||
|
||||
```python
|
||||
>>> train_dataset, test_dataset = dataset.split(split_ratio=0.7)
|
||||
@@ -164,7 +148,7 @@ pip install -e ".[dev]"
|
||||
(700, 150, 150)
|
||||
```
|
||||
|
||||
- Merge multiple datasets together
|
||||
- Merge multiple datasets
|
||||
|
||||
```python
|
||||
>>> ds_1 = sv.DetectionDataset(...)
|
||||
@@ -186,7 +170,7 @@ pip install -e ".[dev]"
|
||||
['cat', 'dog', 'person']
|
||||
```
|
||||
|
||||
- Save object detection / instance segmentation datasets in one of supported formats
|
||||
- Save object detection/instance segmentation datasets in one of the supported formats
|
||||
|
||||
```python
|
||||
>>> dataset.as_yolo(
|
||||
@@ -219,7 +203,7 @@ pip install -e ".[dev]"
|
||||
... )
|
||||
```
|
||||
|
||||
- Load classification datasets in one of supported formats
|
||||
- Load classification datasets in one of the supported formats
|
||||
|
||||
```python
|
||||
>>> cs = sv.ClassificationDataset.from_folder_structure(
|
||||
@@ -227,7 +211,7 @@ pip install -e ".[dev]"
|
||||
... )
|
||||
```
|
||||
|
||||
- Save classification datasets in one of supported formats
|
||||
- Save classification datasets in one of the supported formats
|
||||
|
||||
```python
|
||||
>>> cs.as_folder_structure(
|
||||
@@ -237,6 +221,54 @@ pip install -e ".[dev]"
|
||||
|
||||
</details>
|
||||
|
||||
### [model evaluation](https://roboflow.github.io/supervision/metrics/detection/)
|
||||
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
|
||||
>>> dataset = sv.DetectionDataset.from_yolo(...)
|
||||
|
||||
>>> def callback(image: np.ndarray) -> sv.Detections:
|
||||
... ...
|
||||
|
||||
>>> confusion_matrix = sv.ConfusionMatrix.benchmark(
|
||||
... dataset = dataset,
|
||||
... callback = callback
|
||||
... )
|
||||
|
||||
>>> confusion_matrix.matrix
|
||||
array([
|
||||
[0., 0., 0., 0.],
|
||||
[0., 1., 0., 1.],
|
||||
[0., 1., 1., 0.],
|
||||
[1., 1., 0., 0.]
|
||||
])
|
||||
```
|
||||
|
||||
<details close>
|
||||
<summary>👉 more metrics</summary>
|
||||
|
||||
- Mean average precision (mAP) for object detection tasks.
|
||||
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
|
||||
>>> dataset = sv.DetectionDataset.from_yolo(...)
|
||||
|
||||
>>> def callback(image: np.ndarray) -> sv.Detections:
|
||||
... ...
|
||||
|
||||
>>> mean_average_precision = sv.MeanAveragePrecision.benchmark(
|
||||
... dataset = dataset,
|
||||
... callback = callback
|
||||
... )
|
||||
|
||||
>>> mean_average_precision.map50_95
|
||||
0.433
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 🎬 tutorials
|
||||
|
||||
<p align="left">
|
||||
@@ -255,11 +287,7 @@ pip install -e ".[dev]"
|
||||
|
||||
## 📚 documentation
|
||||
|
||||
Curious how Supervision can help you solve problems on your project? Visit our [documentation](https://roboflow.github.io/supervision) page!
|
||||
|
||||
## 💜 built with supervision
|
||||
|
||||
You built something cool using supervision? [Let us know!](https://github.com/roboflow/supervision/discussions/categories/built-with-supervision)
|
||||
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
|
||||
|
||||
|
||||
+122
-43
@@ -1,10 +1,89 @@
|
||||
### 0.13.0 <small>August 8, 2023</small>
|
||||
|
||||
- Added [#236](https://github.com/roboflow/supervision/pull/236): support for mean average precision (mAP) for object detection models with [`sv.MeanAveragePrecision`](https://roboflow.github.io/supervision/metrics/detection/#meanaverageprecision).
|
||||
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
>>> from ultralytics import YOLO
|
||||
|
||||
>>> dataset = sv.DetectionDataset.from_yolo(...)
|
||||
|
||||
>>> model = YOLO(...)
|
||||
>>> def callback(image: np.ndarray) -> sv.Detections:
|
||||
... result = model(image)[0]
|
||||
... return sv.Detections.from_yolov8(result)
|
||||
|
||||
>>> mean_average_precision = sv.MeanAveragePrecision.benchmark(
|
||||
... dataset = dataset,
|
||||
... callback = callback
|
||||
... )
|
||||
|
||||
>>> mean_average_precision.map50_95
|
||||
0.433
|
||||
```
|
||||
|
||||
- Added [#256](https://github.com/roboflow/supervision/pull/256): support for ByteTrack for object tracking with [`sv.ByteTrack`](https://roboflow.github.io/supervision/tracker/core/#bytetrack).
|
||||
|
||||
- Added [#222](https://github.com/roboflow/supervision/pull/222): [`sv.Detections.from_ultralytics`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.from_ultralytics) to enable seamless integration with [Ultralytics](https://github.com/ultralytics/ultralytics) framework. This will enable you to use `supervision` with all [models](https://docs.ultralytics.com/models/) that Ultralytics supports.
|
||||
|
||||
!!! warning
|
||||
|
||||
[`sv.Detections.from_yolov8`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.from_yolov8) is now deprecated and will be removed with `supervision-0.15.0` release.
|
||||
|
||||
- Added [#191](https://github.com/roboflow/supervision/pull/191): [`sv.Detections.from_paddledet`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.from_paddledet) to enable seamless integration with [PaddleDetection](https://github.com/PaddlePaddle/PaddleDetection) framework.
|
||||
|
||||
- Added [#245](https://github.com/roboflow/supervision/pull/245): support for loading PASCAL VOC segmentation datasets with [`sv.DetectionDataset.`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.from_pascal_voc).
|
||||
|
||||
### 0.12.0 <small>July 24, 2023</small>
|
||||
|
||||
!!! warning
|
||||
|
||||
With the `supervision-0.12.0` release, we are terminating official support for Python 3.7.
|
||||
|
||||
- Added [#177](https://github.com/roboflow/supervision/pull/177): initial support for object detection model benchmarking with [`sv.ConfusionMatrix`](https://roboflow.github.io/supervision/metrics/detection/#confusionmatrix).
|
||||
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
>>> from ultralytics import YOLO
|
||||
|
||||
>>> dataset = sv.DetectionDataset.from_yolo(...)
|
||||
|
||||
>>> model = YOLO(...)
|
||||
>>> def callback(image: np.ndarray) -> sv.Detections:
|
||||
... result = model(image)[0]
|
||||
... return sv.Detections.from_yolov8(result)
|
||||
|
||||
>>> confusion_matrix = sv.ConfusionMatrix.benchmark(
|
||||
... dataset = dataset,
|
||||
... callback = callback
|
||||
... )
|
||||
|
||||
>>> confusion_matrix.matrix
|
||||
array([
|
||||
[0., 0., 0., 0.],
|
||||
[0., 1., 0., 1.],
|
||||
[0., 1., 1., 0.],
|
||||
[1., 1., 0., 0.]
|
||||
])
|
||||
```
|
||||
|
||||
- Added [#173](https://github.com/roboflow/supervision/pull/173): [`Detections.from_mmdetection`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.from_mmdetection) to enable seamless integration with [MMDetection](https://github.com/open-mmlab/mmdetection) framework.
|
||||
|
||||
- Added [#130](https://github.com/roboflow/supervision/issues/130): ability to [install](https://roboflow.github.io/supervision/) package in `headless` or `desktop` mode.
|
||||
|
||||
- Changed [#180](https://github.com/roboflow/supervision/pull/180): packing method from `setup.py` to `pyproject.toml`.
|
||||
|
||||
- Fixed [#188](https://github.com/roboflow/supervision/issues/188): [`sv.DetectionDataset.from_cooc`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.from_coco) can't be loaded when there are images without annotations.
|
||||
|
||||
- Fixed [#226](https://github.com/roboflow/supervision/issues/226): [`sv.DetectionDataset.from_yolo`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.from_yolo) can't load background instances.
|
||||
|
||||
### 0.11.1 <small>June 29, 2023</small>
|
||||
|
||||
- Fix [[#165](https://github.com/roboflow/supervision/pull/165)]: [`as_folder_structure`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.ClassificationDataset.as_folder_structure) fails to save [`sv.ClassificationDataset`](https://roboflow.github.io/supervision/dataset/core/#classificationdataset) when it is result of inference.
|
||||
- Fix [#165](https://github.com/roboflow/supervision/pull/165): [`as_folder_structure`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.ClassificationDataset.as_folder_structure) fails to save [`sv.ClassificationDataset`](https://roboflow.github.io/supervision/dataset/core/#classificationdataset) when it is result of inference.
|
||||
|
||||
### 0.11.0 <small>June 28, 2023</small>
|
||||
|
||||
- Added [[#150](https://github.com/roboflow/supervision/pull/150)]: ability to load and save [`sv.DetectionDataset`](https://roboflow.github.io/supervision/dataset/core/#detectiondataset) in COCO format using [`as_coco`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.as_coco) and [`from_coco`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.from_coco) methods.
|
||||
- Added [#150](https://github.com/roboflow/supervision/pull/150): ability to load and save [`sv.DetectionDataset`](https://roboflow.github.io/supervision/dataset/core/#detectiondataset) in COCO format using [`as_coco`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.as_coco) and [`from_coco`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.from_coco) methods.
|
||||
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
@@ -20,7 +99,7 @@
|
||||
... )
|
||||
```
|
||||
|
||||
- Added [[#158](https://github.com/roboflow/supervision/pull/158)]: ability to marge multiple [`sv.DetectionDataset`](https://roboflow.github.io/supervision/dataset/core/#detectiondataset) together using [`merge`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.merge) method.
|
||||
- Added [#158](https://github.com/roboflow/supervision/pull/158): ability to marge multiple [`sv.DetectionDataset`](https://roboflow.github.io/supervision/dataset/core/#detectiondataset) together using [`merge`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.merge) method.
|
||||
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
@@ -44,13 +123,13 @@
|
||||
['cat', 'dog', 'person']
|
||||
```
|
||||
|
||||
- Added [[#162](https://github.com/roboflow/supervision/pull/162)]: additional `start` and `end` arguments to [`sv.get_video_frames_generator`](https://roboflow.github.io/supervision/utils/video/#get_video_frames_generator) allowing to generate frames only for a selected part of the video.
|
||||
- Added [#162](https://github.com/roboflow/supervision/pull/162): additional `start` and `end` arguments to [`sv.get_video_frames_generator`](https://roboflow.github.io/supervision/utils/video/#get_video_frames_generator) allowing to generate frames only for a selected part of the video.
|
||||
|
||||
- Fix [[#157](https://github.com/roboflow/supervision/pull/157)]: incorrect loading of YOLO dataset class names from `data.yaml`.
|
||||
- Fix [#157](https://github.com/roboflow/supervision/pull/157): incorrect loading of YOLO dataset class names from `data.yaml`.
|
||||
|
||||
### 0.10.0 <small>June 14, 2023</small>
|
||||
|
||||
- Added [[#125](https://github.com/roboflow/supervision/pull/125)]: ability to load and save [`sv.ClassificationDataset`](https://roboflow.github.io/supervision/dataset/core/#classificationdataset) in a folder structure format.
|
||||
- Added [#125](https://github.com/roboflow/supervision/pull/125): ability to load and save [`sv.ClassificationDataset`](https://roboflow.github.io/supervision/dataset/core/#classificationdataset) in a folder structure format.
|
||||
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
@@ -64,17 +143,17 @@
|
||||
... )
|
||||
```
|
||||
|
||||
- Added [[#125](https://github.com/roboflow/supervision/pull/125)]: support for [`sv.ClassificationDataset.split`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.ClassificationDataset.split) allowing to divide `sv.ClassificationDataset` into two parts.
|
||||
- Added [#125](https://github.com/roboflow/supervision/pull/125): support for [`sv.ClassificationDataset.split`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.ClassificationDataset.split) allowing to divide `sv.ClassificationDataset` into two parts.
|
||||
|
||||
- Added [[#110](https://github.com/roboflow/supervision/pull/110)]: ability to extract masks from Roboflow API results using [`sv.Detections.from_roboflow`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.from_roboflow).
|
||||
- Added [#110](https://github.com/roboflow/supervision/pull/110): ability to extract masks from Roboflow API results using [`sv.Detections.from_roboflow`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.from_roboflow).
|
||||
|
||||
- Added [[commit hash](https://github.com/roboflow/supervision/commit/d000292eb2f2342544e0947b65528082e60fb8d6)]: Supervision Quickstart [notebook](https://colab.research.google.com/github/roboflow/supervision/blob/main/demo.ipynb) where you can learn more about Detection, Dataset and Video APIs.
|
||||
- Added [commit hash](https://github.com/roboflow/supervision/commit/d000292eb2f2342544e0947b65528082e60fb8d6): Supervision Quickstart [notebook](https://colab.research.google.com/github/roboflow/supervision/blob/main/demo.ipynb) where you can learn more about Detection, Dataset and Video APIs.
|
||||
|
||||
- Changed [[#135](https://github.com/roboflow/supervision/pull/135)]: `sv.get_video_frames_generator` documentation to better describe actual behavior.
|
||||
- Changed [#135](https://github.com/roboflow/supervision/pull/135): `sv.get_video_frames_generator` documentation to better describe actual behavior.
|
||||
|
||||
### 0.9.0 <small>June 7, 2023</small>
|
||||
|
||||
- Added [[#118](https://github.com/roboflow/supervision/pull/118)]: ability to select [`sv.Detections`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.__getitem__) by index, list of indexes or slice. Here is an example illustrating the new selection methods.
|
||||
- Added [#118](https://github.com/roboflow/supervision/pull/118): ability to select [`sv.Detections`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.__getitem__) by index, list of indexes or slice. Here is an example illustrating the new selection methods.
|
||||
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
@@ -88,11 +167,11 @@
|
||||
2
|
||||
```
|
||||
|
||||
- Added [[#101](https://github.com/roboflow/supervision/pull/101)]: ability to extract masks from YOLOv8 result using [`sv.Detections.from_yolov8`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.from_yolov8). Here is an example illustrating how to extract boolean masks from the result of the YOLOv8 model inference.
|
||||
- Added [#101](https://github.com/roboflow/supervision/pull/101): ability to extract masks from YOLOv8 result using [`sv.Detections.from_yolov8`](https://roboflow.github.io/supervision/detection/core/#supervision.detection.core.Detections.from_yolov8). Here is an example illustrating how to extract boolean masks from the result of the YOLOv8 model inference.
|
||||
|
||||
- Added [[#122](https://github.com/roboflow/supervision/pull/122)]: ability to crop image using [`sv.crop`](https://roboflow.github.io/supervision/utils/image/#crop). Here is an example showing how to get a separate crop for each detection in `sv.Detections`.
|
||||
- Added [#122](https://github.com/roboflow/supervision/pull/122): ability to crop image using [`sv.crop`](https://roboflow.github.io/supervision/utils/image/#crop). Here is an example showing how to get a separate crop for each detection in `sv.Detections`.
|
||||
|
||||
- Added [[#120](https://github.com/roboflow/supervision/pull/120)]: ability to conveniently save multiple images into directory using [`sv.ImageSink`](https://roboflow.github.io/supervision/utils/image/#imagesink). Here is an example showing how to save every tenth video frame as a separate image.
|
||||
- Added [#120](https://github.com/roboflow/supervision/pull/120): ability to conveniently save multiple images into directory using [`sv.ImageSink`](https://roboflow.github.io/supervision/utils/image/#imagesink). Here is an example showing how to save every tenth video frame as a separate image.
|
||||
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
@@ -102,12 +181,12 @@
|
||||
... sink.save_image(image=image)
|
||||
```
|
||||
|
||||
- Fixed [[#106](https://github.com/roboflow/supervision/issues/106)]: inconvenient handling of [`sv.PolygonZone`](https://roboflow.github.io/supervision/detection/tools/polygon_zone/#polygonzone) coordinates. Now `sv.PolygonZone` accepts coordinates in the form of `[[x1, y1], [x2, y2], ...]` that can be both integers and floats.
|
||||
- Fixed [#106](https://github.com/roboflow/supervision/issues/106): inconvenient handling of [`sv.PolygonZone`](https://roboflow.github.io/supervision/detection/tools/polygon_zone/#polygonzone) coordinates. Now `sv.PolygonZone` accepts coordinates in the form of `[[x1, y1], [x2, y2], ...]` that can be both integers and floats.
|
||||
|
||||
### 0.8.0 <small>May 17, 2023</small>
|
||||
|
||||
- Added [[#100](https://github.com/roboflow/supervision/pull/100)]: support for dataset inheritance. The current `Dataset` got renamed to `DetectionDataset`. Now [`DetectionDataset`](https://roboflow.github.io/supervision/dataset/core/#detectiondataset) inherits from `BaseDataset`. This change was made to enforce the future consistency of APIs of different types of computer vision datasets.
|
||||
- Added [[#100](https://github.com/roboflow/supervision/pull/100)]: ability to save datasets in YOLO format using [`DetectionDataset.as_yolo`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.as_yolo).
|
||||
- Added [#100](https://github.com/roboflow/supervision/pull/100): support for dataset inheritance. The current `Dataset` got renamed to `DetectionDataset`. Now [`DetectionDataset`](https://roboflow.github.io/supervision/dataset/core/#detectiondataset) inherits from `BaseDataset`. This change was made to enforce the future consistency of APIs of different types of computer vision datasets.
|
||||
- Added [#100](https://github.com/roboflow/supervision/pull/100): ability to save datasets in YOLO format using [`DetectionDataset.as_yolo`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.as_yolo).
|
||||
|
||||
```python
|
||||
>>> import roboflow
|
||||
@@ -131,7 +210,7 @@
|
||||
['dog', 'person']
|
||||
```
|
||||
|
||||
- Added [[#102](https://github.com/roboflow/supervision/pull/103)]: support for [`DetectionDataset.split`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.split) allowing to divide `DetectionDataset` into two parts.
|
||||
- Added [#102](https://github.com/roboflow/supervision/pull/103): support for [`DetectionDataset.split`](https://roboflow.github.io/supervision/dataset/core/#supervision.dataset.core.DetectionDataset.split) allowing to divide `DetectionDataset` into two parts.
|
||||
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
@@ -143,27 +222,27 @@
|
||||
(700, 300)
|
||||
```
|
||||
|
||||
- Changed [[#100](https://github.com/roboflow/supervision/pull/100)]: default value of `approximation_percentage` parameter from `0.75` to `0.0` in `DetectionDataset.as_yolo` and `DetectionDataset.as_pascal_voc`.
|
||||
- Changed [#100](https://github.com/roboflow/supervision/pull/100): default value of `approximation_percentage` parameter from `0.75` to `0.0` in `DetectionDataset.as_yolo` and `DetectionDataset.as_pascal_voc`.
|
||||
|
||||
### 0.7.0 <small>May 11, 2023</small>
|
||||
|
||||
- Added [[#91](https://github.com/roboflow/supervision/pull/91)]: `Detections.from_yolo_nas` to enable seamless integration with [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) model.
|
||||
- Added [[#86](https://github.com/roboflow/supervision/pull/86)]: ability to load datasets in YOLO format using `Dataset.from_yolo`.
|
||||
- Added [[#84](https://github.com/roboflow/supervision/pull/84)]: `Detections.merge` to merge multiple `Detections` objects together.
|
||||
- Fixed [[#81](https://github.com/roboflow/supervision/pull/81)]: `LineZoneAnnotator.annotate` does not return annotated frame.
|
||||
- Changed [[#44](https://github.com/roboflow/supervision/pull/44)]: `LineZoneAnnotator.annotate` to allow for custom text for the in and out tags.
|
||||
- Added [#91](https://github.com/roboflow/supervision/pull/91): `Detections.from_yolo_nas` to enable seamless integration with [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) model.
|
||||
- Added [#86](https://github.com/roboflow/supervision/pull/86): ability to load datasets in YOLO format using `Dataset.from_yolo`.
|
||||
- Added [#84](https://github.com/roboflow/supervision/pull/84): `Detections.merge` to merge multiple `Detections` objects together.
|
||||
- Fixed [#81](https://github.com/roboflow/supervision/pull/81): `LineZoneAnnotator.annotate` does not return annotated frame.
|
||||
- Changed [#44](https://github.com/roboflow/supervision/pull/44): `LineZoneAnnotator.annotate` to allow for custom text for the in and out tags.
|
||||
|
||||
### 0.6.0 <small>April 19, 2023</small>
|
||||
|
||||
- Added [[#71](https://github.com/roboflow/supervision/pull/71)]: initial `Dataset` support and ability to save `Detections` in Pascal VOC XML format.
|
||||
- Added [[#71](https://github.com/roboflow/supervision/pull/71)]: new `mask_to_polygons`, `filter_polygons_by_area`, `polygon_to_xyxy` and `approximate_polygon` utilities.
|
||||
- Added [[#72](https://github.com/roboflow/supervision/pull/72)]: ability to load Pascal VOC XML **object detections** dataset as `Dataset`.
|
||||
- Changed [[#70](https://github.com/roboflow/supervision/pull/70)]: order of `Detections` attributes to make it consistent with order of objects in `__iter__` tuple.
|
||||
- Changed [[#71](https://github.com/roboflow/supervision/pull/71)]: `generate_2d_mask` to `polygon_to_mask`.
|
||||
- Added [#71](https://github.com/roboflow/supervision/pull/71): initial `Dataset` support and ability to save `Detections` in Pascal VOC XML format.
|
||||
- Added [#71](https://github.com/roboflow/supervision/pull/71): new `mask_to_polygons`, `filter_polygons_by_area`, `polygon_to_xyxy` and `approximate_polygon` utilities.
|
||||
- Added [#72](https://github.com/roboflow/supervision/pull/72): ability to load Pascal VOC XML **object detections** dataset as `Dataset`.
|
||||
- Changed [#70](https://github.com/roboflow/supervision/pull/70): order of `Detections` attributes to make it consistent with order of objects in `__iter__` tuple.
|
||||
- Changed [#71](https://github.com/roboflow/supervision/pull/71): `generate_2d_mask` to `polygon_to_mask`.
|
||||
|
||||
### 0.5.2 <small>April 13, 2023</small>
|
||||
|
||||
- Fixed [[#63](https://github.com/roboflow/supervision/pull/63)]: `LineZone.trigger` function expects 4 values instead of 5.
|
||||
- Fixed [#63](https://github.com/roboflow/supervision/pull/63): `LineZone.trigger` function expects 4 values instead of 5.
|
||||
|
||||
### 0.5.1 <small>April 12, 2023</small>
|
||||
|
||||
@@ -172,28 +251,28 @@
|
||||
|
||||
### 0.5.0 <small>April 10, 2023</small>
|
||||
|
||||
- Added [[#58](https://github.com/roboflow/supervision/pull/58)]: `Detections.mask` to enable segmentation support.
|
||||
- Added [[#58](https://github.com/roboflow/supervision/pull/58)]: `MaskAnnotator` to allow easy `Detections.mask` annotation.
|
||||
- Added [[#58](https://github.com/roboflow/supervision/pull/58)]: `Detections.from_sam` to enable native Segment Anything Model (SAM) support.
|
||||
- Changed [[#58](https://github.com/roboflow/supervision/pull/58)]: `Detections.area` behaviour to work not only with boxes but also with masks.
|
||||
- Added [#58](https://github.com/roboflow/supervision/pull/58): `Detections.mask` to enable segmentation support.
|
||||
- Added [#58](https://github.com/roboflow/supervision/pull/58): `MaskAnnotator` to allow easy `Detections.mask` annotation.
|
||||
- Added [#58](https://github.com/roboflow/supervision/pull/58): `Detections.from_sam` to enable native Segment Anything Model (SAM) support.
|
||||
- Changed [#58](https://github.com/roboflow/supervision/pull/58): `Detections.area` behaviour to work not only with boxes but also with masks.
|
||||
|
||||
### 0.4.0 <small>April 5, 2023</small>
|
||||
|
||||
- Added [[#46](https://github.com/roboflow/supervision/discussions/48)]: `Detections.empty` to allow easy creation of empty `Detections` objects.
|
||||
- Added [[#56](https://github.com/roboflow/supervision/pull/56)]: `Detections.from_roboflow` to allow easy creation of `Detections` objects from Roboflow API inference results.
|
||||
- Added [[#56](https://github.com/roboflow/supervision/pull/56)]: `plot_images_grid` to allow easy plotting of multiple images on single plot.
|
||||
- Added [[#56](https://github.com/roboflow/supervision/pull/56)]: initial support for Pascal VOC XML format with `detections_to_voc_xml` method.
|
||||
- Changed [[#56](https://github.com/roboflow/supervision/pull/56)]: `show_frame_in_notebook` refactored and renamed to `plot_image`.
|
||||
- Added [#46](https://github.com/roboflow/supervision/discussions/48): `Detections.empty` to allow easy creation of empty `Detections` objects.
|
||||
- Added [#56](https://github.com/roboflow/supervision/pull/56): `Detections.from_roboflow` to allow easy creation of `Detections` objects from Roboflow API inference results.
|
||||
- Added [#56](https://github.com/roboflow/supervision/pull/56): `plot_images_grid` to allow easy plotting of multiple images on single plot.
|
||||
- Added [#56](https://github.com/roboflow/supervision/pull/56): initial support for Pascal VOC XML format with `detections_to_voc_xml` method.
|
||||
- Changed [#56](https://github.com/roboflow/supervision/pull/56): `show_frame_in_notebook` refactored and renamed to `plot_image`.
|
||||
|
||||
### 0.3.2 <small>March 23, 2023</small>
|
||||
|
||||
- Changed [[#50](https://github.com/roboflow/supervision/issues/50)]: Allow `Detections.class_id` to be `None`.
|
||||
- Changed [#50](https://github.com/roboflow/supervision/issues/50): Allow `Detections.class_id` to be `None`.
|
||||
|
||||
### 0.3.1 <small>March 6, 2023</small>
|
||||
|
||||
- Fixed [[#41](https://github.com/roboflow/supervision/issues/41)]: `PolygonZone` throws an exception when the object touches the bottom edge of the image.
|
||||
- Fixed [[#42](https://github.com/roboflow/supervision/issues/42)]: `Detections.wth_nms` method throws an exception when `Detections` is empty.
|
||||
- Changed [[#36](https://github.com/roboflow/supervision/pull/36)]: `Detections.wth_nms` support class agnostic and non-class agnostic case.
|
||||
- Fixed [#41](https://github.com/roboflow/supervision/issues/41): `PolygonZone` throws an exception when the object touches the bottom edge of the image.
|
||||
- Fixed [#42](https://github.com/roboflow/supervision/issues/42): `Detections.wth_nms` method throws an exception when `Detections` is empty.
|
||||
- Changed [#36](https://github.com/roboflow/supervision/pull/36): `Detections.wth_nms` support class agnostic and non-class agnostic case.
|
||||
|
||||
### 0.3.0 <small>March 6, 2023</small>
|
||||
|
||||
|
||||
+54
-18
@@ -9,29 +9,65 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## 👋 Welcome
|
||||
## 👋 Hello
|
||||
|
||||
Supervision is a set of easy-to-use utilities that will come in handy in any computer vision project.
|
||||
We write your reusable computer vision tools. Whether you need to load your dataset from your hard drive, draw detections on an image or video, or count how many detections are in a zone. You can count on us!
|
||||
|
||||
**Supervision** is still in
|
||||
pre-release stage 🚧 Keep your eyes open for potential bugs and be aware that at this stage our API is still fluid and may change.
|
||||
|
||||
## 💻 How to Install
|
||||
## 💻 Install
|
||||
|
||||
You can install `supervision` with pip in a
|
||||
[**3.11>=Python>=3.7**](https://www.python.org/) environment.
|
||||
[**3.11>=Python>=3.8**](https://www.python.org/) environment.
|
||||
|
||||
!!! example "Pip install method (recommended)"
|
||||
!!! example "pip install (recommended)"
|
||||
|
||||
```bash
|
||||
pip install supervision
|
||||
```
|
||||
=== "headless"
|
||||
The headless installation of `supervision` is designed for environments where graphical user interfaces (GUI) are not needed, making it more lightweight and suitable for server-side applications.
|
||||
|
||||
!!! example "Git clone method (for development)"
|
||||
```bash
|
||||
pip install supervision
|
||||
```
|
||||
|
||||
=== "desktop"
|
||||
If you require the full version of `supervision` with GUI support you can install the desktop version. This version includes the GUI components of OpenCV, allowing you to display images and videos on the screen.
|
||||
|
||||
```bash
|
||||
git https://github.com/roboflow/supervision.git
|
||||
cd supervision
|
||||
pip install -e '.[dev]'
|
||||
```
|
||||
See contributing section to know more about contributing to the project
|
||||
```bash
|
||||
pip install supervision[desktop]
|
||||
```
|
||||
|
||||
!!! example "git clone (for development)"
|
||||
|
||||
=== "virtualenv"
|
||||
|
||||
```bash
|
||||
# clone repository and navigate to root directory
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
cd supervision
|
||||
|
||||
# setup python environment and activate it
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# headless install
|
||||
pip install -e "."
|
||||
|
||||
# desktop install
|
||||
pip install -e ".[desktop]"
|
||||
```
|
||||
|
||||
=== "poetry"
|
||||
|
||||
```bash
|
||||
# clone repository and navigate to root directory
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
cd supervision
|
||||
|
||||
# setup python environment and activate it
|
||||
poetry env use python 3.10
|
||||
poetry shell
|
||||
|
||||
# headless install
|
||||
poetry install
|
||||
|
||||
# desktop install
|
||||
poetry install --extras "desktop"
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
!!! warning
|
||||
|
||||
Evaluation API is still fluid and may change. If you use Evaluation API in your project until further notice, freeze the
|
||||
`supervision` version in your `requirements.txt` or `setup.py`.
|
||||
|
||||
## ConfusionMatrix
|
||||
|
||||
:::supervision.metrics.detection.ConfusionMatrix
|
||||
|
||||
## MeanAveragePrecision
|
||||
|
||||
:::supervision.metrics.detection.MeanAveragePrecision
|
||||
@@ -0,0 +1,4 @@
|
||||
## ByteTrack
|
||||
|
||||
:::supervision.tracker.byte_tracker.core.ByteTrack
|
||||
|
||||
@@ -37,8 +37,12 @@ nav:
|
||||
- Utils: detection/utils.md
|
||||
- Tools:
|
||||
- Polygon Zone: detection/tools/polygon_zone.md
|
||||
- Trackers:
|
||||
- Core: tracker/core.md
|
||||
- Dataset:
|
||||
- Core: dataset/core.md
|
||||
- Metrics:
|
||||
- Detection Models: metrics/detection.md
|
||||
- Draw:
|
||||
- Utils: draw/utils.md
|
||||
- Utils:
|
||||
|
||||
Generated
+3513
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
[tool.poetry]
|
||||
name = "supervision"
|
||||
version = "0.13.0"
|
||||
description = "A set of easy-to-use utils that will come in handy in any Computer Vision project"
|
||||
authors = ["Piotr Skalski <piotr.skalski92@gmail.com>"]
|
||||
maintainers = ["Piotr Skalski <piotr.skalski92@gmail.com>"]
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
packages = [{include = "supervision"}]
|
||||
homepage = "https://github.com/roboflow/supervision"
|
||||
repository = "https://github.com/roboflow/supervision"
|
||||
documentation = "https://github.com/roboflow/supervision/blob/main/README.md"
|
||||
keywords = ["machine-learning", "deep-learning", "vision", "ML", "DL", "AI", "YOLOv5", "YOLOv8", "Roboflow"]
|
||||
|
||||
classifiers=[
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: Science/Research',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.11',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Topic :: Software Development',
|
||||
'Topic :: Scientific/Engineering',
|
||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||
"Topic :: Scientific/Engineering :: Image Recognition",
|
||||
'Typing :: Typed',
|
||||
'Operating System :: Microsoft :: Windows',
|
||||
'Operating System :: POSIX',
|
||||
'Operating System :: Unix',
|
||||
'Operating System :: MacOS'
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.8,<3.12.0"
|
||||
numpy = "^1.20.0"
|
||||
matplotlib = "^3.7.1"
|
||||
pyyaml = "^6.0"
|
||||
pillow = "^9.4.0"
|
||||
opencv-python = { version = "^4.8.0.74", optional = true }
|
||||
opencv-python-headless = "^4.8.0.74"
|
||||
scipy = "^1.9.0"
|
||||
|
||||
|
||||
[tool.poetry.extras]
|
||||
desktop = ["opencv-python"]
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
twine = "^4.0.2"
|
||||
pytest = "^7.2.2"
|
||||
wheel = "^0.40.0"
|
||||
notebook = "^6.5.3"
|
||||
build = "^0.10.0"
|
||||
ruff = "^0.0.280"
|
||||
isort = "^5.12.0"
|
||||
black = "^23.7.0"
|
||||
mypy = "^1.4.1"
|
||||
pre-commit = "^3.3.3"
|
||||
flake8 = { version = "*", python = ">=3.8.1,<3.12.0" }
|
||||
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
mkdocs-material = "^9.1.4"
|
||||
mkdocstrings = {extras = ["python"], version = "^0.20.0"}
|
||||
|
||||
[tool.setuptools]
|
||||
include-package-data = false
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
exclude = ["docs*", "test*"]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
@@ -1,71 +0,0 @@
|
||||
import setuptools
|
||||
from setuptools import find_packages
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
FILE = Path(__file__).resolve()
|
||||
PARENT = FILE.parent # root directory
|
||||
README = (PARENT / "README.md").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def get_version():
|
||||
file = PARENT / 'supervision/__init__.py'
|
||||
return re.search(r'^__version__ = [\'"]([^\'"]*)[\'"]', file.read_text(encoding="utf-8"), re.M)[1]
|
||||
|
||||
|
||||
setuptools.setup(
|
||||
name='supervision',
|
||||
version=get_version(),
|
||||
author='Piotr Skalski',
|
||||
author_email='piotr.skalski92@gmail.com',
|
||||
license='MIT',
|
||||
description='A set of easy-to-use utils that will come in handy in any Computer Vision project',
|
||||
long_description=README,
|
||||
long_description_content_type='text/markdown',
|
||||
url='https://github.com/roboflow/supervision',
|
||||
install_requires=[
|
||||
'numpy>=1.20.0',
|
||||
'opencv-python',
|
||||
'matplotlib',
|
||||
'pyyaml'
|
||||
],
|
||||
packages=find_packages(exclude=("tests",)),
|
||||
extras_require={
|
||||
'dev': [
|
||||
'flake8',
|
||||
'black==22.3.0',
|
||||
'isort',
|
||||
'twine',
|
||||
'pytest',
|
||||
'wheel',
|
||||
'notebook',
|
||||
'mkdocs-material',
|
||||
'mkdocstrings[python]'
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: Education',
|
||||
'Intended Audience :: Science/Research',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.11',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Topic :: Software Development',
|
||||
'Topic :: Scientific/Engineering',
|
||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||
"Topic :: Scientific/Engineering :: Image Recognition",
|
||||
'Typing :: Typed',
|
||||
'Operating System :: Microsoft :: Windows',
|
||||
'Operating System :: POSIX',
|
||||
'Operating System :: Unix',
|
||||
'Operating System :: MacOS'
|
||||
],
|
||||
keywords="machine-learning, deep-learning, vision, ML, DL, AI, YOLOv5, YOLOv8, SAM, Roboflow",
|
||||
python_requires='>=3.7',
|
||||
)
|
||||
+10
-1
@@ -1,4 +1,11 @@
|
||||
__version__ = "0.11.1"
|
||||
import importlib.metadata as importlib_metadata
|
||||
|
||||
try:
|
||||
# This will read version from pyproject.toml
|
||||
__version__ = importlib_metadata.version(__package__ or __name__)
|
||||
except importlib_metadata.PackageNotFoundError:
|
||||
__version__ = "development"
|
||||
|
||||
|
||||
from supervision.classification.core import Classifications
|
||||
from supervision.dataset.core import (
|
||||
@@ -23,6 +30,8 @@ from supervision.draw.color import Color, ColorPalette
|
||||
from supervision.draw.utils import draw_filled_rectangle, draw_polygon, draw_text
|
||||
from supervision.geometry.core import Point, Position, Rect
|
||||
from supervision.geometry.utils import get_polygon_center
|
||||
from supervision.metrics.detection import ConfusionMatrix, MeanAveragePrecision
|
||||
from supervision.tracker.byte_tracker.core import ByteTrack
|
||||
from supervision.utils.file import list_files_with_extensions
|
||||
from supervision.utils.image import ImageSink, crop
|
||||
from supervision.utils.notebook import plot_image, plot_images_grid
|
||||
|
||||
+11
-29
@@ -31,7 +31,6 @@ from supervision.dataset.utils import (
|
||||
train_test_split,
|
||||
)
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.utils.file import list_files_with_extensions
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -197,7 +196,10 @@ class DetectionDataset(BaseDataset):
|
||||
|
||||
@classmethod
|
||||
def from_pascal_voc(
|
||||
cls, images_directory_path: str, annotations_directory_path: str
|
||||
cls,
|
||||
images_directory_path: str,
|
||||
annotations_directory_path: str,
|
||||
force_masks: bool = False,
|
||||
) -> DetectionDataset:
|
||||
"""
|
||||
Creates a Dataset instance from PASCAL VOC formatted data.
|
||||
@@ -205,6 +207,7 @@ class DetectionDataset(BaseDataset):
|
||||
Args:
|
||||
images_directory_path (str): The path to the directory containing the images.
|
||||
annotations_directory_path (str): The path to the directory containing the PASCAL VOC XML annotations.
|
||||
force_masks (bool, optional): If True, forces masks to be loaded for all annotations, regardless of whether they are present.
|
||||
|
||||
Returns:
|
||||
DetectionDataset: A DetectionDataset instance containing the loaded images and annotations.
|
||||
@@ -222,7 +225,7 @@ class DetectionDataset(BaseDataset):
|
||||
>>> project = rf.workspace(WORKSPACE_ID).project(PROJECT_ID)
|
||||
>>> dataset = project.version(PROJECT_VERSION).download("voc")
|
||||
|
||||
>>> ds = sv.DetectionDataset.from_yolo(
|
||||
>>> ds = sv.DetectionDataset.from_pascal_voc(
|
||||
... images_directory_path=f"{dataset.location}/train/images",
|
||||
... annotations_directory_path=f"{dataset.location}/train/labels"
|
||||
... )
|
||||
@@ -231,34 +234,13 @@ class DetectionDataset(BaseDataset):
|
||||
['dog', 'person']
|
||||
```
|
||||
"""
|
||||
image_paths = list_files_with_extensions(
|
||||
directory=images_directory_path, extensions=["jpg", "jpeg", "png"]
|
||||
)
|
||||
annotation_paths = list_files_with_extensions(
|
||||
directory=annotations_directory_path, extensions=["xml"]
|
||||
|
||||
classes, images, annotations = load_pascal_voc_annotations(
|
||||
images_directory_path=images_directory_path,
|
||||
annotations_directory_path=annotations_directory_path,
|
||||
force_masks=force_masks,
|
||||
)
|
||||
|
||||
raw_annotations: List[Tuple[str, Detections, List[str]]] = [
|
||||
load_pascal_voc_annotations(annotation_path=str(annotation_path))
|
||||
for annotation_path in annotation_paths
|
||||
]
|
||||
|
||||
classes = []
|
||||
for annotation in raw_annotations:
|
||||
classes.extend(annotation[2])
|
||||
classes = list(set(classes))
|
||||
|
||||
for annotation in raw_annotations:
|
||||
class_id = [classes.index(class_name) for class_name in annotation[2]]
|
||||
annotation[1].class_id = np.array(class_id)
|
||||
|
||||
images = {
|
||||
image_path.name: cv2.imread(str(image_path)) for image_path in image_paths
|
||||
}
|
||||
|
||||
annotations = {
|
||||
image_name: detections for image_name, detections, _ in raw_annotations
|
||||
}
|
||||
return DetectionDataset(classes=classes, images=images, annotations=annotations)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -6,7 +6,10 @@ from typing import Dict, List, Tuple
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from supervision.dataset.utils import approximate_mask_with_polygons
|
||||
from supervision.dataset.utils import (
|
||||
approximate_mask_with_polygons,
|
||||
map_detections_class_id,
|
||||
)
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.utils import polygon_to_mask
|
||||
from supervision.utils.file import read_json_file, save_json_file
|
||||
@@ -19,6 +22,18 @@ def coco_categories_to_classes(coco_categories: List[dict]) -> List[str]:
|
||||
]
|
||||
|
||||
|
||||
def build_coco_class_index_mapping(
|
||||
coco_categories: List[dict], target_classes: List[str]
|
||||
) -> Dict[int, int]:
|
||||
source_class_to_index = {
|
||||
category["name"]: category["id"] for category in coco_categories
|
||||
}
|
||||
return {
|
||||
source_class_to_index[target_class_name]: target_class_index
|
||||
for target_class_index, target_class_name in enumerate(target_classes)
|
||||
}
|
||||
|
||||
|
||||
def classes_to_coco_categories(classes: List[str]) -> List[dict]:
|
||||
return [
|
||||
{
|
||||
@@ -109,7 +124,7 @@ def detections_to_coco_annotations(
|
||||
"category_id": int(class_id),
|
||||
"bbox": [xyxy[0], xyxy[1], box_width, box_height],
|
||||
"area": box_width * box_height,
|
||||
"segmentation": polygon,
|
||||
"segmentation": [polygon] if polygon else [],
|
||||
"iscrowd": 0,
|
||||
}
|
||||
coco_annotations.append(coco_annotation)
|
||||
@@ -124,6 +139,9 @@ def load_coco_annotations(
|
||||
) -> Tuple[List[str], Dict[str, np.ndarray], Dict[str, Detections]]:
|
||||
coco_data = read_json_file(file_path=annotations_path)
|
||||
classes = coco_categories_to_classes(coco_categories=coco_data["categories"])
|
||||
class_index_mapping = build_coco_class_index_mapping(
|
||||
coco_categories=coco_data["categories"], target_classes=classes
|
||||
)
|
||||
coco_images = coco_data["images"]
|
||||
coco_annotations_groups = group_coco_annotations_by_image_id(
|
||||
coco_annotations=coco_data["annotations"]
|
||||
@@ -147,6 +165,10 @@ def load_coco_annotations(
|
||||
resolution_wh=(image_width, image_height),
|
||||
with_masks=force_masks,
|
||||
)
|
||||
annotation = map_detections_class_id(
|
||||
source_to_target_mapping=class_index_mapping,
|
||||
detections=annotation,
|
||||
)
|
||||
|
||||
images[image_name] = image
|
||||
annotations[image_name] = annotation
|
||||
@@ -177,8 +199,7 @@ def save_coco_annotations(
|
||||
coco_images = []
|
||||
coco_categories = classes_to_coco_categories(classes=classes)
|
||||
|
||||
image_id = 0
|
||||
annotation_id = 0
|
||||
image_id, annotation_id = 1, 1
|
||||
for image_name, image in images.items():
|
||||
image_height, image_width, _ = image.shape
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
from typing import List, Optional, Tuple
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from xml.dom.minidom import parseString
|
||||
from xml.etree.ElementTree import Element, SubElement, parse, tostring
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from supervision.dataset.utils import approximate_mask_with_polygons
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.utils import polygon_to_xyxy
|
||||
from supervision.detection.utils import polygon_to_mask, polygon_to_xyxy
|
||||
from supervision.utils.file import list_files_with_extensions
|
||||
|
||||
|
||||
def object_to_pascal_voc(
|
||||
@@ -120,24 +124,91 @@ def detections_to_pascal_voc(
|
||||
|
||||
|
||||
def load_pascal_voc_annotations(
|
||||
annotation_path: str,
|
||||
) -> Tuple[str, Detections, List[str]]:
|
||||
images_directory_path: str,
|
||||
annotations_directory_path: str,
|
||||
force_masks: bool = False,
|
||||
) -> Tuple[List[str], Dict[str, np.ndarray], Dict[str, Detections]]:
|
||||
"""
|
||||
Loads PASCAL VOC XML annotations and returns the image name, a Detections instance, and a list of class names.
|
||||
Loads PASCAL VOC annotations and returns class names, images, and their corresponding detections.
|
||||
|
||||
Args:
|
||||
annotation_path (str): The path to the PASCAL VOC XML annotations file.
|
||||
images_directory_path (str): The path to the directory containing the images.
|
||||
annotations_directory_path (str): The path to the directory containing the PASCAL VOC annotation files.
|
||||
force_masks (bool, optional): If True, forces masks to be loaded for all annotations, regardless of whether they are present.
|
||||
|
||||
Returns:
|
||||
Tuple[str, Detections, List[str]]: A tuple containing the image name, a Detections instance, and a list of class names of objects in the detections.
|
||||
Tuple[List[str], Dict[str, np.ndarray], Dict[str, Detections]]: A tuple containing a list of class names, a dictionary with image names as keys and images as values, and a dictionary with image names as keys and corresponding Detections instances as values.
|
||||
"""
|
||||
tree = parse(annotation_path)
|
||||
root = tree.getroot()
|
||||
|
||||
image_name = root.find("filename").text
|
||||
image_paths = list_files_with_extensions(
|
||||
directory=images_directory_path, extensions=["jpg", "jpeg", "png"]
|
||||
)
|
||||
|
||||
classes = []
|
||||
images = {}
|
||||
annotations = {}
|
||||
|
||||
for image_path in image_paths:
|
||||
image_name = Path(image_path).stem
|
||||
image = cv2.imread(str(image_path))
|
||||
|
||||
annotation_path = os.path.join(annotations_directory_path, f"{image_name}.xml")
|
||||
if not os.path.exists(annotation_path):
|
||||
images[image_path.name] = image
|
||||
annotations[image_path.name] = Detections.empty()
|
||||
continue
|
||||
|
||||
tree = parse(annotation_path)
|
||||
root = tree.getroot()
|
||||
|
||||
resolution_wh = (image.shape[1], image.shape[0])
|
||||
annotation, classes = detections_from_xml_obj(
|
||||
root, classes, resolution_wh, force_masks
|
||||
)
|
||||
|
||||
images[image_path.name] = image
|
||||
annotations[image_path.name] = annotation
|
||||
|
||||
return classes, images, annotations
|
||||
|
||||
|
||||
def detections_from_xml_obj(
|
||||
root: Element, classes: List[str], resolution_wh, force_masks: bool = False
|
||||
) -> Tuple[Detections, List[str]]:
|
||||
"""
|
||||
Converts an XML object in Pascal VOC format to a Detections object.
|
||||
Expected XML format:
|
||||
<annotation>
|
||||
...
|
||||
<object>
|
||||
<name>dog</name>
|
||||
<bndbox>
|
||||
<xmin>48</xmin>
|
||||
<ymin>240</ymin>
|
||||
<xmax>195</xmax>
|
||||
<ymax>371</ymax>
|
||||
</bndbox>
|
||||
<polygon>
|
||||
<x1>48</x1>
|
||||
<y1>240</y1>
|
||||
<x2>195</x2>
|
||||
<y2>240</y2>
|
||||
<x3>195</x3>
|
||||
<y3>371</y3>
|
||||
<x4>48</x4>
|
||||
<y4>371</y4>
|
||||
</polygon>
|
||||
</object>
|
||||
</annotation>
|
||||
|
||||
Returns:
|
||||
Tuple[Detections, List[str]]: A tuple containing a Detections object and an updated list of class names, extended with the class names from the XML object.
|
||||
"""
|
||||
xyxy = []
|
||||
class_names = []
|
||||
masks = []
|
||||
with_masks = False
|
||||
extended_classes = classes[:]
|
||||
for obj in root.findall("object"):
|
||||
class_name = obj.find("name").text
|
||||
class_names.append(class_name)
|
||||
@@ -150,7 +221,40 @@ def load_pascal_voc_annotations(
|
||||
|
||||
xyxy.append([x1, y1, x2, y2])
|
||||
|
||||
xyxy = np.array(xyxy)
|
||||
detections = Detections(xyxy=xyxy)
|
||||
with_masks = obj.find("polygon") is not None
|
||||
with_masks = force_masks if force_masks else with_masks
|
||||
|
||||
return image_name, detections, class_names
|
||||
for polygon in obj.findall("polygon"):
|
||||
polygon_points = parse_polygon_points(polygon)
|
||||
|
||||
mask_from_polygon = polygon_to_mask(
|
||||
polygon=np.array(polygon_points),
|
||||
resolution_wh=resolution_wh,
|
||||
)
|
||||
masks.append(mask_from_polygon)
|
||||
|
||||
xyxy = np.array(xyxy) if len(xyxy) > 0 else np.empty((0, 4))
|
||||
for k in set(class_names):
|
||||
if k not in extended_classes:
|
||||
extended_classes.append(k)
|
||||
class_id = np.array(
|
||||
[extended_classes.index(class_name) for class_name in class_names]
|
||||
)
|
||||
|
||||
if with_masks:
|
||||
annotation = Detections(
|
||||
xyxy=xyxy, mask=np.array(masks).astype(bool), class_id=class_id
|
||||
)
|
||||
else:
|
||||
annotation = Detections(xyxy=xyxy, class_id=class_id)
|
||||
return annotation, extended_classes
|
||||
|
||||
|
||||
def parse_polygon_points(polygon: Element) -> List[List[int]]:
|
||||
polygon_points = []
|
||||
coords = polygon.findall(".//*")
|
||||
for i in range(0, len(coords), 2):
|
||||
x = int(coords[i].text)
|
||||
y = int(coords[i + 1].text)
|
||||
polygon_points.append([x, y])
|
||||
return polygon_points
|
||||
|
||||
@@ -52,24 +52,16 @@ def _polygons_to_masks(
|
||||
)
|
||||
|
||||
|
||||
def _pair_image_with_annotation(
|
||||
image_paths: List[Union[str, Path]], annotation_paths: List[Union[str, Path]]
|
||||
) -> List[Tuple[Union[str, Path], Union[str, Path]]]:
|
||||
image_dict = {p.stem: p for p in image_paths}
|
||||
return [
|
||||
(image_dict[annotation_path.stem], annotation_path)
|
||||
for annotation_path in annotation_paths
|
||||
if annotation_path.stem in image_dict
|
||||
]
|
||||
|
||||
|
||||
def _with_mask(lines: List[str]) -> bool:
|
||||
return any([len(line.split()) > 5 for line in lines])
|
||||
|
||||
|
||||
def _extract_class_names(file_path: str) -> List[str]:
|
||||
data = read_yaml_file(file_path=file_path)
|
||||
return data["names"]
|
||||
names = data["names"]
|
||||
if isinstance(names, dict):
|
||||
names = [names[key] for key in sorted(names.keys())]
|
||||
return names
|
||||
|
||||
|
||||
def _image_name_to_annotation_name(image_name: str) -> str:
|
||||
@@ -134,17 +126,21 @@ def load_yolo_annotations(
|
||||
image_paths = list_files_with_extensions(
|
||||
directory=images_directory_path, extensions=["jpg", "jpeg", "png"]
|
||||
)
|
||||
annotation_paths = list_files_with_extensions(
|
||||
directory=annotations_directory_path, extensions=["txt"]
|
||||
)
|
||||
path_pairs = _pair_image_with_annotation(
|
||||
image_paths=image_paths, annotation_paths=annotation_paths
|
||||
)
|
||||
|
||||
classes = _extract_class_names(file_path=data_yaml_path)
|
||||
images = {}
|
||||
annotations = {}
|
||||
for image_path, annotation_path in path_pairs:
|
||||
|
||||
for image_path in image_paths:
|
||||
image_name = Path(image_path).stem
|
||||
image = cv2.imread(str(image_path))
|
||||
|
||||
annotation_path = os.path.join(annotations_directory_path, f"{image_name}.txt")
|
||||
if not os.path.exists(annotation_path):
|
||||
images[image_path.name] = image
|
||||
annotations[image_path.name] = Detections.empty()
|
||||
continue
|
||||
|
||||
lines = read_txt_file(str(annotation_path))
|
||||
h, w, _ = image.shape
|
||||
resolution_wh = (w, h)
|
||||
|
||||
@@ -83,9 +83,11 @@ def map_detections_class_id(
|
||||
)
|
||||
|
||||
detections_copy = copy.deepcopy(detections)
|
||||
detections_copy.class_id = np.vectorize(source_to_target_mapping.get)(
|
||||
detections_copy.class_id
|
||||
)
|
||||
|
||||
if len(detections) > 0:
|
||||
detections_copy.class_id = np.vectorize(source_to_target_mapping.get)(
|
||||
detections_copy.class_id
|
||||
)
|
||||
|
||||
return detections_copy
|
||||
|
||||
|
||||
@@ -7,12 +7,13 @@ import cv2
|
||||
import numpy as np
|
||||
|
||||
from supervision.detection.utils import (
|
||||
extract_yolov8_masks,
|
||||
extract_ultralytics_masks,
|
||||
non_max_suppression,
|
||||
process_roboflow_result,
|
||||
xywh_to_xyxy,
|
||||
)
|
||||
from supervision.geometry.core import Position
|
||||
from supervision.utils.internal import deprecated
|
||||
|
||||
|
||||
def _validate_xyxy(xyxy: Any, n: int) -> None:
|
||||
@@ -26,7 +27,7 @@ def _validate_mask(mask: Any, n: int) -> None:
|
||||
isinstance(mask, np.ndarray) and len(mask.shape) == 3 and mask.shape[0] == n
|
||||
)
|
||||
if not is_valid:
|
||||
raise ValueError("mask must be 3d np.ndarray with (n, W, H) shape")
|
||||
raise ValueError("mask must be 3d np.ndarray with (n, H, W) shape")
|
||||
|
||||
|
||||
def _validate_class_id(class_id: Any, n: int) -> None:
|
||||
@@ -59,7 +60,7 @@ class Detections:
|
||||
Data class containing information about the detections in a video frame.
|
||||
Attributes:
|
||||
xyxy (np.ndarray): An array of shape `(n, 4)` containing the bounding boxes coordinates in format `[x1, y1, x2, y2]`
|
||||
mask: (Optional[np.ndarray]): An array of shape `(n, W, H)` containing the segmentation masks.
|
||||
mask: (Optional[np.ndarray]): An array of shape `(n, H, W)` containing the segmentation masks.
|
||||
confidence (Optional[np.ndarray]): An array of shape `(n,)` containing the confidence scores of the detections.
|
||||
class_id (Optional[np.ndarray]): An array of shape `(n,)` containing the class ids of the detections.
|
||||
tracker_id (Optional[np.ndarray]): An array of shape `(n,)` containing the tracker ids of the detections.
|
||||
@@ -170,6 +171,9 @@ class Detections:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@deprecated(
|
||||
"This method is deprecated and removed in 0.15.0 release. Use sv.Detections.from_ultralytics() instead."
|
||||
)
|
||||
def from_yolov8(cls, yolov8_results) -> Detections:
|
||||
"""
|
||||
Creates a Detections instance from a [YOLOv8](https://github.com/ultralytics/ultralytics) inference result.
|
||||
@@ -196,7 +200,47 @@ class Detections:
|
||||
xyxy=yolov8_results.boxes.xyxy.cpu().numpy(),
|
||||
confidence=yolov8_results.boxes.conf.cpu().numpy(),
|
||||
class_id=yolov8_results.boxes.cls.cpu().numpy().astype(int),
|
||||
mask=extract_yolov8_masks(yolov8_results),
|
||||
mask=extract_ultralytics_masks(yolov8_results),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_ultralytics(cls, ultralytics_results) -> Detections:
|
||||
"""
|
||||
Creates a Detections instance from a [YOLOv8](https://github.com/ultralytics/ultralytics) inference result.
|
||||
|
||||
Args:
|
||||
ultralytics_results (ultralytics.yolo.engine.results.Results): The output Results instance from YOLOv8
|
||||
|
||||
Returns:
|
||||
Detections: A new Detections object.
|
||||
|
||||
Example:
|
||||
```python
|
||||
>>> import cv2
|
||||
>>> from ultralytics import YOLO, FastSAM, SAM, RTDETR
|
||||
>>> import supervision as sv
|
||||
|
||||
>>> image = cv2.imread(SOURCE_IMAGE_PATH)
|
||||
>>> model = YOLO('yolov8s.pt')
|
||||
>>> model = SAM('sam_b.pt')
|
||||
>>> model = SAM('mobile_sam.pt')
|
||||
>>> model = FastSAM('FastSAM-s.pt')
|
||||
>>> model = RTDETR('rtdetr-l.pt')
|
||||
>>> # model inferences
|
||||
>>> result = model(image)[0]
|
||||
>>> # if tracker is enabled
|
||||
>>> result = model.track(image)[0]
|
||||
>>> detections = sv.Detections.from_ultralytics(result)
|
||||
```
|
||||
"""
|
||||
return cls(
|
||||
xyxy=ultralytics_results.boxes.xyxy.cpu().numpy(),
|
||||
confidence=ultralytics_results.boxes.conf.cpu().numpy(),
|
||||
class_id=ultralytics_results.boxes.cls.cpu().numpy().astype(int),
|
||||
mask=extract_ultralytics_masks(ultralytics_results),
|
||||
tracker_id=ultralytics_results.boxes.id.int().cpu().numpy()
|
||||
if ultralytics_results.boxes.id is not None
|
||||
else None,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -228,6 +272,35 @@ class Detections:
|
||||
class_id=yolo_nas_results.prediction.labels.astype(int),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_mmdetection(cls, mmdet_results) -> Detections:
|
||||
"""
|
||||
Creates a Detections instance from a [mmdetection](https://github.com/open-mmlab/mmdetection) inference result.
|
||||
Also supported for [mmyolo](https://github.com/open-mmlab/mmyolo)
|
||||
|
||||
Args:
|
||||
mmdet_results (mmdet.structures.DetDataSample): The output Results instance from MMDetection
|
||||
|
||||
Returns:
|
||||
Detections: A new Detections object.
|
||||
|
||||
Example:
|
||||
```python
|
||||
>>> import cv2
|
||||
>>> import supervision as sv
|
||||
>>> from mmdet.apis import DetInferencer
|
||||
|
||||
>>> inferencer = DetInferencer(model_name, checkpoint, device)
|
||||
>>> mmdet_result = inferencer(SOURCE_IMAGE_PATH, out_dir='./output', return_datasample=True)["predictions"][0]
|
||||
>>> detections = sv.Detections.from_mmdet(mmdet_result)
|
||||
```
|
||||
"""
|
||||
return cls(
|
||||
xyxy=mmdet_results.pred_instances.bboxes.cpu().numpy(),
|
||||
confidence=mmdet_results.pred_instances.scores.cpu().numpy(),
|
||||
class_id=mmdet_results.pred_instances.labels.cpu().numpy().astype(int),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_transformers(cls, transformers_results: dict) -> Detections:
|
||||
"""
|
||||
@@ -354,6 +427,42 @@ class Detections:
|
||||
|
||||
return Detections(xyxy=xywh_to_xyxy(boxes_xywh=xywh), mask=mask)
|
||||
|
||||
@classmethod
|
||||
def from_paddledet(cls, paddledet_result) -> Detections:
|
||||
"""
|
||||
Creates a Detections instance from [PaddleDetection](https://github.com/PaddlePaddle/PaddleDetection) inference result.
|
||||
|
||||
Args:
|
||||
paddledet_result (List[dict]): The output Results instance from SAM
|
||||
|
||||
Returns:
|
||||
Detections: A new Detections object.
|
||||
|
||||
Example:
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
>>> import paddle
|
||||
>>> from ppdet.engine import Trainer
|
||||
>>> from ppdet.core.workspace import load_config
|
||||
|
||||
>>> weights = (...)
|
||||
>>> config = (...)
|
||||
|
||||
>>> cfg = load_config(config)
|
||||
>>> trainer = Trainer(cfg, mode='test')
|
||||
>>> trainer.load_weights(weights)
|
||||
|
||||
>>> paddledet_result = trainer.predict([images])[0]
|
||||
|
||||
>>> detections = sv.Detections.from_paddledet(paddledet_result=paddledet_result)
|
||||
```
|
||||
"""
|
||||
return cls(
|
||||
xyxy=paddledet_result["bbox"][:, 2:6],
|
||||
confidence=paddledet_result["bbox"][:, 1],
|
||||
class_id=paddledet_result["bbox"][:, 0].astype(int),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> Detections:
|
||||
"""
|
||||
|
||||
@@ -17,7 +17,7 @@ def polygon_to_mask(polygon: np.ndarray, resolution_wh: Tuple[int, int]) -> np.n
|
||||
np.ndarray: The generated 2D mask, where the polygon is marked with `1`'s and the rest is filled with `0`'s.
|
||||
"""
|
||||
width, height = resolution_wh
|
||||
mask = np.zeros((height, width), dtype=np.uint8)
|
||||
mask = np.zeros((height, width))
|
||||
cv2.fillPoly(mask, [polygon], color=1)
|
||||
return mask
|
||||
|
||||
@@ -260,7 +260,7 @@ def approximate_polygon(
|
||||
return np.squeeze(approximated_points, axis=1)
|
||||
|
||||
|
||||
def extract_yolov8_masks(yolov8_results) -> Optional[np.ndarray]:
|
||||
def extract_ultralytics_masks(yolov8_results) -> Optional[np.ndarray]:
|
||||
if not yolov8_results.masks:
|
||||
return None
|
||||
|
||||
@@ -288,7 +288,10 @@ def extract_yolov8_masks(yolov8_results) -> Optional[np.ndarray]:
|
||||
for i in range(masks.shape[0]):
|
||||
mask = masks[i]
|
||||
mask = mask[top:bottom, left:right]
|
||||
mask = cv2.resize(mask, (orig_shape[1], orig_shape[0]))
|
||||
|
||||
if mask.shape != orig_shape:
|
||||
mask = cv2.resize(mask, (orig_shape[1], orig_shape[0]))
|
||||
|
||||
mask_maps.append(mask)
|
||||
|
||||
return np.asarray(mask_maps, dtype=bool)
|
||||
|
||||
@@ -0,0 +1,792 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, List, Optional, Tuple
|
||||
|
||||
import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from supervision.dataset.core import DetectionDataset
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.utils import box_iou_batch
|
||||
|
||||
|
||||
def detections_to_tensor(
|
||||
detections: Detections, with_confidence: bool = False
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Convert Supervision Detections to numpy tensors for further computation
|
||||
Args:
|
||||
detections (sv.Detections): Detections/Targets in the format of sv.Detections
|
||||
with_confidence (bool): Whether to include confidence in the tensor
|
||||
Returns:
|
||||
(np.ndarray): Detections as numpy tensors as in (xyxy, class_id, confidence) order
|
||||
"""
|
||||
if detections.class_id is None:
|
||||
raise ValueError(
|
||||
"ConfusionMatrix can only be calculated for Detections with class_id"
|
||||
)
|
||||
|
||||
arrays_to_concat = [detections.xyxy, np.expand_dims(detections.class_id, 1)]
|
||||
|
||||
if with_confidence:
|
||||
if detections.confidence is None:
|
||||
raise ValueError(
|
||||
"ConfusionMatrix can only be calculated for Detections with confidence"
|
||||
)
|
||||
arrays_to_concat.append(np.expand_dims(detections.confidence, 1))
|
||||
|
||||
return np.concatenate(arrays_to_concat, axis=1)
|
||||
|
||||
|
||||
def validate_input_tensors(predictions: List[np.ndarray], targets: List[np.ndarray]):
|
||||
"""
|
||||
Checks for shape consistency of input tensors.
|
||||
"""
|
||||
if len(predictions) != len(targets):
|
||||
raise ValueError(
|
||||
f"Number of predictions ({len(predictions)}) and targets ({len(targets)}) must be equal."
|
||||
)
|
||||
if len(predictions) > 0:
|
||||
if not isinstance(predictions[0], np.ndarray) or not isinstance(
|
||||
targets[0], np.ndarray
|
||||
):
|
||||
raise ValueError(
|
||||
f"Predictions and targets must be lists of numpy arrays. Got {type(predictions[0])} and {type(targets[0])} instead."
|
||||
)
|
||||
if predictions[0].shape[1] != 6:
|
||||
raise ValueError(
|
||||
f"Predictions must have shape (N, 6). Got {predictions[0].shape} instead."
|
||||
)
|
||||
if targets[0].shape[1] != 5:
|
||||
raise ValueError(
|
||||
f"Targets must have shape (N, 5). Got {targets[0].shape} instead."
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfusionMatrix:
|
||||
"""
|
||||
Confusion matrix for object detection tasks.
|
||||
|
||||
Attributes:
|
||||
matrix (np.ndarray): An 2D `np.ndarray` of shape `(len(classes) + 1, len(classes) + 1)` containing the number of `TP`, `FP`, `FN` and `TN` for each class.
|
||||
classes (List[str]): Model class names.
|
||||
conf_threshold (float): Detection confidence threshold between `0` and `1`. Detections with lower confidence will be excluded from the matrix.
|
||||
iou_threshold (float): Detection IoU threshold between `0` and `1`. Detections with lower IoU will be classified as `FP`.
|
||||
"""
|
||||
|
||||
matrix: np.ndarray
|
||||
classes: List[str]
|
||||
conf_threshold: float
|
||||
iou_threshold: float
|
||||
|
||||
@classmethod
|
||||
def from_detections(
|
||||
cls,
|
||||
predictions: List[Detections],
|
||||
targets: List[Detections],
|
||||
classes: List[str],
|
||||
conf_threshold: float = 0.3,
|
||||
iou_threshold: float = 0.5,
|
||||
) -> ConfusionMatrix:
|
||||
"""
|
||||
Calculate confusion matrix based on predicted and ground-truth detections.
|
||||
|
||||
Args:
|
||||
targets (List[Detections]): Detections objects from ground-truth.
|
||||
predictions (List[Detections]): Detections objects predicted by the model.
|
||||
classes (List[str]): Model class names.
|
||||
conf_threshold (float): Detection confidence threshold between `0` and `1`. Detections with lower confidence will be excluded.
|
||||
iou_threshold (float): Detection IoU threshold between `0` and `1`. Detections with lower IoU will be classified as `FP`.
|
||||
|
||||
Returns:
|
||||
ConfusionMatrix: New instance of ConfusionMatrix.
|
||||
|
||||
Example:
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
|
||||
>>> targets = [
|
||||
... sv.Detections(...),
|
||||
... sv.Detections(...)
|
||||
... ]
|
||||
|
||||
>>> predictions = [
|
||||
... sv.Detections(...),
|
||||
... sv.Detections(...)
|
||||
... ]
|
||||
|
||||
>>> confusion_matrix = sv.ConfusionMatrix.from_detections(
|
||||
... predictions=predictions,
|
||||
... targets=target,
|
||||
... classes=['person', ...]
|
||||
... )
|
||||
|
||||
>>> confusion_matrix.matrix
|
||||
array([
|
||||
[0., 0., 0., 0.],
|
||||
[0., 1., 0., 1.],
|
||||
[0., 1., 1., 0.],
|
||||
[1., 1., 0., 0.]
|
||||
])
|
||||
```
|
||||
"""
|
||||
|
||||
prediction_tensors = []
|
||||
target_tensors = []
|
||||
for prediction, target in zip(predictions, targets):
|
||||
prediction_tensors.append(
|
||||
detections_to_tensor(prediction, with_confidence=True)
|
||||
)
|
||||
target_tensors.append(detections_to_tensor(target, with_confidence=False))
|
||||
return cls.from_tensors(
|
||||
predictions=prediction_tensors,
|
||||
targets=target_tensors,
|
||||
classes=classes,
|
||||
conf_threshold=conf_threshold,
|
||||
iou_threshold=iou_threshold,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_tensors(
|
||||
cls,
|
||||
predictions: List[np.ndarray],
|
||||
targets: List[np.ndarray],
|
||||
classes: List[str],
|
||||
conf_threshold: float = 0.3,
|
||||
iou_threshold: float = 0.5,
|
||||
) -> ConfusionMatrix:
|
||||
"""
|
||||
Calculate confusion matrix based on predicted and ground-truth detections.
|
||||
|
||||
Args:
|
||||
predictions (List[np.ndarray]): Each element of the list describes a single image and has `shape = (M, 6)` where `M` is the number of detected objects. Each row is expected to be in `(x_min, y_min, x_max, y_max, class, conf)` format.
|
||||
targets (List[np.ndarray]): Each element of the list describes a single image and has `shape = (N, 5)` where `N` is the number of ground-truth objects. Each row is expected to be in `(x_min, y_min, x_max, y_max, class)` format.
|
||||
classes (List[str]): Model class names.
|
||||
conf_threshold (float): Detection confidence threshold between `0` and `1`. Detections with lower confidence will be excluded.
|
||||
iou_threshold (float): Detection iou threshold between `0` and `1`. Detections with lower iou will be classified as `FP`.
|
||||
|
||||
Returns:
|
||||
ConfusionMatrix: New instance of ConfusionMatrix.
|
||||
|
||||
Example:
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
|
||||
>>> targets = (
|
||||
... [
|
||||
... array(
|
||||
... [
|
||||
... [0.0, 0.0, 3.0, 3.0, 1],
|
||||
... [2.0, 2.0, 5.0, 5.0, 1],
|
||||
... [6.0, 1.0, 8.0, 3.0, 2],
|
||||
... ]
|
||||
... ),
|
||||
... array([1.0, 1.0, 2.0, 2.0, 2]),
|
||||
... ]
|
||||
... )
|
||||
|
||||
>>> predictions = [
|
||||
... array(
|
||||
... [
|
||||
... [0.0, 0.0, 3.0, 3.0, 1, 0.9],
|
||||
... [0.1, 0.1, 3.0, 3.0, 0, 0.9],
|
||||
... [6.0, 1.0, 8.0, 3.0, 1, 0.8],
|
||||
... [1.0, 6.0, 2.0, 7.0, 1, 0.8],
|
||||
... ]
|
||||
... ),
|
||||
... array([[1.0, 1.0, 2.0, 2.0, 2, 0.8]])
|
||||
... ]
|
||||
|
||||
>>> confusion_matrix = sv.ConfusionMatrix.from_tensors(
|
||||
... predictions=predictions,
|
||||
... targets=targets,
|
||||
... classes=['person', ...]
|
||||
... )
|
||||
|
||||
>>> confusion_matrix.matrix
|
||||
array([
|
||||
[0., 0., 0., 0.],
|
||||
[0., 1., 0., 1.],
|
||||
[0., 1., 1., 0.],
|
||||
[1., 1., 0., 0.]
|
||||
])
|
||||
```
|
||||
"""
|
||||
validate_input_tensors(predictions, targets)
|
||||
|
||||
num_classes = len(classes)
|
||||
matrix = np.zeros((num_classes + 1, num_classes + 1))
|
||||
for true_batch, detection_batch in zip(targets, predictions):
|
||||
matrix += cls.evaluate_detection_batch(
|
||||
predictions=detection_batch,
|
||||
targets=true_batch,
|
||||
num_classes=num_classes,
|
||||
conf_threshold=conf_threshold,
|
||||
iou_threshold=iou_threshold,
|
||||
)
|
||||
return cls(
|
||||
matrix=matrix,
|
||||
classes=classes,
|
||||
conf_threshold=conf_threshold,
|
||||
iou_threshold=iou_threshold,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def evaluate_detection_batch(
|
||||
predictions: np.ndarray,
|
||||
targets: np.ndarray,
|
||||
num_classes: int,
|
||||
conf_threshold: float,
|
||||
iou_threshold: float,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Calculate confusion matrix for a batch of detections for a single image.
|
||||
|
||||
Args:
|
||||
predictions (np.ndarray): Batch prediction. Describes a single image and has `shape = (M, 6)` where `M` is the number of detected objects. Each row is expected to be in `(x_min, y_min, x_max, y_max, class, conf)` format.
|
||||
targets (np.ndarray): Batch target labels. Describes a single image and has `shape = (N, 5)` where `N` is the number of ground-truth objects. Each row is expected to be in `(x_min, y_min, x_max, y_max, class)` format.
|
||||
num_classes (int): Number of classes.
|
||||
conf_threshold (float): Detection confidence threshold between `0` and `1`. Detections with lower confidence will be excluded.
|
||||
iou_threshold (float): Detection iou threshold between `0` and `1`. Detections with lower iou will be classified as `FP`.
|
||||
|
||||
Returns:
|
||||
np.ndarray: Confusion matrix based on a single image.
|
||||
"""
|
||||
result_matrix = np.zeros((num_classes + 1, num_classes + 1))
|
||||
|
||||
conf_idx = 5
|
||||
confidence = predictions[:, conf_idx]
|
||||
detection_batch_filtered = predictions[confidence > conf_threshold]
|
||||
|
||||
class_id_idx = 4
|
||||
true_classes = np.array(targets[:, class_id_idx], dtype=np.int16)
|
||||
detection_classes = np.array(
|
||||
detection_batch_filtered[:, class_id_idx], dtype=np.int16
|
||||
)
|
||||
true_boxes = targets[:, :class_id_idx]
|
||||
detection_boxes = detection_batch_filtered[:, :class_id_idx]
|
||||
|
||||
iou_batch = box_iou_batch(
|
||||
boxes_true=true_boxes, boxes_detection=detection_boxes
|
||||
)
|
||||
matched_idx = np.asarray(iou_batch > iou_threshold).nonzero()
|
||||
|
||||
if matched_idx[0].shape[0]:
|
||||
matches = np.stack(
|
||||
(matched_idx[0], matched_idx[1], iou_batch[matched_idx]), axis=1
|
||||
)
|
||||
matches = ConfusionMatrix._drop_extra_matches(matches=matches)
|
||||
else:
|
||||
matches = np.zeros((0, 3))
|
||||
|
||||
matched_true_idx, matched_detection_idx, _ = matches.transpose().astype(
|
||||
np.int16
|
||||
)
|
||||
|
||||
for i, true_class_value in enumerate(true_classes):
|
||||
j = matched_true_idx == i
|
||||
if matches.shape[0] > 0 and sum(j) == 1:
|
||||
result_matrix[
|
||||
true_class_value, detection_classes[matched_detection_idx[j]]
|
||||
] += 1 # TP
|
||||
else:
|
||||
result_matrix[true_class_value, num_classes] += 1 # FN
|
||||
|
||||
for i, detection_class_value in enumerate(detection_classes):
|
||||
if not any(matched_detection_idx == i):
|
||||
result_matrix[num_classes, detection_class_value] += 1 # FP
|
||||
|
||||
return result_matrix
|
||||
|
||||
@staticmethod
|
||||
def _drop_extra_matches(matches: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Deduplicate matches. If there are multiple matches for the same true or predicted box,
|
||||
only the one with the highest IoU is kept.
|
||||
"""
|
||||
if matches.shape[0] > 0:
|
||||
matches = matches[matches[:, 2].argsort()[::-1]]
|
||||
matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
|
||||
matches = matches[matches[:, 2].argsort()[::-1]]
|
||||
matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
|
||||
return matches
|
||||
|
||||
@classmethod
|
||||
def benchmark(
|
||||
cls,
|
||||
dataset: DetectionDataset,
|
||||
callback: Callable[[np.ndarray], Detections],
|
||||
conf_threshold: float = 0.3,
|
||||
iou_threshold: float = 0.5,
|
||||
) -> ConfusionMatrix:
|
||||
"""
|
||||
Calculate confusion matrix from dataset and callback function.
|
||||
|
||||
Args:
|
||||
dataset (DetectionDataset): Object detection dataset used for evaluation.
|
||||
callback (Callable[[np.ndarray], Detections]): Function that takes an image as input and returns Detections object.
|
||||
conf_threshold (float): Detection confidence threshold between `0` and `1`. Detections with lower confidence will be excluded.
|
||||
iou_threshold (float): Detection IoU threshold between `0` and `1`. Detections with lower IoU will be classified as `FP`.
|
||||
|
||||
Returns:
|
||||
ConfusionMatrix: New instance of ConfusionMatrix.
|
||||
|
||||
Example:
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
>>> from ultralytics import YOLO
|
||||
|
||||
>>> dataset = sv.DetectionDataset.from_yolo(...)
|
||||
|
||||
>>> model = YOLO(...)
|
||||
>>> def callback(image: np.ndarray) -> sv.Detections:
|
||||
... result = model(image)[0]
|
||||
... return sv.Detections.from_yolov8(result)
|
||||
|
||||
>>> confusion_matrix = sv.ConfusionMatrix.benchmark(
|
||||
... dataset = dataset,
|
||||
... callback = callback
|
||||
... )
|
||||
|
||||
>>> confusion_matrix.matrix
|
||||
array([
|
||||
[0., 0., 0., 0.],
|
||||
[0., 1., 0., 1.],
|
||||
[0., 1., 1., 0.],
|
||||
[1., 1., 0., 0.]
|
||||
])
|
||||
```
|
||||
"""
|
||||
predictions, targets = [], []
|
||||
for img_name, img in dataset.images.items():
|
||||
predictions_batch = callback(img)
|
||||
predictions.append(predictions_batch)
|
||||
targets_batch = dataset.annotations[img_name]
|
||||
targets.append(targets_batch)
|
||||
return cls.from_detections(
|
||||
predictions=predictions,
|
||||
targets=targets,
|
||||
classes=dataset.classes,
|
||||
conf_threshold=conf_threshold,
|
||||
iou_threshold=iou_threshold,
|
||||
)
|
||||
|
||||
def plot(
|
||||
self,
|
||||
save_path: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
classes: Optional[List[str]] = None,
|
||||
normalize: bool = False,
|
||||
fig_size: Tuple[int, int] = (12, 10),
|
||||
) -> matplotlib.figure.Figure:
|
||||
"""
|
||||
Create confusion matrix plot and save it at selected location.
|
||||
|
||||
Args:
|
||||
save_path (Optional[str]): Path to save the plot. If not provided, plot will be displayed.
|
||||
title (Optional[str]): Title of the plot.
|
||||
classes (Optional[List[str]]): List of classes to be displayed on the plot. If not provided, all classes will be displayed.
|
||||
normalize (bool): If True, normalize the confusion matrix.
|
||||
fig_size (Tuple[int, int]): Size of the plot.
|
||||
|
||||
Returns:
|
||||
matplotlib.figure.Figure: Confusion matrix plot.
|
||||
"""
|
||||
|
||||
array = self.matrix.copy()
|
||||
|
||||
if normalize:
|
||||
eps = 1e-8
|
||||
array = array / (array.sum(0).reshape(1, -1) + eps)
|
||||
|
||||
array[array < 0.005] = np.nan
|
||||
|
||||
fig, ax = plt.subplots(figsize=fig_size, tight_layout=True, facecolor="white")
|
||||
|
||||
class_names = classes if classes is not None else self.classes
|
||||
use_labels_for_ticks = class_names is not None and (0 < len(class_names) < 99)
|
||||
if use_labels_for_ticks:
|
||||
x_tick_labels = class_names + ["FN"]
|
||||
y_tick_labels = class_names + ["FP"]
|
||||
num_ticks = len(x_tick_labels)
|
||||
else:
|
||||
x_tick_labels = None
|
||||
y_tick_labels = None
|
||||
num_ticks = len(array)
|
||||
im = ax.imshow(array, cmap="Blues")
|
||||
|
||||
cbar = ax.figure.colorbar(im, ax=ax)
|
||||
cbar.mappable.set_clim(vmin=0, vmax=np.nanmax(array))
|
||||
|
||||
if x_tick_labels is None:
|
||||
tick_interval = 2
|
||||
else:
|
||||
tick_interval = 1
|
||||
ax.set_xticks(np.arange(0, num_ticks, tick_interval), labels=x_tick_labels)
|
||||
ax.set_yticks(np.arange(0, num_ticks, tick_interval), labels=y_tick_labels)
|
||||
|
||||
plt.setp(ax.get_xticklabels(), rotation=90, ha="right", rotation_mode="default")
|
||||
|
||||
labelsize = 10 if num_ticks < 50 else 8
|
||||
ax.tick_params(axis="both", which="both", labelsize=labelsize)
|
||||
|
||||
if num_ticks < 30:
|
||||
for i in range(array.shape[0]):
|
||||
for j in range(array.shape[1]):
|
||||
n_preds = array[i, j]
|
||||
if not np.isnan(n_preds):
|
||||
ax.text(
|
||||
j,
|
||||
i,
|
||||
f"{n_preds:.2f}" if normalize else f"{n_preds:.0f}",
|
||||
ha="center",
|
||||
va="center",
|
||||
color="black"
|
||||
if n_preds < 0.5 * np.nanmax(array)
|
||||
else "white",
|
||||
)
|
||||
|
||||
if title:
|
||||
ax.set_title(title, fontsize=20)
|
||||
|
||||
ax.set_xlabel("Predicted")
|
||||
ax.set_ylabel("True")
|
||||
ax.set_facecolor("white")
|
||||
if save_path:
|
||||
fig.savefig(
|
||||
save_path, dpi=250, facecolor=fig.get_facecolor(), transparent=True
|
||||
)
|
||||
return fig
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MeanAveragePrecision:
|
||||
"""
|
||||
Mean Average Precision for object detection tasks.
|
||||
|
||||
Attributes:
|
||||
map50_95 (float): Mean Average Precision (mAP) calculated over IoU thresholds ranging from `0.50` to `0.95` with a step size of `0.05`.
|
||||
map50 (float): Mean Average Precision (mAP) calculated specifically at an IoU threshold of `0.50`.
|
||||
map75 (float): Mean Average Precision (mAP) calculated specifically at an IoU threshold of `0.75`.
|
||||
per_class_ap50_95 (np.ndarray): Average Precision (AP) values calculated over IoU thresholds ranging from `0.50` to `0.95` with a step size of `0.05`, provided for each individual class.
|
||||
"""
|
||||
|
||||
map50_95: float
|
||||
map50: float
|
||||
map75: float
|
||||
per_class_ap50_95: np.ndarray
|
||||
|
||||
@classmethod
|
||||
def from_detections(
|
||||
cls,
|
||||
predictions: List[Detections],
|
||||
targets: List[Detections],
|
||||
) -> MeanAveragePrecision:
|
||||
"""
|
||||
Calculate mean average precision based on predicted and ground-truth detections.
|
||||
|
||||
Args:
|
||||
targets (List[Detections]): Detections objects from ground-truth.
|
||||
predictions (List[Detections]): Detections objects predicted by the model.
|
||||
Returns:
|
||||
MeanAveragePrecision: New instance of ConfusionMatrix.
|
||||
|
||||
Example:
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
|
||||
>>> targets = [
|
||||
... sv.Detections(...),
|
||||
... sv.Detections(...)
|
||||
... ]
|
||||
|
||||
>>> predictions = [
|
||||
... sv.Detections(...),
|
||||
... sv.Detections(...)
|
||||
... ]
|
||||
|
||||
>>> mean_average_precision = sv.MeanAveragePrecision.from_detections(
|
||||
... predictions=predictions,
|
||||
... targets=target,
|
||||
... )
|
||||
|
||||
>>> mean_average_precison.map50_95
|
||||
0.2899
|
||||
```
|
||||
"""
|
||||
prediction_tensors = []
|
||||
target_tensors = []
|
||||
for prediction, target in zip(predictions, targets):
|
||||
prediction_tensors.append(
|
||||
detections_to_tensor(prediction, with_confidence=True)
|
||||
)
|
||||
target_tensors.append(detections_to_tensor(target, with_confidence=False))
|
||||
return cls.from_tensors(
|
||||
predictions=prediction_tensors,
|
||||
targets=target_tensors,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def benchmark(
|
||||
cls,
|
||||
dataset: DetectionDataset,
|
||||
callback: Callable[[np.ndarray], Detections],
|
||||
) -> MeanAveragePrecision:
|
||||
"""
|
||||
Calculate mean average precision from dataset and callback function.
|
||||
|
||||
Args:
|
||||
dataset (DetectionDataset): Object detection dataset used for evaluation.
|
||||
callback (Callable[[np.ndarray], Detections]): Function that takes an image as input and returns Detections object.
|
||||
Returns:
|
||||
MeanAveragePrecision: New instance of MeanAveragePrecision.
|
||||
|
||||
Example:
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
>>> from ultralytics import YOLO
|
||||
|
||||
>>> dataset = sv.DetectionDataset.from_yolo(...)
|
||||
|
||||
>>> model = YOLO(...)
|
||||
>>> def callback(image: np.ndarray) -> sv.Detections:
|
||||
... result = model(image)[0]
|
||||
... return sv.Detections.from_yolov8(result)
|
||||
|
||||
>>> mean_average_precision = sv.MeanAveragePrecision.benchmark(
|
||||
... dataset = dataset,
|
||||
... callback = callback
|
||||
... )
|
||||
|
||||
>>> mean_average_precision.map50_95
|
||||
0.433
|
||||
```
|
||||
"""
|
||||
predictions, targets = [], []
|
||||
for img_name, img in dataset.images.items():
|
||||
predictions_batch = callback(img)
|
||||
predictions.append(predictions_batch)
|
||||
targets_batch = dataset.annotations[img_name]
|
||||
targets.append(targets_batch)
|
||||
return cls.from_detections(
|
||||
predictions=predictions,
|
||||
targets=targets,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_tensors(
|
||||
cls,
|
||||
predictions: List[np.ndarray],
|
||||
targets: List[np.ndarray],
|
||||
) -> MeanAveragePrecision:
|
||||
"""
|
||||
Calculate Mean Average Precision based on predicted and ground-truth detections at different threshold.
|
||||
|
||||
Args:
|
||||
predictions (List[np.ndarray]): Each element of the list describes a single image and has `shape = (M, 6)` where `M` is the number of detected objects. Each row is expected to be in `(x_min, y_min, x_max, y_max, class, conf)` format.
|
||||
targets (List[np.ndarray]): Each element of the list describes a single image and has `shape = (N, 5)` where `N` is the number of ground-truth objects. Each row is expected to be in `(x_min, y_min, x_max, y_max, class)` format.
|
||||
Returns:
|
||||
MeanAveragePrecision: New instance of MeanAveragePrecision.
|
||||
|
||||
Example:
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
|
||||
>>> targets = (
|
||||
... [
|
||||
... array(
|
||||
... [
|
||||
... [0.0, 0.0, 3.0, 3.0, 1],
|
||||
... [2.0, 2.0, 5.0, 5.0, 1],
|
||||
... [6.0, 1.0, 8.0, 3.0, 2],
|
||||
... ]
|
||||
... ),
|
||||
... array([1.0, 1.0, 2.0, 2.0, 2]),
|
||||
... ]
|
||||
... )
|
||||
|
||||
>>> predictions = [
|
||||
... array(
|
||||
... [
|
||||
... [0.0, 0.0, 3.0, 3.0, 1, 0.9],
|
||||
... [0.1, 0.1, 3.0, 3.0, 0, 0.9],
|
||||
... [6.0, 1.0, 8.0, 3.0, 1, 0.8],
|
||||
... [1.0, 6.0, 2.0, 7.0, 1, 0.8],
|
||||
... ]
|
||||
... ),
|
||||
... array([[1.0, 1.0, 2.0, 2.0, 2, 0.8]])
|
||||
... ]
|
||||
|
||||
>>> mean_average_precison = sv.MeanAveragePrecision.from_tensors(
|
||||
... predictions=predictions,
|
||||
... targets=targets,
|
||||
... )
|
||||
|
||||
>>> mean_average_precison.map50_95
|
||||
0.2899
|
||||
```
|
||||
"""
|
||||
validate_input_tensors(predictions, targets)
|
||||
iou_thresholds = np.linspace(0.5, 0.95, 10)
|
||||
stats = []
|
||||
|
||||
# Gather matching stats for predictions and targets
|
||||
for true_objs, predicted_objs in zip(targets, predictions):
|
||||
if predicted_objs.shape[0] == 0:
|
||||
if true_objs.shape[0]:
|
||||
stats.append(
|
||||
(
|
||||
np.zeros((0, iou_thresholds.size), dtype=bool),
|
||||
*np.zeros((2, 0)),
|
||||
true_objs[:, 4],
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if true_objs.shape[0]:
|
||||
matches = cls._match_detection_batch(
|
||||
predicted_objs, true_objs, iou_thresholds
|
||||
)
|
||||
stats.append(
|
||||
(
|
||||
matches,
|
||||
predicted_objs[:, 5],
|
||||
predicted_objs[:, 4],
|
||||
true_objs[:, 4],
|
||||
)
|
||||
)
|
||||
|
||||
# Compute average precisions if any matches exist
|
||||
if stats:
|
||||
concatenated_stats = [np.concatenate(items, 0) for items in zip(*stats)]
|
||||
average_precisions = cls._average_precisions_per_class(*concatenated_stats)
|
||||
map50 = average_precisions[:, 0].mean()
|
||||
map75 = average_precisions[:, 5].mean()
|
||||
map50_95 = average_precisions.mean()
|
||||
else:
|
||||
map50, map75, map50_95 = 0, 0, 0
|
||||
average_precisions = []
|
||||
|
||||
return cls(
|
||||
map50_95=map50_95,
|
||||
map50=map50,
|
||||
map75=map75,
|
||||
per_class_ap50_95=average_precisions,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def compute_average_precision(recall: np.ndarray, precision: np.ndarray) -> float:
|
||||
"""
|
||||
Compute the average precision using 101-point interpolation (COCO), given the recall and precision curves.
|
||||
|
||||
Args:
|
||||
recall (np.ndarray): The recall curve.
|
||||
precision (np.ndarray): The precision curve.
|
||||
|
||||
Returns:
|
||||
float: Average precision.
|
||||
"""
|
||||
extended_recall = np.concatenate(([0.0], recall, [1.0]))
|
||||
extended_precision = np.concatenate(([1.0], precision, [0.0]))
|
||||
max_accumulated_precision = np.flip(
|
||||
np.maximum.accumulate(np.flip(extended_precision))
|
||||
)
|
||||
interpolated_recall_levels = np.linspace(0, 1, 101)
|
||||
interpolated_precision = np.interp(
|
||||
interpolated_recall_levels, extended_recall, max_accumulated_precision
|
||||
)
|
||||
average_precision = np.trapz(interpolated_precision, interpolated_recall_levels)
|
||||
return average_precision
|
||||
|
||||
@staticmethod
|
||||
def _match_detection_batch(
|
||||
predictions: np.ndarray, targets: np.ndarray, iou_thresholds: np.ndarray
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Match predictions with target labels based on IoU levels.
|
||||
|
||||
Args:
|
||||
predictions (np.ndarray): Batch prediction. Describes a single image and has `shape = (M, 6)` where `M` is the number of detected objects. Each row is expected to be in `(x_min, y_min, x_max, y_max, class, conf)` format.
|
||||
targets (np.ndarray): Batch target labels. Describes a single image and has `shape = (N, 5)` where `N` is the number of ground-truth objects. Each row is expected to be in `(x_min, y_min, x_max, y_max, class)` format.
|
||||
iou_thresholds (np.ndarray): Array contains different IoU thresholds.
|
||||
|
||||
Returns:
|
||||
np.ndarray: Matched prediction with target labels result.
|
||||
"""
|
||||
num_predictions, num_iou_levels = predictions.shape[0], iou_thresholds.shape[0]
|
||||
correct = np.zeros((num_predictions, num_iou_levels), dtype=bool)
|
||||
iou = box_iou_batch(targets[:, :4], predictions[:, :4])
|
||||
correct_class = targets[:, 4:5] == predictions[:, 4]
|
||||
|
||||
for i, iou_level in enumerate(iou_thresholds):
|
||||
matched_indices = np.where((iou >= iou_level) & correct_class)
|
||||
|
||||
if matched_indices[0].shape[0]:
|
||||
combined_indices = np.stack(matched_indices, axis=1)
|
||||
iou_values = iou[matched_indices][:, None]
|
||||
matches = np.hstack([combined_indices, iou_values])
|
||||
|
||||
if matched_indices[0].shape[0] > 1:
|
||||
matches = matches[matches[:, 2].argsort()[::-1]]
|
||||
matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
|
||||
matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
|
||||
|
||||
correct[matches[:, 1].astype(int), i] = True
|
||||
|
||||
return correct
|
||||
|
||||
@staticmethod
|
||||
def _average_precisions_per_class(
|
||||
matches: np.ndarray,
|
||||
prediction_confidence: np.ndarray,
|
||||
prediction_class_ids: np.ndarray,
|
||||
true_class_ids: np.ndarray,
|
||||
eps: float = 1e-16,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Compute the average precision, given the recall and precision curves.
|
||||
Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
|
||||
|
||||
Args:
|
||||
matches (np.ndarray): True positives.
|
||||
prediction_confidence (np.ndarray): Objectness value from 0-1.
|
||||
prediction_class_ids (np.ndarray): Predicted object classes.
|
||||
true_class_ids (np.ndarray): True object classes.
|
||||
eps (float, optional): Small value to prevent division by zero.
|
||||
|
||||
Returns:
|
||||
np.ndarray: Average precision for different IoU levels.
|
||||
"""
|
||||
sorted_indices = np.argsort(-prediction_confidence)
|
||||
matches = matches[sorted_indices]
|
||||
prediction_class_ids = prediction_class_ids[sorted_indices]
|
||||
|
||||
unique_classes, class_counts = np.unique(true_class_ids, return_counts=True)
|
||||
num_classes = unique_classes.shape[0]
|
||||
|
||||
average_precisions = np.zeros((num_classes, matches.shape[1]))
|
||||
|
||||
for class_idx, class_id in enumerate(unique_classes):
|
||||
is_class = prediction_class_ids == class_id
|
||||
total_true = class_counts[class_idx]
|
||||
total_prediction = is_class.sum()
|
||||
|
||||
if total_prediction == 0 or total_true == 0:
|
||||
continue
|
||||
|
||||
false_positives = (1 - matches[is_class]).cumsum(0)
|
||||
true_positives = matches[is_class].cumsum(0)
|
||||
recall = true_positives / (total_true + eps)
|
||||
precision = true_positives / (true_positives + false_positives)
|
||||
|
||||
for iou_level_idx in range(matches.shape[1]):
|
||||
average_precisions[
|
||||
class_idx, iou_level_idx
|
||||
] = MeanAveragePrecision.compute_average_precision(
|
||||
recall[:, iou_level_idx], precision[:, iou_level_idx]
|
||||
)
|
||||
|
||||
return average_precisions
|
||||
@@ -0,0 +1,55 @@
|
||||
from collections import OrderedDict
|
||||
from enum import Enum
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
class TrackState(Enum):
|
||||
New = 0
|
||||
Tracked = 1
|
||||
Lost = 2
|
||||
Removed = 3
|
||||
|
||||
|
||||
class BaseTrack:
|
||||
_count = 0
|
||||
|
||||
def __init__(self):
|
||||
self.track_id = 0
|
||||
self.is_activated = False
|
||||
self.state = TrackState.New
|
||||
|
||||
self.history = OrderedDict()
|
||||
self.features = []
|
||||
self.curr_feature = None
|
||||
self.score = 0
|
||||
self.start_frame = 0
|
||||
self.frame_id = 0
|
||||
self.time_since_update = 0
|
||||
|
||||
# multi-camera
|
||||
self.location = (np.inf, np.inf)
|
||||
|
||||
@property
|
||||
def end_frame(self) -> int:
|
||||
return self.frame_id
|
||||
|
||||
@staticmethod
|
||||
def next_id() -> int:
|
||||
BaseTrack._count += 1
|
||||
return BaseTrack._count
|
||||
|
||||
def activate(self, *args):
|
||||
raise NotImplementedError
|
||||
|
||||
def predict(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def mark_lost(self):
|
||||
self.state = TrackState.Lost
|
||||
|
||||
def mark_removed(self):
|
||||
self.state = TrackState.Removed
|
||||
@@ -0,0 +1,466 @@
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.tracker.byte_tracker import matching
|
||||
from supervision.tracker.byte_tracker.basetrack import BaseTrack, TrackState
|
||||
from supervision.tracker.byte_tracker.kalman_filter import KalmanFilter
|
||||
|
||||
|
||||
class STrack(BaseTrack):
|
||||
shared_kalman = KalmanFilter()
|
||||
|
||||
def __init__(self, tlwh, score, class_ids):
|
||||
# wait activate
|
||||
self._tlwh = np.asarray(tlwh, dtype=np.float32)
|
||||
self.kalman_filter = None
|
||||
self.mean, self.covariance = None, None
|
||||
self.is_activated = False
|
||||
|
||||
self.score = score
|
||||
self.class_ids = class_ids
|
||||
self.tracklet_len = 0
|
||||
|
||||
def predict(self):
|
||||
mean_state = self.mean.copy()
|
||||
if self.state != TrackState.Tracked:
|
||||
mean_state[7] = 0
|
||||
self.mean, self.covariance = self.kalman_filter.predict(
|
||||
mean_state, self.covariance
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def multi_predict(stracks):
|
||||
if len(stracks) > 0:
|
||||
multi_mean = np.asarray([st.mean.copy() for st in stracks])
|
||||
multi_covariance = np.asarray([st.covariance for st in stracks])
|
||||
for i, st in enumerate(stracks):
|
||||
if st.state != TrackState.Tracked:
|
||||
multi_mean[i][7] = 0
|
||||
multi_mean, multi_covariance = STrack.shared_kalman.multi_predict(
|
||||
multi_mean, multi_covariance
|
||||
)
|
||||
for i, (mean, cov) in enumerate(zip(multi_mean, multi_covariance)):
|
||||
stracks[i].mean = mean
|
||||
stracks[i].covariance = cov
|
||||
|
||||
def activate(self, kalman_filter, frame_id):
|
||||
"""Start a new tracklet"""
|
||||
self.kalman_filter = kalman_filter
|
||||
self.track_id = self.next_id()
|
||||
self.mean, self.covariance = self.kalman_filter.initiate(
|
||||
self.tlwh_to_xyah(self._tlwh)
|
||||
)
|
||||
|
||||
self.tracklet_len = 0
|
||||
self.state = TrackState.Tracked
|
||||
if frame_id == 1:
|
||||
self.is_activated = True
|
||||
self.frame_id = frame_id
|
||||
self.start_frame = frame_id
|
||||
|
||||
def re_activate(self, new_track, frame_id, new_id=False):
|
||||
self.mean, self.covariance = self.kalman_filter.update(
|
||||
self.mean, self.covariance, self.tlwh_to_xyah(new_track.tlwh)
|
||||
)
|
||||
self.tracklet_len = 0
|
||||
self.state = TrackState.Tracked
|
||||
self.is_activated = True
|
||||
self.frame_id = frame_id
|
||||
if new_id:
|
||||
self.track_id = self.next_id()
|
||||
self.score = new_track.score
|
||||
|
||||
def update(self, new_track, frame_id):
|
||||
"""
|
||||
Update a matched track
|
||||
:type new_track: STrack
|
||||
:type frame_id: int
|
||||
:type update_feature: bool
|
||||
:return:
|
||||
"""
|
||||
self.frame_id = frame_id
|
||||
self.tracklet_len += 1
|
||||
|
||||
new_tlwh = new_track.tlwh
|
||||
self.mean, self.covariance = self.kalman_filter.update(
|
||||
self.mean, self.covariance, self.tlwh_to_xyah(new_tlwh)
|
||||
)
|
||||
self.state = TrackState.Tracked
|
||||
self.is_activated = True
|
||||
|
||||
self.score = new_track.score
|
||||
|
||||
@property
|
||||
def tlwh(self):
|
||||
"""Get current position in bounding box format `(top left x, top left y,
|
||||
width, height)`.
|
||||
"""
|
||||
if self.mean is None:
|
||||
return self._tlwh.copy()
|
||||
ret = self.mean[:4].copy()
|
||||
ret[2] *= ret[3]
|
||||
ret[:2] -= ret[2:] / 2
|
||||
return ret
|
||||
|
||||
@property
|
||||
def tlbr(self):
|
||||
"""Convert bounding box to format `(min x, min y, max x, max y)`, i.e.,
|
||||
`(top left, bottom right)`.
|
||||
"""
|
||||
ret = self.tlwh.copy()
|
||||
ret[2:] += ret[:2]
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def tlwh_to_xyah(tlwh):
|
||||
"""Convert bounding box to format `(center x, center y, aspect ratio,
|
||||
height)`, where the aspect ratio is `width / height`.
|
||||
"""
|
||||
ret = np.asarray(tlwh).copy()
|
||||
ret[:2] += ret[2:] / 2
|
||||
ret[2] /= ret[3]
|
||||
return ret
|
||||
|
||||
def to_xyah(self):
|
||||
return self.tlwh_to_xyah(self.tlwh)
|
||||
|
||||
@staticmethod
|
||||
def tlbr_to_tlwh(tlbr):
|
||||
ret = np.asarray(tlbr).copy()
|
||||
ret[2:] -= ret[:2]
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def tlwh_to_tlbr(tlwh):
|
||||
ret = np.asarray(tlwh).copy()
|
||||
ret[2:] += ret[:2]
|
||||
return ret
|
||||
|
||||
def __repr__(self):
|
||||
return "OT_{}_({}-{})".format(self.track_id, self.start_frame, self.end_frame)
|
||||
|
||||
|
||||
def detections2boxes(detections: Detections) -> np.ndarray:
|
||||
"""
|
||||
Convert Supervision Detections to numpy tensors for further computation.
|
||||
Args:
|
||||
detections (Detections): Detections/Targets in the format of sv.Detections.
|
||||
Returns:
|
||||
(np.ndarray): Detections as numpy tensors as in `(x_min, y_min, x_max, y_max, confidence, class_id)` order.
|
||||
"""
|
||||
return np.hstack(
|
||||
(
|
||||
detections.xyxy,
|
||||
detections.confidence[:, np.newaxis],
|
||||
detections.class_id[:, np.newaxis],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ByteTrack:
|
||||
"""
|
||||
Initialize the ByteTrack object.
|
||||
|
||||
Parameters:
|
||||
track_thresh (float, optional): Detection confidence threshold for track activation.
|
||||
track_buffer (int, optional): Number of frames to buffer when a track is lost.
|
||||
match_thresh (float, optional): Threshold for matching tracks with detections.
|
||||
frame_rate (int, optional): The frame rate of the video.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
track_thresh: float = 0.25,
|
||||
track_buffer: int = 30,
|
||||
match_thresh: float = 0.8,
|
||||
frame_rate: int = 30,
|
||||
):
|
||||
self.track_thresh = track_thresh
|
||||
self.match_thresh = match_thresh
|
||||
|
||||
self.frame_id = 0
|
||||
self.det_thresh = self.track_thresh + 0.1
|
||||
self.max_time_lost = int(frame_rate / 30.0 * track_buffer)
|
||||
self.kalman_filter = KalmanFilter()
|
||||
|
||||
self.tracked_tracks: List[STrack] = []
|
||||
self.lost_tracks: List[STrack] = []
|
||||
self.removed_tracks: List[STrack] = []
|
||||
|
||||
def update_with_detections(self, detections: Detections) -> Detections:
|
||||
"""
|
||||
Updates the tracker with the provided detections and returns the updated detection results.
|
||||
|
||||
Parameters:
|
||||
detections: The new detections to update with.
|
||||
Returns:
|
||||
Detection: The updated detection results that now include tracking IDs.
|
||||
Example:
|
||||
```python
|
||||
>>> import supervision as sv
|
||||
>>> from ultralytics import YOLO
|
||||
|
||||
>>> model = YOLO(...)
|
||||
>>> byte_tracker = sv.ByteTrack()
|
||||
>>> annotator = sv.BoxAnnotator()
|
||||
|
||||
>>> def callback(frame: np.ndarray, index: int) -> np.ndarray:
|
||||
... results = model(frame)[0]
|
||||
... detections = sv.Detections.from_yolov8(results)
|
||||
... detections = byte_tracker.update_from_detections(detections=detections)
|
||||
... labels = [
|
||||
... f"#{tracker_id} {model.model.names[class_id]} {confidence:0.2f}"
|
||||
... for _, _, confidence, class_id, tracker_id
|
||||
... in detections
|
||||
... ]
|
||||
... return annotator.annotate(scene=frame.copy(), detections=detections, labels=labels)
|
||||
|
||||
>>> sv.process_video(
|
||||
... source_path='...',
|
||||
... target_path='...',
|
||||
... callback=callback
|
||||
... )
|
||||
```
|
||||
"""
|
||||
|
||||
tracks = self.update_with_tensors(
|
||||
tensors=detections2boxes(detections=detections)
|
||||
)
|
||||
detections = Detections.empty()
|
||||
if len(tracks) > 0:
|
||||
detections.xyxy = np.array(
|
||||
[track.tlbr for track in tracks], dtype=np.float32
|
||||
)
|
||||
detections.class_id = np.array(
|
||||
[int(t.class_ids) for t in tracks], dtype=int
|
||||
)
|
||||
detections.tracker_id = np.array(
|
||||
[int(t.track_id) for t in tracks], dtype=int
|
||||
)
|
||||
detections.confidence = np.array(
|
||||
[t.score for t in tracks], dtype=np.float32
|
||||
)
|
||||
|
||||
return detections
|
||||
|
||||
def update_with_tensors(self, tensors: np.ndarray) -> List[STrack]:
|
||||
"""
|
||||
Updates the tracker with the provided tensors and returns the updated tracks.
|
||||
|
||||
Parameters:
|
||||
tensors: The new tensors to update with.
|
||||
|
||||
Returns:
|
||||
List[STrack]: Updated tracks.
|
||||
"""
|
||||
self.frame_id += 1
|
||||
activated_starcks = []
|
||||
refind_stracks = []
|
||||
lost_stracks = []
|
||||
removed_stracks = []
|
||||
|
||||
class_ids = tensors[:, 5]
|
||||
scores = tensors[:, 4]
|
||||
bboxes = tensors[:, :4]
|
||||
|
||||
remain_inds = scores > self.track_thresh
|
||||
inds_low = scores > 0.1
|
||||
inds_high = scores < self.track_thresh
|
||||
|
||||
inds_second = np.logical_and(inds_low, inds_high)
|
||||
dets_second = bboxes[inds_second]
|
||||
dets = bboxes[remain_inds]
|
||||
scores_keep = scores[remain_inds]
|
||||
scores_second = scores[inds_second]
|
||||
|
||||
class_ids_keep = class_ids[remain_inds]
|
||||
class_ids_second = class_ids[inds_second]
|
||||
|
||||
if len(dets) > 0:
|
||||
"""Detections"""
|
||||
detections = [
|
||||
STrack(STrack.tlbr_to_tlwh(tlbr), s, c)
|
||||
for (tlbr, s, c) in zip(dets, scores_keep, class_ids_keep)
|
||||
]
|
||||
else:
|
||||
detections = []
|
||||
|
||||
""" Add newly detected tracklets to tracked_stracks"""
|
||||
unconfirmed = []
|
||||
tracked_stracks = [] # type: list[STrack]
|
||||
for track in self.tracked_tracks:
|
||||
if not track.is_activated:
|
||||
unconfirmed.append(track)
|
||||
else:
|
||||
tracked_stracks.append(track)
|
||||
|
||||
""" Step 2: First association, with high score detection boxes"""
|
||||
strack_pool = joint_tracks(tracked_stracks, self.lost_tracks)
|
||||
# Predict the current location with KF
|
||||
STrack.multi_predict(strack_pool)
|
||||
dists = matching.iou_distance(strack_pool, detections)
|
||||
|
||||
dists = matching.fuse_score(dists, detections)
|
||||
matches, u_track, u_detection = matching.linear_assignment(
|
||||
dists, thresh=self.match_thresh
|
||||
)
|
||||
|
||||
for itracked, idet in matches:
|
||||
track = strack_pool[itracked]
|
||||
det = detections[idet]
|
||||
if track.state == TrackState.Tracked:
|
||||
track.update(detections[idet], self.frame_id)
|
||||
activated_starcks.append(track)
|
||||
else:
|
||||
track.re_activate(det, self.frame_id, new_id=False)
|
||||
refind_stracks.append(track)
|
||||
|
||||
""" Step 3: Second association, with low score detection boxes"""
|
||||
# association the untrack to the low score detections
|
||||
if len(dets_second) > 0:
|
||||
"""Detections"""
|
||||
detections_second = [
|
||||
STrack(STrack.tlbr_to_tlwh(tlbr), s, c)
|
||||
for (tlbr, s, c) in zip(dets_second, scores_second, class_ids_second)
|
||||
]
|
||||
else:
|
||||
detections_second = []
|
||||
r_tracked_stracks = [
|
||||
strack_pool[i]
|
||||
for i in u_track
|
||||
if strack_pool[i].state == TrackState.Tracked
|
||||
]
|
||||
dists = matching.iou_distance(r_tracked_stracks, detections_second)
|
||||
matches, u_track, u_detection_second = matching.linear_assignment(
|
||||
dists, thresh=0.5
|
||||
)
|
||||
for itracked, idet in matches:
|
||||
track = r_tracked_stracks[itracked]
|
||||
det = detections_second[idet]
|
||||
if track.state == TrackState.Tracked:
|
||||
track.update(det, self.frame_id)
|
||||
activated_starcks.append(track)
|
||||
else:
|
||||
track.re_activate(det, self.frame_id, new_id=False)
|
||||
refind_stracks.append(track)
|
||||
|
||||
for it in u_track:
|
||||
track = r_tracked_stracks[it]
|
||||
if not track.state == TrackState.Lost:
|
||||
track.mark_lost()
|
||||
lost_stracks.append(track)
|
||||
|
||||
"""Deal with unconfirmed tracks, usually tracks with only one beginning frame"""
|
||||
detections = [detections[i] for i in u_detection]
|
||||
dists = matching.iou_distance(unconfirmed, detections)
|
||||
|
||||
dists = matching.fuse_score(dists, detections)
|
||||
matches, u_unconfirmed, u_detection = matching.linear_assignment(
|
||||
dists, thresh=0.7
|
||||
)
|
||||
for itracked, idet in matches:
|
||||
unconfirmed[itracked].update(detections[idet], self.frame_id)
|
||||
activated_starcks.append(unconfirmed[itracked])
|
||||
for it in u_unconfirmed:
|
||||
track = unconfirmed[it]
|
||||
track.mark_removed()
|
||||
removed_stracks.append(track)
|
||||
|
||||
""" Step 4: Init new stracks"""
|
||||
for inew in u_detection:
|
||||
track = detections[inew]
|
||||
if track.score < self.det_thresh:
|
||||
continue
|
||||
track.activate(self.kalman_filter, self.frame_id)
|
||||
activated_starcks.append(track)
|
||||
""" Step 5: Update state"""
|
||||
for track in self.lost_tracks:
|
||||
if self.frame_id - track.end_frame > self.max_time_lost:
|
||||
track.mark_removed()
|
||||
removed_stracks.append(track)
|
||||
|
||||
self.tracked_tracks = [
|
||||
t for t in self.tracked_tracks if t.state == TrackState.Tracked
|
||||
]
|
||||
self.tracked_tracks = joint_tracks(self.tracked_tracks, activated_starcks)
|
||||
self.tracked_tracks = joint_tracks(self.tracked_tracks, refind_stracks)
|
||||
self.lost_tracks = sub_tracks(self.lost_tracks, self.tracked_tracks)
|
||||
self.lost_tracks.extend(lost_stracks)
|
||||
self.lost_tracks = sub_tracks(self.lost_tracks, self.removed_tracks)
|
||||
self.removed_tracks.extend(removed_stracks)
|
||||
self.tracked_tracks, self.lost_tracks = remove_duplicate_tracks(
|
||||
self.tracked_tracks, self.lost_tracks
|
||||
)
|
||||
output_stracks = [track for track in self.tracked_tracks if track.is_activated]
|
||||
|
||||
return output_stracks
|
||||
|
||||
|
||||
def joint_tracks(
|
||||
track_list_a: List[STrack], track_list_b: List[STrack]
|
||||
) -> List[STrack]:
|
||||
"""
|
||||
Joins two lists of tracks, ensuring that the resulting list does not
|
||||
contain tracks with duplicate track_id values.
|
||||
|
||||
Parameters:
|
||||
track_list_a: First list of tracks (with track_id attribute).
|
||||
track_list_b: Second list of tracks (with track_id attribute).
|
||||
|
||||
Returns:
|
||||
Combined list of tracks from track_list_a and track_list_b without duplicate track_id values.
|
||||
"""
|
||||
seen_track_ids = set()
|
||||
result = []
|
||||
|
||||
for track in track_list_a + track_list_b:
|
||||
if track.track_id not in seen_track_ids:
|
||||
seen_track_ids.add(track.track_id)
|
||||
result.append(track)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def sub_tracks(track_list_a: List, track_list_b: List) -> List[int]:
|
||||
"""
|
||||
Returns a list of tracks from track_list_a after removing any tracks
|
||||
that share the same track_id with tracks in track_list_b.
|
||||
|
||||
Parameters:
|
||||
track_list_a: List of tracks (with track_id attribute).
|
||||
track_list_b: List of tracks (with track_id attribute) to be subtracted from track_list_a.
|
||||
Returns:
|
||||
List of remaining tracks from track_list_a after subtraction.
|
||||
"""
|
||||
tracks = {track.track_id: track for track in track_list_a}
|
||||
track_ids_b = {track.track_id for track in track_list_b}
|
||||
|
||||
for track_id in track_ids_b:
|
||||
tracks.pop(track_id, None)
|
||||
|
||||
return list(tracks.values())
|
||||
|
||||
|
||||
def remove_duplicate_tracks(tracks_a: List, tracks_b: List) -> Tuple[List, List]:
|
||||
pairwise_distance = matching.iou_distance(tracks_a, tracks_b)
|
||||
matching_pairs = np.where(pairwise_distance < 0.15)
|
||||
|
||||
duplicates_a, duplicates_b = set(), set()
|
||||
for track_index_a, track_index_b in zip(*matching_pairs):
|
||||
time_a = tracks_a[track_index_a].frame_id - tracks_a[track_index_a].start_frame
|
||||
time_b = tracks_b[track_index_b].frame_id - tracks_b[track_index_b].start_frame
|
||||
if time_a > time_b:
|
||||
duplicates_b.add(track_index_b)
|
||||
else:
|
||||
duplicates_a.add(track_index_a)
|
||||
|
||||
result_a = [
|
||||
track for index, track in enumerate(tracks_a) if index not in duplicates_a
|
||||
]
|
||||
result_b = [
|
||||
track for index, track in enumerate(tracks_b) if index not in duplicates_b
|
||||
]
|
||||
|
||||
return result_a, result_b
|
||||
@@ -0,0 +1,190 @@
|
||||
from typing import Tuple
|
||||
|
||||
import numpy as np
|
||||
import scipy.linalg
|
||||
|
||||
|
||||
class KalmanFilter:
|
||||
"""
|
||||
A simple Kalman filter for tracking bounding boxes in image space.
|
||||
|
||||
The 8-dimensional state space
|
||||
|
||||
x, y, a, h, vx, vy, va, vh
|
||||
|
||||
contains the bounding box center position (x, y), aspect ratio a, height h,
|
||||
and their respective velocities.
|
||||
|
||||
Object motion follows a constant velocity model. The bounding box location
|
||||
(x, y, a, h) is taken as direct observation of the state space (linear
|
||||
observation model).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
ndim, dt = 4, 1.0
|
||||
|
||||
self._motion_mat = np.eye(2 * ndim, 2 * ndim)
|
||||
for i in range(ndim):
|
||||
self._motion_mat[i, ndim + i] = dt
|
||||
self._update_mat = np.eye(ndim, 2 * ndim)
|
||||
self._std_weight_position = 1.0 / 20
|
||||
self._std_weight_velocity = 1.0 / 160
|
||||
|
||||
def initiate(self, measurement: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Create track from an unassociated measurement.
|
||||
|
||||
Args:
|
||||
measurement (ndarray): Bounding box coordinates (x, y, a, h) with center position (x, y), aspect ratio a, and height h.
|
||||
|
||||
Returns:
|
||||
Tuple[ndarray, ndarray]: Returns the mean vector (8 dimensional) and covariance matrix (8x8 dimensional) of the new track. Unobserved velocities are initialized to 0 mean.
|
||||
"""
|
||||
mean_pos = measurement
|
||||
mean_vel = np.zeros_like(mean_pos)
|
||||
mean = np.r_[mean_pos, mean_vel]
|
||||
|
||||
std = [
|
||||
2 * self._std_weight_position * measurement[3],
|
||||
2 * self._std_weight_position * measurement[3],
|
||||
1e-2,
|
||||
2 * self._std_weight_position * measurement[3],
|
||||
10 * self._std_weight_velocity * measurement[3],
|
||||
10 * self._std_weight_velocity * measurement[3],
|
||||
1e-5,
|
||||
10 * self._std_weight_velocity * measurement[3],
|
||||
]
|
||||
covariance = np.diag(np.square(std))
|
||||
return mean, covariance
|
||||
|
||||
def predict(
|
||||
self, mean: np.ndarray, covariance: np.ndarray
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Run Kalman filter prediction step.
|
||||
|
||||
Args:
|
||||
mean (ndarray): The 8 dimensional mean vector of the object state at the previous time step.
|
||||
covariance (ndarray): The 8x8 dimensional covariance matrix of the object state at the previous time step.
|
||||
|
||||
Returns:
|
||||
Tuple[ndarray, ndarray]: Returns the mean vector and covariance matrix of the predicted state. Unobserved velocities are initialized to 0 mean.
|
||||
"""
|
||||
std_pos = [
|
||||
self._std_weight_position * mean[3],
|
||||
self._std_weight_position * mean[3],
|
||||
1e-2,
|
||||
self._std_weight_position * mean[3],
|
||||
]
|
||||
std_vel = [
|
||||
self._std_weight_velocity * mean[3],
|
||||
self._std_weight_velocity * mean[3],
|
||||
1e-5,
|
||||
self._std_weight_velocity * mean[3],
|
||||
]
|
||||
motion_cov = np.diag(np.square(np.r_[std_pos, std_vel]))
|
||||
|
||||
mean = np.dot(mean, self._motion_mat.T)
|
||||
covariance = (
|
||||
np.linalg.multi_dot((self._motion_mat, covariance, self._motion_mat.T))
|
||||
+ motion_cov
|
||||
)
|
||||
|
||||
return mean, covariance
|
||||
|
||||
def project(
|
||||
self, mean: np.ndarray, covariance: np.ndarray
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Project state distribution to measurement space.
|
||||
|
||||
Args:
|
||||
mean (ndarray): The state's mean vector (8 dimensional array).
|
||||
covariance (ndarray): The state's covariance matrix (8x8 dimensional).
|
||||
|
||||
Returns:
|
||||
Tuple[ndarray, ndarray]: Returns the projected mean and covariance matrix of the given state estimate.
|
||||
"""
|
||||
std = [
|
||||
self._std_weight_position * mean[3],
|
||||
self._std_weight_position * mean[3],
|
||||
1e-1,
|
||||
self._std_weight_position * mean[3],
|
||||
]
|
||||
innovation_cov = np.diag(np.square(std))
|
||||
|
||||
mean = np.dot(self._update_mat, mean)
|
||||
covariance = np.linalg.multi_dot(
|
||||
(self._update_mat, covariance, self._update_mat.T)
|
||||
)
|
||||
return mean, covariance + innovation_cov
|
||||
|
||||
def multi_predict(
|
||||
self, mean: np.ndarray, covariance: np.ndarray
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Run Kalman filter prediction step (Vectorized version).
|
||||
|
||||
Args:
|
||||
mean (ndarray): The Nx8 dimensional mean matrix of the object states at the previous time step.
|
||||
covariance (ndarray): The Nx8x8 dimensional covariance matrices of the object states at the previous time step.
|
||||
|
||||
Returns:
|
||||
Tuple[ndarray, ndarray]: Returns the mean vector and covariance matrix of the predicted state. Unobserved velocities are initialized to 0 mean.
|
||||
"""
|
||||
std_pos = [
|
||||
self._std_weight_position * mean[:, 3],
|
||||
self._std_weight_position * mean[:, 3],
|
||||
1e-2 * np.ones_like(mean[:, 3]),
|
||||
self._std_weight_position * mean[:, 3],
|
||||
]
|
||||
std_vel = [
|
||||
self._std_weight_velocity * mean[:, 3],
|
||||
self._std_weight_velocity * mean[:, 3],
|
||||
1e-5 * np.ones_like(mean[:, 3]),
|
||||
self._std_weight_velocity * mean[:, 3],
|
||||
]
|
||||
sqr = np.square(np.r_[std_pos, std_vel]).T
|
||||
|
||||
motion_cov = []
|
||||
for i in range(len(mean)):
|
||||
motion_cov.append(np.diag(sqr[i]))
|
||||
motion_cov = np.asarray(motion_cov)
|
||||
|
||||
mean = np.dot(mean, self._motion_mat.T)
|
||||
left = np.dot(self._motion_mat, covariance).transpose((1, 0, 2))
|
||||
covariance = np.dot(left, self._motion_mat.T) + motion_cov
|
||||
|
||||
return mean, covariance
|
||||
|
||||
def update(
|
||||
self, mean: np.ndarray, covariance: np.ndarray, measurement: np.ndarray
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Run Kalman filter correction step.
|
||||
|
||||
Args:
|
||||
mean (ndarray): The predicted state's mean vector (8 dimensional).
|
||||
covariance (ndarray): The state's covariance matrix (8x8 dimensional).
|
||||
measurement (ndarray): The 4-dimensional measurement vector (x, y, a, h), where (x, y) is the center position, a the aspect ratio, and h the height of the bounding box.
|
||||
|
||||
Returns:
|
||||
Tuple[ndarray, ndarray]: Returns the measurement-corrected state distribution.
|
||||
"""
|
||||
projected_mean, projected_cov = self.project(mean, covariance)
|
||||
|
||||
chol_factor, lower = scipy.linalg.cho_factor(
|
||||
projected_cov, lower=True, check_finite=False
|
||||
)
|
||||
kalman_gain = scipy.linalg.cho_solve(
|
||||
(chol_factor, lower),
|
||||
np.dot(covariance, self._update_mat.T).T,
|
||||
check_finite=False,
|
||||
).T
|
||||
innovation = measurement - projected_mean
|
||||
|
||||
new_mean = mean + np.dot(innovation, kalman_gain.T)
|
||||
new_covariance = covariance - np.linalg.multi_dot(
|
||||
(kalman_gain, projected_cov, kalman_gain.T)
|
||||
)
|
||||
return new_mean, new_covariance
|
||||
@@ -0,0 +1,64 @@
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
from scipy.optimize import linear_sum_assignment
|
||||
|
||||
from supervision.detection.utils import box_iou_batch
|
||||
|
||||
|
||||
def indices_to_matches(
|
||||
cost_matrix: np.ndarray, indices: np.ndarray, thresh: float
|
||||
) -> Tuple[np.ndarray, tuple, tuple]:
|
||||
matched_cost = cost_matrix[tuple(zip(*indices))]
|
||||
matched_mask = matched_cost <= thresh
|
||||
|
||||
matches = indices[matched_mask]
|
||||
unmatched_a = tuple(set(range(cost_matrix.shape[0])) - set(matches[:, 0]))
|
||||
unmatched_b = tuple(set(range(cost_matrix.shape[1])) - set(matches[:, 1]))
|
||||
return matches, unmatched_a, unmatched_b
|
||||
|
||||
|
||||
def linear_assignment(
|
||||
cost_matrix: np.ndarray, thresh: float
|
||||
) -> [np.ndarray, Tuple[int], Tuple[int, int]]:
|
||||
if cost_matrix.size == 0:
|
||||
return (
|
||||
np.empty((0, 2), dtype=int),
|
||||
tuple(range(cost_matrix.shape[0])),
|
||||
tuple(range(cost_matrix.shape[1])),
|
||||
)
|
||||
|
||||
cost_matrix[cost_matrix > thresh] = thresh + 1e-4
|
||||
row_ind, col_ind = linear_sum_assignment(cost_matrix)
|
||||
indices = np.column_stack((row_ind, col_ind))
|
||||
|
||||
return indices_to_matches(cost_matrix, indices, thresh)
|
||||
|
||||
|
||||
def iou_distance(atracks: List, btracks: List) -> np.ndarray:
|
||||
if (len(atracks) > 0 and isinstance(atracks[0], np.ndarray)) or (
|
||||
len(btracks) > 0 and isinstance(btracks[0], np.ndarray)
|
||||
):
|
||||
atlbrs = atracks
|
||||
btlbrs = btracks
|
||||
else:
|
||||
atlbrs = [track.tlbr for track in atracks]
|
||||
btlbrs = [track.tlbr for track in btracks]
|
||||
|
||||
_ious = np.zeros((len(atlbrs), len(btlbrs)), dtype=np.float32)
|
||||
if _ious.size != 0:
|
||||
_ious = box_iou_batch(np.asarray(atlbrs), np.asarray(btlbrs))
|
||||
cost_matrix = 1 - _ious
|
||||
|
||||
return cost_matrix
|
||||
|
||||
|
||||
def fuse_score(cost_matrix: np.ndarray, detections: List) -> np.ndarray:
|
||||
if cost_matrix.size == 0:
|
||||
return cost_matrix
|
||||
iou_sim = 1 - cost_matrix
|
||||
det_scores = np.array([det.score for det in detections])
|
||||
det_scores = np.expand_dims(det_scores, axis=0).repeat(cost_matrix.shape[0], axis=0)
|
||||
fuse_sim = iou_sim * det_scores
|
||||
fuse_cost = 1 - fuse_sim
|
||||
return fuse_cost
|
||||
@@ -163,15 +163,15 @@ def process_video(
|
||||
|
||||
Examples:
|
||||
```python
|
||||
>>> from supervision import process_video
|
||||
>>> import supervision as sv
|
||||
|
||||
>>> def process_frame(scene: np.ndarray) -> np.ndarray:
|
||||
>>> def callback(scene: np.ndarray, index: int) -> np.ndarray:
|
||||
... ...
|
||||
|
||||
>>> process_video(
|
||||
... source_path='source_video.mp4',
|
||||
... target_path='target_video.mp4',
|
||||
... callback=process_frame
|
||||
... source_path='...',
|
||||
... target_path='...',
|
||||
... callback=callback
|
||||
... )
|
||||
```
|
||||
"""
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
from typing import List, Tuple
|
||||
from typing import List, Tuple, Dict
|
||||
|
||||
import pytest
|
||||
|
||||
from supervision import Detections
|
||||
from supervision.dataset.formats.coco import classes_to_coco_categories, coco_categories_to_classes, \
|
||||
group_coco_annotations_by_image_id, coco_annotations_to_detections
|
||||
group_coco_annotations_by_image_id, coco_annotations_to_detections, build_coco_class_index_mapping
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -290,3 +290,135 @@ def test_coco_annotations_to_detections(
|
||||
with_masks=with_masks
|
||||
)
|
||||
assert result == expected_result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"coco_categories, target_classes, expected_result, exception",
|
||||
[
|
||||
(
|
||||
[],
|
||||
[],
|
||||
{},
|
||||
DoesNotRaise()
|
||||
), # empty coco categories
|
||||
(
|
||||
[
|
||||
{
|
||||
"id": 0,
|
||||
"name": "fashion-assistant",
|
||||
"supercategory": "none"
|
||||
}
|
||||
],
|
||||
[
|
||||
"fashion-assistant"
|
||||
],
|
||||
{
|
||||
0: 0
|
||||
},
|
||||
DoesNotRaise()
|
||||
), # single coco category starting from 0
|
||||
(
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "fashion-assistant",
|
||||
"supercategory": "none"
|
||||
}
|
||||
],
|
||||
[
|
||||
"fashion-assistant"
|
||||
],
|
||||
{
|
||||
1: 0
|
||||
},
|
||||
DoesNotRaise()
|
||||
), # single coco category starting from 1
|
||||
(
|
||||
[
|
||||
{
|
||||
"id": 0,
|
||||
"name": "fashion-assistant",
|
||||
"supercategory": "none"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "hoodie",
|
||||
"supercategory": "fashion-assistant"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "baseball cap",
|
||||
"supercategory": "fashion-assistant"
|
||||
}
|
||||
],
|
||||
[
|
||||
"fashion-assistant",
|
||||
"baseball cap",
|
||||
"hoodie"
|
||||
],
|
||||
{
|
||||
0: 0,
|
||||
1: 1,
|
||||
2: 2
|
||||
},
|
||||
DoesNotRaise()
|
||||
), # three coco categories
|
||||
(
|
||||
[
|
||||
{
|
||||
"id": 2,
|
||||
"name": "hoodie",
|
||||
"supercategory": "fashion-assistant"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "baseball cap",
|
||||
"supercategory": "fashion-assistant"
|
||||
}
|
||||
],
|
||||
[
|
||||
"baseball cap",
|
||||
"hoodie"
|
||||
],
|
||||
{
|
||||
2: 1,
|
||||
1: 0
|
||||
},
|
||||
DoesNotRaise()
|
||||
), # two coco categories
|
||||
(
|
||||
[
|
||||
{
|
||||
"id": 3,
|
||||
"name": "hoodie",
|
||||
"supercategory": "fashion-assistant"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "baseball cap",
|
||||
"supercategory": "fashion-assistant"
|
||||
}
|
||||
],
|
||||
[
|
||||
"baseball cap",
|
||||
"hoodie"
|
||||
],
|
||||
{
|
||||
3: 1,
|
||||
1: 0
|
||||
},
|
||||
DoesNotRaise()
|
||||
), # two coco categories with missing category
|
||||
]
|
||||
)
|
||||
def test_build_coco_class_index_mapping(
|
||||
coco_categories: List[dict],
|
||||
target_classes: List[str],
|
||||
expected_result: Dict[int, int],
|
||||
exception: Exception
|
||||
) -> None:
|
||||
with exception:
|
||||
result = build_coco_class_index_mapping(
|
||||
coco_categories=coco_categories,
|
||||
target_classes=target_classes
|
||||
)
|
||||
assert result == expected_result
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
from test.utils import mock_detections
|
||||
from typing import List, Optional
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from supervision.dataset.formats.pascal_voc import (
|
||||
detections_from_xml_obj,
|
||||
object_to_pascal_voc,
|
||||
parse_polygon_points,
|
||||
)
|
||||
|
||||
|
||||
def are_xml_elements_equal(elem1, elem2):
|
||||
if (
|
||||
elem1.tag != elem2.tag
|
||||
or elem1.attrib != elem2.attrib
|
||||
or elem1.text != elem2.text
|
||||
or len(elem1) != len(elem2)
|
||||
):
|
||||
return False
|
||||
|
||||
for child1, child2 in zip(elem1, elem2):
|
||||
if not are_xml_elements_equal(child1, child2):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"xyxy, name, polygon, expected_result, exception",
|
||||
[
|
||||
(
|
||||
[0, 0, 10, 10],
|
||||
"test",
|
||||
None,
|
||||
ET.fromstring(
|
||||
"""<object><name>test</name><bndbox><xmin>0</xmin><ymin>0</ymin><xmax>10</xmax><ymax>10</ymax></bndbox></object>"""
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
[0, 0, 10, 10],
|
||||
"test",
|
||||
[[0, 0], [10, 0], [10, 10], [0, 10]],
|
||||
ET.fromstring(
|
||||
"""<object><name>test</name><bndbox><xmin>0</xmin><ymin>0</ymin><xmax>10</xmax><ymax>10</ymax></bndbox><polygon><x1>0</x1><y1>0</y1><x2>10</x2><y2>0</y2><x3>10</x3><y3>10</y3><x4>0</x4><y4>10</y4></polygon></object>"""
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_object_to_pascal_voc(
|
||||
xyxy: np.ndarray,
|
||||
name: str,
|
||||
polygon: Optional[np.ndarray],
|
||||
expected_result,
|
||||
exception: Exception,
|
||||
):
|
||||
with exception:
|
||||
result = object_to_pascal_voc(xyxy=xyxy, name=name, polygon=polygon)
|
||||
assert are_xml_elements_equal(result, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"polygon_element, expected_result, exception",
|
||||
[
|
||||
(
|
||||
ET.fromstring(
|
||||
"""<polygon><x1>0</x1><y1>0</y1><x2>10</x2><y2>0</y2><x3>10</x3><y3>10</y3><x4>0</x4><y4>10</y4></polygon>"""
|
||||
),
|
||||
[[0, 0], [10, 0], [10, 10], [0, 10]],
|
||||
DoesNotRaise(),
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_parse_polygon_points(
|
||||
polygon_element,
|
||||
expected_result: List[list],
|
||||
exception,
|
||||
):
|
||||
with exception:
|
||||
result = parse_polygon_points(polygon_element)
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
ONE_CLASS_N_BBOX = """<annotation><object><name>test</name><bndbox><xmin>0</xmin><ymin>0</ymin><xmax>10</xmax><ymax>10</ymax></bndbox></object><object><name>test</name><bndbox><xmin>10</xmin><ymin>10</ymin><xmax>20</xmax><ymax>20</ymax></bndbox></object></annotation>"""
|
||||
|
||||
|
||||
ONE_CLASS_ONE_BBOX = """<annotation><object><name>test</name><bndbox><xmin>0</xmin><ymin>0</ymin><xmax>10</xmax><ymax>10</ymax></bndbox></object></annotation>"""
|
||||
|
||||
|
||||
N_CLASS_N_BBOX = """<annotation><object><name>test</name><bndbox><xmin>0</xmin><ymin>0</ymin><xmax>10</xmax><ymax>10</ymax></bndbox></object><object><name>test</name><bndbox><xmin>20</xmin><ymin>30</ymin><xmax>30</xmax><ymax>40</ymax></bndbox></object><object><name>test2</name><bndbox><xmin>10</xmin><ymin>10</ymin><xmax>20</xmax><ymax>20</ymax></bndbox></object></annotation>"""
|
||||
|
||||
NO_DETECTIONS = """<annotation></annotation>"""
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"xml_string, classes, resolution_wh, force_masks, expected_result, exception",
|
||||
[
|
||||
(
|
||||
ONE_CLASS_ONE_BBOX,
|
||||
["test"],
|
||||
(100, 100),
|
||||
False,
|
||||
mock_detections(np.array([[0, 0, 10, 10]]), None, [0]),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
ONE_CLASS_N_BBOX,
|
||||
["test"],
|
||||
(100, 100),
|
||||
False,
|
||||
mock_detections(np.array([[0, 0, 10, 10], [10, 10, 20, 20]]), None, [0, 0]),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
N_CLASS_N_BBOX,
|
||||
["test", "test2"],
|
||||
(100, 100),
|
||||
False,
|
||||
mock_detections(
|
||||
np.array([[0, 0, 10, 10], [20, 30, 30, 40], [10, 10, 20, 20]]),
|
||||
None,
|
||||
[0, 0, 1],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
NO_DETECTIONS,
|
||||
[],
|
||||
(100, 100),
|
||||
False,
|
||||
mock_detections(np.empty((0, 4)), None, []),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_detections_from_xml_obj(
|
||||
xml_string, classes, resolution_wh, force_masks, expected_result, exception
|
||||
):
|
||||
with exception:
|
||||
root = ET.fromstring(xml_string)
|
||||
result, _ = detections_from_xml_obj(root, classes, resolution_wh, force_masks)
|
||||
assert result == expected_result
|
||||
@@ -172,5 +172,4 @@ def test_dataset_merge(
|
||||
) -> None:
|
||||
with exception:
|
||||
result = DetectionDataset.merge(dataset_list=dataset_list)
|
||||
print(result.images.keys())
|
||||
assert result == expected_result
|
||||
|
||||
@@ -204,6 +204,12 @@ def test_build_class_index_mapping(
|
||||
mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[1]),
|
||||
DoesNotRaise()
|
||||
), # single mapping
|
||||
(
|
||||
{0: 1, 1: 2},
|
||||
Detections.empty(),
|
||||
Detections.empty(),
|
||||
DoesNotRaise()
|
||||
), # empty detections
|
||||
(
|
||||
{0: 1, 1: 2},
|
||||
mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[0]),
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
from typing import Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.metrics.detection import ConfusionMatrix, detections_to_tensor, MeanAveragePrecision
|
||||
from test.utils import mock_detections, assert_almost_equal
|
||||
|
||||
CLASSES = np.arange(80)
|
||||
NUM_CLASSES = len(CLASSES)
|
||||
|
||||
PREDICTIONS = np.array(
|
||||
[
|
||||
[2254, 906, 2447, 1353, 0.90538, 0],
|
||||
[2049, 1133, 2226, 1371, 0.59002, 56],
|
||||
[727, 1224, 838, 1601, 0.51119, 39],
|
||||
[808, 1214, 910, 1564, 0.45287, 39],
|
||||
[6, 52, 1131, 2133, 0.45057, 72],
|
||||
[299, 1225, 512, 1663, 0.45029, 39],
|
||||
[529, 874, 645, 945, 0.31101, 39],
|
||||
[8, 47, 1935, 2135, 0.28192, 72],
|
||||
[2265, 813, 2328, 901, 0.2714, 62],
|
||||
],
|
||||
dtype=np.float32,
|
||||
)
|
||||
|
||||
TARGET_TENSORS = [
|
||||
np.array(
|
||||
[
|
||||
[2254, 906, 2447, 1353, 0],
|
||||
[2049, 1133, 2226, 1371, 56],
|
||||
[727, 1224, 838, 1601, 39],
|
||||
[808, 1214, 910, 1564, 39],
|
||||
[6, 52, 1131, 2133, 72],
|
||||
[299, 1225, 512, 1663, 39],
|
||||
[529, 874, 645, 945, 39],
|
||||
[8, 47, 1935, 2135, 72],
|
||||
[2265, 813, 2328, 901, 62],
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
DETECTIONS = Detections(
|
||||
xyxy=PREDICTIONS[:, :4],
|
||||
confidence=PREDICTIONS[:, 4],
|
||||
class_id=PREDICTIONS[:, 5].astype(int),
|
||||
)
|
||||
CERTAIN_DETECTIONS = Detections(
|
||||
xyxy=PREDICTIONS[:, :4],
|
||||
confidence=np.ones(len(PREDICTIONS)),
|
||||
class_id=PREDICTIONS[:, 5].astype(int),
|
||||
)
|
||||
|
||||
DETECTION_TENSORS = [
|
||||
np.concatenate(
|
||||
[
|
||||
det.xyxy,
|
||||
np.expand_dims(det.class_id, 1),
|
||||
np.expand_dims(det.confidence, 1),
|
||||
],
|
||||
axis=1,
|
||||
)
|
||||
for det in [DETECTIONS]
|
||||
]
|
||||
CERTAIN_DETECTION_TENSORS = [
|
||||
np.concatenate(
|
||||
[
|
||||
det.xyxy,
|
||||
np.expand_dims(det.class_id, 1),
|
||||
np.ones((len(det), 1)),
|
||||
],
|
||||
axis=1,
|
||||
)
|
||||
for det in [DETECTIONS]
|
||||
]
|
||||
|
||||
IDEAL_MATCHES = np.stack(
|
||||
[
|
||||
np.arange(len(PREDICTIONS)),
|
||||
np.arange(len(PREDICTIONS)),
|
||||
np.ones(len(PREDICTIONS)),
|
||||
],
|
||||
axis=1,
|
||||
)
|
||||
|
||||
|
||||
def create_empty_conf_matrix(num_classes: int, do_add_dummy_class: bool = True):
|
||||
if do_add_dummy_class:
|
||||
num_classes += 1
|
||||
return np.zeros((num_classes, num_classes))
|
||||
|
||||
|
||||
def update_ideal_conf_matrix(conf_matrix: np.ndarray, class_ids: np.ndarray):
|
||||
for class_id, count in zip(*np.unique(class_ids, return_counts=True)):
|
||||
class_id = int(class_id)
|
||||
conf_matrix[class_id, class_id] += count
|
||||
return conf_matrix
|
||||
|
||||
|
||||
def worsen_ideal_conf_matrix(
|
||||
conf_matrix: np.ndarray, class_ids: Union[np.ndarray, list]
|
||||
):
|
||||
for class_id in class_ids:
|
||||
class_id = int(class_id)
|
||||
conf_matrix[class_id, class_id] -= 1
|
||||
conf_matrix[class_id, 80] += 1
|
||||
return conf_matrix
|
||||
|
||||
|
||||
IDEAL_CONF_MATRIX = create_empty_conf_matrix(NUM_CLASSES)
|
||||
IDEAL_CONF_MATRIX = update_ideal_conf_matrix(IDEAL_CONF_MATRIX, PREDICTIONS[:, 5])
|
||||
|
||||
GOOD_CONF_MATRIX = worsen_ideal_conf_matrix(IDEAL_CONF_MATRIX.copy(), [62, 72])
|
||||
|
||||
BAD_CONF_MATRIX = worsen_ideal_conf_matrix(
|
||||
IDEAL_CONF_MATRIX.copy(), [62, 72, 72, 39, 39, 39, 39, 56]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"detections, with_confidence, expected_result, exception",
|
||||
[
|
||||
(
|
||||
Detections.empty(),
|
||||
False,
|
||||
np.empty((0, 5), dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
), # empty detections; no confidence
|
||||
(
|
||||
Detections.empty(),
|
||||
True,
|
||||
np.empty((0, 6), dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
), # empty detections; with confidence
|
||||
(
|
||||
mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[0], confidence=[0.5]),
|
||||
False,
|
||||
np.array([[0, 0, 10, 10, 0]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
), # single detection; no confidence
|
||||
(
|
||||
mock_detections(xyxy=[[0, 0, 10, 10]], class_id=[0], confidence=[0.5]),
|
||||
True,
|
||||
np.array([[0, 0, 10, 10, 0, 0.5]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
), # single detection; with confidence
|
||||
(
|
||||
mock_detections(xyxy=[[0, 0, 10, 10], [0, 0, 20, 20]], class_id=[0, 1], confidence=[0.5, 0.2]),
|
||||
False,
|
||||
np.array([[0, 0, 10, 10, 0], [0, 0, 20, 20, 1]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
), # multiple detections; no confidence
|
||||
(
|
||||
mock_detections(xyxy=[[0, 0, 10, 10], [0, 0, 20, 20]], class_id=[0, 1], confidence=[0.5, 0.2]),
|
||||
True,
|
||||
np.array([[0, 0, 10, 10, 0, 0.5], [0, 0, 20, 20, 1, 0.2]], dtype=np.float32),
|
||||
DoesNotRaise(),
|
||||
), # multiple detections; with confidence
|
||||
],
|
||||
)
|
||||
def test_detections_to_tensor(
|
||||
detections: Detections,
|
||||
with_confidence: bool,
|
||||
expected_result: Optional[np.ndarray],
|
||||
exception: Exception
|
||||
):
|
||||
with exception:
|
||||
result = detections_to_tensor(
|
||||
detections=detections,
|
||||
with_confidence=with_confidence
|
||||
)
|
||||
assert np.array_equal(result, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"predictions, targets, classes, conf_threshold, iou_threshold, expected_result, exception",
|
||||
[
|
||||
(
|
||||
DETECTION_TENSORS,
|
||||
TARGET_TENSORS,
|
||||
CLASSES,
|
||||
0.2,
|
||||
0.5,
|
||||
IDEAL_CONF_MATRIX,
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
[],
|
||||
[],
|
||||
CLASSES,
|
||||
0.2,
|
||||
0.5,
|
||||
create_empty_conf_matrix(NUM_CLASSES),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
DETECTION_TENSORS,
|
||||
TARGET_TENSORS,
|
||||
CLASSES,
|
||||
0.3,
|
||||
0.5,
|
||||
GOOD_CONF_MATRIX,
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
DETECTION_TENSORS,
|
||||
TARGET_TENSORS,
|
||||
CLASSES,
|
||||
0.6,
|
||||
0.5,
|
||||
BAD_CONF_MATRIX,
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
[
|
||||
np.array(
|
||||
[
|
||||
[0.0, 0.0, 3.0, 3.0, 0, 0.9], # correct detection of [0]
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
3.0,
|
||||
3.0,
|
||||
0,
|
||||
0.9,
|
||||
], # additional detection of [0] - FP
|
||||
[
|
||||
6.0,
|
||||
1.0,
|
||||
8.0,
|
||||
3.0,
|
||||
1,
|
||||
0.8,
|
||||
], # correct detection with incorrect class
|
||||
[1.0, 6.0, 2.0, 7.0, 1, 0.8], # incorrect detection - FP
|
||||
[
|
||||
1.0,
|
||||
2.0,
|
||||
2.0,
|
||||
4.0,
|
||||
1,
|
||||
0.8,
|
||||
], # incorrect detection with low IoU - FP
|
||||
]
|
||||
)
|
||||
],
|
||||
[
|
||||
np.array(
|
||||
[
|
||||
[0.0, 0.0, 3.0, 3.0, 0], # [0] detected
|
||||
[2.0, 2.0, 5.0, 5.0, 1], # [1] undetected - FN
|
||||
[
|
||||
6.0,
|
||||
1.0,
|
||||
8.0,
|
||||
3.0,
|
||||
2,
|
||||
], # [2] correct detection with incorrect class
|
||||
]
|
||||
)
|
||||
],
|
||||
CLASSES[:3],
|
||||
0.6,
|
||||
0.5,
|
||||
np.array([[1, 0, 0, 0], [0, 0, 0, 1], [0, 1, 0, 0], [1, 2, 0, 0]]),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
[
|
||||
np.array(
|
||||
[
|
||||
[0.0, 0.0, 3.0, 3.0, 0, 0.9], # correct detection of [0]
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
3.0,
|
||||
3.0,
|
||||
0,
|
||||
0.9,
|
||||
], # additional detection of [0] - FP
|
||||
[
|
||||
6.0,
|
||||
1.0,
|
||||
8.0,
|
||||
3.0,
|
||||
1,
|
||||
0.8,
|
||||
], # correct detection with incorrect class
|
||||
[1.0, 6.0, 2.0, 7.0, 1, 0.8], # incorrect detection - FP
|
||||
[
|
||||
1.0,
|
||||
2.0,
|
||||
2.0,
|
||||
4.0,
|
||||
1,
|
||||
0.8,
|
||||
], # incorrect detection with low IoU - FP
|
||||
]
|
||||
)
|
||||
],
|
||||
[
|
||||
np.array(
|
||||
[
|
||||
[0.0, 0.0, 3.0, 3.0, 0], # [0] detected
|
||||
[2.0, 2.0, 5.0, 5.0, 1], # [1] undetected - FN
|
||||
[
|
||||
6.0,
|
||||
1.0,
|
||||
8.0,
|
||||
3.0,
|
||||
2,
|
||||
], # [2] correct detection with incorrect class
|
||||
]
|
||||
)
|
||||
],
|
||||
CLASSES[:3],
|
||||
0.6,
|
||||
1.0,
|
||||
np.array([[0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1], [2, 3, 0, 0]]),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_from_tensors(
|
||||
predictions,
|
||||
targets,
|
||||
classes,
|
||||
conf_threshold,
|
||||
iou_threshold,
|
||||
expected_result: Optional[np.ndarray],
|
||||
exception: Exception,
|
||||
):
|
||||
with exception:
|
||||
result = ConfusionMatrix.from_tensors(
|
||||
predictions=predictions,
|
||||
targets=targets,
|
||||
classes=classes,
|
||||
conf_threshold=conf_threshold,
|
||||
iou_threshold=iou_threshold,
|
||||
)
|
||||
|
||||
assert result.matrix.diagonal().sum() == expected_result.diagonal().sum()
|
||||
assert np.array_equal(result.matrix, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"predictions, targets, num_classes, conf_threshold, iou_threshold, expected_result, exception",
|
||||
[
|
||||
(
|
||||
DETECTION_TENSORS[0],
|
||||
CERTAIN_DETECTION_TENSORS[0],
|
||||
NUM_CLASSES,
|
||||
0.2,
|
||||
0.5,
|
||||
IDEAL_CONF_MATRIX,
|
||||
DoesNotRaise(),
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_evaluate_detection_batch(
|
||||
predictions,
|
||||
targets,
|
||||
num_classes,
|
||||
conf_threshold,
|
||||
iou_threshold,
|
||||
expected_result: Optional[np.ndarray],
|
||||
exception: Exception,
|
||||
):
|
||||
with exception:
|
||||
result = ConfusionMatrix.evaluate_detection_batch(
|
||||
predictions=predictions,
|
||||
targets=targets,
|
||||
num_classes=num_classes,
|
||||
conf_threshold=conf_threshold,
|
||||
iou_threshold=iou_threshold,
|
||||
)
|
||||
|
||||
assert result.diagonal().sum() == result.sum()
|
||||
assert np.array_equal(result, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"matches, expected_result, exception",
|
||||
[
|
||||
(
|
||||
IDEAL_MATCHES,
|
||||
IDEAL_MATCHES,
|
||||
DoesNotRaise(),
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_drop_extra_matches(
|
||||
matches,
|
||||
expected_result: Optional[np.ndarray],
|
||||
exception: Exception,
|
||||
):
|
||||
with exception:
|
||||
result = ConfusionMatrix._drop_extra_matches(matches)
|
||||
|
||||
assert np.array_equal(result, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'recall, precision, expected_result, exception',
|
||||
[
|
||||
(
|
||||
np.array([1.0]),
|
||||
np.array([1.0]),
|
||||
1.0,
|
||||
DoesNotRaise()
|
||||
), # perfect recall and precision
|
||||
(
|
||||
np.array([0.0]),
|
||||
np.array([0.0]),
|
||||
0.0,
|
||||
DoesNotRaise()
|
||||
), # no recall and precision
|
||||
(
|
||||
np.array([0.0, 0.2, 0.2, 0.8, 0.8, 1.0]),
|
||||
np.array([0.7, 0.8, 0.4, 0.5, 0.1, 0.2]),
|
||||
0.5,
|
||||
DoesNotRaise()
|
||||
),
|
||||
(
|
||||
np.array([0.0, 0.5, 0.5, 1.0]),
|
||||
np.array([0.75, 0.75, 0.75, 0.75]),
|
||||
0.75,
|
||||
DoesNotRaise()
|
||||
)
|
||||
]
|
||||
)
|
||||
def test_compute_average_precision(
|
||||
recall: np.ndarray,
|
||||
precision: np.ndarray,
|
||||
expected_result: float,
|
||||
exception: Exception
|
||||
) -> None:
|
||||
with exception:
|
||||
result = MeanAveragePrecision.compute_average_precision(recall=recall, precision=precision)
|
||||
assert_almost_equal(result, expected_result, tolerance=0.01)
|
||||
+12
-4
@@ -2,18 +2,26 @@ from typing import List
|
||||
|
||||
import numpy as np
|
||||
|
||||
from supervision import Detections
|
||||
from supervision.detection.core import Detections
|
||||
|
||||
|
||||
def mock_detections(
|
||||
xyxy: List[List[float]],
|
||||
confidence: List[float] = None,
|
||||
class_id: List[int] = None,
|
||||
tracker_id: List[int] = None
|
||||
tracker_id: List[int] = None,
|
||||
) -> Detections:
|
||||
return Detections(
|
||||
xyxy=np.array(xyxy, dtype=np.float32),
|
||||
confidence=confidence if confidence is None else np.array(confidence, dtype=np.float32),
|
||||
confidence=confidence
|
||||
if confidence is None
|
||||
else np.array(confidence, dtype=np.float32),
|
||||
class_id=class_id if class_id is None else np.array(class_id, dtype=int),
|
||||
tracker_id=tracker_id if tracker_id is None else np.array(tracker_id, dtype=int)
|
||||
tracker_id=tracker_id
|
||||
if tracker_id is None
|
||||
else np.array(tracker_id, dtype=int),
|
||||
)
|
||||
|
||||
|
||||
def assert_almost_equal(actual, expected, tolerance=1e-5):
|
||||
assert abs(actual - expected) < tolerance, f"Expected {expected}, but got {actual}."
|
||||
|
||||
Reference in New Issue
Block a user