Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9731ef9f3 | ||
|
|
b9475f14f3 | ||
|
|
587c61883b | ||
|
|
f866fe046f | ||
|
|
185322fcd1 | ||
|
|
1dd6ff6ce5 | ||
|
|
9e79cb06d0 | ||
|
|
94131ff56b | ||
|
|
ec0add34ee | ||
|
|
6387513805 | ||
|
|
9ed9cd2592 | ||
|
|
5d6ad66618 | ||
|
|
6f7ca11ea9 | ||
|
|
51a108b5ab | ||
|
|
cfe18afcc9 | ||
|
|
39acd616a6 | ||
|
|
1d0747fbfe | ||
|
|
c3618652cd | ||
|
|
1496888d92 | ||
|
|
058dc169f1 | ||
|
|
0d534346bf | ||
|
|
e73cc438e1 | ||
|
|
6a378a32f7 | ||
|
|
1534501a1d | ||
|
|
941700b9f8 | ||
|
|
4743701286 | ||
|
|
b58862d9f3 | ||
|
|
4b245dd5f1 | ||
|
|
32bf9be5d1 | ||
|
|
f56eb2503e | ||
|
|
b1c0c1b00a | ||
|
|
4c88106d14 | ||
|
|
3d0b4cba65 | ||
|
|
f7fd508400 | ||
|
|
32b10b5335 | ||
|
|
5f981bf4d9 | ||
|
|
5634632944 | ||
|
|
fc62e6a0bc | ||
|
|
837772392c | ||
|
|
386fb2e15f | ||
|
|
ad59b39257 | ||
|
|
57966ce9ec | ||
|
|
ca6609571a | ||
|
|
6690bd678a | ||
|
|
1c489eff98 | ||
|
|
f1e067041e | ||
|
|
73d69172db | ||
|
|
8881b5c588 | ||
|
|
83d9868350 | ||
|
|
c9a3069297 | ||
|
|
8864762ff1 | ||
|
|
39a12db1d4 | ||
|
|
55fafd0a40 | ||
|
|
cc28a51c18 | ||
|
|
9938c1d2ad | ||
|
|
51dba14fee | ||
|
|
78d70085cb | ||
|
|
e2cb82a7da | ||
|
|
78e1c2ceba | ||
|
|
898f2ba72c | ||
|
|
11f2a005e2 | ||
|
|
3ba7c1b8e2 | ||
|
|
3562bbf2ed | ||
|
|
408a9ad0ff | ||
|
|
acf1ef6d90 | ||
|
|
752818e577 | ||
|
|
2e38e8998f | ||
|
|
7878df14be | ||
|
|
dc449c4fff | ||
|
|
afcd0d6e5a | ||
|
|
6b48881de7 | ||
|
|
10beacabc0 | ||
|
|
f180f0f5be | ||
|
|
f906a2978f | ||
|
|
e4949a7de7 | ||
|
|
4e41e9b34f | ||
|
|
e804a01e85 | ||
|
|
d0559e104d | ||
|
|
fa8b6e125c | ||
|
|
47295cd5f7 | ||
|
|
2d48c5dba7 | ||
|
|
13b847d1dc | ||
|
|
7e19c633a6 | ||
|
|
8df41a5683 | ||
|
|
59b652d0f2 | ||
|
|
76429548b6 | ||
|
|
23301bbd17 | ||
|
|
77b7d3733a | ||
|
|
63388fdee9 | ||
|
|
c06d5f0131 | ||
|
|
a151685a57 | ||
|
|
cbaa605e08 | ||
|
|
fb1dbfd084 | ||
|
|
fefd005c2c | ||
|
|
2c53eb10a8 | ||
|
|
d381f43e50 | ||
|
|
dc08d88c5c | ||
|
|
af562950a3 | ||
|
|
ee9568945a | ||
|
|
ea03d55d6a | ||
|
|
3f55c3022e | ||
|
|
a54e491d49 | ||
|
|
4c03f899b9 | ||
|
|
1b5c592abb | ||
|
|
a5cad09782 | ||
|
|
483cc0677f | ||
|
|
b2a1eb5b21 | ||
|
|
fa979cf0ab | ||
|
|
2a1542c465 | ||
|
|
e543aac6dd | ||
|
|
3ee6de3ed3 | ||
|
|
16848e9160 | ||
|
|
071feeadce | ||
|
|
9587439f8d | ||
|
|
a61a6c7144 | ||
|
|
6ae1dfdcb0 | ||
|
|
fc40438f35 | ||
|
|
730bbd246c | ||
|
|
656f639db5 | ||
|
|
663522bed8 | ||
|
|
324872303b | ||
|
|
651ed9d470 | ||
|
|
6aedf4874f | ||
|
|
d33c159a26 | ||
|
|
572ea32c0c | ||
|
|
3415c15365 | ||
|
|
2ed66bfffc | ||
|
|
c6591f609f | ||
|
|
ba7760f9ad | ||
|
|
3f05b107ed | ||
|
|
d4c6fd7426 | ||
|
|
6d8d4fdddb | ||
|
|
adec783d49 | ||
|
|
2cca69be00 | ||
|
|
894ee7f5f9 | ||
|
|
23085eaf8a | ||
|
|
4deb9597b2 | ||
|
|
392d769483 | ||
|
|
f088e16fe2 | ||
|
|
30cf394a7b | ||
|
|
beabf6810c | ||
|
|
dbd6104c8b | ||
|
|
2a5ce95472 | ||
|
|
77ae682120 | ||
|
|
4a0b3bd2cf | ||
|
|
a9aad6727c | ||
|
|
7c6668b58e | ||
|
|
385c26ad78 | ||
|
|
d57078a7ed | ||
|
|
7a5672bbcc | ||
|
|
e6f0a8f3f0 | ||
|
|
7b50eed7d9 | ||
|
|
7c2711745b | ||
|
|
dbade52364 | ||
|
|
63392ab640 | ||
|
|
b466076383 | ||
|
|
14b3c0cf8a | ||
|
|
092085efb1 | ||
|
|
b8d62f6e29 | ||
|
|
6f55d9de9e | ||
|
|
4b75b9bd82 | ||
|
|
55378df392 | ||
|
|
4fd1450f80 | ||
|
|
c894a7a763 | ||
|
|
a4a2d8c05d | ||
|
|
30010b9773 | ||
|
|
fd61460cc2 | ||
|
|
a80be9422f | ||
|
|
eb3b172139 | ||
|
|
9975b63216 | ||
|
|
3c4793a092 | ||
|
|
b6a6746f8b | ||
|
|
a9831323f3 | ||
|
|
e6f29a45a3 | ||
|
|
06f678e5d2 | ||
|
|
47c98173ea | ||
|
|
c9ea75e3ca | ||
|
|
517b25f27f | ||
|
|
40340ef8a6 | ||
|
|
3d62caf4f3 | ||
|
|
dab2194f47 | ||
|
|
c257221fe0 | ||
|
|
7c5320409c | ||
|
|
10f372fe28 | ||
|
|
64dc2e91c8 | ||
|
|
9a4781f2a5 | ||
|
|
1633522768 | ||
|
|
d061a4c89b | ||
|
|
059633435c | ||
|
|
001f85ad0f | ||
|
|
d519861b1e | ||
|
|
12509e756a | ||
|
|
fcf8b7f378 | ||
|
|
ea3b9a5584 | ||
|
|
f7d7786c56 | ||
|
|
a713d3650a | ||
|
|
5385ca744b | ||
|
|
34229c7ad7 | ||
|
|
4c88e03b6c | ||
|
|
37943b2d6f | ||
|
|
f3d27d8381 | ||
|
|
c416474b63 | ||
|
|
ae8e6b2976 | ||
|
|
7141f11c0b | ||
|
|
37eadfe305 | ||
|
|
a4dfcacbfd | ||
|
|
3d1416790b | ||
|
|
348069fb07 | ||
|
|
fac1581fcd | ||
|
|
d075eb1e47 | ||
|
|
eac2691c6e | ||
|
|
a0242b0594 | ||
|
|
1e2bdeee35 | ||
|
|
62a396fe90 | ||
|
|
9941417ced | ||
|
|
79390062ce | ||
|
|
66e02b77b0 | ||
|
|
816a990083 | ||
|
|
93d0313460 | ||
|
|
d229d2b069 | ||
|
|
28fa054fdc | ||
|
|
6889e33da5 | ||
|
|
228b4cff0c | ||
|
|
a5948c8992 | ||
|
|
853cbc428f | ||
|
|
d3f6570a89 | ||
|
|
d1c8adb33a | ||
|
|
5cd6844ff4 | ||
|
|
c0d5a7fd25 | ||
|
|
159c507d7f | ||
|
|
fdd317e53e | ||
|
|
370996358b | ||
|
|
def1cc4b33 | ||
|
|
5f0f90b699 | ||
|
|
ae9628e537 | ||
|
|
32eaf4eb50 | ||
|
|
02dac5adfe | ||
|
|
b3d12ed652 | ||
|
|
7bca2fe765 | ||
|
|
a90b8717ee | ||
|
|
b7e02add30 | ||
|
|
7626c32a56 | ||
|
|
fe93979e0b | ||
|
|
684522a7e8 | ||
|
|
bbbb37434c | ||
|
|
4bfee1096e | ||
|
|
dd0ab6a2a7 | ||
|
|
cdf66fd78d | ||
|
|
6d4b08c753 | ||
|
|
7c57a1b8de | ||
|
|
17e35e2bcf | ||
|
|
69017093e1 | ||
|
|
e54ce38458 | ||
|
|
46416e430c | ||
|
|
2ad5b60c66 | ||
|
|
512edd0c9b | ||
|
|
60b9b716e9 | ||
|
|
4b64d4ddca | ||
|
|
9ded219dfb | ||
|
|
987a8f269f | ||
|
|
a57f4f22ae | ||
|
|
88f113d14b | ||
|
|
8d18328dff | ||
|
|
7ea4b2ac13 | ||
|
|
284901c06b | ||
|
|
b2dc1b2a17 | ||
|
|
d9e3ee9465 | ||
|
|
37b5dd477b | ||
|
|
092809b2be | ||
|
|
742a824d2c | ||
|
|
c88de3641c | ||
|
|
aec0bc3529 | ||
|
|
89769e49f7 | ||
|
|
86f95f914a | ||
|
|
eaeb51a8db | ||
|
|
8d562c16a6 | ||
|
|
dd4ffc6d97 | ||
|
|
2d50beeaa9 | ||
|
|
7b3d88602b | ||
|
|
90168644de | ||
|
|
aae3c69ee3 | ||
|
|
3bb22abe0f | ||
|
|
d99565dcd1 | ||
|
|
3692fb65f9 | ||
|
|
1af9c7d629 | ||
|
|
1d9f0b1994 | ||
|
|
77c71dacac | ||
|
|
cfb462ca0b | ||
|
|
c12c562e29 | ||
|
|
10652efbc4 | ||
|
|
3532130b2d | ||
|
|
79b45a4ea8 | ||
|
|
cc8be562b0 | ||
|
|
a1d8c69654 | ||
|
|
4326b87191 | ||
|
|
38dbf5d05d | ||
|
|
5e76f86ff9 | ||
|
|
01c2912a64 | ||
|
|
5fedfe5220 | ||
|
|
c43ca06609 | ||
|
|
b72ad4656b | ||
|
|
fdb036ecd5 | ||
|
|
e4cf743c1c | ||
|
|
19677bede5 | ||
|
|
a6e1f037d7 | ||
|
|
0bfcdeab6f | ||
|
|
83d4386fa2 | ||
|
|
42bb4c2865 | ||
|
|
4ed05e76fd | ||
|
|
2084f57d76 | ||
|
|
1b101b49ac | ||
|
|
abcb055815 | ||
|
|
c3d7dc0ee3 | ||
|
|
c6c447c527 | ||
|
|
b66702c5ff | ||
|
|
920a2c0cda | ||
|
|
6728527f36 | ||
|
|
b9b802e9ea | ||
|
|
11978410f0 | ||
|
|
f4850b870f | ||
|
|
ecf5b1334f | ||
|
|
a2e2e3725a | ||
|
|
ba559c2658 |
@@ -0,0 +1,4 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. They will be requested for review when someone
|
||||
# opens a pull request.
|
||||
* @SkalskiP @onuralpszr
|
||||
@@ -4,13 +4,15 @@ updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: ⬆️
|
||||
target-branch: "develop"
|
||||
# Python
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: ⬆️
|
||||
target-branch: "develop"
|
||||
|
||||
@@ -1,33 +1,42 @@
|
||||
|
||||
name: Clear cache
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 1 * *'
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 1 * *" # Run at midnight on the first day of every month
|
||||
workflow_dispatch:
|
||||
|
||||
# Restrict permissions by default
|
||||
permissions:
|
||||
actions: write
|
||||
actions: write # Required for cache management
|
||||
|
||||
jobs:
|
||||
clear-cache:
|
||||
name: Clear cache
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Clear cache
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
console.log("About to clear")
|
||||
console.log("Starting cache cleanup...")
|
||||
const caches = await github.rest.actions.getActionsCacheList({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
})
|
||||
|
||||
let deletedCount = 0
|
||||
for (const cache of caches.data.actions_caches) {
|
||||
console.log(cache)
|
||||
github.rest.actions.deleteActionsCacheById({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
cache_id: cache.id,
|
||||
})
|
||||
console.log(`Deleting cache: ${cache.key} (${cache.size_in_bytes} bytes)`)
|
||||
try {
|
||||
await github.rest.actions.deleteActionsCacheById({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
cache_id: cache.id,
|
||||
})
|
||||
deletedCount++
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete cache ${cache.key}: ${error.message}`)
|
||||
}
|
||||
}
|
||||
console.log("Clear completed")
|
||||
console.log(`Cache cleanup completed. Deleted ${deletedCount} caches.`)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Combine Dependabot PRs
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 1 * * 3" # Wednesday at 01:00
|
||||
workflow_dispatch: # allows you to manually trigger the workflow
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
checks: read
|
||||
|
||||
jobs:
|
||||
combine-prs:
|
||||
name: Combine
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: combine-prs
|
||||
id: combine-prs
|
||||
uses: github/combine-prs@2909f404763c3177a456e052bdb7f2e85d3a7cb3 # v5.2.0
|
||||
with:
|
||||
labels: combined-pr
|
||||
@@ -1,64 +0,0 @@
|
||||
name: Notebook Check Pull Request
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
comment-welcome:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fetch pull request branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Fetch base develop branch
|
||||
run: git fetch -u "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" develop:develop
|
||||
- name: Create message
|
||||
env:
|
||||
HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
PR_NUM: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
# Preview links and tool usage only needed for notebook changes.
|
||||
readarray -t changed_notebooks < <(git diff --name-only develop | grep '\.ipynb$' || true)
|
||||
if [[ ${#changed_notebooks[@]} == 0 ]]; then
|
||||
echo "No notebooks modified in this pull request."
|
||||
else
|
||||
msg="<h4>Preview</h4>\n"
|
||||
msg+="Preview and run these notebook edits with Google Colab:\n<ul>\n"
|
||||
# Link to PR branch in user's fork that is always current.
|
||||
for fp in "${changed_notebooks[@]}"; do
|
||||
gh_path="${HEAD_REPOSITORY}/blob/${HEAD_REF}/${fp}"
|
||||
colab_url="https://colab.research.google.com/github/${gh_path}"
|
||||
msg+="<li><a href='${colab_url}'>${fp}</a></li>\n"
|
||||
done
|
||||
msg+="</ul>\n"
|
||||
|
||||
reviewnb_url="https://app.reviewnb.com/${GITHUB_REPOSITORY}/pull/${PR_NUM}/files/"
|
||||
msg+="Rendered <a href='${reviewnb_url}'>notebook diffs</a> available on ReviewNB.com.\n"
|
||||
|
||||
msg+="If commits are added to the pull request, synchronize your local branch: <code>git pull origin $HEAD_REF</code>\n"
|
||||
fi
|
||||
echo "MESSAGE=$msg" >> $GITHUB_ENV
|
||||
- name: Post comment
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ISSUE_URL: ${{ github.event.pull_request.issue_url }}
|
||||
run: |
|
||||
# Env var defined in previous step. Escape string for JSON.
|
||||
body="$(echo -n -e $MESSAGE | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')"
|
||||
# Add comment to pull request.
|
||||
curl -X POST \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
"${ISSUE_URL}/comments" \
|
||||
--data "{\"body\": $body}"
|
||||
@@ -1,37 +0,0 @@
|
||||
name: 🔧 Poetry Check and Installation Test Workflow
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'poetry.lock'
|
||||
- 'pyproject.toml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'poetry.lock'
|
||||
- 'pyproject.toml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
poetry-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🐍 Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 📦 Install the base dependencies
|
||||
run: python -m pip install --upgrade poetry
|
||||
|
||||
- name: 🔍 Check the correctness of the project config
|
||||
run: poetry check
|
||||
|
||||
- name: 🚀 Do Install the package Test
|
||||
run: poetry install
|
||||
@@ -1,53 +0,0 @@
|
||||
name: Docs WorkFlow - Develop Tag 📚
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref}}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pages: write
|
||||
pull-requests: write
|
||||
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🔄 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 🐍 Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: 📦 Install mkdocs-material
|
||||
run: pip install "mkdocs-material"
|
||||
- name: 📦 Install mkdocstrings[python]
|
||||
run: pip install "mkdocstrings[python]"
|
||||
- name: 📦 Install mkdocs-material[imaging]
|
||||
run: pip install "mkdocs-material[imaging]"
|
||||
- name: 📦 Install mike
|
||||
run: pip install "mike"
|
||||
- name: 📦 Install mkdocs-git-revision-date-localized-plugin
|
||||
run: pip install "mkdocs-git-revision-date-localized-plugin"
|
||||
- name: 📦 Install JupyterLab
|
||||
run: pip install jupyterlab
|
||||
- name: 📦 Install mkdocs-jupyter
|
||||
run: pip install mkdocs-jupyter
|
||||
- name: 📦 Install mkdocs-git-committers-plugin-2
|
||||
run: pip install mkdocs-git-committers-plugin-2
|
||||
- name: ⚙️ Configure git for github-actions
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
- name: 🚀 Deploy MkDoc-Material with mike
|
||||
run: |
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY=${{ secrets.GITHUB_TOKEN }} mike deploy --push develop
|
||||
@@ -0,0 +1,70 @@
|
||||
name: Build and Publish Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
# Ensure only one concurrent deployment
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref}}
|
||||
cancel-in-progress: true
|
||||
|
||||
# Restrict permissions by default
|
||||
permissions:
|
||||
contents: write # Required for committing to gh-pages
|
||||
pages: write # Required for deploying to Pages
|
||||
pull-requests: write # Required for PR comments
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Publish Docs
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🐍 Install uv and set Python ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 🔑 Create GitHub App token (mkdocs)
|
||||
id: mkdocs_token
|
||||
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.MKDOCS_APP_ID }}
|
||||
private-key: ${{ secrets.MKDOCS_PEM }}
|
||||
owner: roboflow
|
||||
repositories: mkdocs-material-insiders
|
||||
|
||||
- name: 🏗️ Install dependencies
|
||||
run: |
|
||||
uv pip install -r pyproject.toml --extra docs
|
||||
# Install mkdocs-material-insiders using the GitHub App token
|
||||
uv pip install "git+https://roboflow:${{ steps.mkdocs_token.outputs.token }}@github.com/roboflow/mkdocs-material-insiders.git@9.5.49-insiders-4.53.14#egg=mkdocs-material[imaging]"
|
||||
|
||||
- name: ⚙️ Configure git for github-actions
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: 🚀 Deploy Development Docs
|
||||
if: (github.event_name == 'push' && github.ref == 'refs/heads/develop') || github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY=${{ secrets.GITHUB_TOKEN }} uv run mike deploy --push develop
|
||||
|
||||
- name: 🚀 Deploy Release Docs
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
run: |
|
||||
latest_tag=$(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY=${{ secrets.GITHUB_TOKEN }} uv run mike deploy --push --update-aliases $latest_tag latest
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Publish Supervision Pre-Releases to PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]+a[0-9]"
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]+b[0-9]"
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]+rc[0-9]"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {} # Explicitly remove all permissions by default
|
||||
|
||||
jobs:
|
||||
publish-pre-release:
|
||||
name: Publish Pre-release Package
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: test
|
||||
url: https://pypi.org/project/supervision/
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
id-token: write # Required for PyPI publishing
|
||||
contents: read # Required for checkout
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 🏗️ Build source and wheel distributions
|
||||
run: |
|
||||
uv pip install -r pyproject.toml --extra build
|
||||
uv build
|
||||
uv run twine check --strict dist/*
|
||||
|
||||
- name: 🚀 Publish to PyPi
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
||||
with:
|
||||
attestations: true
|
||||
@@ -1,55 +0,0 @@
|
||||
name: Supervision Release Documentation Workflow 📚
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref}}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pages: write
|
||||
pull-requests: write
|
||||
|
||||
|
||||
jobs:
|
||||
doc-build-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
steps:
|
||||
- name: 🛎️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.head_ref }}
|
||||
|
||||
- name: 🐍 Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: 📦 Install mkdocs-material
|
||||
run: pip install "mkdocs-material"
|
||||
- name: 📦 Install mkdocstrings[python]
|
||||
run: pip install "mkdocstrings[python]"
|
||||
- name: 📦 Install mkdocs-material[imaging]
|
||||
run: pip install "mkdocs-material[imaging]"
|
||||
- name: 📦 Install mike
|
||||
run: pip install "mike"
|
||||
- name: 📦 Install mkdocs-git-revision-date-localized-plugin
|
||||
run: pip install "mkdocs-git-revision-date-localized-plugin"
|
||||
- name: 📦 Install JupyterLab
|
||||
run: pip install jupyterlab
|
||||
- name: 📦 Install mkdocs-jupyter
|
||||
run: pip install mkdocs-jupyter
|
||||
- name: 📦 Install mkdocs-git-committers-plugin-2
|
||||
run: pip install mkdocs-git-committers-plugin-2
|
||||
- name: ⚙️ Configure git for github-actions 👷
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
- name: 🚀 Deploy MkDoc-Material 📚
|
||||
run: |
|
||||
latest_tag=$(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY=${{ secrets.GITHUB_TOKEN }} mike deploy --push --update-aliases $latest_tag latest
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Publish Supervision Releases to PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {} # Explicitly remove all permissions by default
|
||||
|
||||
jobs:
|
||||
publish-release:
|
||||
name: Publish Release Package
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
url: https://pypi.org/project/supervision/
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
id-token: write # Required for PyPI publishing
|
||||
contents: read # Required for checkout
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 🏗️ Build source and wheel distributions
|
||||
run: |
|
||||
uv pip install -r pyproject.toml --extra build
|
||||
uv build
|
||||
uv run twine check --strict dist/*
|
||||
|
||||
- name: 🚀 Publish to PyPi
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
||||
with:
|
||||
attestations: true
|
||||
@@ -1,43 +0,0 @@
|
||||
name: Publish Supervision Pre-Releases to PyPI and TestPyPI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]+a[0-9]"
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]+b[0-9]"
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]+rc[0-9]"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-publish-pre-release-pypi:
|
||||
name: Build and publish to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
environment: test
|
||||
permissions:
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
steps:
|
||||
- name: 🛎️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- name: 🐍 Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 🏗️ Build source and wheel distributions
|
||||
run: |
|
||||
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.10
|
||||
|
||||
- name: 🚀 Publish to Test-PyPi
|
||||
uses: pypa/gh-action-pypi-publish@release/v1.10
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Publish Supervision Releases to TestPyPI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {} # Explicitly remove all permissions by default
|
||||
|
||||
jobs:
|
||||
publish-testpypi:
|
||||
name: Publish Release Package
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
url: https://pypi.org/project/supervision/
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
id-token: write # Required for PyPI publishing
|
||||
contents: read # Required for checkout
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 🏗️ Build source and wheel distributions
|
||||
run: |
|
||||
uv pip install -r pyproject.toml --extra build
|
||||
uv build
|
||||
uv run twine check --strict dist/*
|
||||
|
||||
- name: 🚀 Publish to Test-PyPi
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
attestations: true
|
||||
@@ -1,40 +0,0 @@
|
||||
name: Publish Supervision Releases to PyPI and TestPyPI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+[0-9]+.[0-9]"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-publish-pre-release:
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
permissions:
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
steps:
|
||||
- name: 🛎️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- name: 🐍 Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 🏗️ Build source and wheel distributions
|
||||
run: |
|
||||
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.10
|
||||
|
||||
- name: 🚀 Publish to Test-PyPi
|
||||
uses: pypa/gh-action-pypi-publish@release/v1.10
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
@@ -4,18 +4,34 @@ on:
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
# Restrict permissions by default
|
||||
permissions:
|
||||
contents: read # Required for checkout
|
||||
checks: write # Required for test reporting
|
||||
|
||||
jobs:
|
||||
docs-build-test:
|
||||
name: Test docs build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
steps:
|
||||
- name: 🔄 Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: 🐍 Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: 🏗️ Install dependencies and Test Docs Build
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🐍 Install uv and set Python ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 🏗️ Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install "mkdocs-material" "mkdocstrings[python]" "mkdocs-material[imaging]" mike "mkdocs-git-revision-date-localized-plugin" jupyterlab mkdocs-jupyter mkdocs-git-committers-plugin-2
|
||||
mkdocs build --verbose
|
||||
uv pip install -r pyproject.toml --extra docs
|
||||
|
||||
|
||||
- name: 🧪 Test Docs Build
|
||||
run: uv run mkdocs build --verbose
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
name: Python 3.8 - Min Dep Test WorkFlow
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
jobs:
|
||||
build-min-dep-test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8"]
|
||||
steps:
|
||||
- name: 🛎️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: 🐍 Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
# id based on python version
|
||||
id: python-setup
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
|
||||
- name: 📦 Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install \
|
||||
attrs==23.1.0 \
|
||||
certifi==2023.7.22 \
|
||||
charset-normalizer==2.0.12 \
|
||||
cycler==0.12.1 \
|
||||
exceptiongroup==1.1.3 \
|
||||
fonttools==4.43.1 \
|
||||
idna==3.4 \
|
||||
iniconfig==2.0.0 \
|
||||
kiwisolver==1.4.5 \
|
||||
matplotlib==3.5.0 \
|
||||
numpy==1.21.2 \
|
||||
opencv-python==4.5.5.64 \
|
||||
Pillow==10.1.0 \
|
||||
packaging==23.2 \
|
||||
pluggy==1.3.0 \
|
||||
pyparsing==3.1.1 \
|
||||
pytest==7.2.0 \
|
||||
python-dateutil==2.8.2 \
|
||||
PyYAML==5.3 \
|
||||
requests==2.26.0 \
|
||||
scipy==1.10.0 \
|
||||
setuptools-scm==8.0.4 \
|
||||
six==1.16.0 \
|
||||
tomli==2.0.1 \
|
||||
tqdm==4.62.3 \
|
||||
typing_extensions==4.8.0 \
|
||||
urllib3==1.26.18 \
|
||||
defusedxml==0.7.1
|
||||
|
||||
- name: 🧪 Test
|
||||
run: "python -m pytest ./test"
|
||||
@@ -1,29 +0,0 @@
|
||||
name: Test WorkFlow
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
jobs:
|
||||
build-dev-test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
steps:
|
||||
- name: 🛎️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: 🐍 Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
|
||||
- name: 📦 Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install .
|
||||
pip install pytest
|
||||
|
||||
- name: 🧪 Test
|
||||
run: "python -m pytest ./test"
|
||||
@@ -0,0 +1,33 @@
|
||||
name: 🔧 Pytest/Test Workflow
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
jobs:
|
||||
run-tests:
|
||||
name: Import Test and Pytest Run
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🐍 Install uv and set Python version ${{ matrix.python-version }}
|
||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 🚀 Install Packages
|
||||
run: uv pip install -r pyproject.toml --extra dev --extra docs --extra metrics
|
||||
|
||||
- name: 🧪 Run the Import test
|
||||
run: uv run python -c "import supervision; from supervision import assets; from supervision import metrics; print(supervision.__version__)"
|
||||
|
||||
- name: 🧪 Run the Test
|
||||
run: uv run pytest
|
||||
@@ -25,14 +25,14 @@ repos:
|
||||
- id: mixed-line-ending
|
||||
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: '1.7.10'
|
||||
rev: '1.8.3'
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: ["-c", "pyproject.toml"]
|
||||
additional_dependencies: ["bandit[toml]"]
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.7.2
|
||||
rev: v0.11.6
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
@@ -48,7 +48,7 @@ repos:
|
||||
# args: ["--number"]
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.3.0
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies:
|
||||
|
||||
+20
-20
@@ -128,15 +128,26 @@ PRs must pass all tests and linting requirements before they can be merged.
|
||||
|
||||
Before starting your work on the project, set up your development environment:
|
||||
|
||||
1. Clone your fork of the project:
|
||||
1. Clone your fork of the project (recommended to use shallow clone of develop branch):
|
||||
|
||||
**Option A: Recommended for most contributors (shallow clone of develop branch):**
|
||||
|
||||
```bash
|
||||
git clone --depth 1 -b develop https://github.com/YOUR_USERNAME/supervision.git
|
||||
cd supervision
|
||||
```
|
||||
|
||||
Replace `YOUR_USERNAME` with your GitHub username.
|
||||
|
||||
> Note: Using `--depth 1` creates a shallow clone with minimal history and `-b develop` ensures you start with the development branch. This significantly reduces download size while providing everything needed to contribute.
|
||||
|
||||
**Option B: Full repository clone (if you need complete history):**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/supervision.git
|
||||
cd supervision
|
||||
```
|
||||
|
||||
Replace `YOUR_USERNAME` with your GitHub username.
|
||||
|
||||
2. Create and activate a virtual environment:
|
||||
|
||||
```bash
|
||||
@@ -144,31 +155,20 @@ Before starting your work on the project, set up your development environment:
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
3. Install Poetry:
|
||||
3. Install `uv`:
|
||||
|
||||
Using pip:
|
||||
|
||||
```bash
|
||||
pip install -U pip setuptools
|
||||
pip install poetry
|
||||
```
|
||||
|
||||
Or using pipx (recommended for global installation):
|
||||
|
||||
```bash
|
||||
pipx install poetry
|
||||
```
|
||||
Follow the instructions on the [uv installation page](https://docs.astral.sh/uv/getting-started/installation/).
|
||||
|
||||
4. Install project dependencies:
|
||||
|
||||
```bash
|
||||
poetry install
|
||||
uv pip install -r pyproject.toml --extra dev --extra docs --extra metrics
|
||||
```
|
||||
|
||||
5. Run pytest to verify the setup:
|
||||
|
||||
```bash
|
||||
poetry run pytest
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
## 🎨 Code Style and Quality
|
||||
@@ -181,7 +181,7 @@ Furthermore, we have integrated a pre-commit GitHub Action into our workflow. Th
|
||||
|
||||
To run the pre-commit tool, follow these steps:
|
||||
|
||||
1. Install pre-commit by running the following command: `poetry install --with dev`. It will not only install pre-commit but also install all the deps and dev-deps of project
|
||||
1. Install pre-commit by running the following command: `uv pip install -r pyproject.toml --extra dev`. It will not only install pre-commit but also install all the deps and dev-deps of project
|
||||
|
||||
2. Once pre-commit is installed, navigate to the project's root directory.
|
||||
|
||||
@@ -203,7 +203,7 @@ So far, **there is no type checking with mypy**. See [issue](https://github.com/
|
||||
|
||||
The `supervision` documentation is stored in a folder called `docs`. The project documentation is built using `mkdocs`.
|
||||
|
||||
To run the documentation, install the project requirements with `poetry install --with dev`. Then, run `mkdocs serve` to start the documentation server.
|
||||
To run the documentation, install the project requirements with `uv pip install -r pyproject.toml --extra dev --extra docs`. Then, run `mkdocs serve` to start the documentation server.
|
||||
|
||||
You can learn more about mkdocs on the [mkdocs website](https://www.mkdocs.org/).
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
[](https://badge.fury.io/py/supervision)
|
||||
[](https://colab.research.google.com/github/roboflow/supervision/blob/main/demo.ipynb)
|
||||
[](https://huggingface.co/spaces/Roboflow/Annotators)
|
||||
[](https://discord.gg/GbfgXGJ8Bk)
|
||||
[](https://discord.gg/GbfgXGJ8Bk)
|
||||
[](https://squidfunk.github.io/mkdocs-material/)
|
||||
|
||||
<div align="center">
|
||||
@@ -135,88 +135,88 @@ for path, image, annotation in ds:
|
||||
|
||||
- load
|
||||
|
||||
```python
|
||||
dataset = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
)
|
||||
```python
|
||||
dataset = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
)
|
||||
|
||||
dataset = sv.DetectionDataset.from_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
dataset = sv.DetectionDataset.from_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
|
||||
dataset = sv.DetectionDataset.from_coco(
|
||||
images_directory_path=...,
|
||||
annotations_path=...
|
||||
)
|
||||
```
|
||||
dataset = sv.DetectionDataset.from_coco(
|
||||
images_directory_path=...,
|
||||
annotations_path=...
|
||||
)
|
||||
```
|
||||
|
||||
- split
|
||||
|
||||
```python
|
||||
train_dataset, test_dataset = dataset.split(split_ratio=0.7)
|
||||
test_dataset, valid_dataset = test_dataset.split(split_ratio=0.5)
|
||||
```python
|
||||
train_dataset, test_dataset = dataset.split(split_ratio=0.7)
|
||||
test_dataset, valid_dataset = test_dataset.split(split_ratio=0.5)
|
||||
|
||||
len(train_dataset), len(test_dataset), len(valid_dataset)
|
||||
# (700, 150, 150)
|
||||
```
|
||||
len(train_dataset), len(test_dataset), len(valid_dataset)
|
||||
# (700, 150, 150)
|
||||
```
|
||||
|
||||
- merge
|
||||
|
||||
```python
|
||||
ds_1 = sv.DetectionDataset(...)
|
||||
len(ds_1)
|
||||
# 100
|
||||
ds_1.classes
|
||||
# ['dog', 'person']
|
||||
```python
|
||||
ds_1 = sv.DetectionDataset(...)
|
||||
len(ds_1)
|
||||
# 100
|
||||
ds_1.classes
|
||||
# ['dog', 'person']
|
||||
|
||||
ds_2 = sv.DetectionDataset(...)
|
||||
len(ds_2)
|
||||
# 200
|
||||
ds_2.classes
|
||||
# ['cat']
|
||||
ds_2 = sv.DetectionDataset(...)
|
||||
len(ds_2)
|
||||
# 200
|
||||
ds_2.classes
|
||||
# ['cat']
|
||||
|
||||
ds_merged = sv.DetectionDataset.merge([ds_1, ds_2])
|
||||
len(ds_merged)
|
||||
# 300
|
||||
ds_merged.classes
|
||||
# ['cat', 'dog', 'person']
|
||||
```
|
||||
ds_merged = sv.DetectionDataset.merge([ds_1, ds_2])
|
||||
len(ds_merged)
|
||||
# 300
|
||||
ds_merged.classes
|
||||
# ['cat', 'dog', 'person']
|
||||
```
|
||||
|
||||
- save
|
||||
|
||||
```python
|
||||
dataset.as_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
)
|
||||
```python
|
||||
dataset.as_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
)
|
||||
|
||||
dataset.as_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
dataset.as_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
|
||||
dataset.as_coco(
|
||||
images_directory_path=...,
|
||||
annotations_path=...
|
||||
)
|
||||
```
|
||||
dataset.as_coco(
|
||||
images_directory_path=...,
|
||||
annotations_path=...
|
||||
)
|
||||
```
|
||||
|
||||
- convert
|
||||
|
||||
```python
|
||||
sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
).as_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
```
|
||||
```python
|
||||
sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
).as_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -353,7 +353,7 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install -q ultralytics"
|
||||
"!pip install -q \"ultralytics<=8.3.40\""
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,16 +7,6 @@ comments: true
|
||||
Supervision offers an assets download utility that allows you to download video files
|
||||
that you can use in your demos.
|
||||
|
||||
## Install extra
|
||||
|
||||
To install the Supervision assets utility, you can use `pip`. This utility is available
|
||||
as an extra within the Supervision package.
|
||||
|
||||
!!! example "pip install"
|
||||
```bash
|
||||
pip install "supervision[assets]"
|
||||
```
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.assets.downloader.download_assets.download_assets">download_assets</a></h2>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,149 @@
|
||||
# CHANGELOG
|
||||
|
||||
### 0.25.0 <small>Nov 12, 2024</small>
|
||||
|
||||
- No removals or deprecations in this release!
|
||||
|
||||
- Essential update to the [`LineZone`](https://supervision.roboflow.com/0.25.0/detection/tools/line_zone/): when computing line crossings, detections that jitter might be counted twice (or more). This can now be solved with the `minimum_crossing_threshold` argument. If you set it to `2` or more, extra frames will be used to confirm the crossing, improving the accuracy significantly. ([#1540](https://github.com/roboflow/supervision/pull/1540))
|
||||
|
||||
- It is now possible to track objects detected as [`KeyPoints`](https://supervision.roboflow.com/0.25.0/keypoint/core/#supervision.keypoint.core.KeyPoints). See the complete step-by-step guide in the [Object Tracking Guide](https://supervision.roboflow.com/latest/how_to/track_objects/#keypoints). ([#1658](https://github.com/roboflow/supervision/pull/1658))
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
|
||||
model = YOLO("yolov8m-pose.pt")
|
||||
tracker = sv.ByteTrack()
|
||||
trace_annotator = sv.TraceAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model(frame)[0]
|
||||
key_points = sv.KeyPoints.from_ultralytics(results)
|
||||
|
||||
detections = key_points.as_detections()
|
||||
detections = tracker.update_with_detections(detections)
|
||||
|
||||
annotated_image = trace_annotator.annotate(frame.copy(), detections)
|
||||
return annotated_image
|
||||
|
||||
sv.process_video(
|
||||
source_path="input_video.mp4",
|
||||
target_path="output_video.mp4",
|
||||
callback=callback
|
||||
)
|
||||
```
|
||||
|
||||
- Added `is_empty` method to [`KeyPoints`](https://supervision.roboflow.com/0.25.0/keypoint/core/#supervision.keypoint.core.KeyPoints) to check if there are any keypoints in the object. ([#1658](https://github.com/roboflow/supervision/pull/1658))
|
||||
|
||||
- Added `as_detections` method to [`KeyPoints`](https://supervision.roboflow.com/0.25.0/keypoint/core/#supervision.keypoint.core.KeyPoints) that converts `KeyPoints` to `Detections`. ([#1658](https://github.com/roboflow/supervision/pull/1658))
|
||||
|
||||
- Added a new video to `supervision[assets]`. ([#1657](https://github.com/roboflow/supervision/pull/1657))
|
||||
|
||||
```python
|
||||
from supervision.assets import download_assets, VideoAssets
|
||||
|
||||
path_to_video = download_assets(VideoAssets.SKIING)
|
||||
```
|
||||
|
||||
- Supervision can now be used with [`Python 3.13`](https://docs.python.org/3/whatsnew/3.13.html). The most renowned update is the ability to run Python [without Global Interpreter Lock (GIL)](https://docs.python.org/3/whatsnew/3.13.html#whatsnew313-free-threaded-cpython). We expect support for this among our dependencies to be inconsistent, but if you do attempt it - let us know the results! ([#1595](https://github.com/roboflow/supervision/pull/1595))
|
||||
|
||||
- Added [`Mean Average Recall`](https://supervision.roboflow.com/latest/metrics/mean_average_recall/) mAR metric, which returns a recall score, averaged over IoU thresholds, detected object classes, and limits imposed on maximum considered detections. ([#1661](https://github.com/roboflow/supervision/pull/1661))
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
from supervision.metrics import MeanAverageRecall
|
||||
|
||||
predictions = sv.Detections(...)
|
||||
targets = sv.Detections(...)
|
||||
|
||||
map_metric = MeanAverageRecall()
|
||||
map_result = map_metric.update(predictions, targets).compute()
|
||||
|
||||
map_result.plot()
|
||||
```
|
||||
|
||||
- Added [`Precision`](https://supervision.roboflow.com/latest/metrics/precision/) and [`Recall`](https://supervision.roboflow.com/latest/metrics/recall/) metrics, providing a baseline for comparing model outputs to ground truth or another model ([#1609](https://github.com/roboflow/supervision/pull/1609))
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
from supervision.metrics import Recall
|
||||
|
||||
predictions = sv.Detections(...)
|
||||
targets = sv.Detections(...)
|
||||
|
||||
recall_metric = Recall()
|
||||
recall_result = recall_metric.update(predictions, targets).compute()
|
||||
|
||||
recall_result.plot()
|
||||
```
|
||||
|
||||
- All Metrics now support Oriented Bounding Boxes (OBB) ([#1593](https://github.com/roboflow/supervision/pull/1593))
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
from supervision.metrics import F1_Score
|
||||
|
||||
predictions = sv.Detections(...)
|
||||
targets = sv.Detections(...)
|
||||
|
||||
f1_metric = MeanAverageRecall(metric_target=sv.MetricTarget.ORIENTED_BOUNDING_BOXES)
|
||||
f1_result = f1_metric.update(predictions, targets).compute()
|
||||
```
|
||||
|
||||
- Introducing Smart Labels! When `smart_position` is set for [`LabelAnnotator`](https://supervision.roboflow.com/0.25.0/detection/annotators/#supervision.annotators.core.LabelAnnotator), [`RichLabelAnnotator`](https://supervision.roboflow.com/0.25.0/detection/annotators/#supervision.annotators.core.RichLabelAnnotator) or [`VertexLabelAnnotator`](https://supervision.roboflow.com/0.25.0/detection/annotators/#supervision.annotators.core.RichLabelAnnotator), the labels will move around to avoid overlapping others. ([#1625](https://github.com/roboflow/supervision/pull/1625))
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
|
||||
image = cv2.imread("image.jpg")
|
||||
|
||||
label_annotator = sv.LabelAnnotator(smart_position=True)
|
||||
|
||||
model = YOLO("yolo11m.pt")
|
||||
results = model(image)[0]
|
||||
detections = sv.Detections.from_ultralytics(results)
|
||||
|
||||
annotated_frame = label_annotator.annotate(first_frame.copy(), detections)
|
||||
sv.plot_image(annotated_frame)
|
||||
```
|
||||
|
||||
- Added the `metadata` variable to [`Detections`](https://supervision.roboflow.com/0.25.0/detection/core/#supervision.detection.core.Detections). It allows you to store custom data per-image, rather than per-detected-object as was possible with `data` variable. For example, `metadata` could be used to store the source video path, camera model or camera parameters. ([#1589](https://github.com/roboflow/supervision/pull/1589))
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
|
||||
model = YOLO("yolov8m")
|
||||
|
||||
result = model("image.png")[0]
|
||||
detections = sv.Detections.from_ultralytics(result)
|
||||
|
||||
# Items in `data` must match length of detections
|
||||
object_ids = [num for num in range(len(detections))]
|
||||
detections.data["object_number"] = object_ids
|
||||
|
||||
# Items in `metadata` can be of any length.
|
||||
detections.metadata["camera_model"] = "Luxonis OAK-D"
|
||||
```
|
||||
|
||||
- Added a `py.typed` type hints metafile. It should provide a stronger signal to type annotators and IDEs that type support is available. ([#1586](https://github.com/roboflow/supervision/pull/1586))
|
||||
|
||||
- `ByteTrack` no longer requires `detections` to have a `class_id` ([#1637](https://github.com/roboflow/supervision/pull/1637))
|
||||
- `draw_line`, `draw_rectangle`, `draw_filled_rectangle`, `draw_polygon`, `draw_filled_polygon` and `PolygonZoneAnnotator` now comes with a default color ([#1591](https://github.com/roboflow/supervision/pull/1591))
|
||||
- Dataset classes are treated as case-sensitive when merging multiple datasets. ([#1643](https://github.com/roboflow/supervision/pull/1643))
|
||||
- Expanded [metrics documentation](https://supervision.roboflow.com/0.25.0/metrics/f1_score/) with example plots and printed results ([#1660](https://github.com/roboflow/supervision/pull/1660))
|
||||
- Added usage example for polygon zone ([#1608](https://github.com/roboflow/supervision/pull/1608))
|
||||
- Small improvements to error handling in polygons: ([#1602](https://github.com/roboflow/supervision/pull/1602))
|
||||
|
||||
- Updated [`ByteTrack`](https://supervision.roboflow.com/0.25.0/trackers/#supervision.tracker.byte_tracker.core.ByteTrack), removing shared variables. Previously, multiple instances of `ByteTrack` would share some date, requiring liberal use of `tracker.reset()`. ([#1603](https://github.com/roboflow/supervision/pull/1603)), ([#1528](https://github.com/roboflow/supervision/pull/1528))
|
||||
- Fixed a bug where `class_agnostic` setting in `MeanAveragePrecision` would not work. ([#1577](https://github.com/roboflow/supervision/pull/1577)) hacktoberfest
|
||||
- Removed welcome workflow from our CI system. ([#1596](https://github.com/roboflow/supervision/pull/1596))
|
||||
|
||||
- Large refactor of `ByteTrack`: STrack moved to separate class, removed superfluous `BaseTrack` class, removed unused variables ([#1603](https://github.com/roboflow/supervision/pull/1603))
|
||||
- Large refactor of `RichLabelAnnotator`, matching its contents with `LabelAnnotator`. ([#1625](https://github.com/roboflow/supervision/pull/1625))
|
||||
|
||||
### 0.24.0 <small>Oct 4, 2024</small>
|
||||
|
||||
- Added [F1 score](https://supervision.roboflow.com/0.24.0/metrics/f1_score/#supervision.metrics.f1_score.F1Score) as a new metric for detection and segmentation. [#1521](https://github.com/roboflow/supervision/pull/1521)
|
||||
|
||||
@@ -19,6 +19,10 @@ These features are phased out due to better alternatives or potential issues in
|
||||
|
||||
# Removed
|
||||
|
||||
### 0.25.0
|
||||
|
||||
No removals in this version!
|
||||
|
||||
### 0.24.0
|
||||
|
||||
- The `frame_resolution_wh ` parameter in [`sv.PolygonZone`](detection/tools/polygon_zone.md/#supervision.detection.tools.polygon_zone.PolygonZone) has been removed.
|
||||
|
||||
@@ -28,26 +28,28 @@ Annotators accept detections and apply box or mask visualizations to the detecti
|
||||
|
||||
</div>
|
||||
|
||||
=== "RoundBox"
|
||||
<!--
|
||||
=== "RoundBox"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
image = ...
|
||||
detections = sv.Detections(...)
|
||||
image = ...
|
||||
detections = sv.Detections(...)
|
||||
|
||||
round_box_annotator = sv.RoundBoxAnnotator()
|
||||
annotated_frame = round_box_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections
|
||||
)
|
||||
```
|
||||
round_box_annotator = sv.RoundBoxAnnotator()
|
||||
annotated_frame = round_box_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections
|
||||
)
|
||||
```
|
||||
|
||||
<div class="result" markdown>
|
||||
<div class="result" markdown>
|
||||
|
||||
{ align=center width="800" }
|
||||
{ align=center width="800" }
|
||||
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
=== "BoxCorner"
|
||||
|
||||
@@ -234,7 +236,8 @@ Annotators accept detections and apply box or mask visualizations to the detecti
|
||||
|
||||
<div class="result" markdown>
|
||||
|
||||
{ align=center width="800" }
|
||||
{ align=center width="800" }
|
||||
|
||||
</div>
|
||||
|
||||
@@ -255,7 +258,8 @@ Annotators accept detections and apply box or mask visualizations to the detecti
|
||||
|
||||
<div class="result" markdown>
|
||||
|
||||
{ align=center width="800" }
|
||||
{ align=center width="800" }
|
||||
|
||||
</div>
|
||||
|
||||
@@ -283,7 +287,8 @@ Annotators accept detections and apply box or mask visualizations to the detecti
|
||||
|
||||
<div class="result" markdown>
|
||||
|
||||
{ align=center width="800" }
|
||||
{ align=center width="800" }
|
||||
|
||||
</div>
|
||||
|
||||
@@ -314,7 +319,8 @@ Annotators accept detections and apply box or mask visualizations to the detecti
|
||||
|
||||
<div class="result" markdown>
|
||||
|
||||
{ align=center width="800" }
|
||||
{ align=center width="800" }
|
||||
|
||||
</div>
|
||||
|
||||
@@ -341,24 +347,32 @@ Annotators accept detections and apply box or mask visualizations to the detecti
|
||||
|
||||
<div class="result" markdown>
|
||||
|
||||
{ align=center width="800" }
|
||||
{ align=center width="800" }
|
||||
|
||||
</div>
|
||||
|
||||
=== "Crop"
|
||||
<!-- === "Crop"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
image = ...
|
||||
detections = sv.Detections(...)
|
||||
image = ...
|
||||
detections = sv.Detections(...)
|
||||
|
||||
crop_annotator = sv.CropAnnotator()
|
||||
annotated_frame = crop_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections
|
||||
)
|
||||
```
|
||||
crop_annotator = sv.CropAnnotator()
|
||||
annotated_frame = crop_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections
|
||||
)
|
||||
```
|
||||
<div class="result" markdown>
|
||||
|
||||
{ align=center width="800" }
|
||||
|
||||
</div>
|
||||
-->
|
||||
|
||||
=== "Blur"
|
||||
|
||||
@@ -377,7 +391,8 @@ Annotators accept detections and apply box or mask visualizations to the detecti
|
||||
|
||||
<div class="result" markdown>
|
||||
|
||||
{ align=center width="800" }
|
||||
{ align=center width="800" }
|
||||
|
||||
</div>
|
||||
|
||||
@@ -398,7 +413,8 @@ Annotators accept detections and apply box or mask visualizations to the detecti
|
||||
|
||||
<div class="result" markdown>
|
||||
|
||||
{ align=center width="800" }
|
||||
{ align=center width="800" }
|
||||
|
||||
</div>
|
||||
|
||||
@@ -429,7 +445,8 @@ Annotators accept detections and apply box or mask visualizations to the detecti
|
||||
|
||||
<div class="result" markdown>
|
||||
|
||||
{ align=center width="800" }
|
||||
{ align=center width="800" }
|
||||
|
||||
</div>
|
||||
|
||||
@@ -458,7 +475,8 @@ Annotators accept detections and apply box or mask visualizations to the detecti
|
||||
|
||||
<div class="result" markdown>
|
||||
|
||||
{ align=center width="800" }
|
||||
{ align=center width="800" }
|
||||
|
||||
</div>
|
||||
|
||||
@@ -479,7 +497,31 @@ Annotators accept detections and apply box or mask visualizations to the detecti
|
||||
|
||||
<div class="result" markdown>
|
||||
|
||||

|
||||
{ align=center width="800" }
|
||||
|
||||
</div>
|
||||
|
||||
=== "Comparison"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
image = ...
|
||||
detections_1 = sv.Detections(...)
|
||||
detections_2 = sv.Detections(...)
|
||||
|
||||
comparison_annotator = sv.ComparisonAnnotator()
|
||||
annotated_frame = comparison_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections_1=detections_1,
|
||||
detections_2=detections_2
|
||||
)
|
||||
```
|
||||
|
||||
<div class="result" markdown>
|
||||
|
||||
{ align=center width="800" }
|
||||
|
||||
</div>
|
||||
|
||||
@@ -622,6 +664,12 @@ Annotators accept detections and apply box or mask visualizations to the detecti
|
||||
|
||||
:::supervision.annotators.core.BackgroundOverlayAnnotator
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.annotators.core.ComparisonAnnotator">ComparisonAnnotator</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.annotators.core.ComparisonAnnotator
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.annotators.core.ColorLookup">ColorLookup</a></h2>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# InferenceSlicer
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
<div class="md-typeset">
|
||||
|
||||
@@ -89,6 +89,18 @@ status: new
|
||||
|
||||
:::supervision.detection.utils.xywh_to_xyxy
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.xyxy_to_xywh">xyxy_to_xywh</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.utils.xyxy_to_xywh
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.xyxy_to_xcycarh">xyxy_to_xcycarh</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.utils.xyxy_to_xcycarh
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.xcycwh_to_xyxy">xcycwh_to_xyxy</a></h2>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||

|
||||
|
||||
# Benchmark a Model
|
||||
|
||||
Have you ever trained multiple detection models and wondered which one performs best on your specific use case? Or maybe you've downloaded a pre-trained model and want to verify its performance on your dataset? Model benchmarking is essential for making informed decisions about which model to deploy in production.
|
||||
|
||||
This guide will show an easy way to benchmark your results using `supervision`. It will go over:
|
||||
|
||||
1. [Loading a dataset](#loading-a-dataset)
|
||||
2. [Loading a model](#loading-a-model)
|
||||
3. [Benchmarking Basics](#benchmarking-basics)
|
||||
4. [Running a Model](#running-a-model)
|
||||
5. [Remapping Classes](#remapping-classes)
|
||||
6. [Visual Benchmarking](#visual-benchmarking)
|
||||
7. [Benchmarking Metrics](#benchmarking-metrics)
|
||||
8. [Mean Average Precision (mAP)](#mean-average-precision-map)
|
||||
9. [F1 Score](#f1-score)
|
||||
10. [Bonus: Model Leaderboard](#model-leaderboard)
|
||||
|
||||
This guide will use an instance segmentation model, but it applies to object detection, instance segmentation, and oriented bounding box models (OBB) too.
|
||||
|
||||
A condensed version of this guide is available as a [Colab Notebook](https://colab.research.google.com/drive/1HoOY9pZoVwGiRMmLHtir0qT6Uj45w6Ps?usp=sharing).
|
||||
|
||||
## Loading a Dataset
|
||||
|
||||
Suppose you start with a dataset. Perhaps you found it on [Universe](https://universe.roboflow.com/); perhaps you [labeled your own](https://roboflow.com/how-to-label/yolo11). In either case, this guide assumes you know of a labelled dataset at hand.
|
||||
|
||||
We'll use the following libraries:
|
||||
|
||||
- `roboflow` to manage the dataset and deploy models
|
||||
- `inference` to run the models
|
||||
- `supervision` to evaluate the model results
|
||||
|
||||
```bash
|
||||
pip install roboflow supervision
|
||||
pip install git+https://github.com/roboflow/inference.git@linas/allow-latest-rc-supervision
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
We're updating `inference` at the moment. Please install it as shown above.
|
||||
|
||||
Here's how you can download a dataset:
|
||||
|
||||
```python
|
||||
from roboflow import Roboflow
|
||||
|
||||
rf = Roboflow(api_key="<YOUR_API_KEY>")
|
||||
project = rf.workspace("<WORKSPACE_NAME>").project("<PROJECT_NAME>")
|
||||
dataset = project.version(<DATASET_VERSION_NUMBER>).download("<FORMAT>")
|
||||
```
|
||||
|
||||
If your dataset is from Universe, go to `Dataset` > `Download Dataset` > select the format (e.g. `YOLOv11`) > `Show download code`.
|
||||
|
||||
If labeling your own data, go to the [dashboard](https://app.roboflow.com/) and check this [guide](https://docs.roboflow.com/api-reference/workspace-and-project-ids) to find your workspace and project IDs.
|
||||
|
||||
In this guide, we shall use a small [Corgi v2](https://universe.roboflow.com/model-examples/segmented-animals-basic) dataset. It is well-labeled and comes with a test set.
|
||||
|
||||
```python
|
||||
from roboflow import Roboflow
|
||||
|
||||
rf = Roboflow(api_key="<YOUR_API_KEY>")
|
||||
project = rf.workspace("fbamse1-gm2os").project("corgi-v2")
|
||||
dataset = project.version(4).download("yolov11")
|
||||
```
|
||||
|
||||
This will create a folder called `Corgi-v2-4` with the dataset in the current working directory, with `train`, `test`, and `valid` folders and a `data.yaml` file.
|
||||
|
||||
## Loading a Model
|
||||
|
||||
Let's load a model.
|
||||
|
||||
=== "Inference, Local"
|
||||
|
||||
Roboflow supports a range of state-of-the-art [pre-trained models](https://inference.roboflow.com/quickstart/aliases/) for object detection, instance segmentation, and pose tracking. You don't even need an API key!
|
||||
|
||||
Let's load such a model with inference [`inference`](https://inference.roboflow.com/).
|
||||
|
||||
```python
|
||||
from inference import get_model
|
||||
|
||||
model = get_model(model_id="yolov11s-seg-640")
|
||||
```
|
||||
|
||||
=== "Inference, Deployed"
|
||||
|
||||
You can train and deploy a model without leaving the Roboflow platform. See this [guide](https://docs.roboflow.com/train/train/train-from-scratch) for more details.
|
||||
|
||||
To load a model, you can use inference:
|
||||
|
||||
```python
|
||||
from inference import get_model
|
||||
|
||||
model_id = "<PROJECT_NAME>/<MODEL_VERSION>"
|
||||
model = get_model(model_id=model_id)
|
||||
```
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
Similarly to Inference, Ultralytics allows you to run a variety of models.
|
||||
|
||||
```bash
|
||||
pip install "ultralytics<=8.3.40"
|
||||
```
|
||||
|
||||
```python
|
||||
from ultralytics import YOLO
|
||||
|
||||
model = YOLO("yolo11s-seg.pt")
|
||||
```
|
||||
|
||||
## Benchmarking Basics
|
||||
|
||||
Evaluating your model requires careful selection of the dataset. Which images should you use?Let's go over the different scenarios.
|
||||
|
||||
- **Unrelated Dataset**: If you have a dataset that was not used to train the model, this is the best choice.
|
||||
- **Training Set**: This is the set of images used to train the model. This is fine if the model was not trained on this dataset. Otherwise, **never** use it for benchmarking - the results will seem unrealistically good.
|
||||
- **Validation Set**: This is the set of images used to validate the model during training. Every Nth training epoch, the model is evaluated on the validation set. Often the training is stopped once the validation loss stops improving. Therefore, even while the images aren't used to train the model, it still indirectly influences the training outcome.
|
||||
- **Test Set**: This is the set of images kept aside for model testing. It is exactly the set you should use for benchmarking. If the dataset was split correctly, none of these images would be shown to the model during training.
|
||||
|
||||
Therefore, an unrelated dataset or the `test` set is the best choice for benchmarking.
|
||||
Several other problems may arise:
|
||||
|
||||
- **Extra Classes**: An unrelated dataset may contain additional classes which you may need to [filter out](https://supervision.roboflow.com/how_to/filter_detections/#by-set-of-classes) before computing metrics.
|
||||
- **Class Mismatch**: In an unrelated dataset, the class names or IDs may be different to what your model produces, you'll need to remap them, which is [shown in this guide](#running-a-model).
|
||||
- **Data Contamination**: The `test` set may not be split correctly, with images from the test set also present in `training` or `validation` set and used during training. In this case, the results will be overly optimistic. This also applies when **very similar** images are used for training and testing - e.g. those taken in the same environment, same lighting conditions, similar angle, etc.
|
||||
- **Missing Test Set**: Some datasets do not come with a test set. In this case, you should collect and [label](https://roboflow.com/annotate) your own data. Alternatively, a validation set could be used, but the results could be overly optimistic. Make sure to test in the real world as soon as possible.
|
||||
|
||||
## Running a Model
|
||||
|
||||
At this stage, you should have:
|
||||
|
||||
- A dataset of labeled images to evaluate the model.
|
||||
- A model prepared for benchmarking.
|
||||
|
||||
With these ready, we can now run the model and obtain predictions.
|
||||
We'll use `supervision` to create a dataset iterator, and then run the model on each image.
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
test_set = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=f"{dataset.location}/test/images",
|
||||
annotations_directory_path=f"{dataset.location}/test/labels",
|
||||
data_yaml_path=f"{dataset.location}/data.yaml"
|
||||
)
|
||||
|
||||
image_paths = []
|
||||
predictions_list = []
|
||||
targets_list = []
|
||||
|
||||
for image_path, image, label in test_set:
|
||||
result = model.infer(image)[0]
|
||||
predictions = sv.Detections.from_inference(result)
|
||||
|
||||
image_paths.append(image_path)
|
||||
predictions_list.append(predictions)
|
||||
targets_list.append(label)
|
||||
```
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
test_set = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=f"{dataset.location}/test/images",
|
||||
annotations_directory_path=f"{dataset.location}/test/labels",
|
||||
data_yaml_path=f"{dataset.location}/data.yaml"
|
||||
)
|
||||
|
||||
image_paths = []
|
||||
predictions_list = []
|
||||
targets_list = []
|
||||
|
||||
for image_path, image, label in test_set:
|
||||
result = model(image)[0]
|
||||
predictions = sv.Detections.from_ultralytics(result)
|
||||
|
||||
image_paths.append(image_path)
|
||||
predictions_list.append(predictions)
|
||||
targets_list.append(label)
|
||||
```
|
||||
|
||||
## Remapping classes
|
||||
|
||||
Did you notice an issue in the above logic?
|
||||
Since we're using an unrelated dataset, the class names and IDs may be different from what the model was trained on.
|
||||
|
||||
We need to remap them to match the dataset classes. Here's how to do it:
|
||||
|
||||
```python
|
||||
def remap_classes(
|
||||
detections: sv.Detections,
|
||||
class_ids_from_to: dict[int, int],
|
||||
class_names_from_to: dict[str, str]
|
||||
) -> None:
|
||||
new_class_ids = [
|
||||
class_ids_from_to.get(class_id, class_id) for class_id in detections.class_id]
|
||||
detections.class_id = np.array(new_class_ids)
|
||||
|
||||
new_class_names = [
|
||||
class_names_from_to.get(name, name) for name in detections["class_name"]]
|
||||
predictions["class_name"] = np.array(new_class_names)
|
||||
```
|
||||
|
||||
Let's also remove the predictions that are not in the dataset classes.
|
||||
|
||||
=== "Inference"
|
||||
|
||||
Dataset class names and IDs can be found in the `data.yaml` file, or by printing `dataset.classes`.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
test_set = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=f"{dataset.location}/test/images",
|
||||
annotations_directory_path=f"{dataset.location}/test/labels",
|
||||
data_yaml_path=f"{dataset.location}/data.yaml"
|
||||
)
|
||||
|
||||
image_paths = []
|
||||
predictions_list = []
|
||||
targets_list = []
|
||||
|
||||
for image_path, image, label in test_set:
|
||||
result = model.infer(image)[0]
|
||||
predictions = sv.Detections.from_inference(result)
|
||||
|
||||
remap_classes(
|
||||
detections=predictions,
|
||||
class_ids_from_to={16: 0},
|
||||
class_names_from_to={"dog": "Corgi"}
|
||||
)
|
||||
predictions = predictions[
|
||||
np.isin(predictions["class_name"], test_set.classes)
|
||||
]
|
||||
|
||||
image_paths.append(image_path)
|
||||
predictions_list.append(predictions)
|
||||
targets_list.append(label)
|
||||
```
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
Dataset class names and IDs can be found in the `data.yaml` file, or by printing `dataset.classes`.
|
||||
|
||||
Each model will have a different class mapping, so make sure to check the model's documentation. In this case, the model was trained on the COCO dataset, with a class
|
||||
configuration found [here](https://github.com/ultralytics/ultralytics/blob/main/ultralytics/cfg/datasets/coco8.yaml).
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
test_set = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=f"{dataset.location}/test/images",
|
||||
annotations_directory_path=f"{dataset.location}/test/labels",
|
||||
data_yaml_path=f"{dataset.location}/data.yaml"
|
||||
)
|
||||
|
||||
image_paths = []
|
||||
predictions_list = []
|
||||
targets_list = []
|
||||
|
||||
for image_path, image, label in test_set:
|
||||
result = model(image)[0]
|
||||
predictions = sv.Detections.from_ultralytics(result)
|
||||
|
||||
remap_classes(
|
||||
detections=predictions,
|
||||
class_ids_from_to={16: 0},
|
||||
class_names_from_to={"dog": "Corgi"}
|
||||
)
|
||||
predictions = predictions[
|
||||
np.isin(predictions["class_name"], test_set.classes)
|
||||
]
|
||||
|
||||
image_paths.append(image_path)
|
||||
predictions_list.append(predictions)
|
||||
targets_list.append(label)
|
||||
```
|
||||
|
||||
## Visualizing Predictions
|
||||
|
||||
The first step in evaluating your model’s performance is to visualize its predictions.
|
||||
This gives an intuitive sense of how well your model is detecting objects and where it might be failing.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
N = 9
|
||||
GRID_SIZE = (3, 3)
|
||||
|
||||
target_annotator = sv.PolygonAnnotator(color=sv.Color.from_hex("#8315f9"), thickness=8)
|
||||
prediction_annotator = sv.PolygonAnnotator(color=sv.Color.from_hex("#00cfc6"), thickness=6)
|
||||
|
||||
|
||||
annotated_images = []
|
||||
for image_path, predictions, targets in zip(
|
||||
image_paths[:N], predictions_list[:N], targets_list[:N]
|
||||
):
|
||||
annotated_image = cv2.imread(image_path)
|
||||
annotated_image = target_annotator.annotate(scene=annotated_image, detections=targets)
|
||||
annotated_image = prediction_annotator.annotate(scene=annotated_image, detections=prediction)
|
||||
annotated_images.append(annotated_image)
|
||||
|
||||
sv.plot_images_grid(images=annotated_images, grid_size=GRID_SIZE)
|
||||
```
|
||||
|
||||
Here, predictions in purple are targets (ground truth), and predictions in teal are model predictions.
|
||||
|
||||

|
||||
|
||||
!!! tip
|
||||
|
||||
Use `sv.BoxAnnotator` for object detection and `sv.OrientedBoxAnnotator` for OBB.
|
||||
|
||||
See [annotator documentation](https://supervision.roboflow.com/latest/detection/annotators/) for even more options.
|
||||
|
||||
## Benchmarking Metrics
|
||||
|
||||
With multiple models, fine details matter. Visual inspection may not be enough. `supervision` provides a collection of metrics that help obtain precise numerical results of model performance.
|
||||
|
||||
### Mean Average Precision (mAP)
|
||||
|
||||
We'll start with [MeanAveragePrecision (mAP)](https://supervision.roboflow.com/latest/metrics/mean_average_precision/#supervision.metrics.mean_average_precision.MeanAveragePrecision), which is the most commonly used metric for object detection. It measures the average precision across all classes and IoU thresholds.
|
||||
|
||||
For a thorough explanation, check out our [blog](https://blog.roboflow.com/mean-average-precision/) and [Youtube video](https://www.youtube.com/watch?v=oqXDdxF_Wuw).
|
||||
|
||||
Here, the most popular value is `mAP 50:95`. It represents the average precision across all classes and IoU thresholds (`0.5` to `0.95`), whereas other values such as `mAP 50` or `mAP 75` only consider a single IoU threshold (`0.5` and `0.75` respectively).
|
||||
|
||||
Let's compute the mAP:
|
||||
|
||||
```python
|
||||
from supervision.metrics import MeanAveragePrecision, MetricTarget
|
||||
|
||||
map_metric = MeanAveragePrecision(metric_target=MetricTarget.MASKS)
|
||||
map_result = map_metric.update(predictions_list, targets_list).compute()
|
||||
```
|
||||
|
||||
Try printing the result to see it at a glance:
|
||||
|
||||
```python
|
||||
print(map_result)
|
||||
```
|
||||
|
||||
```
|
||||
MeanAveragePrecisionResult:
|
||||
Metric target: MetricTarget.MASKS
|
||||
Class agnostic: False
|
||||
mAP @ 50:95: 0.2409
|
||||
mAP @ 50: 0.3591
|
||||
mAP @ 75: 0.2915
|
||||
mAP scores: [0.35909 0.3468 0.34556 ...]
|
||||
IoU thresh: [0.5 0.55 0.6 ...]
|
||||
AP per class:
|
||||
0: [0.35909 0.3468 0.34556 ...]
|
||||
...
|
||||
Small objects: ...
|
||||
Medium objects: ...
|
||||
Large objects: ...
|
||||
```
|
||||
|
||||
You can also plot the results:
|
||||
|
||||
```python
|
||||
map_result.plot()
|
||||
```
|
||||
|
||||

|
||||
|
||||
The metric also breaks down the results by detected object area. Small, medium and large are simply those with area less than 32², between 32² and 96², and greater than 96² pixels respectively.
|
||||
|
||||
### F1 Score
|
||||
|
||||
The [F1 Score](https://supervision.roboflow.com/latest/metrics/f1_score/) is another useful metric, especially when dealing with an imbalance between false positives and false negatives. It’s the harmonic mean of **precision** (how many predictions are correct) and **recall** (how many actual instances were detected).
|
||||
|
||||
Here's how you can compute the F1 score:
|
||||
|
||||
```python
|
||||
from supervision.metrics import F1Score, MetricTarget
|
||||
|
||||
f1_metric = F1Score(metric_target=MetricTarget.MASKS)
|
||||
f1_result = f1_metric.update(predictions_list, targets_list).compute()
|
||||
```
|
||||
|
||||
As with mAP, you can also print the result:
|
||||
|
||||
```python
|
||||
print(f1_result)
|
||||
```
|
||||
|
||||
```
|
||||
F1ScoreResult:
|
||||
Metric target: MetricTarget.MASKS
|
||||
Averaging method: AveragingMethod.WEIGHTED
|
||||
F1 @ 50: 0.5341
|
||||
F1 @ 75: 0.4636
|
||||
F1 @ thresh: [0.53406 0.5278 0.52153 ...]
|
||||
IoU thresh: [0.5 0.55 0.6 ...]
|
||||
F1 per class:
|
||||
0: [0.53406 0.5278 0.52153 ...]
|
||||
...
|
||||
Small objects: ...
|
||||
Medium objects: ...
|
||||
Large objects: ...
|
||||
```
|
||||
|
||||
Similarly, you can plot the results:
|
||||
|
||||
```python
|
||||
f1_result.plot()
|
||||
```
|
||||
|
||||

|
||||
|
||||
As with mAP, the metric also breaks down the results by detected object area. Small, medium and large are simply those with area less than 32², between 32² and 96², and greater than 96² pixels respectively.
|
||||
|
||||
## Model Leaderboard
|
||||
|
||||
Here to compare the basic models? We've got you covered. Check out our [Model Leaderboard](https://leaderboard.roboflow.com/) to see how different models perform and to get a sense of the state-of-the-art results. It's a great place to understand what the leading models can achieve and to compare your own results.
|
||||
|
||||
Even better, the repository is open source! You can see how the models were benchmarked, run the evaluation yourself, and even add your own models to the leaderboard. Check it out on [GitHub](https://github.com/roboflow/model-leaderboard)!
|
||||
|
||||

|
||||
|
||||
## Conclusion
|
||||
|
||||
In this guide, you've learned how to set up your environment, train or use pre-trained models, visualize predictions, and evaluate model performance with metrics like [mAP](https://supervision.roboflow.com/latest/metrics/mean_average_precision/), [F1 score](https://supervision.roboflow.com/latest/metrics/f1_score/), and got to know our Model Leaderboard.
|
||||
|
||||
A condensed version of this guide is also available as a [Colab Notebook](https://colab.research.google.com/drive/1HoOY9pZoVwGiRMmLHtir0qT6Uj45w6Ps?usp=sharing).
|
||||
|
||||
For more details, be sure to check out our [documentation](https://supervision.roboflow.com/latest/) and join our community discussions. If you find any issues, please let us know on [GitHub](https://github.com/roboflow/supervision/issues).
|
||||
|
||||
Best of luck with your benchmarking!
|
||||
+292
-51
@@ -1,5 +1,6 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Track Objects
|
||||
@@ -13,6 +14,8 @@ take you through the steps to perform inference using the YOLOv8 model via eithe
|
||||
you'll discover how to track these objects efficiently and annotate your video content
|
||||
for a deeper analysis.
|
||||
|
||||
## Object Detection & Segmentation
|
||||
|
||||
To make it easier for you to follow our tutorial download the video we will use as an
|
||||
example. You can do this using
|
||||
[`supervision[assets]`](/latest/assets/) extension.
|
||||
@@ -21,14 +24,13 @@ example. You can do this using
|
||||
from supervision.assets import download_assets, VideoAssets
|
||||
|
||||
download_assets(VideoAssets.PEOPLE_WALKING)
|
||||
download_assets(VideoAssets.SKIING)
|
||||
```
|
||||
|
||||
<video controls>
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/people-walking.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
## Run Inference
|
||||
### Run Inference
|
||||
|
||||
First, you'll need to obtain predictions from your object detection or segmentation
|
||||
model. In this tutorial, we are using the YOLOv8 model as an example. However,
|
||||
@@ -41,6 +43,10 @@ by obtaining model predictions and then annotating the frame based on these pred
|
||||
This `callback` function will be essential in the subsequent steps of the tutorial, as
|
||||
it will be modified to include tracking, labeling, and trace annotations.
|
||||
|
||||
!!! tip
|
||||
|
||||
Both object detection and segmentation models are supported. Try it with `yolov8n.pt` or `yolov8n-640-seg`!
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py }
|
||||
@@ -49,7 +55,7 @@ it will be modified to include tracking, labeling, and trace annotations.
|
||||
from ultralytics import YOLO
|
||||
|
||||
model = YOLO("yolov8n.pt")
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model(frame)[0]
|
||||
@@ -71,7 +77,7 @@ it will be modified to include tracking, labeling, and trace annotations.
|
||||
from inference.models.utils import get_roboflow_model
|
||||
|
||||
model = get_roboflow_model(model_id="yolov8n-640", api_key=<ROBOFLOW API KEY>)
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model.infer(frame)[0]
|
||||
@@ -89,7 +95,7 @@ it will be modified to include tracking, labeling, and trace annotations.
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/run-inference.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
## Tracking
|
||||
### Tracking
|
||||
|
||||
After running inference and obtaining predictions, the next step is to track the
|
||||
detected objects throughout the video. Utilizing Supervision’s
|
||||
@@ -106,7 +112,7 @@ enabling the continuous following of the object's motion path across different f
|
||||
|
||||
model = YOLO("yolov8n.pt")
|
||||
tracker = sv.ByteTrack()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model(frame)[0]
|
||||
@@ -130,7 +136,7 @@ enabling the continuous following of the object's motion path across different f
|
||||
|
||||
model = get_roboflow_model(model_id="yolov8n-640", api_key=<ROBOFLOW API KEY>)
|
||||
tracker = sv.ByteTrack()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model.infer(frame)[0]
|
||||
@@ -145,7 +151,7 @@ enabling the continuous following of the object's motion path across different f
|
||||
)
|
||||
```
|
||||
|
||||
## Annotate Video with Tracking IDs
|
||||
### Annotate Video with Tracking IDs
|
||||
|
||||
Annotating the video with tracking IDs helps in distinguishing and following each object
|
||||
distinctly. With the
|
||||
@@ -162,7 +168,7 @@ offering a clear visual representation of each object's class and unique identif
|
||||
|
||||
model = YOLO("yolov8n.pt")
|
||||
tracker = sv.ByteTrack()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
@@ -197,7 +203,7 @@ offering a clear visual representation of each object's class and unique identif
|
||||
|
||||
model = get_roboflow_model(model_id="yolov8n-640", api_key=<ROBOFLOW API KEY>)
|
||||
tracker = sv.ByteTrack()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
@@ -227,7 +233,7 @@ offering a clear visual representation of each object's class and unique identif
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/annotate-video-with-tracking-ids.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
## Annotate Video with Traces
|
||||
### Annotate Video with Traces
|
||||
|
||||
Adding traces to the video involves overlaying the historical paths of the detected
|
||||
objects. This feature, powered by the
|
||||
@@ -244,7 +250,7 @@ movement patterns and interactions between objects in the video.
|
||||
|
||||
model = YOLO("yolov8n.pt")
|
||||
tracker = sv.ByteTrack()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
trace_annotator = sv.TraceAnnotator()
|
||||
|
||||
@@ -282,7 +288,7 @@ movement patterns and interactions between objects in the video.
|
||||
|
||||
model = get_roboflow_model(model_id="yolov8n-640", api_key=<ROBOFLOW API KEY>)
|
||||
tracker = sv.ByteTrack()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
trace_annotator = sv.TraceAnnotator()
|
||||
|
||||
@@ -315,9 +321,96 @@ movement patterns and interactions between objects in the video.
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/annotate-video-with-traces.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
## Tracking Key Points
|
||||
## Keypoints
|
||||
|
||||
Keypoint tracking is currently supported via the conversion of `KeyPoints` to `Detections`. This is achieved with the [`keypoints_to_detections`](/latest/utils/datatypes/#supervision.utils.datatypes.keypoints_to_detections) function. We'll use a different video as well as [`DetectionsSmoother`](/latest/detection/tools/smoother/) to stabilize the boxes.
|
||||
Models aren't limited to object detection and segmentation. Keypoint detection allows for detailed analysis of body joints and connections, especially valuable for applications like human pose estimation. This section introduces keypoint tracking. We'll walk through the steps of annotating keypoints, converting them into bounding box detections compatible with `ByteTrack`, and applying detection smoothing for enhanced stability.
|
||||
|
||||
To make it easier for you to follow our tutorial, let's download the video we will use as an
|
||||
example. You can do this using [`supervision[assets]`](/latest/assets/) extension.
|
||||
|
||||
```python
|
||||
from supervision.assets import download_assets, VideoAssets
|
||||
|
||||
download_assets(VideoAssets.SKIING)
|
||||
```
|
||||
|
||||
<video controls muted>
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/skiing-hd.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
### Keypoint Detection
|
||||
|
||||
First, you'll need to obtain predictions from your keypoint detection model. In this tutorial, we are using the YOLOv8 model as an example. However,
|
||||
Supervision is versatile and compatible with various models. Check this [link](/latest/keypoint/core/) for guidance on how to plug in other models.
|
||||
|
||||
We will define a `callback` function, which will process each frame of the video by obtaining model predictions and then annotating the frame based on these predictions.
|
||||
|
||||
Let's immediately visualize the results with our [`EdgeAnnotator`](/latest/keypoint/annotators/#supervision.keypoint.annotators.EdgeAnnotator) and [`VertexAnnotator`](https://supervision.roboflow.com/latest/keypoint/annotators/#supervision.keypoint.annotators.VertexAnnotator).
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="5 10-11" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
|
||||
model = YOLO("yolov8m-pose.pt")
|
||||
edge_annotator = sv.EdgeAnnotator()
|
||||
vertex_annotator = sv.VertexAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model(frame)[0]
|
||||
key_points = sv.KeyPoints.from_ultralytics(results)
|
||||
|
||||
annotated_frame = edge_annotator.annotate(
|
||||
frame.copy(), key_points=key_points)
|
||||
return vertex_annotator.annotate(
|
||||
annotated_frame, key_points=key_points)
|
||||
|
||||
sv.process_video(
|
||||
source_path="skiing.mp4",
|
||||
target_path="result.mp4",
|
||||
callback=callback
|
||||
)
|
||||
```
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="5-6 11-12" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from inference.models.utils import get_roboflow_model
|
||||
|
||||
model = get_roboflow_model(
|
||||
model_id="yolov8m-pose-640", api_key=<ROBOFLOW API KEY>)
|
||||
edge_annotator = sv.EdgeAnnotator()
|
||||
vertex_annotator = sv.VertexAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model.infer(frame)[0]
|
||||
key_points = sv.KeyPoints.from_inference(results)
|
||||
|
||||
annotated_frame = edge_annotator.annotate(
|
||||
frame.copy(), key_points=key_points)
|
||||
return vertex_annotator.annotate(
|
||||
annotated_frame, key_points=key_points)
|
||||
|
||||
sv.process_video(
|
||||
source_path="skiing.mp4",
|
||||
target_path="result.mp4",
|
||||
callback=callback
|
||||
)
|
||||
```
|
||||
|
||||
<video controls>
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/track-keypoints-only-keypoints.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
### Convert to Detections
|
||||
|
||||
Keypoint tracking is currently supported via the conversion of `KeyPoints` to `Detections`. This is achieved with the [`KeyPoints.as_detections()`](/latest/keypoint/core/#supervision.keypoint.core.KeyPoints.as_detections) function.
|
||||
|
||||
Let's convert to detections and visualize the results with our [`BoxAnnotator`](/latest/detection/annotators/#supervision.annotators.core.BoxAnnotator).
|
||||
|
||||
!!! tip
|
||||
|
||||
@@ -325,35 +418,102 @@ Keypoint tracking is currently supported via the conversion of `KeyPoints` to `D
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="5 7 14-15 17 33" }
|
||||
```{ .py hl_lines="8 13 19-20" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
|
||||
model = YOLO("yolov8m-pose.pt")
|
||||
edge_annotator = sv.EdgeAnnotator()
|
||||
vertex_annotator = sv.VertexAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model(frame)[0]
|
||||
key_points = sv.KeyPoints.from_ultralytics(results)
|
||||
detections = key_points.as_detections()
|
||||
|
||||
annotated_frame = edge_annotator.annotate(
|
||||
frame.copy(), key_points=key_points)
|
||||
annotated_frame = vertex_annotator.annotate(
|
||||
annotated_frame, key_points=key_points)
|
||||
return box_annotator.annotate(
|
||||
annotated_frame, detections=detections)
|
||||
|
||||
sv.process_video(
|
||||
source_path="skiing.mp4",
|
||||
target_path="result.mp4",
|
||||
callback=callback
|
||||
)
|
||||
```
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="9 14 20-21" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from inference.models.utils import get_roboflow_model
|
||||
|
||||
model = get_roboflow_model(
|
||||
model_id="yolov8m-pose-640", api_key=<ROBOFLOW API KEY>)
|
||||
edge_annotator = sv.EdgeAnnotator()
|
||||
vertex_annotator = sv.VertexAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model.infer(frame)[0]
|
||||
key_points = sv.KeyPoints.from_inference(results)
|
||||
detections = key_points.as_detections()
|
||||
|
||||
annotated_frame = edge_annotator.annotate(
|
||||
frame.copy(), key_points=key_points)
|
||||
annotated_frame = vertex_annotator.annotate(
|
||||
annotated_frame, key_points=key_points)
|
||||
return box_annotator.annotate(
|
||||
annotated_frame, detections=detections)
|
||||
|
||||
sv.process_video(
|
||||
source_path="skiing.mp4",
|
||||
target_path="result.mp4",
|
||||
callback=callback
|
||||
)
|
||||
```
|
||||
|
||||
<video controls>
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/track-keypoints-converted-to-detections.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
### Keypoint Tracking
|
||||
|
||||
Now that we have a `Detections` object, we can track it throughout the video. Utilizing Supervision’s [`sv.ByteTrack`](/latest/trackers/#supervision.tracker.byte_tracker.core.ByteTrack) functionality, each detected object is assigned a unique tracker ID, enabling the continuous following of the object's motion path across different frames. We shall visualize the result with `TraceAnnotator`.
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="10-11 17 25-26" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
|
||||
model = YOLO("yolov8m-pose.pt")
|
||||
edge_annotator = sv.EdgeAnnotator()
|
||||
vertex_annotator = sv.VertexAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
tracker = sv.ByteTrack()
|
||||
smoother = sv.DetectionsSmoother()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
trace_annotator = sv.TraceAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model(frame)[0]
|
||||
keypoints = sv.KeyPoints.from_ultralytics(results)
|
||||
detections = sv.keypoints_to_detections(keypoints)
|
||||
key_points = sv.KeyPoints.from_ultralytics(results)
|
||||
detections = key_points.as_detections()
|
||||
detections = tracker.update_with_detections(detections)
|
||||
detections = smoother.update_with_detections(detections)
|
||||
|
||||
labels = [
|
||||
f"#{tracker_id} {results.names[class_id]}"
|
||||
for class_id, tracker_id
|
||||
in zip(detections.class_id, detections.tracker_id)
|
||||
]
|
||||
|
||||
annotated_frame = edge_annotator.annotate(
|
||||
frame.copy(), key_points=key_points)
|
||||
annotated_frame = vertex_annotator.annotate(
|
||||
annotated_frame, key_points=key_points)
|
||||
annotated_frame = box_annotator.annotate(
|
||||
frame.copy(), detections=detections)
|
||||
annotated_frame = label_annotator.annotate(
|
||||
annotated_frame, detections=detections, labels=labels)
|
||||
annotated_frame, detections=detections)
|
||||
return trace_annotator.annotate(
|
||||
annotated_frame, detections=detections)
|
||||
|
||||
@@ -366,36 +526,32 @@ Keypoint tracking is currently supported via the conversion of `KeyPoints` to `D
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="5-6 8 15-16 18 34" }
|
||||
```{ .py hl_lines="11-12 18 26-27" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from inference.models.utils import get_roboflow_model
|
||||
|
||||
model = get_roboflow_model(
|
||||
model_id="yolov8m-pose-640", api_key=<ROBOFLOW API KEY>)
|
||||
edge_annotator = sv.EdgeAnnotator()
|
||||
vertex_annotator = sv.VertexAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
tracker = sv.ByteTrack()
|
||||
smoother = sv.DetectionsSmoother()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
trace_annotator = sv.TraceAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model.infer(frame)[0]
|
||||
keypoints = sv.KeyPoints.from_inference(results)
|
||||
detections = sv.keypoints_to_detections(keypoints)
|
||||
key_points = sv.KeyPoints.from_inference(results)
|
||||
detections = key_points.as_detections()
|
||||
detections = tracker.update_with_detections(detections)
|
||||
detections = smoother.update_with_detections(detections)
|
||||
|
||||
labels = [
|
||||
f"#{tracker_id} {results.names[class_id]}"
|
||||
for class_id, tracker_id
|
||||
in zip(detections.class_id, detections.tracker_id)
|
||||
]
|
||||
|
||||
annotated_frame = edge_annotator.annotate(
|
||||
frame.copy(), key_points=key_points)
|
||||
annotated_frame = vertex_annotator.annotate(
|
||||
annotated_frame, key_points=key_points)
|
||||
annotated_frame = box_annotator.annotate(
|
||||
frame.copy(), detections=detections)
|
||||
annotated_frame = label_annotator.annotate(
|
||||
annotated_frame, detections=detections, labels=labels)
|
||||
annotated_frame, detections=detections)
|
||||
return trace_annotator.annotate(
|
||||
annotated_frame, detections=detections)
|
||||
|
||||
@@ -407,9 +563,94 @@ Keypoint tracking is currently supported via the conversion of `KeyPoints` to `D
|
||||
```
|
||||
|
||||
<video controls>
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/track-keypoints.mp4" type="video/mp4">
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/track-keypoints-with-tracking.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
This structured walkthrough should give a detailed pathway to annotate videos
|
||||
effectively using Supervision’s various functionalities, including object tracking and
|
||||
trace annotations.
|
||||
### Bonus: Smoothing
|
||||
|
||||
We could stop here as we have successfully tracked the object detected by the keypoint model. However, we can further enhance the stability of the boxes by applying [`DetectionsSmoother`](/latest/detection/tools/smoother/). This tool helps in stabilizing the boxes by smoothing the bounding box coordinates across frames. It is very simple to use:
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="11 19" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
|
||||
model = YOLO("yolov8m-pose.pt")
|
||||
edge_annotator = sv.EdgeAnnotator()
|
||||
vertex_annotator = sv.VertexAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
tracker = sv.ByteTrack()
|
||||
smoother = sv.DetectionsSmoother()
|
||||
trace_annotator = sv.TraceAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model(frame)[0]
|
||||
key_points = sv.KeyPoints.from_ultralytics(results)
|
||||
detections = key_points.as_detections()
|
||||
detections = tracker.update_with_detections(detections)
|
||||
detections = smoother.update_with_detections(detections)
|
||||
|
||||
annotated_frame = edge_annotator.annotate(
|
||||
frame.copy(), key_points=key_points)
|
||||
annotated_frame = vertex_annotator.annotate(
|
||||
annotated_frame, key_points=key_points)
|
||||
annotated_frame = box_annotator.annotate(
|
||||
annotated_frame, detections=detections)
|
||||
return trace_annotator.annotate(
|
||||
annotated_frame, detections=detections)
|
||||
|
||||
sv.process_video(
|
||||
source_path="skiing.mp4",
|
||||
target_path="result.mp4",
|
||||
callback=callback
|
||||
)
|
||||
```
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="12 20" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from inference.models.utils import get_roboflow_model
|
||||
|
||||
model = get_roboflow_model(
|
||||
model_id="yolov8m-pose-640", api_key=<ROBOFLOW API KEY>)
|
||||
edge_annotator = sv.EdgeAnnotator()
|
||||
vertex_annotator = sv.VertexAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
tracker = sv.ByteTrack()
|
||||
smoother = sv.DetectionsSmoother()
|
||||
trace_annotator = sv.TraceAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, _: int) -> np.ndarray:
|
||||
results = model.infer(frame)[0]
|
||||
key_points = sv.KeyPoints.from_inference(results)
|
||||
detections = key_points.as_detections()
|
||||
detections = tracker.update_with_detections(detections)
|
||||
detections = smoother.update_with_detections(detections)
|
||||
|
||||
annotated_frame = edge_annotator.annotate(
|
||||
frame.copy(), key_points=key_points)
|
||||
annotated_frame = vertex_annotator.annotate(
|
||||
annotated_frame, key_points=key_points)
|
||||
annotated_frame = box_annotator.annotate(
|
||||
annotated_frame, detections=detections)
|
||||
return trace_annotator.annotate(
|
||||
annotated_frame, detections=detections)
|
||||
|
||||
sv.process_video(
|
||||
source_path="skiing.mp4",
|
||||
target_path="result.mp4",
|
||||
callback=callback
|
||||
)
|
||||
```
|
||||
|
||||
<video controls>
|
||||
<source src="https://media.roboflow.com/supervision/video-examples/how-to/track-objects/track-keypoints-with-smoothing.mp4" type="video/mp4">
|
||||
</video>
|
||||
|
||||
This structured walkthrough should give a detailed pathway to annotate videos effectively using Supervision’s various functionalities, including object tracking and trace annotations.
|
||||
|
||||
+47
-8
@@ -33,8 +33,9 @@ We write your reusable computer vision tools. Whether you need to load your data
|
||||
You can install `supervision` in a
|
||||
[**Python>=3.8**](https://www.python.org/) environment.
|
||||
|
||||
!!! example "pip install (recommended)"
|
||||
=== "pip"
|
||||
!!! example "Installation"
|
||||
|
||||
=== "pip (recommended)"
|
||||
[](https://badge.fury.io/py/supervision)
|
||||
[](https://pypistats.org/packages/supervision)
|
||||
[](https://github.com/roboflow/supervision/blob/main/LICENSE.md)
|
||||
@@ -44,6 +45,43 @@ You can install `supervision` in a
|
||||
pip install supervision
|
||||
```
|
||||
|
||||
=== "poetry"
|
||||
[](https://badge.fury.io/py/supervision)
|
||||
[](https://pypistats.org/packages/supervision)
|
||||
[](https://github.com/roboflow/supervision/blob/main/LICENSE.md)
|
||||
[](https://badge.fury.io/py/supervision)
|
||||
|
||||
```bash
|
||||
poetry add supervision
|
||||
```
|
||||
|
||||
=== "uv"
|
||||
[](https://badge.fury.io/py/supervision)
|
||||
[](https://pypistats.org/packages/supervision)
|
||||
[](https://github.com/roboflow/supervision/blob/main/LICENSE.md)
|
||||
[](https://badge.fury.io/py/supervision)
|
||||
|
||||
```bash
|
||||
uv pip install supervision
|
||||
```
|
||||
|
||||
For uv projects:
|
||||
|
||||
```bash
|
||||
uv add supervision
|
||||
```
|
||||
|
||||
=== "rye"
|
||||
[](https://badge.fury.io/py/supervision)
|
||||
[](https://pypistats.org/packages/supervision)
|
||||
[](https://github.com/roboflow/supervision/blob/main/LICENSE.md)
|
||||
[](https://badge.fury.io/py/supervision)
|
||||
|
||||
```bash
|
||||
rye add supervision
|
||||
```
|
||||
|
||||
|
||||
!!! example "conda/mamba install"
|
||||
=== "conda"
|
||||
[](https://anaconda.org/conda-forge/supervision) [](https://anaconda.org/conda-forge/supervision) [](https://anaconda.org/conda-forge/supervision) [](https://anaconda.org/conda-forge/supervision)
|
||||
@@ -63,7 +101,7 @@ You can install `supervision` in a
|
||||
=== "virtualenv"
|
||||
```bash
|
||||
# clone repository and navigate to root directory
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision
|
||||
|
||||
# setup python environment and activate it
|
||||
@@ -75,18 +113,19 @@ You can install `supervision` in a
|
||||
pip install -e "."
|
||||
```
|
||||
|
||||
=== "poetry"
|
||||
=== "uv"
|
||||
```bash
|
||||
# clone repository and navigate to root directory
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision
|
||||
|
||||
# setup python environment and activate it
|
||||
poetry env use python3.10
|
||||
poetry shell
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
|
||||
# installation
|
||||
poetry install
|
||||
uv pip install -r pyproject.toml -e . --all-extras
|
||||
|
||||
```
|
||||
|
||||
## 🚀 Quickstart
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
!function(){var i="analytics",analytics=window[i]=window[i]||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","screen","once","off","on","addSourceMiddleware","addIntegrationMiddleware","setAnonymousId","addDestinationMiddleware","register"];analytics.factory=function(e){return function(){if(window[i].initialized)return window[i][e].apply(window[i],arguments);var n=Array.prototype.slice.call(arguments);if(["track","screen","alias","group","page","identify"].indexOf(e)>-1){var c=document.querySelector("link[rel='canonical']");n.push({__t:"bpc",c:c&&c.getAttribute("href")||void 0,p:location.pathname,u:location.href,s:location.search,t:document.title,r:document.referrer})}n.unshift(e);analytics.push(n);return analytics}};for(var n=0;n<analytics.methods.length;n++){var key=analytics.methods[n];analytics[key]=analytics.factory(key)}analytics.load=function(key,n){var t=document.createElement("script");t.type="text/javascript";t.async=!0;t.setAttribute("data-global-segment-analytics-key",i);t.src="https://cdn.segment.com/analytics.js/v1/" + key + "/analytics.min.js";var r=document.getElementsByTagName("script")[0];r.parentNode.insertBefore(t,r);analytics._loadOptions=n};analytics._writeKey="rMvrPeZBJYyOPJSGCNhMlnTJb8VhFiWU";;analytics.SNIPPET_VERSION="5.2.0";
|
||||
analytics.load("eohFog7VZiAhGJGEr5Sh7BM1mFKmUvDC");
|
||||
document$.subscribe(analytics.page);
|
||||
}}();
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Annotators
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Keypoint Detection
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Mean Average Recall
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.metrics.mean_average_recall.MeanAverageRecall">MeanAverageRecall</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.metrics.mean_average_recall.MeanAverageRecall
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.metrics.mean_average_recall.MeanAverageRecallResult">MeanAverageRecallResult</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.metrics.mean_average_recall.MeanAverageRecallResult
|
||||
+1
-1
@@ -130,7 +130,7 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install -q ultralytics supervision==0.18.0"
|
||||
"!pip install -q \"ultralytics<=8.3.40\" \"supervision==0.18.0\""
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
Vendored
+1
-1
@@ -300,7 +300,7 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install -q ultralytics"
|
||||
"!pip install -q \"ultralytics<=8.3.40\""
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# ByteTrack
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Data Types Utils
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.utils.datatypes.keypoints_to_detections">keypoints_to_detections</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.utils.datatypes.keypoints_to_detections
|
||||
@@ -17,7 +17,7 @@ https://github.com/roboflow/supervision/assets/26109316/f84db7b5-79e2-4142-a1da-
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/count_people_in_zone
|
||||
```
|
||||
|
||||
|
||||
@@ -35,9 +35,7 @@ def load_zones_config(file_path: str) -> List[np.ndarray]:
|
||||
|
||||
def initiate_annotators(
|
||||
polygons: List[np.ndarray], resolution_wh: Tuple[int, int]
|
||||
) -> Tuple[
|
||||
List[sv.PolygonZone], List[sv.PolygonZoneAnnotator], List[sv.BoundingBoxAnnotator]
|
||||
]:
|
||||
) -> Tuple[List[sv.PolygonZone], List[sv.PolygonZoneAnnotator], List[sv.BoxAnnotator]]:
|
||||
line_thickness = sv.calculate_optimal_line_thickness(resolution_wh=resolution_wh)
|
||||
text_scale = sv.calculate_optimal_text_scale(resolution_wh=resolution_wh)
|
||||
|
||||
@@ -54,7 +52,7 @@ def initiate_annotators(
|
||||
text_thickness=line_thickness * 2,
|
||||
text_scale=text_scale * 2,
|
||||
)
|
||||
box_annotator = sv.BoundingBoxAnnotator(
|
||||
box_annotator = sv.BoxAnnotator(
|
||||
color=COLORS.by_idx(index), thickness=line_thickness
|
||||
)
|
||||
zones.append(zone)
|
||||
@@ -97,7 +95,7 @@ def annotate(
|
||||
frame: np.ndarray,
|
||||
zones: List[sv.PolygonZone],
|
||||
zone_annotators: List[sv.PolygonZoneAnnotator],
|
||||
box_annotators: List[sv.BoundingBoxAnnotator],
|
||||
box_annotators: List[sv.BoxAnnotator],
|
||||
detections: sv.Detections,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
@@ -108,7 +106,7 @@ def annotate(
|
||||
zones (List[sv.PolygonZone]): A list of polygon zones used for detection.
|
||||
zone_annotators (List[sv.PolygonZoneAnnotator]): A list of annotators for
|
||||
drawing zone annotations.
|
||||
box_annotators (List[sv.BoundingBoxAnnotator]): A list of annotators for
|
||||
box_annotators (List[sv.BoxAnnotator]): A list of annotators for
|
||||
drawing box annotations.
|
||||
detections (sv.Detections): Detections to be used for annotation.
|
||||
|
||||
|
||||
@@ -2,4 +2,4 @@ gdown
|
||||
inference==0.9.17
|
||||
supervision>=0.20.0
|
||||
tqdm
|
||||
ultralytics
|
||||
ultralytics<=8.3.40
|
||||
|
||||
@@ -33,9 +33,7 @@ def load_zones_config(file_path: str) -> List[np.ndarray]:
|
||||
|
||||
def initiate_annotators(
|
||||
polygons: List[np.ndarray], resolution_wh: Tuple[int, int]
|
||||
) -> Tuple[
|
||||
List[sv.PolygonZone], List[sv.PolygonZoneAnnotator], List[sv.BoundingBoxAnnotator]
|
||||
]:
|
||||
) -> Tuple[List[sv.PolygonZone], List[sv.PolygonZoneAnnotator], List[sv.BoxAnnotator]]:
|
||||
line_thickness = sv.calculate_optimal_line_thickness(resolution_wh=resolution_wh)
|
||||
text_scale = sv.calculate_optimal_text_scale(resolution_wh=resolution_wh)
|
||||
|
||||
@@ -52,7 +50,7 @@ def initiate_annotators(
|
||||
text_thickness=line_thickness * 2,
|
||||
text_scale=text_scale * 2,
|
||||
)
|
||||
box_annotator = sv.BoundingBoxAnnotator(
|
||||
box_annotator = sv.BoxAnnotator(
|
||||
color=COLORS.by_idx(index), thickness=line_thickness
|
||||
)
|
||||
zones.append(zone)
|
||||
@@ -94,7 +92,7 @@ def annotate(
|
||||
frame: np.ndarray,
|
||||
zones: List[sv.PolygonZone],
|
||||
zone_annotators: List[sv.PolygonZoneAnnotator],
|
||||
box_annotators: List[sv.BoundingBoxAnnotator],
|
||||
box_annotators: List[sv.BoxAnnotator],
|
||||
detections: sv.Detections,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
@@ -105,7 +103,7 @@ def annotate(
|
||||
zones (List[sv.PolygonZone]): A list of polygon zones used for detection.
|
||||
zone_annotators (List[sv.PolygonZoneAnnotator]): A list of annotators for
|
||||
drawing zone annotations.
|
||||
box_annotators (List[sv.BoundingBoxAnnotator]): A list of annotators for
|
||||
box_annotators (List[sv.BoxAnnotator]): A list of annotators for
|
||||
drawing box annotations.
|
||||
detections (sv.Detections): Detections to be used for annotation.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ supervision package for multiple tasks such as drawing heatmap annotations, trac
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/heatmap_and_track
|
||||
```
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
supervision[assets]==0.19.0
|
||||
ultralytics
|
||||
ultralytics<=8.3.40
|
||||
|
||||
@@ -22,7 +22,7 @@ https://github.com/roboflow/supervision/assets/26109316/d50118c1-2ae4-458d-915a-
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/speed_estimation
|
||||
```
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ https://github.com/roboflow/supervision/assets/26109316/d051cc8a-dd15-41d4-aa36-
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/time_in_zone
|
||||
```
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
opencv-python
|
||||
supervision>=0.20.0
|
||||
ultralytics
|
||||
ultralytics<=8.3.40
|
||||
inference==0.9.17
|
||||
pytube
|
||||
|
||||
@@ -10,7 +10,7 @@ detection and Supervision for tracking and annotation.
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/tracking
|
||||
```
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ def process_video(
|
||||
model = get_roboflow_model(model_id=model_id, api_key=roboflow_api_key)
|
||||
|
||||
tracker = sv.ByteTrack()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
frame_generator = sv.get_video_frames_generator(source_path=source_video_path)
|
||||
video_info = sv.VideoInfo.from_video_path(video_path=source_video_path)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
inference==0.9.17
|
||||
supervision==0.19.0
|
||||
tqdm
|
||||
ultralytics
|
||||
ultralytics<=8.3.40
|
||||
|
||||
@@ -16,7 +16,7 @@ def process_video(
|
||||
model = YOLO(source_weights_path)
|
||||
|
||||
tracker = sv.ByteTrack()
|
||||
box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
frame_generator = sv.get_video_frames_generator(source_path=source_video_path)
|
||||
video_info = sv.VideoInfo.from_video_path(video_path=source_video_path)
|
||||
|
||||
@@ -13,7 +13,7 @@ https://github.com/roboflow/supervision/assets/26109316/c9436828-9fbf-4c25-ae8c-
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/traffic_analysis
|
||||
```
|
||||
|
||||
|
||||
@@ -2,4 +2,4 @@ gdown
|
||||
inference==0.9.17
|
||||
supervision>=0.20.0
|
||||
tqdm
|
||||
ultralytics
|
||||
ultralytics<=8.3.40
|
||||
|
||||
+9
-3
@@ -42,6 +42,7 @@ nav:
|
||||
- Detect Small Objects: how_to/detect_small_objects.md
|
||||
- Track Objects on Video: how_to/track_objects.md
|
||||
- Process Datasets: how_to/process_datasets.md
|
||||
- Benchmark a Model: how_to/benchmark_a_model.md
|
||||
|
||||
- Reference - Code API:
|
||||
- Detection and Segmentation:
|
||||
@@ -66,6 +67,7 @@ nav:
|
||||
- Utils: datasets/utils.md
|
||||
- Metrics:
|
||||
- mAP: metrics/mean_average_precision.md
|
||||
- mAR: metrics/mean_average_recall.md
|
||||
- Precision: metrics/precision.md
|
||||
- Recall: metrics/recall.md
|
||||
- F1 Score: metrics/f1_score.md
|
||||
@@ -79,7 +81,6 @@ nav:
|
||||
- File: utils/file.md
|
||||
- Draw: utils/draw.md
|
||||
- Geometry: utils/geometry.md
|
||||
- Datatypes: utils/datatypes.md
|
||||
- Assets: assets.md
|
||||
- Cookbooks: cookbooks.md
|
||||
- Cheatsheet: https://roboflow.github.io/cheatsheet-supervision/
|
||||
@@ -148,9 +149,12 @@ plugins:
|
||||
group_by_category: true
|
||||
docstring_style: google
|
||||
show_symbol_type_heading: true
|
||||
show_root_heading: True
|
||||
show_symbol_type_toc: true
|
||||
show_category_heading: true
|
||||
domains: [std, py]
|
||||
inventories:
|
||||
- url: https://docs.python-requests.org/en/master/objects.inv
|
||||
domains: [std, py]
|
||||
- git-committers:
|
||||
repository: roboflow/supervision
|
||||
branch: develop
|
||||
@@ -175,11 +179,13 @@ markdown_extensions:
|
||||
check_paths: true
|
||||
- pymdownx.highlight:
|
||||
anchor_linenums: true
|
||||
line_spans: __span
|
||||
pygments_lang_class: true
|
||||
|
||||
extra_javascript:
|
||||
- "https://widget.kapa.ai/kapa-widget.bundle.js"
|
||||
- "javascripts/init_kapa_widget.js"
|
||||
- "javascripts/cookbooks-card.js"
|
||||
- "javascripts/segment.js"
|
||||
- "https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.8/purify.min.js"
|
||||
|
||||
# Messages shown during document build
|
||||
|
||||
Generated
-4784
File diff suppressed because it is too large
Load Diff
+58
-93
@@ -1,18 +1,16 @@
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "supervision"
|
||||
version = "0.25.0rc2"
|
||||
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>",
|
||||
"Linas Kondrackis <linas@roboflow.com>",
|
||||
]
|
||||
license = { text = "MIT" }
|
||||
version = "0.26.0rc5"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
packages = [{ include = "supervision" }, { include = "supervision/py.typed" }]
|
||||
homepage = "https://github.com/roboflow/supervision"
|
||||
repository = "https://github.com/roboflow/supervision"
|
||||
documentation = "https://supervision.roboflow.com/latest/"
|
||||
requires-python = ">=3.8"
|
||||
authors = [
|
||||
{ name = "Piotr Skalski", email = "piotr.skalski92@gmail.com" }
|
||||
]
|
||||
maintainers = [
|
||||
{ name = "Piotr Skalski", email = "piotr.skalski92@gmail.com" },
|
||||
]
|
||||
keywords = [
|
||||
"machine-learning",
|
||||
"deep-learning",
|
||||
@@ -22,7 +20,6 @@ keywords = [
|
||||
"AI",
|
||||
"Roboflow",
|
||||
]
|
||||
|
||||
classifiers = [
|
||||
'Development Status :: 4 - Beta',
|
||||
'Intended Audience :: Developers',
|
||||
@@ -40,95 +37,59 @@ classifiers = [
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Operating System :: MacOS',
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
numpy = [
|
||||
{ version = ">=1.21.2,<1.23.3", python = "<=3.10" },
|
||||
{ version = ">=1.23.3", python = ">3.10" },
|
||||
{ version = ">=2.1.0", python = ">=3.13" },
|
||||
]
|
||||
scipy = [
|
||||
{ version = "1.10.0", python = "<3.9" },
|
||||
{ version = "^1.10.0", python = ">=3.9" },
|
||||
{ version = ">=1.14.1", python = ">=3.13" },
|
||||
|
||||
dependencies = [
|
||||
"numpy>=1.21.2",
|
||||
"scipy>=1.10.0",
|
||||
"matplotlib>=3.6.0",
|
||||
"pyyaml>=5.3",
|
||||
"defusedxml>=0.7.1",
|
||||
"pillow>=9.4",
|
||||
"requests>=2.26.0",
|
||||
"tqdm>=4.62.3",
|
||||
"opencv-python>=4.5.5.64"
|
||||
]
|
||||
|
||||
# Matplotlib sub-dependency
|
||||
# The 'contourpy' package is required by Matplotlib for contour plotting.
|
||||
# We need to ensure compatibility with both Python 3.8 and Python 3.13.
|
||||
#
|
||||
# For Python 3.8 and above, we use version 1.0.7 or higher, as it is the lowest major version that supports Python 3.8.
|
||||
# For Python 3.13 and above, we use version 1.3.0 or higher, as it is the first version that explicitly supports Python 3.13.
|
||||
contourpy = [
|
||||
{ version = ">=1.0.7", python = ">=3.8" },
|
||||
{ version = ">=1.3.0", python = ">=3.13" },
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/roboflow/supervision"
|
||||
Repository = "https://github.com/roboflow/supervision"
|
||||
Documentation = "https://supervision.roboflow.com/latest/"
|
||||
|
||||
[project.optional-dependencies]
|
||||
metrics = [
|
||||
"pandas>=2.0.0",
|
||||
]
|
||||
|
||||
matplotlib = ">=3.6.0"
|
||||
pyyaml = ">=5.3"
|
||||
defusedxml = "^0.7.1"
|
||||
pillow = ">=9.4"
|
||||
requests = { version = ">=2.26.0,<=2.32.3", optional = true }
|
||||
tqdm = { version = ">=4.62.3,<=4.66.6", optional = true }
|
||||
# pandas: picked lowest major version that supports Python 3.8
|
||||
pandas = { version = ">=2.0.0", optional = true }
|
||||
pandas-stubs = { version = ">=2.0.0.230412", optional = true }
|
||||
opencv-python = ">=4.5.5.64"
|
||||
|
||||
[tool.poetry.extras]
|
||||
assets = ["requests", "tqdm"]
|
||||
metrics = ["pandas", "pandas-stubs"]
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
twine = "^5.1.1"
|
||||
pytest = ">=7.2.2,<9.0.0"
|
||||
wheel = ">=0.40,<0.45"
|
||||
build = ">=0.10,<1.3"
|
||||
ruff = ">=0.1.0"
|
||||
mypy = "^1.4.1"
|
||||
pre-commit = "^3.3.3"
|
||||
tox = "^4.11.4"
|
||||
notebook = ">=6.5.3,<8.0.0"
|
||||
ipywidgets = "^8.1.1"
|
||||
jupytext = "^1.16.1"
|
||||
nbconvert = "^7.14.2"
|
||||
docutils = [
|
||||
{ version = "^0.20.1", python = "<3.9" },
|
||||
{ version = "^0.21.1", python = ">=3.9" },
|
||||
dev = [
|
||||
"pytest>=7.2.2,<9.0.0",
|
||||
"tox>=4.11.4",
|
||||
"notebook>=6.5.3,<8.0.0",
|
||||
"ipywidgets>=8.1.1",
|
||||
"jupytext>=1.16.1",
|
||||
"nbconvert>=7.14.2",
|
||||
"docutils!=0.21"
|
||||
]
|
||||
docs = [
|
||||
"mkdocs-material[imaging]>=9.5.5",
|
||||
"mkdocstrings>=0.25.2,<0.30.0",
|
||||
"mkdocstrings-python>=1.10.9",
|
||||
"mike>=2.0.0",
|
||||
"mkdocs-jupyter>=0.24.3",
|
||||
"mkdocs-git-committers-plugin-2>=2.4.1; python_version >= '3.8' and python_version < '4'",
|
||||
"mkdocs-git-revision-date-localized-plugin>=1.2.4"
|
||||
]
|
||||
build = [
|
||||
"twine>=5.1.1,<7.0.0",
|
||||
"wheel>=0.40,<0.46",
|
||||
"build>=0.10,<1.3"
|
||||
]
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
mkdocs-material = { extras = ["imaging"], version = "^9.5.5" }
|
||||
mkdocstrings = ">=0.25.2,<0.27.0"
|
||||
mkdocstrings-python = "^1.10.9"
|
||||
mike = "^2.0.0"
|
||||
# For Documentation Development use Python 3.10 or above
|
||||
# Use Latest mkdocs-jupyter min 0.24.6 for Jupyter Notebook Theme support
|
||||
mkdocs-jupyter = "^0.24.3"
|
||||
mkdocs-git-committers-plugin-2 = "^2.4.1"
|
||||
mkdocs-git-revision-date-localized-plugin = "^1.2.4"
|
||||
|
||||
[tool.poetry.group.typecheck]
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.typecheck.dependencies]
|
||||
types-pyyaml = "^6.0.12.20240808"
|
||||
types-cffi = "^1.16.0.20240331"
|
||||
types-requests = "^2.32.0.20240712"
|
||||
types-tqdm = "^4.66.0.20240417"
|
||||
|
||||
|
||||
[tool.bandit]
|
||||
target = ["test", "supervision"]
|
||||
tests = ["B201", "B301", "B318", "B314", "B303", "B413", "B412", "B410"]
|
||||
tests = ["B201", "B301", "B318", "B314", "B303", "B413", "B412"]
|
||||
|
||||
[tool.autoflake]
|
||||
check = true
|
||||
imports = ["cv2", "supervision"]
|
||||
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py38"
|
||||
|
||||
@@ -252,7 +213,7 @@ skip-magic-trailing-comma = false
|
||||
line-ending = "auto"
|
||||
|
||||
[tool.codespell]
|
||||
skip = "*.ipynb,poetry.lock"
|
||||
skip = "*.ipynb"
|
||||
count = true
|
||||
quiet-level = 3
|
||||
ignore-words-list = "STrack,sTrack,strack"
|
||||
@@ -261,8 +222,12 @@ ignore-words-list = "STrack,sTrack,strack"
|
||||
include-package-data = false
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["supervision*"]
|
||||
exclude = ["docs*", "test*", "examples*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
supervision = ["py.typed"]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["setuptools >= 61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
+113
-3
@@ -9,11 +9,11 @@ except importlib_metadata.PackageNotFoundError:
|
||||
from supervision.annotators.core import (
|
||||
BackgroundOverlayAnnotator,
|
||||
BlurAnnotator,
|
||||
BoundingBoxAnnotator,
|
||||
BoxAnnotator,
|
||||
BoxCornerAnnotator,
|
||||
CircleAnnotator,
|
||||
ColorAnnotator,
|
||||
ComparisonAnnotator,
|
||||
CropAnnotator,
|
||||
DotAnnotator,
|
||||
EllipseAnnotator,
|
||||
@@ -45,7 +45,6 @@ from supervision.detection.line_zone import (
|
||||
LineZoneAnnotator,
|
||||
LineZoneAnnotatorMulticlass,
|
||||
)
|
||||
from supervision.detection.lmm import LMM
|
||||
from supervision.detection.overlap_filter import (
|
||||
OverlapFilter,
|
||||
box_non_max_merge,
|
||||
@@ -76,7 +75,11 @@ from supervision.detection.utils import (
|
||||
scale_boxes,
|
||||
xcycwh_to_xyxy,
|
||||
xywh_to_xyxy,
|
||||
xyxy_to_polygons,
|
||||
xyxy_to_xcycarh,
|
||||
xyxy_to_xywh,
|
||||
)
|
||||
from supervision.detection.vlm import LMM, VLM
|
||||
from supervision.draw.color import Color, ColorPalette
|
||||
from supervision.draw.utils import (
|
||||
calculate_optimal_line_thickness,
|
||||
@@ -100,7 +103,6 @@ from supervision.keypoint.core import KeyPoints
|
||||
from supervision.metrics.detection import ConfusionMatrix, MeanAveragePrecision
|
||||
from supervision.tracker.byte_tracker.core import ByteTrack
|
||||
from supervision.utils.conversion import cv2_to_pillow, pillow_to_cv2
|
||||
from supervision.utils.datatypes import keypoints_to_detections
|
||||
from supervision.utils.file import list_files_with_extensions
|
||||
from supervision.utils.image import (
|
||||
ImageSink,
|
||||
@@ -119,3 +121,111 @@ from supervision.utils.video import (
|
||||
get_video_frames_generator,
|
||||
process_video,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LMM",
|
||||
"BackgroundOverlayAnnotator",
|
||||
"BaseDataset",
|
||||
"BlurAnnotator",
|
||||
"BoxAnnotator",
|
||||
"BoxCornerAnnotator",
|
||||
"ByteTrack",
|
||||
"CSVSink",
|
||||
"CircleAnnotator",
|
||||
"ClassificationDataset",
|
||||
"Classifications",
|
||||
"Color",
|
||||
"ColorAnnotator",
|
||||
"ColorLookup",
|
||||
"ColorPalette",
|
||||
"ComparisonAnnotator",
|
||||
"ConfusionMatrix",
|
||||
"CropAnnotator",
|
||||
"DetectionDataset",
|
||||
"Detections",
|
||||
"DetectionsSmoother",
|
||||
"DotAnnotator",
|
||||
"EdgeAnnotator",
|
||||
"EllipseAnnotator",
|
||||
"FPSMonitor",
|
||||
"HaloAnnotator",
|
||||
"HeatMapAnnotator",
|
||||
"IconAnnotator",
|
||||
"ImageSink",
|
||||
"InferenceSlicer",
|
||||
"JSONSink",
|
||||
"KeyPoints",
|
||||
"LabelAnnotator",
|
||||
"LineZone",
|
||||
"LineZoneAnnotator",
|
||||
"LineZoneAnnotatorMulticlass",
|
||||
"MaskAnnotator",
|
||||
"MeanAveragePrecision",
|
||||
"OrientedBoxAnnotator",
|
||||
"OverlapFilter",
|
||||
"PercentageBarAnnotator",
|
||||
"PixelateAnnotator",
|
||||
"Point",
|
||||
"PolygonAnnotator",
|
||||
"PolygonZone",
|
||||
"PolygonZoneAnnotator",
|
||||
"Position",
|
||||
"Rect",
|
||||
"RichLabelAnnotator",
|
||||
"RoundBoxAnnotator",
|
||||
"TraceAnnotator",
|
||||
"TriangleAnnotator",
|
||||
"VertexAnnotator",
|
||||
"VertexLabelAnnotator",
|
||||
"VideoInfo",
|
||||
"VideoSink",
|
||||
"box_iou_batch",
|
||||
"box_non_max_merge",
|
||||
"box_non_max_suppression",
|
||||
"calculate_masks_centroids",
|
||||
"calculate_optimal_line_thickness",
|
||||
"calculate_optimal_text_scale",
|
||||
"clip_boxes",
|
||||
"contains_holes",
|
||||
"contains_multiple_segments",
|
||||
"create_tiles",
|
||||
"crop_image",
|
||||
"cv2_to_pillow",
|
||||
"draw_filled_polygon",
|
||||
"draw_filled_rectangle",
|
||||
"draw_image",
|
||||
"draw_line",
|
||||
"draw_polygon",
|
||||
"draw_rectangle",
|
||||
"draw_text",
|
||||
"filter_polygons_by_area",
|
||||
"get_polygon_center",
|
||||
"get_video_frames_generator",
|
||||
"letterbox_image",
|
||||
"list_files_with_extensions",
|
||||
"mask_iou_batch",
|
||||
"mask_non_max_suppression",
|
||||
"mask_to_polygons",
|
||||
"mask_to_rle",
|
||||
"mask_to_xyxy",
|
||||
"move_boxes",
|
||||
"move_masks",
|
||||
"oriented_box_iou_batch",
|
||||
"overlay_image",
|
||||
"pad_boxes",
|
||||
"pillow_to_cv2",
|
||||
"plot_image",
|
||||
"plot_images_grid",
|
||||
"polygon_to_mask",
|
||||
"polygon_to_xyxy",
|
||||
"process_video",
|
||||
"resize_image",
|
||||
"rle_to_mask",
|
||||
"scale_boxes",
|
||||
"scale_image",
|
||||
"xcycwh_to_xyxy",
|
||||
"xywh_to_xyxy",
|
||||
"xyxy_to_polygons",
|
||||
"xyxy_to_xyah",
|
||||
"xyxy_to_xywh",
|
||||
]
|
||||
|
||||
+524
-209
@@ -16,10 +16,16 @@ from supervision.annotators.utils import (
|
||||
)
|
||||
from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.utils import clip_boxes, mask_to_polygons
|
||||
from supervision.detection.utils import (
|
||||
clip_boxes,
|
||||
mask_to_polygons,
|
||||
polygon_to_mask,
|
||||
spread_out_boxes,
|
||||
xyxy_to_polygons,
|
||||
)
|
||||
from supervision.draw.color import Color, ColorPalette
|
||||
from supervision.draw.utils import draw_polygon
|
||||
from supervision.geometry.core import Position
|
||||
from supervision.draw.utils import draw_polygon, draw_rounded_rectangle, draw_text
|
||||
from supervision.geometry.core import Point, Position, Rect
|
||||
from supervision.utils.conversion import (
|
||||
ensure_cv2_image_for_annotation,
|
||||
ensure_pil_image_for_annotation,
|
||||
@@ -30,7 +36,8 @@ from supervision.utils.image import (
|
||||
overlay_image,
|
||||
scale_image,
|
||||
)
|
||||
from supervision.utils.internal import deprecated
|
||||
|
||||
CV2_FONT = cv2.FONT_HERSHEY_SIMPLEX
|
||||
|
||||
|
||||
class BoxAnnotator(BaseAnnotator):
|
||||
@@ -116,92 +123,6 @@ class BoxAnnotator(BaseAnnotator):
|
||||
return scene
|
||||
|
||||
|
||||
@deprecated(
|
||||
"`BoundingBoxAnnotator` is deprecated and has been renamed to `BoxAnnotator`."
|
||||
" `BoundingBoxAnnotator` will be removed in supervision-0.26.0."
|
||||
)
|
||||
class BoundingBoxAnnotator(BaseAnnotator):
|
||||
"""
|
||||
A class for drawing bounding boxes on an image using provided detections.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
color: Union[Color, ColorPalette] = ColorPalette.DEFAULT,
|
||||
thickness: int = 2,
|
||||
color_lookup: ColorLookup = ColorLookup.CLASS,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
color (Union[Color, ColorPalette]): The color or color palette to use for
|
||||
annotating detections.
|
||||
thickness (int): Thickness of the bounding box lines.
|
||||
color_lookup (ColorLookup): Strategy for mapping colors to annotations.
|
||||
Options are `INDEX`, `CLASS`, `TRACK`.
|
||||
"""
|
||||
self.color: Union[Color, ColorPalette] = color
|
||||
self.thickness: int = thickness
|
||||
self.color_lookup: ColorLookup = color_lookup
|
||||
|
||||
@ensure_cv2_image_for_annotation
|
||||
def annotate(
|
||||
self,
|
||||
scene: ImageType,
|
||||
detections: Detections,
|
||||
custom_color_lookup: Optional[np.ndarray] = None,
|
||||
) -> ImageType:
|
||||
"""
|
||||
Annotates the given scene with bounding boxes based on the provided detections.
|
||||
|
||||
Args:
|
||||
scene (ImageType): The image where bounding boxes will be drawn. `ImageType`
|
||||
is a flexible type, accepting either `numpy.ndarray` or `PIL.Image.Image`.
|
||||
detections (Detections): Object detections to annotate.
|
||||
custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
|
||||
Allows to override the default color mapping strategy.
|
||||
|
||||
Returns:
|
||||
The annotated image, matching the type of `scene` (`numpy.ndarray`
|
||||
or `PIL.Image.Image`)
|
||||
|
||||
Example:
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
image = ...
|
||||
detections = sv.Detections(...)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
annotated_frame = bounding_box_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
"""
|
||||
assert isinstance(scene, np.ndarray)
|
||||
for detection_idx in range(len(detections)):
|
||||
x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
|
||||
color = resolve_color(
|
||||
color=self.color,
|
||||
detections=detections,
|
||||
detection_idx=detection_idx,
|
||||
color_lookup=self.color_lookup
|
||||
if custom_color_lookup is None
|
||||
else custom_color_lookup,
|
||||
)
|
||||
cv2.rectangle(
|
||||
img=scene,
|
||||
pt1=(x1, y1),
|
||||
pt2=(x2, y2),
|
||||
color=color.as_bgr(),
|
||||
thickness=self.thickness,
|
||||
)
|
||||
return scene
|
||||
|
||||
|
||||
class OrientedBoxAnnotator(BaseAnnotator):
|
||||
"""
|
||||
A class for drawing oriented bounding boxes on an image using provided detections.
|
||||
@@ -1054,6 +975,7 @@ class LabelAnnotator(BaseAnnotator):
|
||||
text_position: Position = Position.TOP_LEFT,
|
||||
color_lookup: ColorLookup = ColorLookup.CLASS,
|
||||
border_radius: int = 0,
|
||||
smart_position: bool = False,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
@@ -1070,6 +992,7 @@ class LabelAnnotator(BaseAnnotator):
|
||||
Options are `INDEX`, `CLASS`, `TRACK`.
|
||||
border_radius (int): The radius to apply round edges. If the selected
|
||||
value is higher than the lower dimension, width or height, is clipped.
|
||||
smart_position (bool): Spread out the labels to avoid overlapping.
|
||||
"""
|
||||
self.border_radius: int = border_radius
|
||||
self.color: Union[Color, ColorPalette] = color
|
||||
@@ -1079,6 +1002,7 @@ class LabelAnnotator(BaseAnnotator):
|
||||
self.text_padding: int = text_padding
|
||||
self.text_anchor: Position = text_position
|
||||
self.color_lookup: ColorLookup = color_lookup
|
||||
self.smart_position = smart_position
|
||||
|
||||
@ensure_cv2_image_for_annotation
|
||||
def annotate(
|
||||
@@ -1128,11 +1052,29 @@ class LabelAnnotator(BaseAnnotator):
|
||||

|
||||
"""
|
||||
|
||||
assert isinstance(scene, np.ndarray)
|
||||
font = cv2.FONT_HERSHEY_SIMPLEX
|
||||
anchors_coordinates = detections.get_anchors_coordinates(
|
||||
anchor=self.text_anchor
|
||||
).astype(int)
|
||||
self._validate_labels(labels, detections)
|
||||
|
||||
labels = self._get_labels_text(detections, labels)
|
||||
label_properties = self._get_label_properties(detections, labels)
|
||||
|
||||
if self.smart_position:
|
||||
xyxy = label_properties[:, :4]
|
||||
xyxy = spread_out_boxes(xyxy)
|
||||
label_properties[:, :4] = xyxy
|
||||
|
||||
self._draw_labels(
|
||||
scene=scene,
|
||||
labels=labels,
|
||||
label_properties=label_properties,
|
||||
detections=detections,
|
||||
custom_color_lookup=custom_color_lookup,
|
||||
)
|
||||
|
||||
return scene
|
||||
|
||||
def _validate_labels(self, labels: Optional[List[str]], detections: Detections):
|
||||
if labels is not None and len(labels) != len(detections):
|
||||
raise ValueError(
|
||||
f"The number of labels ({len(labels)}) does not match the "
|
||||
@@ -1140,72 +1082,121 @@ class LabelAnnotator(BaseAnnotator):
|
||||
f"should have exactly 1 label."
|
||||
)
|
||||
|
||||
for detection_idx, center_coordinates in enumerate(anchors_coordinates):
|
||||
color = resolve_color(
|
||||
color=self.color,
|
||||
detections=detections,
|
||||
detection_idx=detection_idx,
|
||||
color_lookup=(
|
||||
self.color_lookup
|
||||
if custom_color_lookup is None
|
||||
else custom_color_lookup
|
||||
),
|
||||
)
|
||||
def _get_label_properties(
|
||||
self,
|
||||
detections: Detections,
|
||||
labels: List[str],
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Calculate the numerical properties required to draw the labels on the image.
|
||||
|
||||
text_color = resolve_color(
|
||||
color=self.text_color,
|
||||
detections=detections,
|
||||
detection_idx=detection_idx,
|
||||
color_lookup=(
|
||||
self.color_lookup
|
||||
if custom_color_lookup is None
|
||||
else custom_color_lookup
|
||||
),
|
||||
)
|
||||
Returns:
|
||||
(np.ndarray): An array of label properties, containing columns:
|
||||
`min_x`, `min_y`, `max_x`, `max_y`, `padded_text_height`.
|
||||
"""
|
||||
label_properties = []
|
||||
anchors_coordinates = detections.get_anchors_coordinates(
|
||||
anchor=self.text_anchor
|
||||
).astype(int)
|
||||
|
||||
if labels is not None:
|
||||
text = labels[detection_idx]
|
||||
elif CLASS_NAME_DATA_FIELD in detections.data:
|
||||
text = detections.data[CLASS_NAME_DATA_FIELD][detection_idx]
|
||||
elif detections.class_id is not None:
|
||||
text = str(detections.class_id[detection_idx])
|
||||
else:
|
||||
text = str(detection_idx)
|
||||
|
||||
text_w, text_h = cv2.getTextSize(
|
||||
text=text,
|
||||
fontFace=font,
|
||||
for label, center_coords in zip(labels, anchors_coordinates):
|
||||
(text_w, text_h) = cv2.getTextSize(
|
||||
text=label,
|
||||
fontFace=CV2_FONT,
|
||||
fontScale=self.text_scale,
|
||||
thickness=self.text_thickness,
|
||||
)[0]
|
||||
text_w_padded = text_w + 2 * self.text_padding
|
||||
text_h_padded = text_h + 2 * self.text_padding
|
||||
|
||||
width_padded = text_w + 2 * self.text_padding
|
||||
height_padded = text_h + 2 * self.text_padding
|
||||
|
||||
text_background_xyxy = resolve_text_background_xyxy(
|
||||
center_coordinates=tuple(center_coordinates),
|
||||
text_wh=(text_w_padded, text_h_padded),
|
||||
center_coordinates=tuple(center_coords),
|
||||
text_wh=(width_padded, height_padded),
|
||||
position=self.text_anchor,
|
||||
)
|
||||
|
||||
text_x = text_background_xyxy[0] + self.text_padding
|
||||
text_y = text_background_xyxy[1] + self.text_padding + text_h
|
||||
label_properties.append(
|
||||
[
|
||||
*text_background_xyxy,
|
||||
text_h,
|
||||
]
|
||||
)
|
||||
|
||||
return np.array(label_properties).reshape(-1, 5)
|
||||
|
||||
@staticmethod
|
||||
def _get_labels_text(
|
||||
detections: Detections, custom_labels: Optional[List[str]]
|
||||
) -> List[str]:
|
||||
if custom_labels is not None:
|
||||
return [str(label) for label in custom_labels]
|
||||
|
||||
labels = []
|
||||
for idx in range(len(detections)):
|
||||
if CLASS_NAME_DATA_FIELD in detections.data:
|
||||
labels.append(detections.data[CLASS_NAME_DATA_FIELD][idx])
|
||||
elif detections.class_id is not None:
|
||||
labels.append(str(detections.class_id[idx]))
|
||||
else:
|
||||
labels.append(str(idx))
|
||||
return labels
|
||||
|
||||
def _draw_labels(
|
||||
self,
|
||||
scene: np.ndarray,
|
||||
labels: List[str],
|
||||
label_properties: np.ndarray,
|
||||
detections: Detections,
|
||||
custom_color_lookup: Optional[np.ndarray],
|
||||
) -> None:
|
||||
assert len(labels) == len(label_properties) == len(detections), (
|
||||
f"Number of label properties ({len(label_properties)}), "
|
||||
f"labels ({len(labels)}) and detections ({len(detections)}) "
|
||||
"do not match."
|
||||
)
|
||||
|
||||
color_lookup = (
|
||||
custom_color_lookup
|
||||
if custom_color_lookup is not None
|
||||
else self.color_lookup
|
||||
)
|
||||
|
||||
for idx, label_property in enumerate(label_properties):
|
||||
background_color = resolve_color(
|
||||
color=self.color,
|
||||
detections=detections,
|
||||
detection_idx=idx,
|
||||
color_lookup=color_lookup,
|
||||
)
|
||||
text_color = resolve_color(
|
||||
color=self.text_color,
|
||||
detections=detections,
|
||||
detection_idx=idx,
|
||||
color_lookup=color_lookup,
|
||||
)
|
||||
|
||||
box_xyxy = label_property[:4]
|
||||
text_height_padded = label_property[4]
|
||||
self.draw_rounded_rectangle(
|
||||
scene=scene,
|
||||
xyxy=text_background_xyxy,
|
||||
color=color.as_bgr(),
|
||||
xyxy=box_xyxy,
|
||||
color=background_color.as_bgr(),
|
||||
border_radius=self.border_radius,
|
||||
)
|
||||
|
||||
text_x = box_xyxy[0] + self.text_padding
|
||||
text_y = box_xyxy[1] + self.text_padding + text_height_padded
|
||||
cv2.putText(
|
||||
img=scene,
|
||||
text=text,
|
||||
text=labels[idx],
|
||||
org=(text_x, text_y),
|
||||
fontFace=font,
|
||||
fontFace=CV2_FONT,
|
||||
fontScale=self.text_scale,
|
||||
color=text_color.as_bgr(),
|
||||
thickness=self.text_thickness,
|
||||
lineType=cv2.LINE_AA,
|
||||
)
|
||||
return scene
|
||||
|
||||
@staticmethod
|
||||
def draw_rounded_rectangle(
|
||||
@@ -1266,6 +1257,7 @@ class RichLabelAnnotator(BaseAnnotator):
|
||||
text_position: Position = Position.TOP_LEFT,
|
||||
color_lookup: ColorLookup = ColorLookup.CLASS,
|
||||
border_radius: int = 0,
|
||||
smart_position: bool = False,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
@@ -1282,6 +1274,7 @@ class RichLabelAnnotator(BaseAnnotator):
|
||||
Options are `INDEX`, `CLASS`, `TRACK`.
|
||||
border_radius (int): The radius to apply round edges. If the selected
|
||||
value is higher than the lower dimension, width or height, is clipped.
|
||||
smart_position (bool): Spread out the labels to avoid overlapping.
|
||||
"""
|
||||
self.color = color
|
||||
self.text_color = text_color
|
||||
@@ -1289,14 +1282,8 @@ class RichLabelAnnotator(BaseAnnotator):
|
||||
self.text_anchor = text_position
|
||||
self.color_lookup = color_lookup
|
||||
self.border_radius = border_radius
|
||||
if font_path is not None:
|
||||
try:
|
||||
self.font = ImageFont.truetype(font_path, font_size)
|
||||
except OSError:
|
||||
print(f"Font path '{font_path}' not found. Using PIL's default font.")
|
||||
self.font = self._load_default_font(font_size)
|
||||
else:
|
||||
self.font = self._load_default_font(font_size)
|
||||
self.smart_position = smart_position
|
||||
self.font = self._load_font(font_size, font_path)
|
||||
|
||||
@ensure_pil_image_for_annotation
|
||||
def annotate(
|
||||
@@ -1346,88 +1333,157 @@ class RichLabelAnnotator(BaseAnnotator):
|
||||
|
||||
"""
|
||||
assert isinstance(scene, Image.Image)
|
||||
self._validate_labels(labels, detections)
|
||||
|
||||
draw = ImageDraw.Draw(scene)
|
||||
anchors_coordinates = detections.get_anchors_coordinates(
|
||||
anchor=self.text_anchor
|
||||
).astype(int)
|
||||
labels = self._get_labels_text(detections, labels)
|
||||
label_properties = self._get_label_properties(draw, detections, labels)
|
||||
|
||||
if self.smart_position:
|
||||
xyxy = label_properties[:, :4]
|
||||
xyxy = spread_out_boxes(xyxy)
|
||||
label_properties[:, :4] = xyxy
|
||||
|
||||
self._draw_labels(
|
||||
draw=draw,
|
||||
labels=labels,
|
||||
label_properties=label_properties,
|
||||
detections=detections,
|
||||
custom_color_lookup=custom_color_lookup,
|
||||
)
|
||||
|
||||
return scene
|
||||
|
||||
def _validate_labels(self, labels: Optional[List[str]], detections: Detections):
|
||||
if labels is not None and len(labels) != len(detections):
|
||||
raise ValueError(
|
||||
f"The number of labels provided ({len(labels)}) does not match the "
|
||||
f"number of detections ({len(detections)}). Each detection should have "
|
||||
f"a corresponding label."
|
||||
)
|
||||
for detection_idx, center_coordinates in enumerate(anchors_coordinates):
|
||||
color = resolve_color(
|
||||
color=self.color,
|
||||
detections=detections,
|
||||
detection_idx=detection_idx,
|
||||
color_lookup=(
|
||||
self.color_lookup
|
||||
if custom_color_lookup is None
|
||||
else custom_color_lookup
|
||||
),
|
||||
f"The number of labels ({len(labels)}) does not match the "
|
||||
f"number of detections ({len(detections)}). Each detection "
|
||||
f"should have exactly 1 label."
|
||||
)
|
||||
|
||||
text_color = resolve_color(
|
||||
color=self.text_color,
|
||||
detections=detections,
|
||||
detection_idx=detection_idx,
|
||||
color_lookup=(
|
||||
self.color_lookup
|
||||
if custom_color_lookup is None
|
||||
else custom_color_lookup
|
||||
),
|
||||
def _get_label_properties(
|
||||
self, draw, detections: Detections, labels: List[str]
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Calculate the numerical properties required to draw the labels on the image.
|
||||
|
||||
Returns:
|
||||
(np.ndarray): An array of label properties, containing columns:
|
||||
`min_x`, `min_y`, `max_x`, `max_y`, `text_left_coordinate`,
|
||||
`text_top_coordinate`. The first 4 values are already padded
|
||||
with `text_padding`.
|
||||
"""
|
||||
label_properties = []
|
||||
|
||||
anchor_coordinates = detections.get_anchors_coordinates(
|
||||
anchor=self.text_anchor
|
||||
).astype(int)
|
||||
|
||||
for label, center_coords in zip(labels, anchor_coordinates):
|
||||
text_left, text_top, text_right, text_bottom = draw.textbbox(
|
||||
(0, 0), label, font=self.font
|
||||
)
|
||||
text_width = text_right - text_left
|
||||
text_height = text_bottom - text_top
|
||||
width_padded = text_width + 2 * self.text_padding
|
||||
height_padded = text_height + 2 * self.text_padding
|
||||
|
||||
if labels is not None:
|
||||
text = labels[detection_idx]
|
||||
elif CLASS_NAME_DATA_FIELD in detections.data:
|
||||
text = detections.data[CLASS_NAME_DATA_FIELD][detection_idx]
|
||||
elif detections.class_id is not None:
|
||||
text = str(detections.class_id[detection_idx])
|
||||
else:
|
||||
text = str(detection_idx)
|
||||
|
||||
left, top, right, bottom = draw.textbbox((0, 0), text, font=self.font)
|
||||
text_width = right - left
|
||||
text_height = bottom - top
|
||||
text_w_padded = text_width + 2 * self.text_padding
|
||||
text_h_padded = text_height + 2 * self.text_padding
|
||||
text_background_xyxy = resolve_text_background_xyxy(
|
||||
center_coordinates=tuple(center_coordinates),
|
||||
text_wh=(text_w_padded, text_h_padded),
|
||||
center_coordinates=tuple(center_coords),
|
||||
text_wh=(width_padded, height_padded),
|
||||
position=self.text_anchor,
|
||||
)
|
||||
|
||||
text_x = text_background_xyxy[0] + self.text_padding - left
|
||||
text_y = text_background_xyxy[1] + self.text_padding - top
|
||||
label_properties.append([*text_background_xyxy, text_left, text_top])
|
||||
|
||||
return np.array(label_properties).reshape(-1, 6)
|
||||
|
||||
@staticmethod
|
||||
def _get_labels_text(
|
||||
detections: Detections, custom_labels: Optional[List[str]]
|
||||
) -> List[str]:
|
||||
if custom_labels is not None:
|
||||
return custom_labels
|
||||
|
||||
labels = []
|
||||
for idx in range(len(detections)):
|
||||
if CLASS_NAME_DATA_FIELD in detections.data:
|
||||
labels.append(detections.data[CLASS_NAME_DATA_FIELD][idx])
|
||||
elif detections.class_id is not None:
|
||||
labels.append(str(detections.class_id[idx]))
|
||||
else:
|
||||
labels.append(str(idx))
|
||||
return labels
|
||||
|
||||
def _draw_labels(
|
||||
self,
|
||||
draw,
|
||||
labels: List[str],
|
||||
label_properties: np.ndarray,
|
||||
detections: Detections,
|
||||
custom_color_lookup: Optional[np.ndarray],
|
||||
) -> None:
|
||||
assert len(labels) == len(label_properties) == len(detections), (
|
||||
f"Number of label properties ({len(label_properties)}), "
|
||||
f"labels ({len(labels)}) and detections ({len(detections)}) "
|
||||
"do not match."
|
||||
)
|
||||
color_lookup = (
|
||||
custom_color_lookup
|
||||
if custom_color_lookup is not None
|
||||
else self.color_lookup
|
||||
)
|
||||
|
||||
for idx, label_property in enumerate(label_properties):
|
||||
background_color = resolve_color(
|
||||
color=self.color,
|
||||
detections=detections,
|
||||
detection_idx=idx,
|
||||
color_lookup=color_lookup,
|
||||
)
|
||||
text_color = resolve_color(
|
||||
color=self.text_color,
|
||||
detections=detections,
|
||||
detection_idx=idx,
|
||||
color_lookup=color_lookup,
|
||||
)
|
||||
|
||||
box_xyxy = label_property[:4]
|
||||
text_left = label_property[4]
|
||||
text_top = label_property[5]
|
||||
label_x_position = box_xyxy[0] + self.text_padding - text_left
|
||||
label_y_position = box_xyxy[1] + self.text_padding - text_top
|
||||
|
||||
draw.rounded_rectangle(
|
||||
text_background_xyxy,
|
||||
tuple(box_xyxy),
|
||||
radius=self.border_radius,
|
||||
fill=color.as_rgb(),
|
||||
fill=background_color.as_rgb(),
|
||||
outline=None,
|
||||
)
|
||||
draw.text(
|
||||
xy=(text_x, text_y),
|
||||
text=text,
|
||||
xy=(label_x_position, label_y_position),
|
||||
text=labels[idx],
|
||||
font=self.font,
|
||||
fill=text_color.as_rgb(),
|
||||
)
|
||||
return scene
|
||||
|
||||
@staticmethod
|
||||
def _load_default_font(size):
|
||||
"""
|
||||
PIL either loads a font that accepts a size (e.g. on my machine)
|
||||
or raises an error saying `load_default` does not accept arguments
|
||||
(e.g. in Colab).
|
||||
"""
|
||||
def _load_font(font_size: int, font_path: Optional[str]):
|
||||
def load_default_font(size):
|
||||
try:
|
||||
return ImageFont.load_default(size)
|
||||
except TypeError:
|
||||
return ImageFont.load_default()
|
||||
|
||||
if font_path is None:
|
||||
return load_default_font(font_size)
|
||||
|
||||
try:
|
||||
font = ImageFont.load_default(size)
|
||||
except TypeError:
|
||||
font = ImageFont.load_default()
|
||||
return font
|
||||
return ImageFont.truetype(font_path, font_size)
|
||||
except OSError:
|
||||
print(f"Font path '{font_path}' not found. Using PIL's default font.")
|
||||
return load_default_font(font_size)
|
||||
|
||||
|
||||
class IconAnnotator(BaseAnnotator):
|
||||
@@ -1801,11 +1857,9 @@ class HeatMapAnnotator(BaseAnnotator):
|
||||
temp = temp.astype(np.uint8)
|
||||
if self.kernel_size is not None:
|
||||
temp = cv2.blur(temp, (self.kernel_size, self.kernel_size))
|
||||
hsv = np.zeros(scene.shape)
|
||||
hsv = np.full(scene.shape, 255, dtype=np.uint8)
|
||||
hsv[..., 0] = temp
|
||||
hsv[..., 1] = 255
|
||||
hsv[..., 2] = 255
|
||||
temp = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
|
||||
temp = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
|
||||
mask = cv2.cvtColor(self.heat_mask.astype(np.uint8), cv2.COLOR_GRAY2BGR) > 0
|
||||
scene[mask] = cv2.addWeighted(temp, self.opacity, scene, 1 - self.opacity, 0)[
|
||||
mask
|
||||
@@ -2390,6 +2444,9 @@ class CropAnnotator(BaseAnnotator):
|
||||
detections=detections
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
"""
|
||||
assert isinstance(scene, np.ndarray)
|
||||
crops = [
|
||||
@@ -2546,3 +2603,261 @@ class BackgroundOverlayAnnotator(BaseAnnotator):
|
||||
|
||||
np.copyto(scene, colored_mask)
|
||||
return scene
|
||||
|
||||
|
||||
class ComparisonAnnotator:
|
||||
"""
|
||||
Highlights the differences between two sets of detections.
|
||||
Useful for comparing results from two different models, or the difference
|
||||
between a ground truth and a prediction.
|
||||
|
||||
If present, uses the oriented bounding box data.
|
||||
Otherwise, if present, uses a mask.
|
||||
Otherwise, uses the bounding box data.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
color_1: Color = Color.RED,
|
||||
color_2: Color = Color.GREEN,
|
||||
color_overlap: Color = Color.BLUE,
|
||||
*,
|
||||
opacity: float = 0.75,
|
||||
label_1: str = "",
|
||||
label_2: str = "",
|
||||
label_overlap: str = "",
|
||||
label_scale: float = 1.0,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
color_1 (Color): Color of areas only present in the first set of
|
||||
detections.
|
||||
color_2 (Color): Color of areas only present in the second set of
|
||||
detections.
|
||||
color_overlap (Color): Color of areas present in both sets of detections.
|
||||
opacity (float): Annotator opacity, from `0` to `1`.
|
||||
label_1 (str): Label for the first set of detections.
|
||||
label_2 (str): Label for the second set of detections.
|
||||
label_overlap (str): Label for areas present in both sets of detections.
|
||||
label_scale (float): Controls how large the labels are.
|
||||
"""
|
||||
|
||||
self.color_1 = color_1
|
||||
self.color_2 = color_2
|
||||
self.color_overlap = color_overlap
|
||||
|
||||
self.opacity = opacity
|
||||
self.label_1 = label_1
|
||||
self.label_2 = label_2
|
||||
self.label_overlap = label_overlap
|
||||
self.label_scale = label_scale
|
||||
self.text_thickness = int(self.label_scale + 1.2)
|
||||
|
||||
@ensure_cv2_image_for_annotation
|
||||
def annotate(
|
||||
self, scene: ImageType, detections_1: Detections, detections_2: Detections
|
||||
) -> ImageType:
|
||||
"""
|
||||
Highlights the differences between two sets of detections.
|
||||
|
||||
Args:
|
||||
scene (ImageType): The image where detections will be drawn.
|
||||
`ImageType` is a flexible type, accepting either `numpy.ndarray`
|
||||
or `PIL.Image.Image`.
|
||||
detections_1 (Detections): The first set of detections or predictions.
|
||||
detections_2 (Detections): The second set of detections to compare or
|
||||
ground truth.
|
||||
|
||||
Returns:
|
||||
The annotated image.
|
||||
|
||||
Example:
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
image = ...
|
||||
detections_1 = sv.Detections(...)
|
||||
detections_2 = sv.Detections(...)
|
||||
|
||||
comparison_annotator = sv.ComparisonAnnotator()
|
||||
annotated_frame = comparison_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections_1=detections_1,
|
||||
detections_2=detections_2
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
"""
|
||||
assert isinstance(scene, np.ndarray)
|
||||
if detections_1.is_empty() and detections_2.is_empty():
|
||||
return scene
|
||||
|
||||
use_obb = self._use_obb(detections_1, detections_2)
|
||||
use_mask = self._use_mask(detections_1, detections_2)
|
||||
|
||||
if use_obb:
|
||||
mask_1 = self._mask_from_obb(scene, detections_1)
|
||||
mask_2 = self._mask_from_obb(scene, detections_2)
|
||||
|
||||
elif use_mask:
|
||||
mask_1 = self._mask_from_mask(scene, detections_1)
|
||||
mask_2 = self._mask_from_mask(scene, detections_2)
|
||||
|
||||
else:
|
||||
mask_1 = self._mask_from_xyxy(scene, detections_1)
|
||||
mask_2 = self._mask_from_xyxy(scene, detections_2)
|
||||
|
||||
mask_overlap = mask_1 & mask_2
|
||||
mask_1 = mask_1 & ~mask_overlap
|
||||
mask_2 = mask_2 & ~mask_overlap
|
||||
|
||||
color_layer = np.zeros_like(scene, dtype=np.uint8)
|
||||
color_layer[mask_overlap] = self.color_overlap.as_bgr()
|
||||
color_layer[mask_1] = self.color_1.as_bgr()
|
||||
color_layer[mask_2] = self.color_2.as_bgr()
|
||||
|
||||
scene[mask_overlap] = (1 - self.opacity) * scene[
|
||||
mask_overlap
|
||||
] + self.opacity * color_layer[mask_overlap]
|
||||
scene[mask_1] = (1 - self.opacity) * scene[mask_1] + self.opacity * color_layer[
|
||||
mask_1
|
||||
]
|
||||
scene[mask_2] = (1 - self.opacity) * scene[mask_2] + self.opacity * color_layer[
|
||||
mask_2
|
||||
]
|
||||
|
||||
self._draw_labels(scene)
|
||||
|
||||
return scene
|
||||
|
||||
@staticmethod
|
||||
def _use_obb(detections_1: Detections, detections_2: Detections) -> bool:
|
||||
assert not detections_1.is_empty() or not detections_2.is_empty()
|
||||
is_obb_1 = ORIENTED_BOX_COORDINATES in detections_1.data
|
||||
is_obb_2 = ORIENTED_BOX_COORDINATES in detections_2.data
|
||||
return (
|
||||
(is_obb_1 and is_obb_2)
|
||||
or (is_obb_1 and detections_2.is_empty())
|
||||
or (detections_1.is_empty() and is_obb_2)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _use_mask(detections_1: Detections, detections_2: Detections) -> bool:
|
||||
assert not detections_1.is_empty() or not detections_2.is_empty()
|
||||
is_mask_1 = detections_1.mask is not None
|
||||
is_mask_2 = detections_2.mask is not None
|
||||
return (
|
||||
(is_mask_1 and is_mask_2)
|
||||
or (is_mask_1 and detections_2.is_empty())
|
||||
or (detections_1.is_empty() and is_mask_2)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _mask_from_xyxy(scene: np.ndarray, detections: Detections) -> np.ndarray:
|
||||
mask = np.zeros(scene.shape[:2], dtype=np.bool_)
|
||||
if detections.is_empty():
|
||||
return mask
|
||||
|
||||
resolution_wh = scene.shape[1], scene.shape[0]
|
||||
polygons = xyxy_to_polygons(detections.xyxy)
|
||||
|
||||
for polygon in polygons:
|
||||
polygon_mask = polygon_to_mask(polygon, resolution_wh=resolution_wh)
|
||||
mask |= polygon_mask.astype(np.bool_)
|
||||
return mask
|
||||
|
||||
@staticmethod
|
||||
def _mask_from_obb(scene: np.ndarray, detections: Detections) -> np.ndarray:
|
||||
mask = np.zeros(scene.shape[:2], dtype=np.bool_)
|
||||
if detections.is_empty():
|
||||
return mask
|
||||
|
||||
resolution_wh = scene.shape[1], scene.shape[0]
|
||||
|
||||
for polygon in detections.data[ORIENTED_BOX_COORDINATES]:
|
||||
polygon_mask = polygon_to_mask(polygon, resolution_wh=resolution_wh)
|
||||
mask |= polygon_mask.astype(np.bool_)
|
||||
return mask
|
||||
|
||||
@staticmethod
|
||||
def _mask_from_mask(scene: np.ndarray, detections: Detections) -> np.ndarray:
|
||||
mask = np.zeros(scene.shape[:2], dtype=np.bool_)
|
||||
if detections.is_empty():
|
||||
return mask
|
||||
assert detections.mask is not None
|
||||
|
||||
for detections_mask in detections.mask:
|
||||
mask |= detections_mask.astype(np.bool_)
|
||||
return mask
|
||||
|
||||
def _draw_labels(self, scene: np.ndarray) -> None:
|
||||
"""
|
||||
Draw the labels, explaining what each color represents, with automatically
|
||||
computed positions.
|
||||
|
||||
Args:
|
||||
scene (np.ndarray): The image where the labels will be drawn.
|
||||
"""
|
||||
margin = int(50 * self.label_scale)
|
||||
gap = int(40 * self.label_scale)
|
||||
y0 = int(50 * self.label_scale)
|
||||
height = int(50 * self.label_scale)
|
||||
|
||||
marker_size = int(20 * self.label_scale)
|
||||
padding = int(10 * self.label_scale)
|
||||
text_box_corner_radius = int(10 * self.label_scale)
|
||||
marker_corner_radius = int(4 * self.label_scale)
|
||||
text_scale = self.label_scale
|
||||
|
||||
label_color_pairs = [
|
||||
(self.label_1, self.color_1),
|
||||
(self.label_2, self.color_2),
|
||||
(self.label_overlap, self.color_overlap),
|
||||
]
|
||||
|
||||
x0 = margin
|
||||
for text, color in label_color_pairs:
|
||||
if not text:
|
||||
continue
|
||||
|
||||
(text_w, _) = cv2.getTextSize(
|
||||
text=text,
|
||||
fontFace=CV2_FONT,
|
||||
fontScale=self.label_scale,
|
||||
thickness=self.text_thickness,
|
||||
)[0]
|
||||
|
||||
width = text_w + marker_size + padding * 4
|
||||
center_x = x0 + width // 2
|
||||
center_y = y0 + height // 2
|
||||
|
||||
draw_rounded_rectangle(
|
||||
scene=scene,
|
||||
rect=Rect(x=x0, y=y0, width=width, height=height),
|
||||
color=Color.WHITE,
|
||||
border_radius=text_box_corner_radius,
|
||||
)
|
||||
|
||||
draw_rounded_rectangle(
|
||||
scene=scene,
|
||||
rect=Rect(
|
||||
x=x0 + padding,
|
||||
y=center_y - marker_size / 2,
|
||||
width=marker_size,
|
||||
height=marker_size,
|
||||
),
|
||||
color=color,
|
||||
border_radius=marker_corner_radius,
|
||||
)
|
||||
|
||||
draw_text(
|
||||
scene,
|
||||
text,
|
||||
text_anchor=Point(x=center_x + marker_size, y=center_y),
|
||||
text_scale=text_scale,
|
||||
text_thickness=self.text_thickness,
|
||||
)
|
||||
|
||||
x0 += width + gap
|
||||
|
||||
@@ -7,6 +7,9 @@ from supervision.detection.core import Detections
|
||||
from supervision.draw.color import Color, ColorPalette
|
||||
from supervision.geometry.core import Position
|
||||
|
||||
PENDING_TRACK_COLOR = Color.GREY
|
||||
PENDING_TRACK_ID = -1
|
||||
|
||||
|
||||
class ColorLookup(Enum):
|
||||
"""
|
||||
@@ -136,6 +139,8 @@ def resolve_color(
|
||||
detection_idx=detection_idx,
|
||||
color_lookup=color_lookup,
|
||||
)
|
||||
if color_lookup == ColorLookup.TRACK and idx == PENDING_TRACK_ID:
|
||||
return PENDING_TRACK_COLOR
|
||||
return get_color_by_index(color=color, idx=idx)
|
||||
|
||||
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
from supervision.assets.downloader import download_assets
|
||||
from supervision.assets.list import VideoAssets
|
||||
|
||||
__all__ = ["VideoAssets", "download_assets"]
|
||||
|
||||
@@ -4,19 +4,10 @@ from pathlib import Path
|
||||
from shutil import copyfileobj
|
||||
from typing import Union
|
||||
|
||||
from supervision.assets.list import VIDEO_ASSETS, VideoAssets
|
||||
from requests import get
|
||||
from tqdm.auto import tqdm
|
||||
|
||||
try:
|
||||
from requests import get
|
||||
from tqdm.auto import tqdm
|
||||
except ImportError:
|
||||
raise ValueError(
|
||||
"\n"
|
||||
"Please install requests and tqdm to download assets \n"
|
||||
"or install supervision with assets \n"
|
||||
"pip install supervision[assets] \n"
|
||||
"\n"
|
||||
)
|
||||
from supervision.assets.list import VIDEO_ASSETS, VideoAssets
|
||||
|
||||
|
||||
def is_md5_hash_matching(filename: str, original_md5_hash: str) -> bool:
|
||||
|
||||
@@ -705,28 +705,6 @@ class ClassificationDataset(BaseDataset):
|
||||
"a list of paths `List[str]` instead."
|
||||
)
|
||||
|
||||
@property
|
||||
@deprecated(
|
||||
"`DetectionDataset.images` property is deprecated and will be removed in "
|
||||
"`supervision-0.26.0`. Iterate with `for path, image, annotation in dataset:` "
|
||||
"instead."
|
||||
)
|
||||
def images(self) -> Dict[str, np.ndarray]:
|
||||
"""
|
||||
Load all images to memory and return them as a dictionary.
|
||||
|
||||
!!! warning
|
||||
|
||||
Only use this when you need all images at once.
|
||||
It is much more memory-efficient to initialize dataset with
|
||||
image paths and use `for path, image, annotation in dataset:`.
|
||||
"""
|
||||
if self._images_in_memory:
|
||||
return self._images_in_memory
|
||||
|
||||
images = {image_path: cv2.imread(image_path) for image_path in self.image_paths}
|
||||
return images
|
||||
|
||||
def _get_image(self, image_path: str) -> np.ndarray:
|
||||
"""Assumes that image is in dataset"""
|
||||
if self._images_in_memory:
|
||||
|
||||
@@ -2,8 +2,8 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from supervision.config import ORIENTED_BOX_COORDINATES
|
||||
from supervision.dataset.utils import approximate_mask_with_polygons
|
||||
@@ -153,7 +153,18 @@ def load_yolo_annotations(
|
||||
image_paths = [
|
||||
str(path)
|
||||
for path in list_files_with_extensions(
|
||||
directory=images_directory_path, extensions=["jpg", "jpeg", "png"]
|
||||
directory=images_directory_path,
|
||||
extensions=[
|
||||
"bmp",
|
||||
"dng",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"mpo",
|
||||
"png",
|
||||
"tif",
|
||||
"tiff",
|
||||
"webp",
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
@@ -167,10 +178,16 @@ def load_yolo_annotations(
|
||||
annotations[image_path] = Detections.empty()
|
||||
continue
|
||||
|
||||
image = cv2.imread(image_path)
|
||||
# PIL is much faster than cv2 for checking image shape and mode: https://github.com/roboflow/supervision/issues/1554
|
||||
image = Image.open(image_path)
|
||||
lines = read_txt_file(file_path=annotation_path, skip_empty=True)
|
||||
h, w, _ = image.shape
|
||||
w, h = image.size
|
||||
resolution_wh = (w, h)
|
||||
if image.mode not in ("RGB", "L"):
|
||||
raise ValueError(
|
||||
f"Images must be 'RGB' or 'grayscale', \
|
||||
but {image_path} mode is '{image.mode}'."
|
||||
)
|
||||
|
||||
with_masks = _with_mask(lines=lines)
|
||||
with_masks = force_masks if force_masks else with_masks
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
@@ -10,12 +10,6 @@ from supervision.config import (
|
||||
CLASS_NAME_DATA_FIELD,
|
||||
ORIENTED_BOX_COORDINATES,
|
||||
)
|
||||
from supervision.detection.lmm import (
|
||||
LMM,
|
||||
from_florence_2,
|
||||
from_paligemma,
|
||||
validate_lmm_parameters,
|
||||
)
|
||||
from supervision.detection.overlap_filter import (
|
||||
box_non_max_merge,
|
||||
box_non_max_suppression,
|
||||
@@ -39,8 +33,16 @@ from supervision.detection.utils import (
|
||||
process_roboflow_result,
|
||||
xywh_to_xyxy,
|
||||
)
|
||||
from supervision.detection.vlm import (
|
||||
LMM,
|
||||
VLM,
|
||||
from_florence_2,
|
||||
from_paligemma,
|
||||
from_qwen_2_5_vl,
|
||||
validate_vlm_parameters,
|
||||
)
|
||||
from supervision.geometry.core import Position
|
||||
from supervision.utils.internal import get_instance_variables
|
||||
from supervision.utils.internal import deprecated, get_instance_variables
|
||||
from supervision.validators import validate_detections_fields
|
||||
|
||||
|
||||
@@ -117,7 +119,7 @@ class Detections:
|
||||
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, H, W)` containing the segmentation masks.
|
||||
`(n, H, W)` containing the segmentation masks (`bool` data type).
|
||||
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
|
||||
@@ -606,8 +608,10 @@ class Detections:
|
||||
detections = sv.Detections.from_inference(result)
|
||||
```
|
||||
"""
|
||||
with suppress(AttributeError):
|
||||
if hasattr(roboflow_result, "dict"):
|
||||
roboflow_result = roboflow_result.dict(exclude_none=True, by_alias=True)
|
||||
elif hasattr(roboflow_result, "json"):
|
||||
roboflow_result = roboflow_result.json()
|
||||
xyxy, confidence, class_id, masks, trackers, data = process_roboflow_result(
|
||||
roboflow_result=roboflow_result
|
||||
)
|
||||
@@ -711,7 +715,7 @@ class Detections:
|
||||
"""
|
||||
if "error" in azure_result:
|
||||
raise ValueError(
|
||||
f'Azure API returned an error {azure_result["error"]["message"]}'
|
||||
f"Azure API returned an error {azure_result['error']['message']}"
|
||||
)
|
||||
|
||||
xyxy, confidences, class_ids = [], [], []
|
||||
@@ -798,6 +802,10 @@ class Detections:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@deprecated(
|
||||
"`Detections.from_lmm` property is deprecated and will be removed in "
|
||||
"`supervision-0.31.0`. Use Detections.from_vlm instead."
|
||||
)
|
||||
def from_lmm(
|
||||
cls, lmm: Union[LMM, str], result: Union[str, dict], **kwargs: Any
|
||||
) -> Detections:
|
||||
@@ -836,16 +844,51 @@ class Detections:
|
||||
# array([0])
|
||||
```
|
||||
"""
|
||||
lmm = validate_lmm_parameters(lmm, result, kwargs)
|
||||
# filler logic mapping old from_lmm to new from_vlm
|
||||
lmm_to_vlm = {
|
||||
LMM.PALIGEMMA: VLM.PALIGEMMA,
|
||||
LMM.FLORENCE_2: VLM.FLORENCE_2,
|
||||
LMM.QWEN_2_5_VL: VLM.QWEN_2_5_VL,
|
||||
}
|
||||
|
||||
if lmm == LMM.PALIGEMMA:
|
||||
assert isinstance(result, str)
|
||||
# (this works even if the LMM enum is wrapped by @deprecated)
|
||||
if isinstance(lmm, Enum) and lmm.__class__.__name__ == "LMM":
|
||||
vlm = lmm_to_vlm[lmm]
|
||||
|
||||
elif isinstance(lmm, str):
|
||||
try:
|
||||
lmm_enum = LMM(lmm.lower())
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"Invalid LMM string '{lmm}'. Must be one of "
|
||||
f"{[m.value for m in LMM]}"
|
||||
)
|
||||
vlm = lmm_to_vlm[lmm_enum]
|
||||
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid type for 'lmm': {type(lmm)}. Must be LMM or str."
|
||||
)
|
||||
|
||||
return cls.from_vlm(vlm=vlm, result=result, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_vlm(
|
||||
cls, vlm: Union[VLM, str], result: Union[str, dict], **kwargs: Any
|
||||
) -> Detections:
|
||||
vlm = validate_vlm_parameters(vlm, result, kwargs)
|
||||
|
||||
if vlm == VLM.PALIGEMMA:
|
||||
xyxy, class_id, class_name = from_paligemma(result, **kwargs)
|
||||
data = {CLASS_NAME_DATA_FIELD: class_name}
|
||||
return cls(xyxy=xyxy, class_id=class_id, data=data)
|
||||
|
||||
if lmm == LMM.FLORENCE_2:
|
||||
assert isinstance(result, dict)
|
||||
if vlm == VLM.QWEN_2_5_VL:
|
||||
xyxy, class_id, class_name = from_qwen_2_5_vl(result, **kwargs)
|
||||
data = {CLASS_NAME_DATA_FIELD: class_name}
|
||||
return cls(xyxy=xyxy, class_id=class_id, data=data)
|
||||
|
||||
if vlm == VLM.FLORENCE_2:
|
||||
xyxy, labels, mask, xyxyxyxy = from_florence_2(result, **kwargs)
|
||||
if len(xyxy) == 0:
|
||||
return cls.empty()
|
||||
@@ -858,8 +901,6 @@ class Detections:
|
||||
|
||||
return cls(xyxy=xyxy, mask=mask, data=data)
|
||||
|
||||
raise ValueError(f"Unsupported LMM: {lmm}")
|
||||
|
||||
@classmethod
|
||||
def from_easyocr(cls, easyocr_results: list) -> Detections:
|
||||
"""
|
||||
@@ -1026,16 +1067,16 @@ class Detections:
|
||||
detections_1 = sv.Detections(
|
||||
xyxy=np.array([[15, 15, 100, 100], [200, 200, 300, 300]]),
|
||||
class_id=np.array([1, 2]),
|
||||
data={'feature_vector': np.array([0.1, 0.2)])}
|
||||
)
|
||||
data={'feature_vector': np.array([0.1, 0.2])}
|
||||
)
|
||||
|
||||
detections_2 = sv.Detections(
|
||||
xyxy=np.array([[30, 30, 120, 120]]),
|
||||
class_id=np.array([1]),
|
||||
data={'feature_vector': [np.array([0.3])]}
|
||||
)
|
||||
data={'feature_vector': np.array([0.3])}
|
||||
)
|
||||
|
||||
merged_detections = Detections.merge([detections_1, detections_2])
|
||||
merged_detections = sv.Detections.merge([detections_1, detections_2])
|
||||
|
||||
merged_detections.xyxy
|
||||
array([[ 15, 15, 100, 100],
|
||||
@@ -1201,6 +1242,8 @@ class Detections:
|
||||
"""
|
||||
if isinstance(index, str):
|
||||
return self.data.get(index)
|
||||
if self.is_empty():
|
||||
return self
|
||||
if isinstance(index, int):
|
||||
index = [index]
|
||||
return Detections(
|
||||
@@ -1303,9 +1346,9 @@ class Detections:
|
||||
if len(self) == 0:
|
||||
return self
|
||||
|
||||
assert (
|
||||
self.confidence is not None
|
||||
), "Detections confidence must be given for NMS to be executed."
|
||||
assert self.confidence is not None, (
|
||||
"Detections confidence must be given for NMS to be executed."
|
||||
)
|
||||
|
||||
if class_agnostic:
|
||||
predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1)))
|
||||
@@ -1359,9 +1402,9 @@ class Detections:
|
||||
if len(self) == 0:
|
||||
return self
|
||||
|
||||
assert (
|
||||
self.confidence is not None
|
||||
), "Detections confidence must be given for NMM to be executed."
|
||||
assert self.confidence is not None, (
|
||||
"Detections confidence must be given for NMM to be executed."
|
||||
)
|
||||
|
||||
if class_agnostic:
|
||||
predictions = np.hstack((self.xyxy, self.confidence.reshape(-1, 1)))
|
||||
|
||||
@@ -42,6 +42,10 @@ class LineZone:
|
||||
to inside.
|
||||
out_count (int): The number of objects that have crossed the line from inside
|
||||
to outside.
|
||||
in_count_per_class (Dict[int, int]): Number of objects of each class that have
|
||||
crossed the line from outside to inside.
|
||||
out_count_per_class (Dict[int, int]): Number of objects of each class that have
|
||||
crossed the line from inside to outside.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@@ -75,7 +79,7 @@ class LineZone:
|
||||
Position.BOTTOM_LEFT,
|
||||
Position.BOTTOM_RIGHT,
|
||||
),
|
||||
crossing_acceptance_threshold: int = 1,
|
||||
minimum_crossing_threshold: int = 1,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
@@ -86,7 +90,7 @@ class LineZone:
|
||||
to consider when deciding on whether the detection
|
||||
has passed the line counter or not. By default, this
|
||||
contains the four corners of the detection's bounding box
|
||||
crossing_acceptance_threshold (int): Detection needs to be seen
|
||||
minimum_crossing_threshold (int): Detection needs to be seen
|
||||
on the other side of the line for this many frames to be
|
||||
considered as having crossed the line. This is useful when
|
||||
dealing with unstable bounding boxes or when detections
|
||||
@@ -94,7 +98,7 @@ class LineZone:
|
||||
"""
|
||||
self.vector = Vector(start=start, end=end)
|
||||
self.limits = self._calculate_region_of_interest_limits(vector=self.vector)
|
||||
self.crossing_history_length = max(2, crossing_acceptance_threshold + 1)
|
||||
self.crossing_history_length = max(2, minimum_crossing_threshold + 1)
|
||||
self.crossing_state_history: Dict[int, Deque[bool]] = defaultdict(
|
||||
lambda: deque(maxlen=self.crossing_history_length)
|
||||
)
|
||||
@@ -107,34 +111,18 @@ class LineZone:
|
||||
|
||||
@property
|
||||
def in_count(self) -> int:
|
||||
"""
|
||||
Number of objects that have crossed the line from
|
||||
outside to inside.
|
||||
"""
|
||||
return sum(self._in_count_per_class.values())
|
||||
|
||||
@property
|
||||
def out_count(self) -> int:
|
||||
"""
|
||||
Number of objects that have crossed the line from
|
||||
inside to outside.
|
||||
"""
|
||||
return sum(self._out_count_per_class.values())
|
||||
|
||||
@property
|
||||
def in_count_per_class(self) -> Dict[int, int]:
|
||||
"""
|
||||
Number of objects of each class that have crossed
|
||||
the line from outside to inside.
|
||||
"""
|
||||
return dict(self._in_count_per_class)
|
||||
|
||||
@property
|
||||
def out_count_per_class(self) -> Dict[int, int]:
|
||||
"""
|
||||
Number of objects of each class that have crossed the line
|
||||
from inside to outside.
|
||||
"""
|
||||
return dict(self._out_count_per_class)
|
||||
|
||||
def trigger(self, detections: Detections) -> Tuple[np.ndarray, np.ndarray]:
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
import re
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from supervision.detection.utils import polygon_to_mask, polygon_to_xyxy
|
||||
|
||||
|
||||
class LMM(Enum):
|
||||
PALIGEMMA = "paligemma"
|
||||
FLORENCE_2 = "florence_2"
|
||||
|
||||
|
||||
RESULT_TYPES: Dict[LMM, type] = {LMM.PALIGEMMA: str, LMM.FLORENCE_2: dict}
|
||||
|
||||
REQUIRED_ARGUMENTS: Dict[LMM, List[str]] = {
|
||||
LMM.PALIGEMMA: ["resolution_wh"],
|
||||
LMM.FLORENCE_2: ["resolution_wh"],
|
||||
}
|
||||
|
||||
ALLOWED_ARGUMENTS: Dict[LMM, List[str]] = {
|
||||
LMM.PALIGEMMA: ["resolution_wh", "classes"],
|
||||
LMM.FLORENCE_2: ["resolution_wh"],
|
||||
}
|
||||
|
||||
SUPPORTED_TASKS_FLORENCE_2 = [
|
||||
"<OD>",
|
||||
"<CAPTION_TO_PHRASE_GROUNDING>",
|
||||
"<DENSE_REGION_CAPTION>",
|
||||
"<REGION_PROPOSAL>",
|
||||
"<OCR_WITH_REGION>",
|
||||
"<REFERRING_EXPRESSION_SEGMENTATION>",
|
||||
"<REGION_TO_SEGMENTATION>",
|
||||
"<OPEN_VOCABULARY_DETECTION>",
|
||||
"<REGION_TO_CATEGORY>",
|
||||
"<REGION_TO_DESCRIPTION>",
|
||||
]
|
||||
|
||||
|
||||
def validate_lmm_parameters(
|
||||
lmm: Union[LMM, str], result: Any, kwargs: Dict[str, Any]
|
||||
) -> LMM:
|
||||
if isinstance(lmm, str):
|
||||
try:
|
||||
lmm = LMM(lmm.lower())
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"Invalid lmm value: {lmm}. Must be one of {[e.value for e in LMM]}"
|
||||
)
|
||||
|
||||
if not isinstance(result, RESULT_TYPES[lmm]):
|
||||
raise ValueError(
|
||||
f"Invalid LMM result type: {type(result)}. Must be {RESULT_TYPES[lmm]}"
|
||||
)
|
||||
|
||||
required_args = REQUIRED_ARGUMENTS.get(lmm, [])
|
||||
for arg in required_args:
|
||||
if arg not in kwargs:
|
||||
raise ValueError(f"Missing required argument: {arg}")
|
||||
|
||||
allowed_args = ALLOWED_ARGUMENTS.get(lmm, [])
|
||||
for arg in kwargs:
|
||||
if arg not in allowed_args:
|
||||
raise ValueError(f"Argument {arg} is not allowed for {lmm.name}")
|
||||
|
||||
return lmm
|
||||
|
||||
|
||||
def from_paligemma(
|
||||
result: str, resolution_wh: Tuple[int, int], classes: Optional[List[str]] = None
|
||||
) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]:
|
||||
w, h = resolution_wh
|
||||
pattern = re.compile(
|
||||
r"(?<!<loc\d{4}>)<loc(\d{4})><loc(\d{4})><loc(\d{4})><loc(\d{4})> ([\w\s\-]+)"
|
||||
)
|
||||
matches = pattern.findall(result)
|
||||
matches = np.array(matches) if matches else np.empty((0, 5))
|
||||
|
||||
xyxy, class_name = matches[:, [1, 0, 3, 2]], matches[:, 4]
|
||||
xyxy = xyxy.astype(int) / 1024 * np.array([w, h, w, h])
|
||||
class_name = np.char.strip(class_name.astype(str))
|
||||
class_id = None
|
||||
|
||||
if classes is not None:
|
||||
mask = np.array([name in classes for name in class_name]).astype(bool)
|
||||
xyxy, class_name = xyxy[mask], class_name[mask]
|
||||
class_id = np.array([classes.index(name) for name in class_name])
|
||||
|
||||
return xyxy, class_id, class_name
|
||||
|
||||
|
||||
def from_florence_2(
|
||||
result: dict, resolution_wh: Tuple[int, int]
|
||||
) -> Tuple[
|
||||
np.ndarray, Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray]
|
||||
]:
|
||||
"""
|
||||
Parse results from the Florence 2 multi-model model.
|
||||
https://huggingface.co/microsoft/Florence-2-large
|
||||
|
||||
Parameters:
|
||||
result: dict containing the model output
|
||||
|
||||
Returns:
|
||||
xyxy (np.ndarray): An array of shape `(n, 4)` containing
|
||||
the bounding boxes coordinates in format `[x1, y1, x2, y2]`
|
||||
labels: (Optional[np.ndarray]): An array of shape `(n,)` containing
|
||||
the class labels for each bounding box
|
||||
masks: (Optional[np.ndarray]): An array of shape `(n, h, w)` containing
|
||||
the segmentation masks for each bounding box
|
||||
obb_boxes: (Optional[np.ndarray]): An array of shape `(n, 4, 2)` containing
|
||||
oriented bounding boxes.
|
||||
"""
|
||||
assert len(result) == 1, f"Expected result with a single element. Got: {result}"
|
||||
task = next(iter(result.keys()))
|
||||
if task not in SUPPORTED_TASKS_FLORENCE_2:
|
||||
raise ValueError(
|
||||
f"{task} not supported. Supported tasks are: {SUPPORTED_TASKS_FLORENCE_2}"
|
||||
)
|
||||
result = result[task]
|
||||
|
||||
if task in ["<OD>", "<CAPTION_TO_PHRASE_GROUNDING>", "<DENSE_REGION_CAPTION>"]:
|
||||
xyxy = np.array(result["bboxes"], dtype=np.float32)
|
||||
labels = np.array(result["labels"])
|
||||
return xyxy, labels, None, None
|
||||
|
||||
if task == "<REGION_PROPOSAL>":
|
||||
xyxy = np.array(result["bboxes"], dtype=np.float32)
|
||||
# provides labels, but they are ["", "", "", ...]
|
||||
return xyxy, None, None, None
|
||||
|
||||
if task == "<OCR_WITH_REGION>":
|
||||
xyxyxyxy = np.array(result["quad_boxes"], dtype=np.float32)
|
||||
xyxyxyxy = xyxyxyxy.reshape(-1, 4, 2)
|
||||
xyxy = np.array([polygon_to_xyxy(polygon) for polygon in xyxyxyxy])
|
||||
labels = np.array(result["labels"])
|
||||
return xyxy, labels, None, xyxyxyxy
|
||||
|
||||
if task in ["<REFERRING_EXPRESSION_SEGMENTATION>", "<REGION_TO_SEGMENTATION>"]:
|
||||
xyxy_list = []
|
||||
masks_list = []
|
||||
for polygons_of_same_class in result["polygons"]:
|
||||
for polygon in polygons_of_same_class:
|
||||
polygon = np.reshape(polygon, (-1, 2)).astype(np.int32)
|
||||
mask = polygon_to_mask(polygon, resolution_wh).astype(bool)
|
||||
masks_list.append(mask)
|
||||
xyxy = polygon_to_xyxy(polygon)
|
||||
xyxy_list.append(xyxy)
|
||||
# per-class labels also provided, but they are ["", "", "", ...]
|
||||
# when we figure out how to set class names, we can do
|
||||
# zip(result["labels"], result["polygons"])
|
||||
xyxy = np.array(xyxy_list, dtype=np.float32)
|
||||
masks = np.array(masks_list)
|
||||
return xyxy, None, masks, None
|
||||
|
||||
if task == "<OPEN_VOCABULARY_DETECTION>":
|
||||
xyxy = np.array(result["bboxes"], dtype=np.float32)
|
||||
labels = np.array(result["bboxes_labels"])
|
||||
# Also has "polygons" and "polygons_labels", but they don't seem to be used
|
||||
return xyxy, labels, None, None
|
||||
|
||||
if task in ["<REGION_TO_CATEGORY>", "<REGION_TO_DESCRIPTION>"]:
|
||||
assert isinstance(
|
||||
result, str
|
||||
), f"Expected string as <REGION_TO_CATEGORY> result, got {type(result)}"
|
||||
|
||||
if result == "No object detected.":
|
||||
return np.empty((0, 4), dtype=np.float32), np.array([]), None, None
|
||||
|
||||
pattern = re.compile(r"<loc_(\d+)><loc_(\d+)><loc_(\d+)><loc_(\d+)>")
|
||||
match = pattern.search(result)
|
||||
assert (
|
||||
match is not None
|
||||
), f"Expected string to end in location tags, but got {result}"
|
||||
|
||||
w, h = resolution_wh
|
||||
xyxy = np.array([match.groups()], dtype=np.float32)
|
||||
xyxy *= np.array([w, h, w, h]) / 1000
|
||||
result_string = result[: match.start()]
|
||||
labels = np.array([result_string])
|
||||
return xyxy, labels, None, None
|
||||
|
||||
assert False, f"Unimplemented task: {task}"
|
||||
@@ -226,8 +226,7 @@ def box_non_max_merge(
|
||||
for merge_group in merge_groups:
|
||||
if len(merge_group) == 0:
|
||||
raise ValueError(
|
||||
f"Empty group detected when non-max-merging "
|
||||
f"detections: {merge_groups}"
|
||||
f"Empty group detected when non-max-merging detections: {merge_groups}"
|
||||
)
|
||||
return merge_groups
|
||||
|
||||
|
||||
+238
-14
@@ -11,6 +11,25 @@ from supervision.geometry.core import Vector
|
||||
MIN_POLYGON_POINT_COUNT = 3
|
||||
|
||||
|
||||
def xyxy_to_polygons(box: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Convert an array of boxes to an array of polygons.
|
||||
Retains the input datatype.
|
||||
|
||||
Args:
|
||||
box (np.ndarray): An array of boxes (N, 4), where each box is represented as a
|
||||
list of four coordinates in the format `(x_min, y_min, x_max, y_max)`.
|
||||
|
||||
Returns:
|
||||
np.ndarray: An array of polygons (N, 4, 2), where each polygon is
|
||||
represented as a list of four coordinates in the format `(x, y)`.
|
||||
"""
|
||||
polygon = np.zeros((box.shape[0], 4, 2), dtype=box.dtype)
|
||||
polygon[:, :, 0] = box[:, [0, 2, 2, 0]]
|
||||
polygon[:, :, 1] = box[:, [1, 1, 3, 3]]
|
||||
return polygon
|
||||
|
||||
|
||||
def polygon_to_mask(polygon: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray:
|
||||
"""Generate a mask from a polygon.
|
||||
|
||||
@@ -302,6 +321,43 @@ def xywh_to_xyxy(xywh: np.ndarray) -> np.ndarray:
|
||||
return xyxy
|
||||
|
||||
|
||||
def xyxy_to_xywh(xyxy: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Converts bounding box coordinates from `(x_min, y_min, x_max, y_max)`
|
||||
format to `(x, y, width, height)` format.
|
||||
|
||||
Args:
|
||||
xyxy (np.ndarray): A numpy array of shape `(N, 4)` where each row
|
||||
corresponds to a bounding box in the format `(x_min, y_min, x_max,
|
||||
y_max)`.
|
||||
|
||||
Returns:
|
||||
np.ndarray: A numpy array of shape `(N, 4)` where each row corresponds
|
||||
to a bounding box in the format `(x, y, width, height)`.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
|
||||
xyxy = np.array([
|
||||
[10, 20, 40, 60],
|
||||
[15, 25, 50, 70]
|
||||
])
|
||||
|
||||
sv.xyxy_to_xywh(xyxy=xyxy)
|
||||
# array([
|
||||
# [10, 20, 30, 40],
|
||||
# [15, 25, 35, 45]
|
||||
# ])
|
||||
```
|
||||
"""
|
||||
xywh = xyxy.copy()
|
||||
xywh[:, 2] = xyxy[:, 2] - xyxy[:, 0]
|
||||
xywh[:, 3] = xyxy[:, 3] - xyxy[:, 1]
|
||||
return xywh
|
||||
|
||||
|
||||
def xcycwh_to_xyxy(xcycwh: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Converts bounding box coordinates from `(center_x, center_y, width, height)`
|
||||
@@ -341,6 +397,56 @@ def xcycwh_to_xyxy(xcycwh: np.ndarray) -> np.ndarray:
|
||||
return xyxy
|
||||
|
||||
|
||||
def xyxy_to_xcycarh(xyxy: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Converts bounding box coordinates from `(x_min, y_min, x_max, y_max)`
|
||||
into measurement space to format `(center x, center y, aspect ratio, height)`,
|
||||
where the aspect ratio is `width / height`.
|
||||
|
||||
Args:
|
||||
xyxy (np.ndarray): Bounding box in format `(x1, y1, x2, y2)`.
|
||||
Expected shape is `(N, 4)`.
|
||||
Returns:
|
||||
np.ndarray: Bounding box in format
|
||||
`(center x, center y, aspect ratio, height)`. Shape `(N, 4)`.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
|
||||
xyxy = np.array([
|
||||
[10, 20, 40, 60],
|
||||
[15, 25, 50, 70]
|
||||
])
|
||||
|
||||
sv.xyxy_to_xcycarh(xyxy=xyxy)
|
||||
# array([
|
||||
# [25. , 40. , 0.75, 40. ],
|
||||
# [32.5 , 47.5 , 0.77777778, 45. ]
|
||||
# ])
|
||||
```
|
||||
|
||||
"""
|
||||
if xyxy.size == 0:
|
||||
return np.empty((0, 4), dtype=float)
|
||||
|
||||
x1, y1, x2, y2 = xyxy.T
|
||||
width = x2 - x1
|
||||
height = y2 - y1
|
||||
center_x = x1 + width / 2
|
||||
center_y = y1 + height / 2
|
||||
|
||||
aspect_ratio = np.divide(
|
||||
width,
|
||||
height,
|
||||
out=np.zeros_like(width, dtype=float),
|
||||
where=height != 0,
|
||||
)
|
||||
result = np.column_stack((center_x, center_y, aspect_ratio, height))
|
||||
return result.astype(float)
|
||||
|
||||
|
||||
def mask_to_xyxy(masks: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Converts a 3D `np.array` of 2D bool masks into a 2D `np.array` of bounding boxes.
|
||||
@@ -701,25 +807,71 @@ def move_masks(
|
||||
masks (npt.NDArray[np.bool_]): A 3D array of binary masks corresponding to the
|
||||
predictions. Shape: `(N, H, W)`, where N is the number of predictions, and
|
||||
H, W are the dimensions of each mask.
|
||||
offset (npt.NDArray[np.int32]): An array of shape `(2,)` containing non-negative
|
||||
int values `[dx, dy]`.
|
||||
offset (npt.NDArray[np.int32]): An array of shape `(2,)` containing int values
|
||||
`[dx, dy]`. Supports both positive and negative values for bidirectional
|
||||
movement.
|
||||
resolution_wh (Tuple[int, int]): The width and height of the desired mask
|
||||
resolution.
|
||||
|
||||
Returns:
|
||||
(npt.NDArray[np.bool_]) repositioned masks, optionally padded to the specified
|
||||
shape.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
|
||||
mask = np.array([[[False, False, False, False],
|
||||
[False, True, True, False],
|
||||
[False, True, True, False],
|
||||
[False, False, False, False]]], dtype=bool)
|
||||
|
||||
offset = np.array([1, 1])
|
||||
sv.move_masks(mask, offset, resolution_wh=(4, 4))
|
||||
# array([[[False, False, False, False],
|
||||
# [False, False, False, False],
|
||||
# [False, False, True, True],
|
||||
# [False, False, True, True]]], dtype=bool)
|
||||
|
||||
offset = np.array([-2, 2])
|
||||
sv.move_masks(mask, offset, resolution_wh=(4, 4))
|
||||
# array([[[False, False, False, False],
|
||||
# [False, False, False, False],
|
||||
# [False, False, False, False],
|
||||
# [True, False, False, False]]], dtype=bool)
|
||||
```
|
||||
"""
|
||||
|
||||
if offset[0] < 0 or offset[1] < 0:
|
||||
raise ValueError(f"Offset values must be non-negative integers. Got: {offset}")
|
||||
|
||||
mask_array = np.full((masks.shape[0], resolution_wh[1], resolution_wh[0]), False)
|
||||
mask_array[
|
||||
:,
|
||||
offset[1] : masks.shape[1] + offset[1],
|
||||
offset[0] : masks.shape[2] + offset[0],
|
||||
] = masks
|
||||
|
||||
if offset[0] < 0:
|
||||
source_x_start = -offset[0]
|
||||
source_x_end = min(masks.shape[2], resolution_wh[0] - offset[0])
|
||||
destination_x_start = 0
|
||||
destination_x_end = min(resolution_wh[0], masks.shape[2] + offset[0])
|
||||
else:
|
||||
source_x_start = 0
|
||||
source_x_end = min(masks.shape[2], resolution_wh[0] - offset[0])
|
||||
destination_x_start = offset[0]
|
||||
destination_x_end = offset[0] + source_x_end - source_x_start
|
||||
|
||||
if offset[1] < 0:
|
||||
source_y_start = -offset[1]
|
||||
source_y_end = min(masks.shape[1], resolution_wh[1] - offset[1])
|
||||
destination_y_start = 0
|
||||
destination_y_end = min(resolution_wh[1], masks.shape[1] + offset[1])
|
||||
else:
|
||||
source_y_start = 0
|
||||
source_y_end = min(masks.shape[1], resolution_wh[1] - offset[1])
|
||||
destination_y_start = offset[1]
|
||||
destination_y_end = offset[1] + source_y_end - source_y_start
|
||||
|
||||
if source_x_end > source_x_start and source_y_end > source_y_start:
|
||||
mask_array[
|
||||
:,
|
||||
destination_y_start:destination_y_end,
|
||||
destination_x_start:destination_x_end,
|
||||
] = masks[:, source_y_start:source_y_end, source_x_start:source_x_end]
|
||||
|
||||
return mask_array
|
||||
|
||||
@@ -919,11 +1071,27 @@ def merge_metadata(metadata_list: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
merged_metadata: Dict[str, Any] = {}
|
||||
for metadata in metadata_list:
|
||||
for key, value in metadata.items():
|
||||
if key in merged_metadata:
|
||||
if key not in merged_metadata:
|
||||
merged_metadata[key] = value
|
||||
continue
|
||||
|
||||
other_value = merged_metadata[key]
|
||||
if isinstance(value, np.ndarray) and isinstance(other_value, np.ndarray):
|
||||
if not np.array_equal(merged_metadata[key], value):
|
||||
raise ValueError(
|
||||
f"Conflicting metadata for key: '{key}': "
|
||||
"{type(value)}, {type(other_value)}."
|
||||
)
|
||||
elif isinstance(value, np.ndarray) or isinstance(other_value, np.ndarray):
|
||||
# Since [] == np.array([]).
|
||||
raise ValueError(
|
||||
f"Conflicting metadata for key: '{key}': "
|
||||
"{type(value)}, {type(other_value)}."
|
||||
)
|
||||
else:
|
||||
print("hm")
|
||||
if merged_metadata[key] != value:
|
||||
raise ValueError(f"Conflicting metadata for key: '{key}'.")
|
||||
else:
|
||||
merged_metadata[key] = value
|
||||
|
||||
return merged_metadata
|
||||
|
||||
@@ -1101,3 +1269,59 @@ def cross_product(anchors: np.ndarray, vector: Vector) -> np.ndarray:
|
||||
)
|
||||
vector_start = np.array([vector.start.x, vector.start.y])
|
||||
return np.cross(vector_at_zero, anchors - vector_start)
|
||||
|
||||
|
||||
def spread_out_boxes(
|
||||
xyxy: np.ndarray,
|
||||
max_iterations: int = 100,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Spread out boxes that overlap with each other.
|
||||
|
||||
Args:
|
||||
xyxy: Numpy array of shape (N, 4) where N is the number of boxes.
|
||||
max_iterations: Maximum number of iterations to run the algorithm for.
|
||||
"""
|
||||
if len(xyxy) == 0:
|
||||
return xyxy
|
||||
|
||||
xyxy_padded = pad_boxes(xyxy, px=1)
|
||||
for _ in range(max_iterations):
|
||||
# NxN
|
||||
iou = box_iou_batch(xyxy_padded, xyxy_padded)
|
||||
np.fill_diagonal(iou, 0)
|
||||
if np.all(iou == 0):
|
||||
break
|
||||
|
||||
overlap_mask = iou > 0
|
||||
|
||||
# Nx2
|
||||
centers = (xyxy_padded[:, :2] + xyxy_padded[:, 2:]) / 2
|
||||
|
||||
# NxNx2
|
||||
delta_centers = centers[:, np.newaxis, :] - centers[np.newaxis, :, :]
|
||||
delta_centers *= overlap_mask[:, :, np.newaxis]
|
||||
|
||||
# Nx2
|
||||
delta_sum = np.sum(delta_centers, axis=1)
|
||||
delta_magnitude = np.linalg.norm(delta_sum, axis=1, keepdims=True)
|
||||
direction_vectors = np.divide(
|
||||
delta_sum,
|
||||
delta_magnitude,
|
||||
out=np.zeros_like(delta_sum),
|
||||
where=delta_magnitude != 0,
|
||||
)
|
||||
|
||||
force_vectors = np.sum(iou, axis=1)
|
||||
force_vectors = force_vectors[:, np.newaxis] * direction_vectors
|
||||
|
||||
force_vectors *= 10
|
||||
force_vectors[(force_vectors > 0) & (force_vectors < 2)] = 2
|
||||
force_vectors[(force_vectors < 0) & (force_vectors > -2)] = -2
|
||||
|
||||
force_vectors = force_vectors.astype(int)
|
||||
|
||||
xyxy_padded[:, [0, 1]] += force_vectors
|
||||
xyxy_padded[:, [2, 3]] += force_vectors
|
||||
|
||||
return pad_boxes(xyxy_padded, px=-1)
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
import json
|
||||
import re
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from supervision.detection.utils import polygon_to_mask, polygon_to_xyxy
|
||||
from supervision.utils.internal import deprecated
|
||||
|
||||
|
||||
@deprecated(
|
||||
"`LMM` enum is deprecated and will be removed in "
|
||||
"`supervision-0.31.0`. Use VLM instead."
|
||||
)
|
||||
class LMM(Enum):
|
||||
PALIGEMMA = "paligemma"
|
||||
FLORENCE_2 = "florence_2"
|
||||
QWEN_2_5_VL = "qwen_2_5_vl"
|
||||
|
||||
|
||||
class VLM(Enum):
|
||||
PALIGEMMA = "paligemma"
|
||||
FLORENCE_2 = "florence_2"
|
||||
QWEN_2_5_VL = "qwen_2_5_vl"
|
||||
|
||||
|
||||
RESULT_TYPES: Dict[VLM, type] = {
|
||||
VLM.PALIGEMMA: str,
|
||||
VLM.FLORENCE_2: dict,
|
||||
VLM.QWEN_2_5_VL: str,
|
||||
}
|
||||
|
||||
REQUIRED_ARGUMENTS: Dict[VLM, List[str]] = {
|
||||
VLM.PALIGEMMA: ["resolution_wh"],
|
||||
VLM.FLORENCE_2: ["resolution_wh"],
|
||||
VLM.QWEN_2_5_VL: ["input_wh", "resolution_wh"],
|
||||
}
|
||||
|
||||
ALLOWED_ARGUMENTS: Dict[VLM, List[str]] = {
|
||||
VLM.PALIGEMMA: ["resolution_wh", "classes"],
|
||||
VLM.FLORENCE_2: ["resolution_wh"],
|
||||
VLM.QWEN_2_5_VL: ["input_wh", "resolution_wh", "classes"],
|
||||
}
|
||||
|
||||
SUPPORTED_TASKS_FLORENCE_2 = [
|
||||
"<OD>",
|
||||
"<CAPTION_TO_PHRASE_GROUNDING>",
|
||||
"<DENSE_REGION_CAPTION>",
|
||||
"<REGION_PROPOSAL>",
|
||||
"<OCR_WITH_REGION>",
|
||||
"<REFERRING_EXPRESSION_SEGMENTATION>",
|
||||
"<REGION_TO_SEGMENTATION>",
|
||||
"<OPEN_VOCABULARY_DETECTION>",
|
||||
"<REGION_TO_CATEGORY>",
|
||||
"<REGION_TO_DESCRIPTION>",
|
||||
]
|
||||
|
||||
|
||||
def validate_vlm_parameters(
|
||||
vlm: Union[VLM, str], result: Any, kwargs: Dict[str, Any]
|
||||
) -> VLM:
|
||||
if isinstance(vlm, str):
|
||||
try:
|
||||
vlm = VLM(vlm.lower())
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"Invalid vlm value: {vlm}. Must be one of {[e.value for e in VLM]}"
|
||||
)
|
||||
|
||||
if not isinstance(result, RESULT_TYPES[vlm]):
|
||||
raise ValueError(
|
||||
f"Invalid VLM result type: {type(result)}. Must be {RESULT_TYPES[vlm]}"
|
||||
)
|
||||
|
||||
required_args = REQUIRED_ARGUMENTS.get(vlm, [])
|
||||
for arg in required_args:
|
||||
if arg not in kwargs:
|
||||
raise ValueError(f"Missing required argument: {arg}")
|
||||
|
||||
allowed_args = ALLOWED_ARGUMENTS.get(vlm, [])
|
||||
for arg in kwargs:
|
||||
if arg not in allowed_args:
|
||||
raise ValueError(f"Argument {arg} is not allowed for {vlm.name}")
|
||||
|
||||
return vlm
|
||||
|
||||
|
||||
def from_paligemma(
|
||||
result: str, resolution_wh: Tuple[int, int], classes: Optional[List[str]] = None
|
||||
) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]:
|
||||
"""
|
||||
Parse bounding boxes from paligemma-formatted text, scale them to the specified
|
||||
resolution, and optionally filter by classes.
|
||||
|
||||
Args:
|
||||
result: String containing paligemma-formatted locations and labels.
|
||||
resolution_wh: Tuple (width, height) to which we scale the box coordinates.
|
||||
classes: Optional list of valid class names. If provided, boxes and labels not
|
||||
in this list are filtered out.
|
||||
|
||||
Returns:
|
||||
xyxy (np.ndarray): An array of shape `(n, 4)` containing
|
||||
the bounding boxes coordinates in format `[x1, y1, x2, y2]`.
|
||||
class_id (Optional[np.ndarray]): An array of shape `(n,)` containing
|
||||
the class indices for each bounding box (or `None` if classes is not
|
||||
provided).
|
||||
class_name (np.ndarray): An array of shape `(n,)` containing
|
||||
the class labels for each bounding box.
|
||||
"""
|
||||
|
||||
w, h = resolution_wh
|
||||
if w <= 0 or h <= 0:
|
||||
raise ValueError(
|
||||
f"Both dimensions in resolution_wh must be positive. Got ({w}, {h})."
|
||||
)
|
||||
|
||||
pattern = re.compile(
|
||||
r"(?<!<loc\d{4}>)<loc(\d{4})><loc(\d{4})><loc(\d{4})><loc(\d{4})> ([\w\s\-]+)"
|
||||
)
|
||||
matches = pattern.findall(result)
|
||||
matches = np.array(matches) if matches else np.empty((0, 5))
|
||||
|
||||
if matches.shape[0] == 0:
|
||||
return np.empty((0, 4)), None, np.empty(0, dtype=str)
|
||||
|
||||
xyxy, class_name = matches[:, [1, 0, 3, 2]], matches[:, 4]
|
||||
xyxy = xyxy.astype(int) / 1024 * np.array([w, h, w, h])
|
||||
class_name = np.char.strip(class_name.astype(str))
|
||||
class_id = None
|
||||
|
||||
if classes is not None:
|
||||
mask = np.array([name in classes for name in class_name], dtype=bool)
|
||||
xyxy = xyxy[mask]
|
||||
class_name = class_name[mask]
|
||||
class_id = np.array([classes.index(name) for name in class_name])
|
||||
|
||||
return xyxy, class_id, class_name
|
||||
|
||||
|
||||
def from_qwen_2_5_vl(
|
||||
result: str,
|
||||
input_wh: Tuple[int, int],
|
||||
resolution_wh: Tuple[int, int],
|
||||
classes: Optional[List[str]] = None,
|
||||
) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]:
|
||||
"""
|
||||
Parse and scale bounding boxes from Qwen-2.5-VL style JSON output.
|
||||
|
||||
The JSON is expected to be enclosed in triple backticks with the format:
|
||||
```json
|
||||
[
|
||||
{"bbox_2d": [x1, y1, x2, y2], "label": "some class name"},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
Args:
|
||||
result: String containing the JSON snippet enclosed by triple backticks.
|
||||
input_wh: (input_width, input_height) describing the original bounding box
|
||||
scale.
|
||||
resolution_wh: (output_width, output_height) to which we rescale the boxes.
|
||||
classes: Optional list of valid class names. If provided, returned boxes/labels
|
||||
are filtered to only those classes found here.
|
||||
|
||||
Returns:
|
||||
xyxy (np.ndarray): An array of shape `(n, 4)` containing
|
||||
the bounding boxes coordinates in format `[x1, y1, x2, y2]`
|
||||
class_id (Optional[np.ndarray]): An array of shape `(n,)` containing
|
||||
the class indices for each bounding box (or None if `classes` is not
|
||||
provided)
|
||||
class_name (np.ndarray): An array of shape `(n,)` containing
|
||||
the class labels for each bounding box
|
||||
"""
|
||||
in_w, in_h = input_wh
|
||||
out_w, out_h = resolution_wh
|
||||
|
||||
if in_w <= 0 or in_h <= 0 or out_w <= 0 or out_h <= 0:
|
||||
raise ValueError(
|
||||
f"Both input and resolution dimensions must be positive. "
|
||||
f"Got input_wh=({in_w}, {in_h}), resolution_wh=({out_w}, {out_h})."
|
||||
)
|
||||
|
||||
pattern = re.compile(r"```json\s*(.*?)\s*```", re.DOTALL)
|
||||
|
||||
match = pattern.search(result)
|
||||
if not match:
|
||||
return np.empty((0, 4)), None, np.empty((0,), dtype=str)
|
||||
|
||||
json_snippet = match.group(1)
|
||||
|
||||
try:
|
||||
data = json.loads(json_snippet)
|
||||
except json.JSONDecodeError:
|
||||
return np.empty((0, 4)), None, np.empty((0,), dtype=str)
|
||||
|
||||
boxes_list = []
|
||||
labels_list = []
|
||||
|
||||
for item in data:
|
||||
if "bbox_2d" not in item or "label" not in item:
|
||||
continue
|
||||
boxes_list.append(item["bbox_2d"])
|
||||
labels_list.append(item["label"])
|
||||
|
||||
if not boxes_list:
|
||||
return np.empty((0, 4)), None, np.empty((0,), dtype=str)
|
||||
|
||||
xyxy = np.array(boxes_list, dtype=float)
|
||||
class_name = np.array(labels_list, dtype=str)
|
||||
|
||||
xyxy = xyxy / [in_w, in_h, in_w, in_h]
|
||||
xyxy = xyxy * [out_w, out_h, out_w, out_h]
|
||||
|
||||
class_id = None
|
||||
|
||||
if classes is not None:
|
||||
mask = np.array([label in classes for label in class_name], dtype=bool)
|
||||
xyxy = xyxy[mask]
|
||||
class_name = class_name[mask]
|
||||
class_id = np.array([classes.index(label) for label in class_name], dtype=int)
|
||||
|
||||
return xyxy, class_id, class_name
|
||||
|
||||
|
||||
def from_florence_2(
|
||||
result: dict, resolution_wh: Tuple[int, int]
|
||||
) -> Tuple[
|
||||
np.ndarray, Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray]
|
||||
]:
|
||||
"""
|
||||
Parse results from the Florence 2 multi-model model.
|
||||
https://huggingface.co/microsoft/Florence-2-large
|
||||
|
||||
Parameters:
|
||||
result: dict containing the model output
|
||||
|
||||
Returns:
|
||||
xyxy (np.ndarray): An array of shape `(n, 4)` containing
|
||||
the bounding boxes coordinates in format `[x1, y1, x2, y2]`
|
||||
labels: (Optional[np.ndarray]): An array of shape `(n,)` containing
|
||||
the class labels for each bounding box
|
||||
masks: (Optional[np.ndarray]): An array of shape `(n, h, w)` containing
|
||||
the segmentation masks for each bounding box
|
||||
obb_boxes: (Optional[np.ndarray]): An array of shape `(n, 4, 2)` containing
|
||||
oriented bounding boxes.
|
||||
"""
|
||||
assert len(result) == 1, f"Expected result with a single element. Got: {result}"
|
||||
task = next(iter(result.keys()))
|
||||
if task not in SUPPORTED_TASKS_FLORENCE_2:
|
||||
raise ValueError(
|
||||
f"{task} not supported. Supported tasks are: {SUPPORTED_TASKS_FLORENCE_2}"
|
||||
)
|
||||
result = result[task]
|
||||
|
||||
if task in ["<OD>", "<CAPTION_TO_PHRASE_GROUNDING>", "<DENSE_REGION_CAPTION>"]:
|
||||
xyxy = np.array(result["bboxes"], dtype=np.float32)
|
||||
labels = np.array(result["labels"])
|
||||
return xyxy, labels, None, None
|
||||
|
||||
if task == "<REGION_PROPOSAL>":
|
||||
xyxy = np.array(result["bboxes"], dtype=np.float32)
|
||||
# provides labels, but they are ["", "", "", ...]
|
||||
return xyxy, None, None, None
|
||||
|
||||
if task == "<OCR_WITH_REGION>":
|
||||
xyxyxyxy = np.array(result["quad_boxes"], dtype=np.float32)
|
||||
xyxyxyxy = xyxyxyxy.reshape(-1, 4, 2)
|
||||
xyxy = np.array([polygon_to_xyxy(polygon) for polygon in xyxyxyxy])
|
||||
labels = np.array(result["labels"])
|
||||
return xyxy, labels, None, xyxyxyxy
|
||||
|
||||
if task in ["<REFERRING_EXPRESSION_SEGMENTATION>", "<REGION_TO_SEGMENTATION>"]:
|
||||
xyxy_list = []
|
||||
masks_list = []
|
||||
for polygons_of_same_class in result["polygons"]:
|
||||
for polygon in polygons_of_same_class:
|
||||
polygon = np.reshape(polygon, (-1, 2)).astype(np.int32)
|
||||
mask = polygon_to_mask(polygon, resolution_wh).astype(bool)
|
||||
masks_list.append(mask)
|
||||
xyxy = polygon_to_xyxy(polygon)
|
||||
xyxy_list.append(xyxy)
|
||||
# per-class labels also provided, but they are ["", "", "", ...]
|
||||
# when we figure out how to set class names, we can do
|
||||
# zip(result["labels"], result["polygons"])
|
||||
xyxy = np.array(xyxy_list, dtype=np.float32)
|
||||
masks = np.array(masks_list)
|
||||
return xyxy, None, masks, None
|
||||
|
||||
if task == "<OPEN_VOCABULARY_DETECTION>":
|
||||
xyxy = np.array(result["bboxes"], dtype=np.float32)
|
||||
labels = np.array(result["bboxes_labels"])
|
||||
# Also has "polygons" and "polygons_labels", but they don't seem to be used
|
||||
return xyxy, labels, None, None
|
||||
|
||||
if task in ["<REGION_TO_CATEGORY>", "<REGION_TO_DESCRIPTION>"]:
|
||||
assert isinstance(result, str), (
|
||||
f"Expected string as <REGION_TO_CATEGORY> result, got {type(result)}"
|
||||
)
|
||||
|
||||
if result == "No object detected.":
|
||||
return np.empty((0, 4), dtype=np.float32), np.array([]), None, None
|
||||
|
||||
pattern = re.compile(r"<loc_(\d+)><loc_(\d+)><loc_(\d+)><loc_(\d+)>")
|
||||
match = pattern.search(result)
|
||||
assert match is not None, (
|
||||
f"Expected string to end in location tags, but got {result}"
|
||||
)
|
||||
|
||||
w, h = resolution_wh
|
||||
xyxy = np.array([match.groups()], dtype=np.float32)
|
||||
xyxy *= np.array([w, h, w, h]) / 1000
|
||||
result_string = result[: match.start()]
|
||||
labels = np.array([result_string])
|
||||
return xyxy, labels, None, None
|
||||
|
||||
assert False, f"Unimplemented task: {task}"
|
||||
@@ -83,15 +83,16 @@ class Color:
|
||||
# Color(r=255, g=255, b=255)
|
||||
```
|
||||
|
||||
| Constant | Hex Code | RGB |
|
||||
|------------|------------|------------------|
|
||||
| `WHITE` | `#FFFFFF` | `(255, 255, 255)`|
|
||||
| `BLACK` | `#000000` | `(0, 0, 0)` |
|
||||
| `RED` | `#FF0000` | `(255, 0, 0)` |
|
||||
| `GREEN` | `#00FF00` | `(0, 255, 0)` |
|
||||
| `BLUE` | `#0000FF` | `(0, 0, 255)` |
|
||||
| `YELLOW` | `#FFFF00` | `(255, 255, 0)` |
|
||||
| `ROBOFLOW` | `#A351FB` | `(163, 81, 251)` |
|
||||
| Constant | Hex Code | RGB |
|
||||
|------------|------------|-------------------|
|
||||
| `WHITE` | `#FFFFFF` | `(255, 255, 255)` |
|
||||
| `BLACK` | `#000000` | `(0, 0, 0)` |
|
||||
| `GREY` | `#808080` | `(128, 128, 128)` |
|
||||
| `RED` | `#FF0000` | `(255, 0, 0)` |
|
||||
| `GREEN` | `#00FF00` | `(0, 255, 0)` |
|
||||
| `BLUE` | `#0000FF` | `(0, 0, 255)` |
|
||||
| `YELLOW` | `#FFFF00` | `(255, 255, 0)` |
|
||||
| `ROBOFLOW` | `#A351FB` | `(163, 81, 251)` |
|
||||
"""
|
||||
|
||||
r: int
|
||||
@@ -235,6 +236,10 @@ class Color:
|
||||
def BLACK(cls) -> Color:
|
||||
return Color.from_hex("#000000")
|
||||
|
||||
@classproperty
|
||||
def GREY(cls) -> Color:
|
||||
return Color.from_hex("#808080")
|
||||
|
||||
@classproperty
|
||||
def RED(cls) -> Color:
|
||||
return Color.from_hex("#FF0000")
|
||||
|
||||
@@ -5,10 +5,11 @@ from typing import List, Optional, Tuple, Union
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from supervision import Rect, pad_boxes
|
||||
from supervision.annotators.base import ImageType
|
||||
from supervision.detection.utils import pad_boxes, spread_out_boxes
|
||||
from supervision.draw.color import Color
|
||||
from supervision.draw.utils import draw_rounded_rectangle
|
||||
from supervision.geometry.core import Rect
|
||||
from supervision.keypoint.core import KeyPoints
|
||||
from supervision.keypoint.skeletons import SKELETONS_BY_VERTEX_COUNT
|
||||
from supervision.utils.conversion import ensure_cv2_image_for_annotation
|
||||
@@ -201,6 +202,7 @@ class VertexLabelAnnotator:
|
||||
text_thickness: int = 1,
|
||||
text_padding: int = 10,
|
||||
border_radius: int = 0,
|
||||
smart_position: bool = False,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
@@ -215,6 +217,7 @@ class VertexLabelAnnotator:
|
||||
text_padding (int): The padding around the text.
|
||||
border_radius (int): The radius of the rounded corners of the
|
||||
boxes. Set to a high value to produce circles.
|
||||
smart_position (bool): Spread out the labels to avoid overlap.
|
||||
"""
|
||||
self.border_radius: int = border_radius
|
||||
self.color: Union[Color, List[Color]] = color
|
||||
@@ -222,6 +225,7 @@ class VertexLabelAnnotator:
|
||||
self.text_scale: float = text_scale
|
||||
self.text_thickness: int = text_thickness
|
||||
self.text_padding: int = text_padding
|
||||
self.smart_position = smart_position
|
||||
|
||||
def annotate(
|
||||
self,
|
||||
@@ -356,9 +360,12 @@ class VertexLabelAnnotator:
|
||||
for anchor, label in zip(anchors, labels)
|
||||
]
|
||||
)
|
||||
|
||||
xyxy_padded = pad_boxes(xyxy=xyxy, px=self.text_padding)
|
||||
|
||||
if self.smart_position:
|
||||
xyxy_padded = spread_out_boxes(xyxy_padded)
|
||||
xyxy = pad_boxes(xyxy=xyxy_padded, px=-self.text_padding)
|
||||
|
||||
for text, color, text_color, box, box_padded in zip(
|
||||
labels, colors, text_colors, xyxy, xyxy_padded
|
||||
):
|
||||
|
||||
+162
-10
@@ -1,13 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
|
||||
from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from supervision.config import CLASS_NAME_DATA_FIELD
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.utils import get_data_item, is_data_equal
|
||||
from supervision.validators import validate_keypoints_fields
|
||||
|
||||
@@ -23,8 +23,8 @@ class KeyPoints:
|
||||
=== "Ultralytics"
|
||||
|
||||
Use [`sv.KeyPoints.from_ultralytics`](/latest/keypoint/core/#supervision.keypoint.core.KeyPoints.from_ultralytics)
|
||||
method, which accepts [YOLOv8](https://github.com/ultralytics/ultralytics)
|
||||
pose result.
|
||||
method, which accepts [YOLOv8-pose](https://docs.ultralytics.com/models/yolov8/), [YOLO11-pose](https://docs.ultralytics.com/models/yolo11/)
|
||||
[pose](https://docs.ultralytics.com/tasks/pose/) result.
|
||||
|
||||
```python
|
||||
import cv2
|
||||
@@ -32,7 +32,7 @@ class KeyPoints:
|
||||
from ultralytics import YOLO
|
||||
|
||||
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
||||
model = YOLO('yolov8s-pose.pt')
|
||||
model = YOLO('yolo11s-pose.pt')
|
||||
|
||||
result = model(image)[0]
|
||||
key_points = sv.KeyPoints.from_ultralytics(result)
|
||||
@@ -61,6 +61,7 @@ class KeyPoints:
|
||||
method, which accepts [MediaPipe](https://github.com/google-ai-edge/mediapipe)
|
||||
pose result.
|
||||
|
||||
|
||||
```python
|
||||
import cv2
|
||||
import mediapipe as mp
|
||||
@@ -91,10 +92,10 @@ class KeyPoints:
|
||||
xy (np.ndarray): An array of shape `(n, m, 2)` containing
|
||||
`n` detected objects, each composed of `m` equally-sized
|
||||
sets of keypoints, where each point is `[x, y]`.
|
||||
confidence (Optional[np.ndarray]): An array of shape
|
||||
`(n, m)` containing the confidence scores of each keypoint.
|
||||
class_id (Optional[np.ndarray]): An array of shape
|
||||
`(n,)` containing the class ids of the detected objects.
|
||||
confidence (Optional[np.ndarray]): An array of shape
|
||||
`(n, m)` containing the confidence scores of each keypoint.
|
||||
data (Dict[str, Union[np.ndarray, List]]): A dictionary containing additional
|
||||
data where each key is a string representing the data type, and the value
|
||||
is either a NumPy array or a list of corresponding data of length `n`
|
||||
@@ -203,9 +204,10 @@ class KeyPoints:
|
||||
"You can retrieve it like so: inference_result = model.infer(image)[0]"
|
||||
)
|
||||
|
||||
with suppress(AttributeError):
|
||||
if hasattr(inference_result, "dict"):
|
||||
inference_result = inference_result.dict(exclude_none=True, by_alias=True)
|
||||
|
||||
elif hasattr(inference_result, "json"):
|
||||
inference_result = inference_result.json()
|
||||
if not inference_result.get("predictions"):
|
||||
return cls.empty()
|
||||
|
||||
@@ -313,6 +315,7 @@ class KeyPoints:
|
||||
key_points = sv.KeyPoints.from_mediapipe(
|
||||
face_landmarker_result, (image_width, image_height))
|
||||
```
|
||||
|
||||
""" # noqa: E501 // docs
|
||||
if hasattr(mediapipe_results, "pose_landmarks"):
|
||||
results = mediapipe_results.pose_landmarks
|
||||
@@ -472,7 +475,7 @@ class KeyPoints:
|
||||
A `sv.KeyPoints` object containing the keypoint coordinates, class IDs,
|
||||
and class names, and confidences of each keypoint.
|
||||
|
||||
Example:
|
||||
Examples:
|
||||
```python
|
||||
import cv2
|
||||
import supervision as sv
|
||||
@@ -509,6 +512,91 @@ class KeyPoints:
|
||||
else:
|
||||
return cls.empty()
|
||||
|
||||
@classmethod
|
||||
def from_transformers(cls, transfomers_results: Any) -> KeyPoints:
|
||||
"""
|
||||
Create a `sv.KeyPoints` object from the
|
||||
[Transformers](https://github.com/huggingface/transformers) inference result.
|
||||
|
||||
Args:
|
||||
transfomers_results (Any): The output of a
|
||||
Transformers model containing instances with prediction data.
|
||||
|
||||
Returns:
|
||||
A `sv.KeyPoints` object containing the keypoint coordinates, class IDs,
|
||||
and class names, and confidences of each keypoint.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
from PIL import Image
|
||||
import requests
|
||||
import supervision as sv
|
||||
import torch
|
||||
from transformers import (
|
||||
AutoProcessor,
|
||||
RTDetrForObjectDetection,
|
||||
VitPoseForPoseEstimation,
|
||||
)
|
||||
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
image = Image.open(<SOURCE_IMAGE_PATH>)
|
||||
|
||||
DETECTION_MODEL_ID = "PekingU/rtdetr_r50vd_coco_o365"
|
||||
|
||||
detection_processor = AutoProcessor.from_pretrained(DETECTION_MODEL_ID, use_fast=True)
|
||||
detection_model = RTDetrForObjectDetection.from_pretrained(DETECTION_MODEL_ID, device_map=DEVICE)
|
||||
|
||||
inputs = detection_processor(images=frame, return_tensors="pt").to(DEVICE)
|
||||
|
||||
with torch.no_grad():
|
||||
outputs = detection_model(**inputs)
|
||||
|
||||
target_size = torch.tensor([(frame.height, frame.width)])
|
||||
results = detection_processor.post_process_object_detection(
|
||||
outputs, target_sizes=target_size, threshold=0.3)
|
||||
|
||||
detections = sv.Detections.from_transformers(results[0])
|
||||
boxes = sv.xyxy_to_xywh(detections[detections.class_id == 0].xyxy)
|
||||
|
||||
POSE_ESTIMATION_MODEL_ID = "usyd-community/vitpose-base-simple"
|
||||
|
||||
pose_estimation_processor = AutoProcessor.from_pretrained(POSE_ESTIMATION_MODEL_ID)
|
||||
pose_estimation_model = VitPoseForPoseEstimation.from_pretrained(
|
||||
POSE_ESTIMATION_MODEL_ID, device_map=DEVICE)
|
||||
|
||||
inputs = pose_estimation_processor(frame, boxes=[boxes], return_tensors="pt").to(DEVICE)
|
||||
|
||||
with torch.no_grad():
|
||||
outputs = pose_estimation_model(**inputs)
|
||||
|
||||
results = pose_estimation_processor.post_process_pose_estimation(outputs, boxes=[boxes])
|
||||
key_point = sv.KeyPoints.from_transformers(results[0])
|
||||
```
|
||||
|
||||
""" # noqa: E501 // docs
|
||||
|
||||
if "keypoints" in transfomers_results[0]:
|
||||
if transfomers_results[0]["keypoints"].cpu().numpy().size == 0:
|
||||
return cls.empty()
|
||||
|
||||
result_data = [
|
||||
(
|
||||
result["keypoints"].cpu().numpy(),
|
||||
result["scores"].cpu().numpy(),
|
||||
)
|
||||
for result in transfomers_results
|
||||
]
|
||||
|
||||
xy, scores = zip(*result_data)
|
||||
|
||||
return cls(
|
||||
xy=np.stack(xy).astype(np.float32),
|
||||
confidence=np.stack(scores).astype(np.float32),
|
||||
class_id=np.arange(len(xy)).astype(int),
|
||||
)
|
||||
else:
|
||||
return cls.empty()
|
||||
|
||||
def __getitem__(
|
||||
self, index: Union[int, slice, List[int], np.ndarray, str]
|
||||
) -> Union[KeyPoints, List, np.ndarray, None]:
|
||||
@@ -620,3 +708,67 @@ class KeyPoints:
|
||||
empty_keypoints = KeyPoints.empty()
|
||||
empty_keypoints.data = self.data
|
||||
return self == empty_keypoints
|
||||
|
||||
def as_detections(
|
||||
self, selected_keypoint_indices: Optional[Iterable[int]] = None
|
||||
) -> Detections:
|
||||
"""
|
||||
Convert a KeyPoints object to a Detections object. This
|
||||
approximates the bounding box of the detected object by
|
||||
taking the bounding box that fits all keypoints.
|
||||
|
||||
Arguments:
|
||||
selected_keypoint_indices (Optional[Iterable[int]]): The
|
||||
indices of the keypoints to include in the bounding box
|
||||
calculation. This helps focus on a subset of keypoints,
|
||||
e.g. when some are occluded. Captures all keypoints by default.
|
||||
|
||||
Returns:
|
||||
detections (Detections): The converted detections object.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
keypoints = sv.KeyPoints.from_inference(...)
|
||||
detections = keypoints.as_detections()
|
||||
```
|
||||
"""
|
||||
if self.is_empty():
|
||||
return Detections.empty()
|
||||
|
||||
detections_list = []
|
||||
for i, xy in enumerate(self.xy):
|
||||
if selected_keypoint_indices:
|
||||
xy = xy[selected_keypoint_indices]
|
||||
|
||||
# [0, 0] used by some frameworks to indicate missing keypoints
|
||||
xy = xy[~np.all(xy == 0, axis=1)]
|
||||
if len(xy) == 0:
|
||||
xyxy = np.array([[0, 0, 0, 0]], dtype=np.float32)
|
||||
else:
|
||||
x_min = xy[:, 0].min()
|
||||
x_max = xy[:, 0].max()
|
||||
y_min = xy[:, 1].min()
|
||||
y_max = xy[:, 1].max()
|
||||
xyxy = np.array([[x_min, y_min, x_max, y_max]], dtype=np.float32)
|
||||
|
||||
if self.confidence is None:
|
||||
confidence = None
|
||||
else:
|
||||
confidence = self.confidence[i]
|
||||
if selected_keypoint_indices:
|
||||
confidence = confidence[selected_keypoint_indices]
|
||||
confidence = np.array([confidence.mean()], dtype=np.float32)
|
||||
|
||||
detections_list.append(
|
||||
Detections(
|
||||
xyxy=xyxy,
|
||||
confidence=confidence,
|
||||
)
|
||||
)
|
||||
|
||||
detections = Detections.merge(detections_list)
|
||||
detections.class_id = self.class_id
|
||||
detections.data = self.data
|
||||
detections = detections[detections.area > 0]
|
||||
|
||||
return detections
|
||||
|
||||
@@ -8,6 +8,10 @@ from supervision.metrics.mean_average_precision import (
|
||||
MeanAveragePrecision,
|
||||
MeanAveragePrecisionResult,
|
||||
)
|
||||
from supervision.metrics.mean_average_recall import (
|
||||
MeanAverageRecall,
|
||||
MeanAverageRecallResult,
|
||||
)
|
||||
from supervision.metrics.precision import Precision, PrecisionResult
|
||||
from supervision.metrics.recall import Recall, RecallResult
|
||||
from supervision.metrics.utils.object_size import (
|
||||
@@ -15,3 +19,22 @@ from supervision.metrics.utils.object_size import (
|
||||
get_detection_size_category,
|
||||
get_object_size_category,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AveragingMethod",
|
||||
"F1Score",
|
||||
"F1ScoreResult",
|
||||
"MeanAveragePrecision",
|
||||
"MeanAveragePrecisionResult",
|
||||
"MeanAverageRecall",
|
||||
"MeanAverageRecallResult",
|
||||
"Metric",
|
||||
"MetricTarget",
|
||||
"ObjectSizeCategory",
|
||||
"Precision",
|
||||
"PrecisionResult",
|
||||
"Recall",
|
||||
"RecallResult",
|
||||
"get_detection_size_category",
|
||||
"get_object_size_category",
|
||||
]
|
||||
|
||||
@@ -47,10 +47,30 @@ class F1Score(Metric):
|
||||
f1_metric = F1Score()
|
||||
f1_result = f1_metric.update(predictions, targets).compute()
|
||||
|
||||
print(f1_result)
|
||||
print(f1_result.f1_50)
|
||||
print(f1_result.small_objects.f1_50)
|
||||
# 0.7618
|
||||
|
||||
print(f1_result)
|
||||
# F1ScoreResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# Averaging method: AveragingMethod.WEIGHTED
|
||||
# F1 @ 50: 0.7618
|
||||
# F1 @ 75: 0.7487
|
||||
# F1 @ thresh: [0.76175 0.76068 0.76068]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# F1 per class:
|
||||
# 0: [0.70968 0.70968 0.70968 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
|
||||
f1_result.plot()
|
||||
```
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -456,11 +476,11 @@ class F1ScoreResult:
|
||||
matched_classes (np.ndarray): the class IDs of all matched classes.
|
||||
Corresponds to the rows of `f1_per_class`.
|
||||
small_objects (Optional[F1ScoreResult]): the F1 metric results
|
||||
for small objects.
|
||||
for small objects (area < 32²).
|
||||
medium_objects (Optional[F1ScoreResult]): the F1 metric results
|
||||
for medium objects.
|
||||
for medium objects (32² ≤ area < 96²).
|
||||
large_objects (Optional[F1ScoreResult]): the F1 metric results
|
||||
for large objects.
|
||||
for large objects (area ≥ 96²).
|
||||
"""
|
||||
|
||||
metric_target: MetricTarget
|
||||
@@ -490,6 +510,19 @@ class F1ScoreResult:
|
||||
Example:
|
||||
```python
|
||||
print(f1_result)
|
||||
# F1ScoreResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# Averaging method: AveragingMethod.WEIGHTED
|
||||
# F1 @ 50: 0.7618
|
||||
# F1 @ 75: 0.7487
|
||||
# F1 @ thresh: [0.76175 0.76068 0.76068]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# F1 per class:
|
||||
# 0: [0.70968 0.70968 0.70968 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
```
|
||||
"""
|
||||
out_str = (
|
||||
@@ -553,6 +586,10 @@ class F1ScoreResult:
|
||||
def plot(self):
|
||||
"""
|
||||
Plot the F1 results.
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
labels = ["F1@50", "F1@75"]
|
||||
|
||||
@@ -42,10 +42,31 @@ class MeanAveragePrecision(Metric):
|
||||
map_metric = MeanAveragePrecision()
|
||||
map_result = map_metric.update(predictions, targets).compute()
|
||||
|
||||
print(map_result)
|
||||
print(map_result.map50_95)
|
||||
# 0.4674
|
||||
|
||||
print(map_result)
|
||||
# MeanAveragePrecisionResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# Class agnostic: False
|
||||
# mAP @ 50:95: 0.4674
|
||||
# mAP @ 50: 0.5048
|
||||
# mAP @ 75: 0.4796
|
||||
# mAP scores: [0.50485 0.50377 0.50377 ...]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# AP per class:
|
||||
# 0: [0.67699 0.67699 0.67699 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
|
||||
map_result.plot()
|
||||
```
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -419,11 +440,11 @@ class MeanAveragePrecisionResult:
|
||||
matched_classes (np.ndarray): the class IDs of all matched classes.
|
||||
Corresponds to the rows of `ap_per_class`.
|
||||
small_objects (Optional[MeanAveragePrecisionResult]): the mAP results
|
||||
for small objects.
|
||||
for small objects (area < 32²).
|
||||
medium_objects (Optional[MeanAveragePrecisionResult]): the mAP results
|
||||
for medium objects.
|
||||
for medium objects (32² ≤ area < 96²).
|
||||
large_objects (Optional[MeanAveragePrecisionResult]): the mAP results
|
||||
for large objects.
|
||||
for large objects (area ≥ 96²).
|
||||
"""
|
||||
|
||||
metric_target: MetricTarget
|
||||
@@ -456,6 +477,20 @@ class MeanAveragePrecisionResult:
|
||||
Example:
|
||||
```python
|
||||
print(map_result)
|
||||
# MeanAveragePrecisionResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# Class agnostic: False
|
||||
# mAP @ 50:95: 0.4674
|
||||
# mAP @ 50: 0.5048
|
||||
# mAP @ 75: 0.4796
|
||||
# mAP scores: [0.50485 0.50377 0.50377 ...]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# AP per class:
|
||||
# 0: [0.67699 0.67699 0.67699 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
```
|
||||
"""
|
||||
|
||||
@@ -527,6 +562,10 @@ class MeanAveragePrecisionResult:
|
||||
def plot(self):
|
||||
"""
|
||||
Plot the mAP results.
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
labels = ["mAP@50:95", "mAP@50", "mAP@75"]
|
||||
|
||||
@@ -0,0 +1,696 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
from matplotlib import pyplot as plt
|
||||
|
||||
from supervision.config import ORIENTED_BOX_COORDINATES
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.utils import (
|
||||
box_iou_batch,
|
||||
mask_iou_batch,
|
||||
oriented_box_iou_batch,
|
||||
)
|
||||
from supervision.draw.color import LEGACY_COLOR_PALETTE
|
||||
from supervision.metrics.core import Metric, MetricTarget
|
||||
from supervision.metrics.utils.object_size import (
|
||||
ObjectSizeCategory,
|
||||
get_detection_size_category,
|
||||
)
|
||||
from supervision.metrics.utils.utils import ensure_pandas_installed
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pandas as pd
|
||||
|
||||
|
||||
class MeanAverageRecall(Metric):
|
||||
"""
|
||||
Mean Average Recall (mAR) measures how well the model detects
|
||||
and retrieves relevant objects by averaging recall over multiple
|
||||
IoU thresholds, classes and detection limits.
|
||||
|
||||
Intuitively, while Recall measures the ability to find all relevant
|
||||
objects, mAR narrows down how many detections are considered for each
|
||||
class. For example, mAR @ 100 considers the top 100 highest confidence
|
||||
detections for each class. mAR @ 1 considers only the highest
|
||||
confidence detection for each class.
|
||||
|
||||
Example:
|
||||
```python
|
||||
import supervision as sv
|
||||
from supervision.metrics import MeanAverageRecall
|
||||
|
||||
predictions = sv.Detections(...)
|
||||
targets = sv.Detections(...)
|
||||
|
||||
map_metric = MeanAverageRecall()
|
||||
map_result = map_metric.update(predictions, targets).compute()
|
||||
|
||||
print(mar_results.mar_at_100)
|
||||
# 0.5241
|
||||
|
||||
print(mar_results)
|
||||
# MeanAverageRecallResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# mAR @ 1: 0.1362
|
||||
# mAR @ 10: 0.4239
|
||||
# mAR @ 100: 0.5241
|
||||
# max detections: [1 10 100]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# mAR per class:
|
||||
# 0: [0.78571 0.78571 0.78571 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
|
||||
mar_results.plot()
|
||||
```
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
metric_target: MetricTarget = MetricTarget.BOXES,
|
||||
):
|
||||
"""
|
||||
Initialize the Mean Average Recall metric.
|
||||
|
||||
Args:
|
||||
metric_target (MetricTarget): The type of detection data to use.
|
||||
"""
|
||||
self._metric_target = metric_target
|
||||
|
||||
self._predictions_list: List[Detections] = []
|
||||
self._targets_list: List[Detections] = []
|
||||
|
||||
self.max_detections = np.array([1, 10, 100])
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Reset the metric to its initial state, clearing all stored data.
|
||||
"""
|
||||
self._predictions_list = []
|
||||
self._targets_list = []
|
||||
|
||||
def update(
|
||||
self,
|
||||
predictions: Union[Detections, List[Detections]],
|
||||
targets: Union[Detections, List[Detections]],
|
||||
) -> MeanAverageRecall:
|
||||
"""
|
||||
Add new predictions and targets to the metric, but do not compute the result.
|
||||
|
||||
Args:
|
||||
predictions (Union[Detections, List[Detections]]): The predicted detections.
|
||||
targets (Union[Detections, List[Detections]]): The target detections.
|
||||
|
||||
Returns:
|
||||
(Recall): The updated metric instance.
|
||||
"""
|
||||
if not isinstance(predictions, list):
|
||||
predictions = [predictions]
|
||||
if not isinstance(targets, list):
|
||||
targets = [targets]
|
||||
|
||||
if len(predictions) != len(targets):
|
||||
raise ValueError(
|
||||
f"The number of predictions ({len(predictions)}) and"
|
||||
f" targets ({len(targets)}) during the update must be the same."
|
||||
)
|
||||
|
||||
self._predictions_list.extend(predictions)
|
||||
self._targets_list.extend(targets)
|
||||
|
||||
return self
|
||||
|
||||
def compute(self) -> MeanAverageRecallResult:
|
||||
"""
|
||||
Calculate the Mean Average Recall metric based on the stored predictions
|
||||
and ground-truth, at different IoU thresholds and maximum detection counts.
|
||||
|
||||
Returns:
|
||||
(MeanAverageRecallResult): The Mean Average Recall metric result.
|
||||
"""
|
||||
result = self._compute(self._predictions_list, self._targets_list)
|
||||
|
||||
small_predictions, small_targets = self._filter_predictions_and_targets_by_size(
|
||||
self._predictions_list, self._targets_list, ObjectSizeCategory.SMALL
|
||||
)
|
||||
result.small_objects = self._compute(small_predictions, small_targets)
|
||||
|
||||
medium_predictions, medium_targets = (
|
||||
self._filter_predictions_and_targets_by_size(
|
||||
self._predictions_list, self._targets_list, ObjectSizeCategory.MEDIUM
|
||||
)
|
||||
)
|
||||
result.medium_objects = self._compute(medium_predictions, medium_targets)
|
||||
|
||||
large_predictions, large_targets = self._filter_predictions_and_targets_by_size(
|
||||
self._predictions_list, self._targets_list, ObjectSizeCategory.LARGE
|
||||
)
|
||||
result.large_objects = self._compute(large_predictions, large_targets)
|
||||
|
||||
return result
|
||||
|
||||
def _compute(
|
||||
self, predictions_list: List[Detections], targets_list: List[Detections]
|
||||
) -> MeanAverageRecallResult:
|
||||
iou_thresholds = np.linspace(0.5, 0.95, 10)
|
||||
stats = []
|
||||
|
||||
for predictions, targets in zip(predictions_list, targets_list):
|
||||
prediction_contents = self._detections_content(predictions)
|
||||
target_contents = self._detections_content(targets)
|
||||
|
||||
if len(targets) > 0:
|
||||
if len(predictions) == 0:
|
||||
stats.append(
|
||||
(
|
||||
np.zeros((0, iou_thresholds.size), dtype=bool),
|
||||
np.zeros((0,), dtype=np.float32),
|
||||
np.zeros((0,), dtype=int),
|
||||
targets.class_id,
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
if self._metric_target == MetricTarget.BOXES:
|
||||
iou = box_iou_batch(target_contents, prediction_contents)
|
||||
elif self._metric_target == MetricTarget.MASKS:
|
||||
iou = mask_iou_batch(target_contents, prediction_contents)
|
||||
elif self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
|
||||
iou = oriented_box_iou_batch(
|
||||
target_contents, prediction_contents
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Unsupported metric target for IoU calculation"
|
||||
)
|
||||
|
||||
matches = self._match_detection_batch(
|
||||
predictions.class_id, targets.class_id, iou, iou_thresholds
|
||||
)
|
||||
stats.append(
|
||||
(
|
||||
matches,
|
||||
predictions.confidence,
|
||||
predictions.class_id,
|
||||
targets.class_id,
|
||||
)
|
||||
)
|
||||
|
||||
if not stats:
|
||||
return MeanAverageRecallResult(
|
||||
metric_target=self._metric_target,
|
||||
recall_scores=np.zeros(iou_thresholds.shape[0]),
|
||||
recall_per_class=np.zeros((0, iou_thresholds.shape[0])),
|
||||
max_detections=self.max_detections,
|
||||
iou_thresholds=iou_thresholds,
|
||||
matched_classes=np.array([], dtype=int),
|
||||
small_objects=None,
|
||||
medium_objects=None,
|
||||
large_objects=None,
|
||||
)
|
||||
|
||||
concatenated_stats = [np.concatenate(items, 0) for items in zip(*stats)]
|
||||
recall_scores_per_k, recall_per_class, unique_classes = (
|
||||
self._compute_average_recall_for_classes(*concatenated_stats)
|
||||
)
|
||||
|
||||
return MeanAverageRecallResult(
|
||||
metric_target=self._metric_target,
|
||||
recall_scores=recall_scores_per_k,
|
||||
recall_per_class=recall_per_class,
|
||||
max_detections=self.max_detections,
|
||||
iou_thresholds=iou_thresholds,
|
||||
matched_classes=unique_classes,
|
||||
small_objects=None,
|
||||
medium_objects=None,
|
||||
large_objects=None,
|
||||
)
|
||||
|
||||
def _compute_average_recall_for_classes(
|
||||
self,
|
||||
matches: np.ndarray,
|
||||
prediction_confidence: np.ndarray,
|
||||
prediction_class_ids: np.ndarray,
|
||||
true_class_ids: np.ndarray,
|
||||
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
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)
|
||||
|
||||
recalls_at_k = []
|
||||
for max_detections in self.max_detections:
|
||||
# Shape: PxTh,P,C,C -> CxThx3
|
||||
confusion_matrix = self._compute_confusion_matrix(
|
||||
matches,
|
||||
prediction_class_ids,
|
||||
unique_classes,
|
||||
class_counts,
|
||||
max_detections=max_detections,
|
||||
)
|
||||
|
||||
# Shape: CxThx3 -> CxTh
|
||||
recall_per_class = self._compute_recall(confusion_matrix)
|
||||
recalls_at_k.append(recall_per_class)
|
||||
|
||||
# Shape: KxCxTh -> KxC
|
||||
recalls_at_k = np.array(recalls_at_k)
|
||||
average_recall_per_class = np.mean(recalls_at_k, axis=2)
|
||||
|
||||
# Shape: KxC -> K
|
||||
recall_scores = np.mean(average_recall_per_class, axis=1)
|
||||
|
||||
return recall_scores, recall_per_class, unique_classes
|
||||
|
||||
@staticmethod
|
||||
def _match_detection_batch(
|
||||
predictions_classes: np.ndarray,
|
||||
target_classes: np.ndarray,
|
||||
iou: np.ndarray,
|
||||
iou_thresholds: np.ndarray,
|
||||
) -> np.ndarray:
|
||||
num_predictions, num_iou_levels = (
|
||||
predictions_classes.shape[0],
|
||||
iou_thresholds.shape[0],
|
||||
)
|
||||
correct = np.zeros((num_predictions, num_iou_levels), dtype=bool)
|
||||
correct_class = target_classes[:, None] == predictions_classes
|
||||
|
||||
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 _compute_confusion_matrix(
|
||||
sorted_matches: np.ndarray,
|
||||
sorted_prediction_class_ids: np.ndarray,
|
||||
unique_classes: np.ndarray,
|
||||
class_counts: np.ndarray,
|
||||
max_detections: Optional[int] = None,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Compute the confusion matrix for each class and IoU threshold.
|
||||
|
||||
Assumes the matches and prediction_class_ids are sorted by confidence
|
||||
in descending order.
|
||||
|
||||
Args:
|
||||
sorted_matches: np.ndarray, bool, shape (P, Th), that is True
|
||||
if the prediction is a true positive at the given IoU threshold.
|
||||
sorted_prediction_class_ids: np.ndarray, int, shape (P,), containing
|
||||
the class id for each prediction.
|
||||
unique_classes: np.ndarray, int, shape (C,), containing the unique
|
||||
class ids.
|
||||
class_counts: np.ndarray, int, shape (C,), containing the number
|
||||
of true instances for each class.
|
||||
max_detections: Optional[int], the maximum number of detections to
|
||||
consider for each class. Extra detections are considered false
|
||||
positives. By default, all detections are considered.
|
||||
|
||||
Returns:
|
||||
np.ndarray, shape (C, Th, 3), containing the true positives, false
|
||||
positives, and false negatives for each class and IoU threshold.
|
||||
"""
|
||||
num_thresholds = sorted_matches.shape[1]
|
||||
num_classes = unique_classes.shape[0]
|
||||
|
||||
confusion_matrix = np.zeros((num_classes, num_thresholds, 3))
|
||||
for class_idx, class_id in enumerate(unique_classes):
|
||||
is_class = sorted_prediction_class_ids == class_id
|
||||
num_true = class_counts[class_idx]
|
||||
num_predictions = is_class.sum()
|
||||
|
||||
if num_predictions == 0:
|
||||
true_positives = np.zeros(num_thresholds)
|
||||
false_positives = np.zeros(num_thresholds)
|
||||
false_negatives = np.full(num_thresholds, num_true)
|
||||
elif num_true == 0:
|
||||
true_positives = np.zeros(num_thresholds)
|
||||
false_positives = np.full(num_thresholds, num_predictions)
|
||||
false_negatives = np.zeros(num_thresholds)
|
||||
else:
|
||||
limited_matches = sorted_matches[is_class][slice(max_detections)]
|
||||
true_positives = limited_matches.sum(0)
|
||||
|
||||
false_positives = (1 - limited_matches).sum(0)
|
||||
false_negatives = num_true - true_positives
|
||||
false_negatives = num_true - true_positives
|
||||
confusion_matrix[class_idx] = np.stack(
|
||||
[true_positives, false_positives, false_negatives], axis=1
|
||||
)
|
||||
|
||||
return confusion_matrix
|
||||
|
||||
@staticmethod
|
||||
def _compute_recall(confusion_matrix: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Broadcastable function, computing the recall from the confusion matrix.
|
||||
|
||||
Arguments:
|
||||
confusion_matrix: np.ndarray, shape (N, ..., 3), where the last dimension
|
||||
contains the true positives, false positives, and false negatives.
|
||||
|
||||
Returns:
|
||||
np.ndarray, shape (N, ...), containing the recall for each element.
|
||||
"""
|
||||
if not confusion_matrix.shape[-1] == 3:
|
||||
raise ValueError(
|
||||
f"Confusion matrix must have shape (..., 3), got "
|
||||
f"{confusion_matrix.shape}"
|
||||
)
|
||||
true_positives = confusion_matrix[..., 0]
|
||||
false_negatives = confusion_matrix[..., 2]
|
||||
|
||||
denominator = true_positives + false_negatives
|
||||
recall = np.where(denominator == 0, 0, true_positives / denominator)
|
||||
|
||||
return recall
|
||||
|
||||
def _detections_content(self, detections: Detections) -> np.ndarray:
|
||||
"""Return boxes, masks or oriented bounding boxes from detections."""
|
||||
if self._metric_target == MetricTarget.BOXES:
|
||||
return detections.xyxy
|
||||
if self._metric_target == MetricTarget.MASKS:
|
||||
return (
|
||||
detections.mask
|
||||
if detections.mask is not None
|
||||
else self._make_empty_content()
|
||||
)
|
||||
if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
|
||||
obb = detections.data.get(ORIENTED_BOX_COORDINATES)
|
||||
if obb is not None and len(obb) > 0:
|
||||
return np.array(obb, dtype=np.float32)
|
||||
return self._make_empty_content()
|
||||
raise ValueError(f"Invalid metric target: {self._metric_target}")
|
||||
|
||||
def _make_empty_content(self) -> np.ndarray:
|
||||
if self._metric_target == MetricTarget.BOXES:
|
||||
return np.empty((0, 4), dtype=np.float32)
|
||||
if self._metric_target == MetricTarget.MASKS:
|
||||
return np.empty((0, 0, 0), dtype=bool)
|
||||
if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
|
||||
return np.empty((0, 4, 2), dtype=np.float32)
|
||||
raise ValueError(f"Invalid metric target: {self._metric_target}")
|
||||
|
||||
def _filter_detections_by_size(
|
||||
self, detections: Detections, size_category: ObjectSizeCategory
|
||||
) -> Detections:
|
||||
"""Return a copy of detections with contents filtered by object size."""
|
||||
new_detections = deepcopy(detections)
|
||||
if detections.is_empty() or size_category == ObjectSizeCategory.ANY:
|
||||
return new_detections
|
||||
|
||||
sizes = get_detection_size_category(new_detections, self._metric_target)
|
||||
size_mask = sizes == size_category.value
|
||||
|
||||
new_detections.xyxy = new_detections.xyxy[size_mask]
|
||||
if new_detections.mask is not None:
|
||||
new_detections.mask = new_detections.mask[size_mask]
|
||||
if new_detections.class_id is not None:
|
||||
new_detections.class_id = new_detections.class_id[size_mask]
|
||||
if new_detections.confidence is not None:
|
||||
new_detections.confidence = new_detections.confidence[size_mask]
|
||||
if new_detections.tracker_id is not None:
|
||||
new_detections.tracker_id = new_detections.tracker_id[size_mask]
|
||||
if new_detections.data is not None:
|
||||
for key, value in new_detections.data.items():
|
||||
new_detections.data[key] = np.array(value)[size_mask]
|
||||
|
||||
return new_detections
|
||||
|
||||
def _filter_predictions_and_targets_by_size(
|
||||
self,
|
||||
predictions_list: List[Detections],
|
||||
targets_list: List[Detections],
|
||||
size_category: ObjectSizeCategory,
|
||||
) -> Tuple[List[Detections], List[Detections]]:
|
||||
new_predictions_list = []
|
||||
new_targets_list = []
|
||||
for predictions, targets in zip(predictions_list, targets_list):
|
||||
new_predictions_list.append(
|
||||
self._filter_detections_by_size(predictions, size_category)
|
||||
)
|
||||
new_targets_list.append(
|
||||
self._filter_detections_by_size(targets, size_category)
|
||||
)
|
||||
return new_predictions_list, new_targets_list
|
||||
|
||||
|
||||
@dataclass
|
||||
class MeanAverageRecallResult:
|
||||
# """
|
||||
# The results of the recall metric calculation.
|
||||
|
||||
# Defaults to `0` if no detections or targets were provided.
|
||||
|
||||
# Attributes:
|
||||
# metric_target (MetricTarget): the type of data used for the metric -
|
||||
# boxes, masks or oriented bounding boxes.
|
||||
# averaging_method (AveragingMethod): the averaging method used to compute the
|
||||
# recall. Determines how the recall is aggregated across classes.
|
||||
# recall_at_50 (float): the recall at IoU threshold of `0.5`.
|
||||
# recall_at_75 (float): the recall at IoU threshold of `0.75`.
|
||||
# recall_scores (np.ndarray): the recall scores at each IoU threshold.
|
||||
# Shape: `(num_iou_thresholds,)`
|
||||
# recall_per_class (np.ndarray): the recall scores per class and IoU threshold.
|
||||
# Shape: `(num_target_classes, num_iou_thresholds)`
|
||||
# iou_thresholds (np.ndarray): the IoU thresholds used in the calculations.
|
||||
# matched_classes (np.ndarray): the class IDs of all matched classes.
|
||||
# Corresponds to the rows of `recall_per_class`.
|
||||
# small_objects (Optional[RecallResult]): the Recall metric results
|
||||
# for small objects.
|
||||
# medium_objects (Optional[RecallResult]): the Recall metric results
|
||||
# for medium objects.
|
||||
# large_objects (Optional[RecallResult]): the Recall metric results
|
||||
# for large objects.
|
||||
# """
|
||||
"""
|
||||
The results of the Mean Average Recall metric calculation.
|
||||
|
||||
Defaults to `0` if no detections or targets were provided.
|
||||
|
||||
Attributes:
|
||||
metric_target (MetricTarget): the type of data used for the metric -
|
||||
boxes, masks or oriented bounding boxes.
|
||||
mAR_at_1 (float): the Mean Average Recall, when considering only the top
|
||||
highest confidence detection for each class.
|
||||
mAR_at_10 (float): the Mean Average Recall, when considering top 10
|
||||
highest confidence detections for each class.
|
||||
mAR_at_100 (float): the Mean Average Recall, when considering top 100
|
||||
highest confidence detections for each class.
|
||||
recall_per_class (np.ndarray): the recall scores per class and IoU threshold.
|
||||
Shape: `(num_target_classes, num_iou_thresholds)`
|
||||
max_detections (np.ndarray): the array with maximum number of detections
|
||||
considered.
|
||||
iou_thresholds (np.ndarray): the IoU thresholds used in the calculations.
|
||||
matched_classes (np.ndarray): the class IDs of all matched classes.
|
||||
Corresponds to the rows of `recall_per_class`.
|
||||
small_objects (Optional[MeanAverageRecallResult]): the Mean Average Recall
|
||||
metric results for small objects (area < 32²).
|
||||
medium_objects (Optional[MeanAverageRecallResult]): the Mean Average Recall
|
||||
metric results for medium objects (32² ≤ area < 96²).
|
||||
large_objects (Optional[MeanAverageRecallResult]): the Mean Average Recall
|
||||
metric results for large objects (area ≥ 96²).
|
||||
"""
|
||||
|
||||
metric_target: MetricTarget
|
||||
|
||||
@property
|
||||
def mAR_at_1(self) -> float:
|
||||
return self.recall_scores[0]
|
||||
|
||||
@property
|
||||
def mAR_at_10(self) -> float:
|
||||
return self.recall_scores[1]
|
||||
|
||||
@property
|
||||
def mAR_at_100(self) -> float:
|
||||
return self.recall_scores[2]
|
||||
|
||||
recall_scores: np.ndarray
|
||||
recall_per_class: np.ndarray
|
||||
max_detections: np.ndarray
|
||||
iou_thresholds: np.ndarray
|
||||
matched_classes: np.ndarray
|
||||
|
||||
small_objects: Optional[MeanAverageRecallResult]
|
||||
medium_objects: Optional[MeanAverageRecallResult]
|
||||
large_objects: Optional[MeanAverageRecallResult]
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Format as a pretty string.
|
||||
|
||||
Example:
|
||||
```python
|
||||
# MeanAverageRecallResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# mAR @ 1: 0.1362
|
||||
# mAR @ 10: 0.4239
|
||||
# mAR @ 100: 0.5241
|
||||
# max detections: [1 10 100]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# mAR per class:
|
||||
# 0: [0.78571 0.78571 0.78571 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
```
|
||||
"""
|
||||
out_str = (
|
||||
f"{self.__class__.__name__}:\n"
|
||||
f"Metric target: {self.metric_target}\n"
|
||||
f"mAR @ 1: {self.mAR_at_1:.4f}\n"
|
||||
f"mAR @ 10: {self.mAR_at_10:.4f}\n"
|
||||
f"mAR @ 100: {self.mAR_at_100:.4f}\n"
|
||||
f"max detections: {self.max_detections}\n"
|
||||
f"IoU thresh: {self.iou_thresholds}\n"
|
||||
f"mAR per class:\n"
|
||||
)
|
||||
if self.recall_per_class.size == 0:
|
||||
out_str += " No results\n"
|
||||
for class_id, recall_of_class in zip(
|
||||
self.matched_classes, self.recall_per_class
|
||||
):
|
||||
out_str += f" {class_id}: {recall_of_class}\n"
|
||||
|
||||
indent = " "
|
||||
if self.small_objects is not None:
|
||||
indented = indent + str(self.small_objects).replace("\n", f"\n{indent}")
|
||||
out_str += f"\nSmall objects:\n{indented}"
|
||||
if self.medium_objects is not None:
|
||||
indented = indent + str(self.medium_objects).replace("\n", f"\n{indent}")
|
||||
out_str += f"\nMedium objects:\n{indented}"
|
||||
if self.large_objects is not None:
|
||||
indented = indent + str(self.large_objects).replace("\n", f"\n{indent}")
|
||||
out_str += f"\nLarge objects:\n{indented}"
|
||||
|
||||
return out_str
|
||||
|
||||
def to_pandas(self) -> "pd.DataFrame":
|
||||
"""
|
||||
Convert the result to a pandas DataFrame.
|
||||
|
||||
Returns:
|
||||
(pd.DataFrame): The result as a DataFrame.
|
||||
"""
|
||||
ensure_pandas_installed()
|
||||
import pandas as pd
|
||||
|
||||
pandas_data = {
|
||||
"mAR @ 1": self.mAR_at_1,
|
||||
"mAR @ 10": self.mAR_at_10,
|
||||
"mAR @ 100": self.mAR_at_100,
|
||||
}
|
||||
|
||||
if self.small_objects is not None:
|
||||
small_objects_df = self.small_objects.to_pandas()
|
||||
for key, value in small_objects_df.items():
|
||||
pandas_data[f"small_objects_{key}"] = value
|
||||
if self.medium_objects is not None:
|
||||
medium_objects_df = self.medium_objects.to_pandas()
|
||||
for key, value in medium_objects_df.items():
|
||||
pandas_data[f"medium_objects_{key}"] = value
|
||||
if self.large_objects is not None:
|
||||
large_objects_df = self.large_objects.to_pandas()
|
||||
for key, value in large_objects_df.items():
|
||||
pandas_data[f"large_objects_{key}"] = value
|
||||
|
||||
return pd.DataFrame(pandas_data, index=[0])
|
||||
|
||||
def plot(self):
|
||||
"""
|
||||
Plot the Mean Average Recall results.
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
labels = ["mAR @ 1", "mAR @ 10", "mAR @ 100"]
|
||||
values = [self.mAR_at_1, self.mAR_at_10, self.mAR_at_100]
|
||||
colors = [LEGACY_COLOR_PALETTE[0]] * 3
|
||||
|
||||
if self.small_objects is not None:
|
||||
small_objects = self.small_objects
|
||||
labels += ["Small: mAR @ 1", "Small: mAR @ 10", "Small: mAR @ 100"]
|
||||
values += [
|
||||
small_objects.mAR_at_1,
|
||||
small_objects.mAR_at_10,
|
||||
small_objects.mAR_at_100,
|
||||
]
|
||||
colors += [LEGACY_COLOR_PALETTE[3]] * 3
|
||||
|
||||
if self.medium_objects is not None:
|
||||
medium_objects = self.medium_objects
|
||||
labels += ["Medium: mAR @ 1", "Medium: mAR @ 10", "Medium: mAR @ 100"]
|
||||
values += [
|
||||
medium_objects.mAR_at_1,
|
||||
medium_objects.mAR_at_10,
|
||||
medium_objects.mAR_at_100,
|
||||
]
|
||||
colors += [LEGACY_COLOR_PALETTE[2]] * 3
|
||||
|
||||
if self.large_objects is not None:
|
||||
large_objects = self.large_objects
|
||||
labels += ["Large: mAR @ 1", "Large: mAR @ 10", "Large: mAR @ 100"]
|
||||
values += [
|
||||
large_objects.mAR_at_1,
|
||||
large_objects.mAR_at_10,
|
||||
large_objects.mAR_at_100,
|
||||
]
|
||||
colors += [LEGACY_COLOR_PALETTE[4]] * 3
|
||||
|
||||
plt.rcParams["font.family"] = "monospace"
|
||||
|
||||
_, ax = plt.subplots(figsize=(10, 6))
|
||||
ax.set_ylim(0, 1)
|
||||
ax.set_ylabel("Value", fontweight="bold")
|
||||
title = (
|
||||
f"Mean Average Recall, by Object Size\n(target: {self.metric_target.value})"
|
||||
)
|
||||
ax.set_title(title, fontweight="bold")
|
||||
|
||||
x_positions = range(len(labels))
|
||||
bars = ax.bar(x_positions, values, color=colors, align="center")
|
||||
|
||||
ax.set_xticks(x_positions)
|
||||
ax.set_xticklabels(labels, rotation=45, ha="right")
|
||||
|
||||
for bar in bars:
|
||||
y_value = bar.get_height()
|
||||
ax.text(
|
||||
bar.get_x() + bar.get_width() / 2,
|
||||
y_value + 0.02,
|
||||
f"{y_value:.2f}",
|
||||
ha="center",
|
||||
va="bottom",
|
||||
)
|
||||
|
||||
plt.rcParams["font.family"] = "sans-serif"
|
||||
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
@@ -50,10 +50,30 @@ class Precision(Metric):
|
||||
precision_metric = Precision()
|
||||
precision_result = precision_metric.update(predictions, targets).compute()
|
||||
|
||||
print(precision_result)
|
||||
print(precision_result.precision_at_50)
|
||||
# 0.8099
|
||||
|
||||
print(precision_result)
|
||||
# PrecisionResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# Averaging method: AveragingMethod.WEIGHTED
|
||||
# P @ 50: 0.8099
|
||||
# P @ 75: 0.7969
|
||||
# P @ thresh: [0.80992 0.80905 0.80905 ...]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# Precision per class:
|
||||
# 0: [0.64706 0.64706 0.64706 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
|
||||
print(precision_result.small_objects.precision_at_50)
|
||||
```
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -459,11 +479,11 @@ class PrecisionResult:
|
||||
matched_classes (np.ndarray): the class IDs of all matched classes.
|
||||
Corresponds to the rows of `precision_per_class`.
|
||||
small_objects (Optional[PrecisionResult]): the Precision metric results
|
||||
for small objects.
|
||||
for small objects (area < 32²).
|
||||
medium_objects (Optional[PrecisionResult]): the Precision metric results
|
||||
for medium objects.
|
||||
for medium objects (32² ≤ area < 96²).
|
||||
large_objects (Optional[PrecisionResult]): the Precision metric results
|
||||
for large objects.
|
||||
for large objects (area ≥ 96²).
|
||||
"""
|
||||
|
||||
metric_target: MetricTarget
|
||||
@@ -493,6 +513,19 @@ class PrecisionResult:
|
||||
Example:
|
||||
```python
|
||||
print(precision_result)
|
||||
# PrecisionResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# Averaging method: AveragingMethod.WEIGHTED
|
||||
# P @ 50: 0.8099
|
||||
# P @ 75: 0.7969
|
||||
# P @ thresh: [0.80992 0.80905 0.80905 ...]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# Precision per class:
|
||||
# 0: [0.64706 0.64706 0.64706 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
```
|
||||
"""
|
||||
out_str = (
|
||||
@@ -558,6 +591,10 @@ class PrecisionResult:
|
||||
def plot(self):
|
||||
"""
|
||||
Plot the precision results.
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
labels = ["Precision@50", "Precision@75"]
|
||||
|
||||
@@ -50,10 +50,31 @@ class Recall(Metric):
|
||||
recall_metric = Recall()
|
||||
recall_result = recall_metric.update(predictions, targets).compute()
|
||||
|
||||
print(recall_result)
|
||||
print(recall_result.recall_at_50)
|
||||
print(recall_result.small_objects.recall_at_50)
|
||||
# 0.7615
|
||||
|
||||
print(recall_result)
|
||||
# RecallResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# Averaging method: AveragingMethod.WEIGHTED
|
||||
# R @ 50: 0.7615
|
||||
# R @ 75: 0.7462
|
||||
# R @ thresh: [0.76151 0.76011 0.76011 0.75732 ...]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# Recall per class:
|
||||
# 0: [0.78571 0.78571 0.78571 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
|
||||
recall_result.plot()
|
||||
|
||||
```
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -115,11 +136,11 @@ class Recall(Metric):
|
||||
|
||||
def compute(self) -> RecallResult:
|
||||
"""
|
||||
Calculate the precision metric based on the stored predictions and ground-truth
|
||||
Calculate the recall metric based on the stored predictions and ground-truth
|
||||
data, at different IoU thresholds.
|
||||
|
||||
Returns:
|
||||
(RecallResult): The precision metric result.
|
||||
(RecallResult): The recall metric result.
|
||||
"""
|
||||
result = self._compute(self._predictions_list, self._targets_list)
|
||||
|
||||
@@ -457,11 +478,11 @@ class RecallResult:
|
||||
matched_classes (np.ndarray): the class IDs of all matched classes.
|
||||
Corresponds to the rows of `recall_per_class`.
|
||||
small_objects (Optional[RecallResult]): the Recall metric results
|
||||
for small objects.
|
||||
for small objects (area < 32²).
|
||||
medium_objects (Optional[RecallResult]): the Recall metric results
|
||||
for medium objects.
|
||||
for medium objects (32² ≤ area < 96²).
|
||||
large_objects (Optional[RecallResult]): the Recall metric results
|
||||
for large objects.
|
||||
for large objects (area ≥ 96²).
|
||||
"""
|
||||
|
||||
metric_target: MetricTarget
|
||||
@@ -491,6 +512,19 @@ class RecallResult:
|
||||
Example:
|
||||
```python
|
||||
print(recall_result)
|
||||
# RecallResult:
|
||||
# Metric target: MetricTarget.BOXES
|
||||
# Averaging method: AveragingMethod.WEIGHTED
|
||||
# R @ 50: 0.7615
|
||||
# R @ 75: 0.7462
|
||||
# R @ thresh: [0.76151 0.76011 0.76011 0.75732 ...]
|
||||
# IoU thresh: [0.5 0.55 0.6 ...]
|
||||
# Recall per class:
|
||||
# 0: [0.78571 0.78571 0.78571 ...]
|
||||
# ...
|
||||
# Small objects: ...
|
||||
# Medium objects: ...
|
||||
# Large objects: ...
|
||||
```
|
||||
"""
|
||||
out_str = (
|
||||
@@ -556,6 +590,10 @@ class RecallResult:
|
||||
def plot(self):
|
||||
"""
|
||||
Plot the recall results.
|
||||
|
||||
{ align=center width="800" }
|
||||
"""
|
||||
|
||||
labels = ["Recall@50", "Recall@75"]
|
||||
|
||||
@@ -5,5 +5,6 @@ def ensure_pandas_installed():
|
||||
raise ImportError(
|
||||
"`metrics` extra is required to run the function."
|
||||
" Run `pip install 'supervision[metrics]'` or"
|
||||
" `poetry add supervision -E metrics`"
|
||||
" `poetry add supervision -E metrics` or"
|
||||
" `uv pip install 'supervision[metrics]'`"
|
||||
)
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
from typing import Iterable, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.keypoint.core import KeyPoints
|
||||
|
||||
|
||||
def keypoints_to_detections(
|
||||
keypoints: KeyPoints, selected_keypoint_indices: Optional[Iterable[int]] = None
|
||||
) -> Detections:
|
||||
"""
|
||||
Convert a KeyPoints object to a Detections object. This
|
||||
approximates the bounding box of the detected object by
|
||||
taking the bounding box that fits all keypoints.
|
||||
|
||||
Arguments:
|
||||
keypoints (KeyPoints): The keypoints to convert to detections.
|
||||
selected_keypoint_indices (Optional[Iterable[int]]): The
|
||||
indices of the keypoints to include in the bounding box
|
||||
calculation. This helps focus on a subset of keypoints,
|
||||
e.g. when some are occluded. Captures all keypoints by default.
|
||||
|
||||
Returns:
|
||||
detections (Detections): The converted detections object.
|
||||
|
||||
Example:
|
||||
```python
|
||||
keypoints = sv.KeyPoints.from_inference(...)
|
||||
detections = keypoints_to_detections(keypoints)
|
||||
```
|
||||
"""
|
||||
if keypoints.is_empty():
|
||||
return Detections.empty()
|
||||
|
||||
detections_list = []
|
||||
for i, xy in enumerate(keypoints.xy):
|
||||
if selected_keypoint_indices:
|
||||
xy = xy[selected_keypoint_indices]
|
||||
|
||||
# [0, 0] used by some frameworks to indicate missing keypoints
|
||||
xy = xy[~np.all(xy == 0, axis=1)]
|
||||
if len(xy) == 0:
|
||||
xyxy = np.array([[0, 0, 0, 0]], dtype=np.float32)
|
||||
else:
|
||||
x_min = xy[:, 0].min()
|
||||
x_max = xy[:, 0].max()
|
||||
y_min = xy[:, 1].min()
|
||||
y_max = xy[:, 1].max()
|
||||
xyxy = np.array([[x_min, y_min, x_max, y_max]], dtype=np.float32)
|
||||
|
||||
if keypoints.confidence is None:
|
||||
confidence = None
|
||||
else:
|
||||
confidence = keypoints.confidence[i]
|
||||
if selected_keypoint_indices:
|
||||
confidence = confidence[selected_keypoint_indices]
|
||||
confidence = np.array([confidence.mean()], dtype=np.float32)
|
||||
|
||||
detections_list.append(
|
||||
Detections(
|
||||
xyxy=xyxy,
|
||||
confidence=confidence,
|
||||
)
|
||||
)
|
||||
|
||||
detections = Detections.merge(detections_list)
|
||||
detections.class_id = keypoints.class_id
|
||||
detections.data = keypoints.data
|
||||
detections = detections[detections.area > 0]
|
||||
|
||||
return detections
|
||||
@@ -7,6 +7,7 @@ from typing import Callable, Generator, Optional, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from tqdm.auto import tqdm
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -195,6 +196,9 @@ def process_video(
|
||||
source_path: str,
|
||||
target_path: str,
|
||||
callback: Callable[[np.ndarray, int], np.ndarray],
|
||||
max_frames: Optional[int] = None,
|
||||
show_progress: bool = False,
|
||||
progress_message: str = "Processing video",
|
||||
) -> None:
|
||||
"""
|
||||
Process a video file by applying a callback function on each frame
|
||||
@@ -207,6 +211,9 @@ def process_video(
|
||||
a numpy ndarray representation of a video frame and an
|
||||
int index of the frame and returns a processed numpy ndarray
|
||||
representation of the frame.
|
||||
max_frames (Optional[int]): The maximum number of frames to process.
|
||||
show_progress (bool): Whether to show a progress bar.
|
||||
progress_message (str): The message to display in the progress bar.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
@@ -223,12 +230,31 @@ def process_video(
|
||||
```
|
||||
"""
|
||||
source_video_info = VideoInfo.from_video_path(video_path=source_path)
|
||||
video_frames_generator = get_video_frames_generator(
|
||||
source_path=source_path, end=max_frames
|
||||
)
|
||||
with VideoSink(target_path=target_path, video_info=source_video_info) as sink:
|
||||
if show_progress:
|
||||
# Calculate total_frames only when needed for tqdm
|
||||
total_frames = (
|
||||
min(source_video_info.total_frames, max_frames)
|
||||
if max_frames is not None
|
||||
else source_video_info.total_frames
|
||||
)
|
||||
for index, frame in enumerate(
|
||||
get_video_frames_generator(source_path=source_path)
|
||||
tqdm(
|
||||
video_frames_generator,
|
||||
total=total_frames,
|
||||
disable=not show_progress,
|
||||
desc=progress_message,
|
||||
)
|
||||
):
|
||||
result_frame = callback(frame, index)
|
||||
sink.write_frame(frame=result_frame)
|
||||
else:
|
||||
for index, frame in enumerate(video_frames_generator):
|
||||
result_frame = callback(frame, index)
|
||||
sink.write_frame(frame=result_frame)
|
||||
|
||||
|
||||
class FPSMonitor:
|
||||
|
||||
@@ -54,7 +54,7 @@ def validate_confidence(confidence: Any, n: int) -> None:
|
||||
|
||||
|
||||
def validate_keypoint_confidence(confidence: Any, n: int, m: int) -> None:
|
||||
expected_shape = f"({n,m})"
|
||||
expected_shape = f"({n, m})"
|
||||
actual_shape = str(getattr(confidence, "shape", None))
|
||||
|
||||
if confidence is not None:
|
||||
|
||||
+211
-2
@@ -106,6 +106,26 @@ TEST_DET_DIFFERENT_DATA = Detections(
|
||||
"never_seen_key": [9],
|
||||
},
|
||||
)
|
||||
TEST_DET_WITH_METADATA = Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
metadata={"source": "camera1"},
|
||||
)
|
||||
|
||||
TEST_DET_WITH_METADATA_2 = Detections(
|
||||
xyxy=np.array([[30, 30, 40, 40]]),
|
||||
class_id=np.array([2]),
|
||||
metadata={"source": "camera1"},
|
||||
)
|
||||
TEST_DET_NO_METADATA = Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
)
|
||||
TEST_DET_DIFFERENT_METADATA = Detections(
|
||||
xyxy=np.array([[50, 50, 60, 60]]),
|
||||
class_id=np.array([3]),
|
||||
metadata={"source": "camera2"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -203,6 +223,12 @@ TEST_DET_DIFFERENT_DATA = Detections(
|
||||
None,
|
||||
pytest.raises(IndexError),
|
||||
),
|
||||
(
|
||||
Detections.empty(),
|
||||
np.isin(Detections.empty()["class_name"], ["cat", "dog"]),
|
||||
Detections.empty(),
|
||||
DoesNotRaise(),
|
||||
), # Filter an empty detections by specific class names
|
||||
],
|
||||
)
|
||||
def test_getitem(
|
||||
@@ -258,6 +284,11 @@ def test_getitem(
|
||||
TEST_DET_1,
|
||||
DoesNotRaise(),
|
||||
), # Single detection and empty-array fields
|
||||
(
|
||||
[TEST_DET_ZERO_LENGTH, TEST_DET_ZERO_LENGTH],
|
||||
TEST_DET_ZERO_LENGTH,
|
||||
DoesNotRaise(),
|
||||
), # Zero-length fields across all Detections
|
||||
(
|
||||
[
|
||||
TEST_DET_1,
|
||||
@@ -287,12 +318,190 @@ def test_getitem(
|
||||
Detections.empty(),
|
||||
],
|
||||
mock_detections(
|
||||
xyxy=[[10, 10, 20, 20]],
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=[1],
|
||||
mask=[np.zeros((4, 4), dtype=bool)],
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # Segmentation + Empty
|
||||
# Metadata
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
metadata={"source": "camera1"},
|
||||
),
|
||||
Detections.empty(),
|
||||
],
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
metadata={"source": "camera1"},
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # Metadata merge with empty detections
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
metadata={"source": "camera1"},
|
||||
),
|
||||
Detections(xyxy=np.array([[30, 30, 40, 40]]), class_id=np.array([2])),
|
||||
],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
), # Empty and non-empty metadata
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
metadata={"source": "camera1"},
|
||||
)
|
||||
],
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
metadata={"source": "camera1"},
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # Single detection with metadata
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
metadata={"source": "camera1"},
|
||||
),
|
||||
Detections(
|
||||
xyxy=np.array([[30, 30, 40, 40]]),
|
||||
class_id=np.array([2]),
|
||||
metadata={"source": "camera1"},
|
||||
),
|
||||
],
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20], [30, 30, 40, 40]]),
|
||||
class_id=np.array([1, 2]),
|
||||
metadata={"source": "camera1"},
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # Multiple metadata entries with identical values
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
class_id=np.array([1]),
|
||||
metadata={"source": "camera1"},
|
||||
),
|
||||
Detections(
|
||||
xyxy=np.array([[50, 50, 60, 60]]),
|
||||
class_id=np.array([3]),
|
||||
metadata={"source": "camera2"},
|
||||
),
|
||||
],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
), # Different metadata values
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
metadata={"source": "camera1", "resolution": "1080p"},
|
||||
),
|
||||
Detections(
|
||||
xyxy=np.array([[30, 30, 40, 40]]),
|
||||
metadata={"source": "camera1", "resolution": "1080p"},
|
||||
),
|
||||
],
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20], [30, 30, 40, 40]]),
|
||||
metadata={"source": "camera1", "resolution": "1080p"},
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # Large metadata with multiple identical entries
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]), metadata={"source": "camera1"}
|
||||
),
|
||||
Detections(
|
||||
xyxy=np.array([[30, 30, 40, 40]]), metadata={"source": ["camera1"]}
|
||||
),
|
||||
],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
), # Inconsistent types in metadata values
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]), metadata={"source": "camera1"}
|
||||
),
|
||||
Detections(
|
||||
xyxy=np.array([[30, 30, 40, 40]]), metadata={"location": "indoor"}
|
||||
),
|
||||
],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
), # Metadata key mismatch
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
metadata={
|
||||
"source": "camera1",
|
||||
"settings": {"resolution": "1080p", "fps": 30},
|
||||
},
|
||||
),
|
||||
Detections(
|
||||
xyxy=np.array([[30, 30, 40, 40]]),
|
||||
metadata={
|
||||
"source": "camera1",
|
||||
"settings": {"resolution": "1080p", "fps": 30},
|
||||
},
|
||||
),
|
||||
],
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20], [30, 30, 40, 40]]),
|
||||
metadata={
|
||||
"source": "camera1",
|
||||
"settings": {"resolution": "1080p", "fps": 30},
|
||||
},
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # multi-field metadata
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
metadata={"calibration_matrix": np.array([[1, 0], [0, 1]])},
|
||||
),
|
||||
Detections(
|
||||
xyxy=np.array([[30, 30, 40, 40]]),
|
||||
metadata={"calibration_matrix": np.array([[1, 0], [0, 1]])},
|
||||
),
|
||||
],
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20], [30, 30, 40, 40]]),
|
||||
metadata={"calibration_matrix": np.array([[1, 0], [0, 1]])},
|
||||
),
|
||||
DoesNotRaise(),
|
||||
), # Identical 2D numpy arrays in metadata
|
||||
(
|
||||
[
|
||||
Detections(
|
||||
xyxy=np.array([[10, 10, 20, 20]]),
|
||||
metadata={"calibration_matrix": np.array([[1, 0], [0, 1]])},
|
||||
),
|
||||
Detections(
|
||||
xyxy=np.array([[30, 30, 40, 40]]),
|
||||
metadata={"calibration_matrix": np.array([[2, 0], [0, 2]])},
|
||||
),
|
||||
],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
), # Mismatching 2D numpy arrays in metadata
|
||||
],
|
||||
)
|
||||
def test_merge(
|
||||
@@ -302,7 +511,7 @@ def test_merge(
|
||||
) -> None:
|
||||
with exception:
|
||||
result = Detections.merge(detections_list=detections_list)
|
||||
assert result == expected_result
|
||||
assert result == expected_result, f"Expected: {expected_result}, Got: {result}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -408,8 +408,8 @@ def assert_csv_equal(file_name, expected_rows):
|
||||
with open(file_name, mode="r", newline="") as file:
|
||||
reader = csv.reader(file)
|
||||
for i, row in enumerate(reader):
|
||||
assert (
|
||||
[str(item) for item in expected_rows[i]] == row
|
||||
), f"Row in CSV didn't match expected output: {row} != {expected_rows[i]}"
|
||||
assert [str(item) for item in expected_rows[i]] == row, (
|
||||
f"Row in CSV didn't match expected output: {row} != {expected_rows[i]}"
|
||||
)
|
||||
|
||||
os.remove(file_name)
|
||||
|
||||
@@ -242,8 +242,8 @@ def test_json_sink(
|
||||
def assert_json_equal(file_name, expected_rows):
|
||||
with open(file_name, "r") as file:
|
||||
data = json.load(file)
|
||||
assert (
|
||||
data == expected_rows
|
||||
), f"Data in JSON file didn't match expected output: {data} != {expected_rows}"
|
||||
assert data == expected_rows, (
|
||||
f"Data in JSON file didn't match expected output: {data} != {expected_rows}"
|
||||
)
|
||||
|
||||
os.remove(file_name)
|
||||
|
||||
@@ -250,12 +250,12 @@ def test_line_zone_one_detection_default_anchors(
|
||||
crossed_in_list.append(crossed_in[0])
|
||||
crossed_out_list.append(crossed_out[0])
|
||||
|
||||
assert (
|
||||
crossed_in_list == expected_crossed_in
|
||||
), f"expected {expected_crossed_in}, got {crossed_in_list}"
|
||||
assert (
|
||||
crossed_out_list == expected_crossed_out
|
||||
), f"expected {expected_crossed_out}, got {crossed_out_list}"
|
||||
assert crossed_in_list == expected_crossed_in, (
|
||||
f"expected {expected_crossed_in}, got {crossed_in_list}"
|
||||
)
|
||||
assert crossed_out_list == expected_crossed_out, (
|
||||
f"expected {expected_crossed_out}, got {crossed_out_list}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -415,12 +415,12 @@ def test_line_zone_one_detection(
|
||||
crossed_in_list.append(crossed_in[0])
|
||||
crossed_out_list.append(crossed_out[0])
|
||||
|
||||
assert (
|
||||
crossed_in_list == expected_crossed_in
|
||||
), f"expected {expected_crossed_in}, got {crossed_in_list}"
|
||||
assert (
|
||||
crossed_out_list == expected_crossed_out
|
||||
), f"expected {expected_crossed_out}, got {crossed_out_list}"
|
||||
assert crossed_in_list == expected_crossed_in, (
|
||||
f"expected {expected_crossed_in}, got {crossed_in_list}"
|
||||
)
|
||||
assert crossed_out_list == expected_crossed_out, (
|
||||
f"expected {expected_crossed_out}, got {crossed_out_list}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -493,7 +493,7 @@ def test_line_zone_multiple_detections(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"vector, xyxy_sequence, triggering_anchors, crossing_acceptance_threshold, "
|
||||
"vector, xyxy_sequence, triggering_anchors, minimum_crossing_threshold, "
|
||||
"expected_crossed_in, expected_crossed_out",
|
||||
[
|
||||
( # Detection lingers around line, all crosses counted
|
||||
@@ -578,7 +578,7 @@ def test_line_zone_one_detection_long_horizon(
|
||||
vector: Vector,
|
||||
xyxy_sequence: List[List[float]],
|
||||
triggering_anchors: List[Position],
|
||||
crossing_acceptance_threshold: int,
|
||||
minimum_crossing_threshold: int,
|
||||
expected_crossed_in: List[bool],
|
||||
expected_crossed_out: List[bool],
|
||||
) -> None:
|
||||
@@ -586,7 +586,7 @@ def test_line_zone_one_detection_long_horizon(
|
||||
start=vector.start,
|
||||
end=vector.end,
|
||||
triggering_anchors=triggering_anchors,
|
||||
crossing_acceptance_threshold=crossing_acceptance_threshold,
|
||||
minimum_crossing_threshold=minimum_crossing_threshold,
|
||||
)
|
||||
|
||||
crossed_in_list = []
|
||||
@@ -600,16 +600,16 @@ def test_line_zone_one_detection_long_horizon(
|
||||
crossed_in_list.append(crossed_in[0])
|
||||
crossed_out_list.append(crossed_out[0])
|
||||
|
||||
assert (
|
||||
crossed_in_list == expected_crossed_in
|
||||
), f"expected {expected_crossed_in}, got {crossed_in_list}"
|
||||
assert (
|
||||
crossed_out_list == expected_crossed_out
|
||||
), f"expected {expected_crossed_out}, got {crossed_out_list}"
|
||||
assert crossed_in_list == expected_crossed_in, (
|
||||
f"expected {expected_crossed_in}, got {crossed_in_list}"
|
||||
)
|
||||
assert crossed_out_list == expected_crossed_out, (
|
||||
f"expected {expected_crossed_out}, got {crossed_out_list}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"vector, xyxy_sequence, anchors, crossing_acceptance_threshold, "
|
||||
"vector, xyxy_sequence, anchors, minimum_crossing_threshold, "
|
||||
"expected_crossed_in, expected_crossed_out, expected_count_in, "
|
||||
"expected_count_out, exception",
|
||||
[
|
||||
@@ -743,7 +743,7 @@ def test_line_zone_long_horizon_disappearing_detections(
|
||||
vector: Vector,
|
||||
xyxy_sequence: List[List[Optional[List[float]]]],
|
||||
anchors: List[Position],
|
||||
crossing_acceptance_threshold: int,
|
||||
minimum_crossing_threshold: int,
|
||||
expected_crossed_in: List[List[bool]],
|
||||
expected_crossed_out: List[List[bool]],
|
||||
expected_count_in: List[int],
|
||||
@@ -755,7 +755,7 @@ def test_line_zone_long_horizon_disappearing_detections(
|
||||
start=vector.start,
|
||||
end=vector.end,
|
||||
triggering_anchors=anchors,
|
||||
crossing_acceptance_threshold=crossing_acceptance_threshold,
|
||||
minimum_crossing_threshold=minimum_crossing_threshold,
|
||||
)
|
||||
crossed_in_list = []
|
||||
crossed_out_list = []
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from supervision.detection.lmm import from_paligemma
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"result, resolution_wh, classes, expected_results",
|
||||
[
|
||||
(
|
||||
"",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # empty response
|
||||
(
|
||||
"",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # empty response with classes
|
||||
(
|
||||
"\n",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # new line response
|
||||
(
|
||||
"the quick brown fox jumps over the lazy dog.",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # response with no location
|
||||
(
|
||||
"<loc0256><loc0768><loc0768> cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # response with missing location
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768><loc0768> cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # response with extra location
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768>",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # response with no class
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> catt",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(np.empty((0, 4)), np.empty(0), np.empty(0).astype(str)),
|
||||
), # response with invalid class
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
None,
|
||||
np.array(["cat"]).astype(str),
|
||||
),
|
||||
), # correct response; no classes
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> black cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
None,
|
||||
np.array(["black cat"]).astype(np.dtype("U")),
|
||||
),
|
||||
), # correct response; class name with space; no classes
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> black-cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
None,
|
||||
np.array(["black-cat"]).astype(np.dtype("U")),
|
||||
),
|
||||
), # correct response; class name with hyphen; no classes
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> black_cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
None,
|
||||
np.array(["black_cat"]).astype(np.dtype("U")),
|
||||
),
|
||||
), # correct response; class name with underscore; no classes
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat ;",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
np.array([0]),
|
||||
np.array(["cat"]).astype(str),
|
||||
),
|
||||
), # correct response; with classes
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat ; <loc0256><loc0256><loc0768><loc0768> dog", # noqa: E501
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0], [250.0, 250.0, 750.0, 750.0]]),
|
||||
np.array([0, 1]),
|
||||
np.array(["cat", "dog"]).astype(np.dtype("U")),
|
||||
),
|
||||
), # correct response; with classes
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat ; <loc0256><loc0256><loc0768> cat", # noqa: E501
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
np.array([0]),
|
||||
np.array(["cat"]).astype(str),
|
||||
),
|
||||
), # partially correct response; with classes
|
||||
(
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat ; <loc0256><loc0256><loc0768><loc0768><loc0768> cat", # noqa: E501
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
np.array([0]),
|
||||
np.array(["cat"]).astype(str),
|
||||
),
|
||||
), # partially correct response; with classes
|
||||
],
|
||||
)
|
||||
def test_from_paligemma(
|
||||
result: str,
|
||||
resolution_wh: Tuple[int, int],
|
||||
classes: Optional[List[str]],
|
||||
expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray],
|
||||
) -> None:
|
||||
result = from_paligemma(result=result, resolution_wh=resolution_wh, classes=classes)
|
||||
np.testing.assert_array_equal(result[0], expected_results[0])
|
||||
np.testing.assert_array_equal(result[1], expected_results[1])
|
||||
np.testing.assert_array_equal(result[2], expected_results[2])
|
||||
+518
-18
@@ -14,11 +14,15 @@ from supervision.detection.utils import (
|
||||
filter_polygons_by_area,
|
||||
get_data_item,
|
||||
merge_data,
|
||||
merge_metadata,
|
||||
move_boxes,
|
||||
move_masks,
|
||||
process_roboflow_result,
|
||||
scale_boxes,
|
||||
xcycwh_to_xyxy,
|
||||
xywh_to_xyxy,
|
||||
xyxy_to_xcycarh,
|
||||
xyxy_to_xywh,
|
||||
)
|
||||
|
||||
TEST_MASK = np.zeros((1, 1000, 1000), dtype=bool)
|
||||
@@ -386,13 +390,13 @@ def test_process_roboflow_result(
|
||||
)
|
||||
for key in result[5]:
|
||||
if isinstance(result[5][key], np.ndarray):
|
||||
assert np.array_equal(
|
||||
result[5][key], expected_result[5][key]
|
||||
), f"Mismatch in arrays for key {key}"
|
||||
assert np.array_equal(result[5][key], expected_result[5][key]), (
|
||||
f"Mismatch in arrays for key {key}"
|
||||
)
|
||||
else:
|
||||
assert (
|
||||
result[5][key] == expected_result[5][key]
|
||||
), f"Mismatch in non-array data for key {key}"
|
||||
assert result[5][key] == expected_result[5][key], (
|
||||
f"Mismatch in non-array data for key {key}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -441,6 +445,268 @@ def test_move_boxes(
|
||||
assert np.array_equal(result, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"masks, offset, resolution_wh, expected_result, exception",
|
||||
[
|
||||
(
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[False, False, False, False],
|
||||
[False, True, True, False],
|
||||
[False, True, True, False],
|
||||
[False, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
np.array([0, 0]),
|
||||
(4, 4),
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[False, False, False, False],
|
||||
[False, True, True, False],
|
||||
[False, True, True, False],
|
||||
[False, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[False, False, False, False],
|
||||
[False, True, True, False],
|
||||
[False, True, True, False],
|
||||
[False, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
np.array([-1, -1]),
|
||||
(4, 4),
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[True, True, False, False],
|
||||
[True, True, False, False],
|
||||
[False, False, False, False],
|
||||
[False, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[False, False, False, False],
|
||||
[False, True, True, False],
|
||||
[False, True, True, False],
|
||||
[False, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
np.array([-2, -2]),
|
||||
(4, 4),
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[True, False, False, False],
|
||||
[False, False, False, False],
|
||||
[False, False, False, False],
|
||||
[False, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[False, False, False, False],
|
||||
[False, True, True, False],
|
||||
[False, True, True, False],
|
||||
[False, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
np.array([-3, -3]),
|
||||
(4, 4),
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[False, False, False, False],
|
||||
[False, False, False, False],
|
||||
[False, False, False, False],
|
||||
[False, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[False, False, False, False],
|
||||
[False, True, True, False],
|
||||
[False, True, True, False],
|
||||
[False, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
np.array([-2, -1]),
|
||||
(4, 4),
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[True, False, False, False],
|
||||
[True, False, False, False],
|
||||
[False, False, False, False],
|
||||
[False, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[False, False, False, False],
|
||||
[False, True, True, False],
|
||||
[False, True, True, False],
|
||||
[False, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
np.array([-1, -2]),
|
||||
(4, 4),
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[True, True, False, False],
|
||||
[False, False, False, False],
|
||||
[False, False, False, False],
|
||||
[False, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[False, False, False, False],
|
||||
[False, True, True, False],
|
||||
[False, True, True, False],
|
||||
[False, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
np.array([-2, 2]),
|
||||
(4, 4),
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[False, False, False, False],
|
||||
[False, False, False, False],
|
||||
[False, False, False, False],
|
||||
[True, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[False, False, False, False],
|
||||
[False, True, True, False],
|
||||
[False, True, True, False],
|
||||
[False, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
np.array([3, 3]),
|
||||
(4, 4),
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[False, False, False, False],
|
||||
[False, False, False, False],
|
||||
[False, False, False, False],
|
||||
[False, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
(
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[False, False, False, False],
|
||||
[False, True, True, False],
|
||||
[False, True, True, False],
|
||||
[False, False, False, False],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
np.array([3, 3]),
|
||||
(6, 6),
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[False, False, False, False, False, False],
|
||||
[False, False, False, False, False, False],
|
||||
[False, False, False, False, False, False],
|
||||
[False, False, False, False, False, False],
|
||||
[False, False, False, False, True, True],
|
||||
[False, False, False, False, True, True],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_move_masks(
|
||||
masks: np.ndarray,
|
||||
offset: np.ndarray,
|
||||
resolution_wh: Tuple[int, int],
|
||||
expected_result: np.ndarray,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
with exception:
|
||||
result = move_masks(masks=masks, offset=offset, resolution_wh=resolution_wh)
|
||||
np.testing.assert_array_equal(result, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"xyxy, factor, expected_result, exception",
|
||||
[
|
||||
@@ -777,13 +1043,13 @@ def test_merge_data(
|
||||
|
||||
for key in result:
|
||||
if isinstance(result[key], np.ndarray):
|
||||
assert np.array_equal(
|
||||
result[key], expected_result[key]
|
||||
), f"Mismatch in arrays for key {key}"
|
||||
assert np.array_equal(result[key], expected_result[key]), (
|
||||
f"Mismatch in arrays for key {key}"
|
||||
)
|
||||
else:
|
||||
assert (
|
||||
result[key] == expected_result[key]
|
||||
), f"Mismatch in non-array data for key {key}"
|
||||
assert result[key] == expected_result[key], (
|
||||
f"Mismatch in non-array data for key {key}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -950,13 +1216,13 @@ def test_get_data_item(
|
||||
result = get_data_item(data=data, index=index)
|
||||
for key in result:
|
||||
if isinstance(result[key], np.ndarray):
|
||||
assert np.array_equal(
|
||||
result[key], expected_result[key]
|
||||
), f"Mismatch in arrays for key {key}"
|
||||
assert np.array_equal(result[key], expected_result[key]), (
|
||||
f"Mismatch in arrays for key {key}"
|
||||
)
|
||||
else:
|
||||
assert (
|
||||
result[key] == expected_result[key]
|
||||
), f"Mismatch in non-array data for key {key}"
|
||||
assert result[key] == expected_result[key], (
|
||||
f"Mismatch in non-array data for key {key}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -1117,6 +1383,80 @@ def test_xywh_to_xyxy(xywh: np.ndarray, expected_result: np.ndarray) -> None:
|
||||
np.testing.assert_array_equal(result, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"xyxy, expected_result",
|
||||
[
|
||||
(np.array([[10, 20, 40, 60]]), np.array([[10, 20, 30, 40]])), # standard case
|
||||
(np.array([[0, 0, 0, 0]]), np.array([[0, 0, 0, 0]])), # zero size bounding box
|
||||
(
|
||||
np.array([[50, 50, 150, 150]]),
|
||||
np.array([[50, 50, 100, 100]]),
|
||||
), # large bounding box
|
||||
(
|
||||
np.array([[-10, -20, 20, 20]]),
|
||||
np.array([[-10, -20, 30, 40]]),
|
||||
), # negative coordinates
|
||||
(np.array([[50, 50, 50, 80]]), np.array([[50, 50, 0, 30]])), # zero width
|
||||
(np.array([[50, 50, 70, 50]]), np.array([[50, 50, 20, 0]])), # zero height
|
||||
(np.array([]).reshape(0, 4), np.array([]).reshape(0, 4)), # empty array
|
||||
],
|
||||
)
|
||||
def test_xyxy_to_xywh(xyxy: np.ndarray, expected_result: np.ndarray) -> None:
|
||||
result = xyxy_to_xywh(xyxy)
|
||||
np.testing.assert_array_equal(result, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"xyxy, expected_result",
|
||||
[
|
||||
# Empty and zero cases
|
||||
(np.array([]).reshape(0, 4), np.array([]).reshape(0, 4)), # empty array
|
||||
(
|
||||
np.array([[0, 0, 0, 0]]),
|
||||
np.array([[0, 0, 0.0, 0]]),
|
||||
), # zero size bounding box
|
||||
(
|
||||
np.array([[10, 10, 10, 10]]),
|
||||
np.array([[10, 10, 0.0, 0]]),
|
||||
), # point (x1=x2, y1=y2)
|
||||
# Zero width/height cases
|
||||
(np.array([[50, 50, 80, 50]]), np.array([[65, 50, 0.0, 0]])), # zero height
|
||||
(np.array([[50, 50, 50, 80]]), np.array([[50, 65, 0.0, 30]])), # zero width
|
||||
# Standard cases
|
||||
(np.array([[10, 20, 40, 60]]), np.array([[25, 40, 0.75, 40]])), # standard case
|
||||
(
|
||||
np.array([[-30, -40, -10, -20]]),
|
||||
np.array([[-20, -30, 1.0, 20]]),
|
||||
), # all negative values
|
||||
(
|
||||
np.array([[0.1, 0.2, 0.4, 0.6]]),
|
||||
np.array([[0.25, 0.4, 0.75, 0.4]]),
|
||||
), # values between 0-1
|
||||
# Different aspect ratios
|
||||
(
|
||||
np.array([[10, 20, 50, 100]]),
|
||||
np.array([[30, 60, 0.5, 80]]),
|
||||
), # tall rectangle (height > width)
|
||||
(
|
||||
np.array([[20, 10, 100, 50]]),
|
||||
np.array([[60, 30, 2.0, 40]]),
|
||||
), # wide rectangle (width > height)
|
||||
(
|
||||
np.array([[50, 50, 150, 150]]),
|
||||
np.array([[100, 100, 1.0, 100]]),
|
||||
), # height == width
|
||||
# Multiple boxes in one array
|
||||
(
|
||||
np.array([[0, 0, 0, 0], [10, 20, 40, 60]]),
|
||||
np.array([[0, 0, 0.0, 0], [25, 40, 0.75, 40]]),
|
||||
), # one zero-sized box and one normal box
|
||||
],
|
||||
)
|
||||
def test_xyxy_to_xcycarh(xyxy: np.ndarray, expected_result: np.ndarray) -> None:
|
||||
result = xyxy_to_xcycarh(xyxy)
|
||||
np.testing.assert_allclose(result, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"xcycwh, expected_result",
|
||||
[
|
||||
@@ -1138,3 +1478,163 @@ def test_xywh_to_xyxy(xywh: np.ndarray, expected_result: np.ndarray) -> None:
|
||||
def test_xcycwh_to_xyxy(xcycwh: np.ndarray, expected_result: np.ndarray) -> None:
|
||||
result = xcycwh_to_xyxy(xcycwh)
|
||||
np.testing.assert_array_equal(result, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"metadata_list, expected_result, exception",
|
||||
[
|
||||
# Identical metadata with a single key
|
||||
([{"key1": "value1"}, {"key1": "value1"}], {"key1": "value1"}, DoesNotRaise()),
|
||||
# Identical metadata with multiple keys
|
||||
(
|
||||
[
|
||||
{"key1": "value1", "key2": "value2"},
|
||||
{"key1": "value1", "key2": "value2"},
|
||||
],
|
||||
{"key1": "value1", "key2": "value2"},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Conflicting values for the same key
|
||||
([{"key1": "value1"}, {"key1": "value2"}], None, pytest.raises(ValueError)),
|
||||
# Different sets of keys across dictionaries
|
||||
([{"key1": "value1"}, {"key2": "value2"}], None, pytest.raises(ValueError)),
|
||||
# Empty metadata list
|
||||
([], {}, DoesNotRaise()),
|
||||
# Empty metadata dictionaries
|
||||
([{}, {}], {}, DoesNotRaise()),
|
||||
# Different declaration order for keys
|
||||
(
|
||||
[
|
||||
{"key1": "value1", "key2": "value2"},
|
||||
{"key2": "value2", "key1": "value1"},
|
||||
],
|
||||
{"key1": "value1", "key2": "value2"},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Nested metadata dictionaries
|
||||
(
|
||||
[{"key1": {"sub_key": "sub_value"}}, {"key1": {"sub_key": "sub_value"}}],
|
||||
{"key1": {"sub_key": "sub_value"}},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Large metadata dictionaries with many keys
|
||||
(
|
||||
[
|
||||
{f"key{i}": f"value{i}" for i in range(100)},
|
||||
{f"key{i}": f"value{i}" for i in range(100)},
|
||||
],
|
||||
{f"key{i}": f"value{i}" for i in range(100)},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Mixed types in list metadata values
|
||||
(
|
||||
[{"key1": ["value1", 2, True]}, {"key1": ["value1", 2, True]}],
|
||||
{"key1": ["value1", 2, True]},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Identical lists across metadata dictionaries
|
||||
(
|
||||
[{"key1": [1, 2, 3]}, {"key1": [1, 2, 3]}],
|
||||
{"key1": [1, 2, 3]},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Identical numpy arrays across metadata dictionaries
|
||||
(
|
||||
[{"key1": np.array([1, 2, 3])}, {"key1": np.array([1, 2, 3])}],
|
||||
{"key1": np.array([1, 2, 3])},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Identical numpy arrays across metadata dictionaries, different datatype
|
||||
(
|
||||
[
|
||||
{"key1": np.array([1, 2, 3], dtype=np.int32)},
|
||||
{"key1": np.array([1, 2, 3], dtype=np.int64)},
|
||||
],
|
||||
{"key1": np.array([1, 2, 3])},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Conflicting lists for the same key
|
||||
([{"key1": [1, 2, 3]}, {"key1": [4, 5, 6]}], None, pytest.raises(ValueError)),
|
||||
# Conflicting numpy arrays for the same key
|
||||
(
|
||||
[{"key1": np.array([1, 2, 3])}, {"key1": np.array([4, 5, 6])}],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
# Mixed data types: list and numpy array for the same key
|
||||
(
|
||||
[{"key1": [1, 2, 3]}, {"key1": np.array([1, 2, 3])}],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
# Empty lists and numpy arrays for the same key
|
||||
([{"key1": []}, {"key1": np.array([])}], None, pytest.raises(ValueError)),
|
||||
# Identical multi-dimensional lists across metadata dictionaries
|
||||
(
|
||||
[{"key1": [[1, 2], [3, 4]]}, {"key1": [[1, 2], [3, 4]]}],
|
||||
{"key1": [[1, 2], [3, 4]]},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Identical multi-dimensional numpy arrays across metadata dictionaries
|
||||
(
|
||||
[
|
||||
{"key1": np.arange(4).reshape(2, 2)},
|
||||
{"key1": np.arange(4).reshape(2, 2)},
|
||||
],
|
||||
{"key1": np.arange(4).reshape(2, 2)},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Conflicting multi-dimensional lists for the same key
|
||||
(
|
||||
[{"key1": [[1, 2], [3, 4]]}, {"key1": [[5, 6], [7, 8]]}],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
# Conflicting multi-dimensional numpy arrays for the same key
|
||||
(
|
||||
[
|
||||
{"key1": np.arange(4).reshape(2, 2)},
|
||||
{"key1": np.arange(4, 8).reshape(2, 2)},
|
||||
],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
# Mixed types with multi-dimensional list and array for the same key
|
||||
(
|
||||
[{"key1": [[1, 2], [3, 4]]}, {"key1": np.arange(4).reshape(2, 2)}],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
# Identical higher-dimensional (3D) numpy arrays across
|
||||
# metadata dictionaries
|
||||
(
|
||||
[
|
||||
{"key1": np.arange(8).reshape(2, 2, 2)},
|
||||
{"key1": np.arange(8).reshape(2, 2, 2)},
|
||||
],
|
||||
{"key1": np.arange(8).reshape(2, 2, 2)},
|
||||
DoesNotRaise(),
|
||||
),
|
||||
# Differently-shaped higher-dimensional (3D) numpy arrays
|
||||
# across metadata dictionaries
|
||||
(
|
||||
[
|
||||
{"key1": np.arange(8).reshape(2, 2, 2)},
|
||||
{"key1": np.arange(8).reshape(4, 1, 2)},
|
||||
],
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_merge_metadata(metadata_list, expected_result, exception):
|
||||
with exception:
|
||||
result = merge_metadata(metadata_list)
|
||||
if expected_result is None:
|
||||
assert result is None, f"Expected an error, but got a result {result}"
|
||||
for key, value in result.items():
|
||||
assert key in expected_result
|
||||
if isinstance(value, np.ndarray):
|
||||
np.testing.assert_array_equal(value, expected_result[key])
|
||||
else:
|
||||
assert value == expected_result[key]
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
from contextlib import nullcontext as does_not_raise
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from supervision.detection.vlm import from_paligemma, from_qwen_2_5_vl
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception, result, resolution_wh, classes, expected_results",
|
||||
[
|
||||
(
|
||||
does_not_raise(),
|
||||
"",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # empty text
|
||||
(
|
||||
does_not_raise(),
|
||||
"",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # empty text, classes
|
||||
(
|
||||
does_not_raise(),
|
||||
"\n",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # newline only
|
||||
(
|
||||
does_not_raise(),
|
||||
"the quick brown fox jumps over the lazy dog.",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # random text, no location
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0768><loc0768> cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # partial location
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768><loc0768> cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # extra loc
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768>",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0).astype(str)),
|
||||
), # no class
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> catt",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(np.empty((0, 4)), np.empty(0), np.empty(0).astype(str)),
|
||||
), # invalid class
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
None,
|
||||
np.array(["cat"]).astype(str),
|
||||
),
|
||||
), # single box, no classes
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> black cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
None,
|
||||
np.array(["black cat"]).astype(np.dtype("U")),
|
||||
),
|
||||
), # class with space
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> black-cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
None,
|
||||
np.array(["black-cat"]).astype(np.dtype("U")),
|
||||
),
|
||||
), # class with hyphen
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> black_cat",
|
||||
(1000, 1000),
|
||||
None,
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
None,
|
||||
np.array(["black_cat"]).astype(np.dtype("U")),
|
||||
),
|
||||
), # class with underscore
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat ;",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
np.array([0]),
|
||||
np.array(["cat"]).astype(str),
|
||||
),
|
||||
), # correct class filter
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat ; "
|
||||
"<loc0256><loc0256><loc0768><loc0768> dog",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0], [250.0, 250.0, 750.0, 750.0]]),
|
||||
np.array([0, 1]),
|
||||
np.array(["cat", "dog"]).astype(np.dtype("U")),
|
||||
),
|
||||
), # multiple correct boxes, classes
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat ; "
|
||||
"<loc0256><loc0256><loc0768> cat",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
np.array([0]),
|
||||
np.array(["cat"]).astype(str),
|
||||
),
|
||||
), # partial valid boxes
|
||||
(
|
||||
does_not_raise(),
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat ; "
|
||||
"<loc0256><loc0256><loc0768><loc0768><loc0768> cat",
|
||||
(1000, 1000),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
||||
np.array([0]),
|
||||
np.array(["cat"]).astype(str),
|
||||
),
|
||||
), # partial valid again
|
||||
(
|
||||
pytest.raises(ValueError),
|
||||
"<loc0256><loc0256><loc0768><loc0768> cat",
|
||||
(0, 1000),
|
||||
None,
|
||||
None,
|
||||
), # zero width -> ValueError
|
||||
(
|
||||
pytest.raises(ValueError),
|
||||
"<loc0256><loc0256><loc0768><loc0768> dog",
|
||||
(1000, -200),
|
||||
None,
|
||||
None,
|
||||
), # negative height -> ValueError
|
||||
],
|
||||
)
|
||||
def test_from_paligemma(
|
||||
exception,
|
||||
result: str,
|
||||
resolution_wh: Tuple[int, int],
|
||||
classes: Optional[List[str]],
|
||||
expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray],
|
||||
) -> None:
|
||||
with exception:
|
||||
result = from_paligemma(
|
||||
result=result, resolution_wh=resolution_wh, classes=classes
|
||||
)
|
||||
np.testing.assert_array_equal(result[0], expected_results[0])
|
||||
np.testing.assert_array_equal(result[1], expected_results[1])
|
||||
np.testing.assert_array_equal(result[2], expected_results[2])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception, result, input_wh, resolution_wh, classes, expected_results",
|
||||
[
|
||||
(
|
||||
does_not_raise(),
|
||||
"some random text without triple backticks",
|
||||
(640, 640),
|
||||
(1280, 720),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0, dtype=str)),
|
||||
), # no snippet
|
||||
(
|
||||
does_not_raise(),
|
||||
"```json\nnot valid json\n```",
|
||||
(640, 640),
|
||||
(1280, 720),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0, dtype=str)),
|
||||
), # invalid JSON
|
||||
(
|
||||
does_not_raise(),
|
||||
"```json\n[]\n```",
|
||||
(640, 640),
|
||||
(1280, 720),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0, dtype=str)),
|
||||
), # empty list
|
||||
(
|
||||
does_not_raise(),
|
||||
"""```json
|
||||
[
|
||||
{"bbox_2d": [10, 10, 100, 100]},
|
||||
{"label": "missing box"},
|
||||
{"bbox_2d": [50, 60, 110, 120], "unused": "something"}
|
||||
]
|
||||
```""",
|
||||
(640, 640),
|
||||
(1280, 720),
|
||||
None,
|
||||
(np.empty((0, 4)), None, np.empty(0, dtype=str)),
|
||||
), # missing keys
|
||||
(
|
||||
does_not_raise(),
|
||||
"""```json
|
||||
[
|
||||
{"bbox_2d": [10, 20, 110, 120], "label": "cat"}
|
||||
]
|
||||
```""",
|
||||
(640, 640),
|
||||
(1280, 720),
|
||||
None,
|
||||
(
|
||||
np.array([[20.0, 22.5, 220.0, 135.0]]),
|
||||
None,
|
||||
np.array(["cat"], dtype=str),
|
||||
),
|
||||
), # single box no classes
|
||||
(
|
||||
does_not_raise(),
|
||||
"""```json
|
||||
[
|
||||
{"bbox_2d": [0, 0, 64, 64], "label": "dog"},
|
||||
{"bbox_2d": [100, 200, 300, 400], "label": "cat"}
|
||||
]
|
||||
```""",
|
||||
(640, 640),
|
||||
(640, 640),
|
||||
None,
|
||||
(
|
||||
np.array([[0, 0, 64, 64], [100, 200, 300, 400]], dtype=float),
|
||||
None,
|
||||
np.array(["dog", "cat"], dtype=str),
|
||||
),
|
||||
), # multiple no classes
|
||||
(
|
||||
does_not_raise(),
|
||||
"""```json
|
||||
[
|
||||
{"bbox_2d": [10, 20, 110, 120], "label": "bird"}
|
||||
]
|
||||
```""",
|
||||
(640, 640),
|
||||
(1280, 720),
|
||||
["cat", "dog"],
|
||||
(np.empty((0, 4)), np.empty(0, dtype=int), np.empty(0, dtype=str)),
|
||||
), # class mismatch
|
||||
(
|
||||
does_not_raise(),
|
||||
"""```json
|
||||
[
|
||||
{"bbox_2d": [10, 20, 110, 120], "label": "cat"},
|
||||
{"bbox_2d": [50, 100, 150, 200], "label": "dog"}
|
||||
]
|
||||
```""",
|
||||
(640, 640),
|
||||
(640, 480),
|
||||
["cat", "dog"],
|
||||
(
|
||||
np.array([[10.0, 15.0, 110.0, 90.0], [50.0, 75.0, 150.0, 150.0]]),
|
||||
np.array([0, 1], dtype=int),
|
||||
np.array(["cat", "dog"], dtype=str),
|
||||
),
|
||||
), # partial filtering
|
||||
(
|
||||
does_not_raise(),
|
||||
"""```json
|
||||
[
|
||||
{"bbox_2d": [-10, 0, 700, 700], "label": "dog"}
|
||||
]
|
||||
```""",
|
||||
(640, 640),
|
||||
(1280, 720),
|
||||
None,
|
||||
(
|
||||
np.array([[-20.0, 0.0, 1400.0, 787.5]]),
|
||||
None,
|
||||
np.array(["dog"], dtype=str),
|
||||
),
|
||||
), # out-of-bounds box
|
||||
(
|
||||
pytest.raises(ValueError),
|
||||
"""```json
|
||||
[
|
||||
{"bbox_2d": [10, 20, 110, 120], "label": "cat"}
|
||||
]
|
||||
```""",
|
||||
(0, 640),
|
||||
(1280, 720),
|
||||
None,
|
||||
None, # won't be compared because we expect an exception
|
||||
), # zero input width -> ValueError
|
||||
(
|
||||
pytest.raises(ValueError),
|
||||
"""```json
|
||||
[
|
||||
{"bbox_2d": [10, 20, 110, 120], "label": "dog"}
|
||||
]
|
||||
```""",
|
||||
(640, 640),
|
||||
(1280, -100),
|
||||
None,
|
||||
None,
|
||||
), # negative resolution height -> ValueError
|
||||
],
|
||||
)
|
||||
def test_from_qwen_2_5_vl(
|
||||
exception,
|
||||
result: str,
|
||||
input_wh: Tuple[int, int],
|
||||
resolution_wh: Tuple[int, int],
|
||||
classes: Optional[List[str]],
|
||||
expected_results,
|
||||
) -> None:
|
||||
with exception:
|
||||
xyxy, class_id, class_name = from_qwen_2_5_vl(
|
||||
result=result,
|
||||
input_wh=input_wh,
|
||||
resolution_wh=resolution_wh,
|
||||
classes=classes,
|
||||
)
|
||||
if expected_results is not None:
|
||||
np.testing.assert_array_equal(xyxy, expected_results[0])
|
||||
np.testing.assert_array_equal(class_id, expected_results[1])
|
||||
np.testing.assert_array_equal(class_name, expected_results[2])
|
||||
@@ -4,7 +4,7 @@ from typing import Optional, Tuple
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from supervision.detection.lmm import from_florence_2
|
||||
from supervision.detection.vlm import from_florence_2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -22,33 +22,26 @@ def mock_callback():
|
||||
@pytest.mark.parametrize(
|
||||
"slice_wh, overlap_ratio_wh, overlap_wh, expected_overlap, exception",
|
||||
[
|
||||
# Valid case: overlap_ratio_wh provided, overlap calculated from the ratio
|
||||
((128, 128), (0.2, 0.2), None, None, DoesNotRaise()),
|
||||
# Valid case: overlap_wh in pixels, no ratio provided
|
||||
# Valid case: explicit overlap_wh in pixels
|
||||
((128, 128), None, (26, 26), (26, 26), DoesNotRaise()),
|
||||
# Valid case: overlap_wh in pixels
|
||||
((128, 128), None, (20, 20), (20, 20), DoesNotRaise()),
|
||||
# Invalid case: overlap_ratio_wh greater than 1, should raise ValueError
|
||||
((128, 128), (1.1, 0.5), None, None, pytest.raises(ValueError)),
|
||||
# Invalid case: negative overlap_wh, should raise ValueError
|
||||
((128, 128), None, (-10, 20), None, pytest.raises(ValueError)),
|
||||
# Invalid case:
|
||||
# overlap_ratio_wh and overlap_wh provided, should raise ValueError
|
||||
((128, 128), (0.5, 0.5), (20, 20), (20, 20), pytest.raises(ValueError)),
|
||||
# Valid case: no overlap_ratio_wh, overlap_wh = 50 pixels
|
||||
((256, 256), None, (50, 50), (50, 50), DoesNotRaise()),
|
||||
# Valid case: overlap_ratio_wh provided, overlap calculated from (0.3, 0.3)
|
||||
((200, 200), (0.3, 0.3), None, None, DoesNotRaise()),
|
||||
# Valid case: small overlap_ratio_wh values
|
||||
((100, 100), (0.1, 0.1), None, None, DoesNotRaise()),
|
||||
# Invalid case: negative overlap_ratio_wh value, should raise ValueError
|
||||
((128, 128), (-0.1, 0.2), None, None, pytest.raises(ValueError)),
|
||||
# Invalid case: negative overlap_ratio_wh with overlap_wh provided
|
||||
((128, 128), (-0.1, 0.2), (30, 30), None, pytest.raises(ValueError)),
|
||||
# Invalid case: overlap_wh greater than slice size, should raise ValueError
|
||||
((128, 128), None, (150, 150), (150, 150), DoesNotRaise()),
|
||||
# Valid case: overlap_ratio_wh is 0, no overlap
|
||||
((128, 128), (0.0, 0.0), None, None, DoesNotRaise()),
|
||||
# Invalid case: no overlaps defined, no overlap
|
||||
# Invalid case: no overlaps defined
|
||||
((128, 128), None, None, None, pytest.raises(ValueError)),
|
||||
# Valid case: overlap_wh = 50 pixels
|
||||
((256, 256), None, (50, 50), (50, 50), DoesNotRaise()),
|
||||
# Valid case: overlap_wh = 60 pixels
|
||||
((200, 200), None, (60, 60), (60, 60), DoesNotRaise()),
|
||||
# Valid case: small overlap_wh values
|
||||
((100, 100), None, (0.1, 0.1), (0.1, 0.1), DoesNotRaise()),
|
||||
# Invalid case: negative overlap_wh values
|
||||
((128, 128), None, (-10, -10), None, pytest.raises(ValueError)),
|
||||
# Invalid case: overlap_wh greater than slice size
|
||||
((128, 128), None, (150, 150), (150, 150), DoesNotRaise()),
|
||||
# Valid case: zero overlap
|
||||
((128, 128), None, (0, 0), (0, 0), DoesNotRaise()),
|
||||
],
|
||||
)
|
||||
def test_inference_slicer_overlap(
|
||||
@@ -187,6 +180,6 @@ def test_generate_offset(
|
||||
)
|
||||
|
||||
# Verify that the generated offsets match the expected offsets
|
||||
assert np.array_equal(
|
||||
offsets, expected_offsets
|
||||
), f"Expected {expected_offsets}, got {offsets}"
|
||||
assert np.array_equal(offsets, expected_offsets), (
|
||||
f"Expected {expected_offsets}, got {offsets}"
|
||||
)
|
||||
|
||||
@@ -22,15 +22,15 @@ def test_ensure_cv2_image_for_processing_when_pillow_image_submitted(
|
||||
param_a: int,
|
||||
param_b: str,
|
||||
) -> np.ndarray:
|
||||
assert np.allclose(
|
||||
image, empty_cv2_image
|
||||
), "Expected conversion to OpenCV image to happen"
|
||||
assert (
|
||||
param_a == param_a_value
|
||||
), f"Parameter a expected to be {param_a_value} in target function"
|
||||
assert (
|
||||
param_b == param_b_value
|
||||
), f"Parameter b expected to be {param_b_value} in target function"
|
||||
assert np.allclose(image, empty_cv2_image), (
|
||||
"Expected conversion to OpenCV image to happen"
|
||||
)
|
||||
assert param_a == param_a_value, (
|
||||
f"Parameter a expected to be {param_a_value} in target function"
|
||||
)
|
||||
assert param_b == param_b_value, (
|
||||
f"Parameter b expected to be {param_b_value} in target function"
|
||||
)
|
||||
return image
|
||||
|
||||
# when
|
||||
@@ -61,15 +61,15 @@ def test_ensure_cv2_image_for_processing_when_cv2_image_submitted(
|
||||
param_a: int,
|
||||
param_b: str,
|
||||
) -> np.ndarray:
|
||||
assert np.allclose(
|
||||
image, empty_cv2_image
|
||||
), "Expected conversion to OpenCV image to happen"
|
||||
assert (
|
||||
param_a == param_a_value
|
||||
), f"Parameter a expected to be {param_a_value} in target function"
|
||||
assert (
|
||||
param_b == param_b_value
|
||||
), f"Parameter b expected to be {param_b_value} in target function"
|
||||
assert np.allclose(image, empty_cv2_image), (
|
||||
"Expected conversion to OpenCV image to happen"
|
||||
)
|
||||
assert param_a == param_a_value, (
|
||||
f"Parameter a expected to be {param_a_value} in target function"
|
||||
)
|
||||
assert param_b == param_b_value, (
|
||||
f"Parameter b expected to be {param_b_value} in target function"
|
||||
)
|
||||
return image
|
||||
|
||||
# when
|
||||
@@ -91,9 +91,9 @@ def test_cv2_to_pillow(
|
||||
|
||||
# then
|
||||
difference = ImageChops.difference(result, empty_pillow_image)
|
||||
assert (
|
||||
difference.getbbox() is None
|
||||
), "Conversion to PIL.Image expected not to change the content of image"
|
||||
assert difference.getbbox() is None, (
|
||||
"Conversion to PIL.Image expected not to change the content of image"
|
||||
)
|
||||
|
||||
|
||||
def test_pillow_to_cv2(
|
||||
@@ -103,9 +103,9 @@ def test_pillow_to_cv2(
|
||||
result = pillow_to_cv2(image=empty_pillow_image)
|
||||
|
||||
# then
|
||||
assert np.allclose(
|
||||
result, empty_cv2_image
|
||||
), "Conversion to OpenCV image expected not to change the content of image"
|
||||
assert np.allclose(result, empty_cv2_image), (
|
||||
"Conversion to OpenCV image expected not to change the content of image"
|
||||
)
|
||||
|
||||
|
||||
def test_images_to_cv2_when_empty_input_provided() -> None:
|
||||
@@ -128,9 +128,9 @@ def test_images_to_cv2_when_only_cv2_images_provided(
|
||||
# then
|
||||
assert len(result) == 5, "Expected the same number of output element as input ones"
|
||||
for result_element in result:
|
||||
assert (
|
||||
result_element is empty_cv2_image
|
||||
), "Expected CV images not to be touched by conversion"
|
||||
assert result_element is empty_cv2_image, (
|
||||
"Expected CV images not to be touched by conversion"
|
||||
)
|
||||
|
||||
|
||||
def test_images_to_cv2_when_only_pillow_images_provided(
|
||||
@@ -146,9 +146,9 @@ def test_images_to_cv2_when_only_pillow_images_provided(
|
||||
# then
|
||||
assert len(result) == 5, "Expected the same number of output element as input ones"
|
||||
for result_element in result:
|
||||
assert np.allclose(
|
||||
result_element, empty_cv2_image
|
||||
), "Output images expected to be equal to empty OpenCV image"
|
||||
assert np.allclose(result_element, empty_cv2_image), (
|
||||
"Output images expected to be equal to empty OpenCV image"
|
||||
)
|
||||
|
||||
|
||||
def test_images_to_cv2_when_mixed_input_provided(
|
||||
@@ -163,9 +163,9 @@ def test_images_to_cv2_when_mixed_input_provided(
|
||||
|
||||
# then
|
||||
assert len(result) == 2, "Expected the same number of output element as input ones"
|
||||
assert np.allclose(
|
||||
result[0], empty_cv2_image
|
||||
), "PIL image should be converted to OpenCV one, equal to example empty image"
|
||||
assert (
|
||||
result[1] is empty_cv2_image
|
||||
), "Expected CV images not to be touched by conversion"
|
||||
assert np.allclose(result[0], empty_cv2_image), (
|
||||
"PIL image should be converted to OpenCV one, equal to example empty image"
|
||||
)
|
||||
assert result[1] is empty_cv2_image, (
|
||||
"Expected CV images not to be touched by conversion"
|
||||
)
|
||||
|
||||
@@ -21,9 +21,9 @@ def test_resize_image_for_opencv_image() -> None:
|
||||
)
|
||||
|
||||
# then
|
||||
assert np.allclose(
|
||||
result, expected_result
|
||||
), "Expected output shape to be (w, h): (1024, 768)"
|
||||
assert np.allclose(result, expected_result), (
|
||||
"Expected output shape to be (w, h): (1024, 768)"
|
||||
)
|
||||
|
||||
|
||||
def test_resize_image_for_pillow_image() -> None:
|
||||
@@ -41,9 +41,9 @@ def test_resize_image_for_pillow_image() -> None:
|
||||
# then
|
||||
assert result.size == (1024, 768), "Expected output shape to be (w, h): (1024, 768)"
|
||||
difference = ImageChops.difference(result, expected_result)
|
||||
assert (
|
||||
difference.getbbox() is None
|
||||
), "Expected no difference in resized image content as the image is all zeros"
|
||||
assert difference.getbbox() is None, (
|
||||
"Expected no difference in resized image content as the image is all zeros"
|
||||
)
|
||||
|
||||
|
||||
def test_letterbox_image_for_opencv_image() -> None:
|
||||
@@ -95,9 +95,9 @@ def test_letterbox_image_for_pillow_image() -> None:
|
||||
1024,
|
||||
), "Expected output shape to be (w, h): (1024, 1024)"
|
||||
difference = ImageChops.difference(result, expected_result)
|
||||
assert (
|
||||
difference.getbbox() is None
|
||||
), "Expected padding to be added top and bottom with padding added top and bottom"
|
||||
assert difference.getbbox() is None, (
|
||||
"Expected padding to be added top and bottom with padding added top and bottom"
|
||||
)
|
||||
|
||||
|
||||
def test_create_tiles_with_one_image(
|
||||
|
||||
Reference in New Issue
Block a user