Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27c68f293d | ||
|
|
ee3d1768e3 | ||
|
|
f1124e04df | ||
|
|
55c8c07b90 | ||
|
|
6bb5929e09 | ||
|
|
6ac0f84e27 | ||
|
|
162f9ed6d2 | ||
|
|
7600bef8e7 | ||
|
|
9df96572ac | ||
|
|
8ff0d31e57 | ||
|
|
5160d20fbc | ||
|
|
9f730b9fb5 | ||
|
|
a7ecc57432 | ||
|
|
ec5790a90c | ||
|
|
57e5c108de | ||
|
|
84bb44d222 | ||
|
|
026a243f81 | ||
|
|
d1e5e031e3 | ||
|
|
a5bb93ddec | ||
|
|
fe27e44081 | ||
|
|
d21a6b1ee8 | ||
|
|
395d90b5a6 | ||
|
|
635272b892 | ||
|
|
0edb590bb5 | ||
|
|
5aa4216c86 | ||
|
|
1ae8fc798e | ||
|
|
e8ec271f0b | ||
|
|
9e3a150196 | ||
|
|
acd23bf662 | ||
|
|
5532731910 | ||
|
|
029abbd614 | ||
|
|
46ecb808f7 | ||
|
|
5befed7332 | ||
|
|
15a192c63a | ||
|
|
317485b122 | ||
|
|
0f23c6d425 | ||
|
|
44d531fdb8 | ||
|
|
a3262a8f9c | ||
|
|
7e9dca632b | ||
|
|
cc7de0edb7 | ||
|
|
808814bce7 | ||
|
|
89000c1c33 | ||
|
|
71cbd82ef4 | ||
|
|
dff15cb8d6 | ||
|
|
befa12bb23 | ||
|
|
f86a2d3a82 | ||
|
|
3d7c1ba1ec | ||
|
|
a64a2fc243 | ||
|
|
b65d2cc73f | ||
|
|
207260aca1 | ||
|
|
81b27340e5 | ||
|
|
a92665e85c | ||
|
|
e7bf00aa95 | ||
|
|
392c6930bb | ||
|
|
ed253fef99 | ||
|
|
cef25cc601 | ||
|
|
e1b13e369c | ||
|
|
ed09f05424 | ||
|
|
369831eb76 | ||
|
|
0d17a8e6b5 | ||
|
|
2e4f05e8ec | ||
|
|
1891b2edde | ||
|
|
477faee1c7 | ||
|
|
5957b250f0 | ||
|
|
7d27e67966 | ||
|
|
c31d96dabe | ||
|
|
4ab9ae5a71 | ||
|
|
888da4e208 | ||
|
|
8f480ead6a | ||
|
|
cdc3b4d6b1 | ||
|
|
bae1797e83 | ||
|
|
7319a026ab | ||
|
|
509b6da063 | ||
|
|
10ea37b732 | ||
|
|
6ce126d6d8 | ||
|
|
ab76a47eb8 | ||
|
|
5261efebc3 | ||
|
|
ca5486dc5a | ||
|
|
eecc0dea91 | ||
|
|
cf7c95e700 | ||
|
|
366bc21d1e | ||
|
|
526ddacd2f | ||
|
|
ab45b19e7e | ||
|
|
6289d9cab0 | ||
|
|
85e0a544fe | ||
|
|
6f47cd318a | ||
|
|
66758448cb | ||
|
|
c8673fe517 | ||
|
|
b5a958745b | ||
|
|
cfd079bbcb | ||
|
|
0e8365ff80 | ||
|
|
758b9d60bd | ||
|
|
b40cddd144 | ||
|
|
dabd18c0de | ||
|
|
e1ffd0e8a9 | ||
|
|
731d580a83 | ||
|
|
d4f95eca45 | ||
|
|
0477499b77 | ||
|
|
29de4d245e | ||
|
|
2012ec7bcb | ||
|
|
5b3ce80a34 | ||
|
|
c3641de724 | ||
|
|
822dd4d555 | ||
|
|
09e88da71a | ||
|
|
31a2b26144 | ||
|
|
f7713a32aa | ||
|
|
c97a885f9e | ||
|
|
b1cc5c3564 | ||
|
|
91d8691bc4 | ||
|
|
072072e9b2 | ||
|
|
148bb63aa8 | ||
|
|
d60443751a | ||
|
|
1421f2c743 | ||
|
|
751ea27263 | ||
|
|
78b6020ea4 | ||
|
|
d08d22dec6 | ||
|
|
ee1f15eebe | ||
|
|
d755bca7c8 | ||
|
|
5565241ef5 | ||
|
|
a4cf2604ee | ||
|
|
3e860a5449 | ||
|
|
0305ceed45 | ||
|
|
4add8c871e | ||
|
|
6f566b5518 | ||
|
|
d6d71a0366 | ||
|
|
4c903cf7f2 | ||
|
|
b061f4fd84 | ||
|
|
2b73aea438 | ||
|
|
db958d9df8 | ||
|
|
f699f087db | ||
|
|
7d7e5f17a5 | ||
|
|
d486dd1083 | ||
|
|
5123085037 | ||
|
|
43041c26c5 | ||
|
|
ca2902cc46 | ||
|
|
2365ed47e1 | ||
|
|
78689590e6 | ||
|
|
d73187f3dd | ||
|
|
d1df96a953 | ||
|
|
c1650d3770 | ||
|
|
a72c5044f4 | ||
|
|
4e7f69a502 | ||
|
|
6d719fcd60 | ||
|
|
ac09b58620 | ||
|
|
b9cc02ecb9 | ||
|
|
00b00d2f9a | ||
|
|
5a9e85a60f | ||
|
|
fea77d86a7 | ||
|
|
11466ff261 | ||
|
|
80549933fc | ||
|
|
a428fac98d | ||
|
|
a109b264ef | ||
|
|
389b8968c2 | ||
|
|
b000d162e7 | ||
|
|
66e1672f37 | ||
|
|
52e7fe1d30 | ||
|
|
b61c5f23c6 | ||
|
|
0285bfa50a | ||
|
|
0069d6b289 | ||
|
|
ebba78528e | ||
|
|
da2bc575e0 | ||
|
|
25ea63042d | ||
|
|
24debfe377 | ||
|
|
414ee9166d | ||
|
|
edc4dd33b0 | ||
|
|
22b970e4d4 | ||
|
|
e367e307fb | ||
|
|
69ffca50d5 | ||
|
|
01fd82c43b | ||
|
|
f04bdb2491 | ||
|
|
aa6c673316 | ||
|
|
b2752ac6c6 | ||
|
|
7c038f1d45 | ||
|
|
b6547ba58e | ||
|
|
fde9cd4735 | ||
|
|
1d3e423617 | ||
|
|
9fc66563dd | ||
|
|
8a82f4a516 | ||
|
|
17d1934ed5 | ||
|
|
21fcf1bd04 | ||
|
|
ebd606ff00 | ||
|
|
f1241e4fe8 | ||
|
|
9de14b5c67 | ||
|
|
706c2d19b9 | ||
|
|
7a81c13543 | ||
|
|
4648276cc6 | ||
|
|
2c235dfce9 | ||
|
|
dacd8f5455 | ||
|
|
da261ff8cf | ||
|
|
a5ae72d7ae | ||
|
|
fd568e2419 | ||
|
|
7c7c3d83ef | ||
|
|
03dba9c7a2 | ||
|
|
1d0616640e | ||
|
|
f6437ab778 | ||
|
|
7ad22c73a8 | ||
|
|
54903b1cc0 | ||
|
|
e566ac9eb3 | ||
|
|
f95f81eaf3 | ||
|
|
f2ede43d0e | ||
|
|
f2ef9f817c | ||
|
|
7bba367f98 | ||
|
|
977892ad1f | ||
|
|
2ca696e064 | ||
|
|
3bb384d789 | ||
|
|
ac1fa2edca | ||
|
|
4ce9d76b65 | ||
|
|
1a1a2d27ce | ||
|
|
c9598ef732 | ||
|
|
4c01eb4a3d | ||
|
|
f9471184d7 | ||
|
|
bb0e4ec059 | ||
|
|
7f8b6d3c5e | ||
|
|
c6a756352a | ||
|
|
66456a893c | ||
|
|
8704a46e96 | ||
|
|
3de8ca609f | ||
|
|
586387ab3a | ||
|
|
37a988fd9f | ||
|
|
cb6045b026 | ||
|
|
9956d5dd82 | ||
|
|
b103ce4eb7 | ||
|
|
22df9a5252 | ||
|
|
b030d1dc9f | ||
|
|
387a759614 | ||
|
|
5dd8910e02 | ||
|
|
b19c812f58 | ||
|
|
e58939235f | ||
|
|
b8e1612511 | ||
|
|
a0abf2dee7 | ||
|
|
f5bced4510 | ||
|
|
a8d91b0ae5 | ||
|
|
307821ca25 | ||
|
|
c5d052ed16 | ||
|
|
e2f33faa67 | ||
|
|
1f6694ba50 | ||
|
|
01922f849e | ||
|
|
3fb6a7ba6e | ||
|
|
16b8102900 | ||
|
|
acb5dd0dd4 | ||
|
|
b325eebe00 | ||
|
|
eb0041d275 | ||
|
|
3ccd74d93e | ||
|
|
e0d06f9a64 | ||
|
|
d63e7b4677 | ||
|
|
0210f51944 | ||
|
|
073500bf75 | ||
|
|
60d33ca140 | ||
|
|
e65892c80d | ||
|
|
24320405b4 | ||
|
|
b5ea2cd1cb | ||
|
|
c26814da87 | ||
|
|
da8761bd96 | ||
|
|
b8a2fb50d4 | ||
|
|
310b566c9b | ||
|
|
43c31f933c | ||
|
|
3a54afe506 | ||
|
|
891b8523f0 | ||
|
|
bc12bd38b4 | ||
|
|
0cb66e5551 | ||
|
|
b81e92ca94 | ||
|
|
7ca5b01a75 | ||
|
|
44753536f3 | ||
|
|
d774c91d7b | ||
|
|
2abb76e167 | ||
|
|
75d4b9f20a | ||
|
|
056401a858 | ||
|
|
b678ac929e | ||
|
|
b7b19b0357 | ||
|
|
46ac1812d8 | ||
|
|
b21a478528 | ||
|
|
65f2bbba57 | ||
|
|
d0111bf41d | ||
|
|
31db574e63 | ||
|
|
c930dd06ec | ||
|
|
83dd92726e | ||
|
|
59f3e19d2d | ||
|
|
73ba5f039b | ||
|
|
e76dec7e15 | ||
|
|
8a67ca073a | ||
|
|
2b7cd6ab23 | ||
|
|
9b0568c5e7 | ||
|
|
48f6c8d3c1 | ||
|
|
2544d7293d | ||
|
|
c71d9458c8 | ||
|
|
c28c9abd5b | ||
|
|
c69ee8046a | ||
|
|
65894c9e39 | ||
|
|
55fd9fca1f | ||
|
|
f24e846272 | ||
|
|
d91113c9e7 | ||
|
|
94a9b73117 | ||
|
|
4ef027462f | ||
|
|
a788623ea3 | ||
|
|
94d3f3829b | ||
|
|
321e01157a | ||
|
|
97b2ba2d6a | ||
|
|
5ee8fcfea2 | ||
|
|
c9d4f2b82e | ||
|
|
aa003d0161 | ||
|
|
7decfc7d1e | ||
|
|
93c1b947fc | ||
|
|
66d1f4c67d | ||
|
|
70a13fbf1e | ||
|
|
1db8e12e1b | ||
|
|
8853321058 | ||
|
|
0a5e830159 | ||
|
|
fb041f9b6c | ||
|
|
28f83b18f9 | ||
|
|
cea854edf5 | ||
|
|
9a61b3c41b | ||
|
|
113d4b697a | ||
|
|
feb779bae9 | ||
|
|
521dc57b76 | ||
|
|
a0775544df | ||
|
|
14a0e299a1 | ||
|
|
1e783b623b | ||
|
|
c7a8654390 | ||
|
|
a7f8aee2c7 | ||
|
|
5ed03491f9 | ||
|
|
2260cdd3de | ||
|
|
377ca27cc9 | ||
|
|
5d979df2f9 | ||
|
|
9d5a477064 | ||
|
|
5006f8c2f7 | ||
|
|
8570ce68e2 | ||
|
|
3d3bbb4095 | ||
|
|
a1e3543897 | ||
|
|
4b82679b00 | ||
|
|
7fa7907219 | ||
|
|
60d5660286 | ||
|
|
5d4abe6e0d | ||
|
|
0405c74cf3 | ||
|
|
13fe6e0942 | ||
|
|
03c92f7652 | ||
|
|
084926ded3 | ||
|
|
382ed8e609 | ||
|
|
2ed2a793cd | ||
|
|
e0fb73f18a | ||
|
|
de896189b8 | ||
|
|
fbfd078795 | ||
|
|
e31aad242a | ||
|
|
93c5c39f8f | ||
|
|
810148550b | ||
|
|
172877a3cf | ||
|
|
a91fc1e4f3 | ||
|
|
f4fd595783 | ||
|
|
e690b92688 | ||
|
|
c8f771ec69 | ||
|
|
6b09e1d67c | ||
|
|
12195705f5 | ||
|
|
9f7871b4be | ||
|
|
44890dc712 | ||
|
|
57ae7467b9 | ||
|
|
1e5e25c54a | ||
|
|
ba7c4f4404 | ||
|
|
f3b52c08a7 | ||
|
|
1a36842407 | ||
|
|
a8b12914c8 | ||
|
|
d3db15fcd0 | ||
|
|
c40734fb8d | ||
|
|
55a9cef270 | ||
|
|
4b9b70fbb2 | ||
|
|
e61a1074ac | ||
|
|
e6c7f970f0 | ||
|
|
f06dcf73a5 | ||
|
|
0bb25fdd91 | ||
|
|
9501a0c72e | ||
|
|
d9b9143f07 | ||
|
|
b7e6bcc15f | ||
|
|
08fbcbd8ea | ||
|
|
e5114a233c | ||
|
|
dea7f83e50 | ||
|
|
034d3eaaac | ||
|
|
7397d8a240 | ||
|
|
a023259445 | ||
|
|
09a06e10a1 | ||
|
|
8d26a9291b | ||
|
|
28d219c86d | ||
|
|
89ef841955 | ||
|
|
b164ef1111 | ||
|
|
2f39a2c3c6 | ||
|
|
d8f990231c | ||
|
|
74d19dd069 | ||
|
|
552c8dbede | ||
|
|
e421af494f | ||
|
|
5b3fc12aea | ||
|
|
854fb18b34 | ||
|
|
185ae462a4 | ||
|
|
3676f07168 | ||
|
|
099258baa6 | ||
|
|
0bb3839d29 | ||
|
|
f66444bc91 | ||
|
|
6de21048d2 | ||
|
|
db9d6c623b | ||
|
|
fb7ecd73d9 | ||
|
|
2bc19d69af | ||
|
|
a377b54a60 | ||
|
|
db1998c155 | ||
|
|
ef6369b93d | ||
|
|
a21a17ba29 | ||
|
|
c973647cb8 | ||
|
|
71bdb01547 | ||
|
|
92a5a931a8 | ||
|
|
0ecd88d4d1 | ||
|
|
4d12853b95 | ||
|
|
9333460ee9 | ||
|
|
94b4c22594 | ||
|
|
d5cebd4fd2 | ||
|
|
61579af25a | ||
|
|
bcd3e1be17 | ||
|
|
9ef697cd10 | ||
|
|
19bf417810 | ||
|
|
3df33f0f9d | ||
|
|
b8b31f6f00 | ||
|
|
fc11f4d7af | ||
|
|
ec517124f8 | ||
|
|
f00dcede1a | ||
|
|
6fb90f6b75 | ||
|
|
23e63501eb | ||
|
|
0df56b1d66 | ||
|
|
ec75a86a83 | ||
|
|
4599548eeb | ||
|
|
bb513f880a | ||
|
|
fbce6e45d4 | ||
|
|
a00e2d22ba | ||
|
|
04c5f18da0 | ||
|
|
8b933f640e | ||
|
|
f01ca33511 | ||
|
|
2f88bb2b8e | ||
|
|
b2b251a424 | ||
|
|
eec3615250 | ||
|
|
272a9cb35e | ||
|
|
0d0328ef22 | ||
|
|
d821fca2af | ||
|
|
ff9a63a68b | ||
|
|
a6222a065c | ||
|
|
1489f82e0f | ||
|
|
12722df65b | ||
|
|
900952acae | ||
|
|
95308126c7 | ||
|
|
e7de6859e9 | ||
|
|
feeea38e6e | ||
|
|
6ea07a1c2a | ||
|
|
fbb770a486 | ||
|
|
09f663139c | ||
|
|
9efd04925c | ||
|
|
14ce0799ce | ||
|
|
a81f24e08d | ||
|
|
257e73fb37 | ||
|
|
dcfb122816 | ||
|
|
79ea168fd4 | ||
|
|
d35830ddeb | ||
|
|
c5ff58910d | ||
|
|
15d9b75225 | ||
|
|
49318ffe31 | ||
|
|
b52a0297f1 | ||
|
|
eceebb33e3 | ||
|
|
fa7821b1c4 | ||
|
|
a56a16937f | ||
|
|
0c95a6126d | ||
|
|
c14642accf | ||
|
|
8c0e2fb362 | ||
|
|
e5fb43aea6 | ||
|
|
32b70c5d37 | ||
|
|
b9e98a5e20 | ||
|
|
246b8a720c | ||
|
|
a43970fb65 | ||
|
|
ff5709def2 | ||
|
|
2805f41675 | ||
|
|
4eaa897f2c | ||
|
|
f96e7766f3 | ||
|
|
07cb0d8ccf | ||
|
|
3725beb2db | ||
|
|
d5f8b94d65 | ||
|
|
6f7f728044 | ||
|
|
aad3c4fce4 | ||
|
|
52724ca673 | ||
|
|
e3fc238c70 | ||
|
|
cfe29cd05f | ||
|
|
0f76f731ee | ||
|
|
c239291541 | ||
|
|
a446ffd021 | ||
|
|
c2084c4d0f | ||
|
|
e3cb417129 | ||
|
|
27de665940 | ||
|
|
93b21a8d2d | ||
|
|
a4bdfc2062 | ||
|
|
fd1a07cb6b | ||
|
|
edfb814fc4 | ||
|
|
3d5437b960 | ||
|
|
68c2504d61 | ||
|
|
6984d72f4a | ||
|
|
6f5b477ac0 | ||
|
|
b19511968d | ||
|
|
165c77d830 | ||
|
|
558581943b | ||
|
|
8acbc422c9 | ||
|
|
9995771604 | ||
|
|
bcc1bc1ff2 | ||
|
|
cce69d893e | ||
|
|
3e2fe835db | ||
|
|
47f1c44e20 | ||
|
|
420543534d | ||
|
|
1ea9345845 | ||
|
|
6ca09ccbfc | ||
|
|
7f78f93407 | ||
|
|
05ceca70ef | ||
|
|
e8f8c38f39 | ||
|
|
0739e24b25 | ||
|
|
560760993a | ||
|
|
859f0258bc | ||
|
|
2f644c52e3 | ||
|
|
a82c37c7fa | ||
|
|
fc2710c9c9 | ||
|
|
29161a485e | ||
|
|
523d218dd4 | ||
|
|
46bd267079 | ||
|
|
d025791805 | ||
|
|
7e4fc8a485 | ||
|
|
26d70219bf | ||
|
|
daf13442f3 | ||
|
|
a6b985ff4b | ||
|
|
4fea0031ba | ||
|
|
490a341d5d | ||
|
|
2c3fa27929 | ||
|
|
51c7ebdf60 | ||
|
|
2653f5a090 | ||
|
|
d309d74f37 | ||
|
|
b85035c45e | ||
|
|
687f8d9621 | ||
|
|
668f2f063f | ||
|
|
f93c712dca | ||
|
|
88f995803f | ||
|
|
c5ac30c303 | ||
|
|
0d72846b40 | ||
|
|
5d8da9633c | ||
|
|
325bd46740 |
@@ -0,0 +1,64 @@
|
||||
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}"
|
||||
@@ -14,21 +14,8 @@ jobs:
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: 📦 Install mkdocs-material
|
||||
run: pip install "mkdocs-material[all]"
|
||||
- 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: 🧪 Test documentation build
|
||||
run: mkdocs build --verbose
|
||||
- name: 🏗️ Install dependencies and Test Docs Build
|
||||
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
|
||||
|
||||
+2
-15
@@ -9,7 +9,6 @@ repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
exclude: test/.*\.py
|
||||
- id: check-yaml
|
||||
@@ -19,33 +18,21 @@ repos:
|
||||
- id: check-case-conflict
|
||||
- id: check-added-large-files
|
||||
- id: detect-private-key
|
||||
- id: forbid-new-submodules
|
||||
- id: pretty-format-json
|
||||
exclude: demo.ipynb
|
||||
args: ['--autofix', '--no-sort-keys', '--indent=4']
|
||||
- id: end-of-file-fixer
|
||||
- id: mixed-line-ending
|
||||
|
||||
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: '1.7.8'
|
||||
rev: '1.7.9'
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: ["-c", "pyproject.toml"]
|
||||
additional_dependencies: ["bandit[toml]"]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
- id: isort
|
||||
name: isort (pyi)
|
||||
types: [pyi]
|
||||
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.4.7
|
||||
rev: v0.6.2
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
|
||||
+122
-35
@@ -4,6 +4,25 @@ Thank you for your interest in contributing to Supervision!
|
||||
|
||||
We are actively improving this library to reduce the amount of work you need to do to solve common computer vision problems.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Please read and adhere to our [Code of Conduct](CODE_OF_CONDUCT.md). This document outlines the expected behavior for all participants in our project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Contribution Guidelines](#contribution-guidelines)
|
||||
- [Contributing Features](#contributing-features-)
|
||||
- [How to Contribute Changes](#how-to-contribute-changes)
|
||||
- [Installation for Contributors](#installation-for-contributors)
|
||||
- [Code Style and Quality](#-code-style-and-quality)
|
||||
- [Pre-commit tool](#pre-commit-tool)
|
||||
- [Docstrings](#docstrings)
|
||||
- [Type checking](#type-checking)
|
||||
- [Documentation](#-documentation)
|
||||
- [Cookbooks](#-cookbooks)
|
||||
- [Tests](#-tests)
|
||||
- [License](#-license)
|
||||
|
||||
## Contribution Guidelines
|
||||
|
||||
We welcome contributions to:
|
||||
@@ -32,49 +51,49 @@ First, fork this repository to your own GitHub account. Click "fork" in the top
|
||||
|
||||
Then, run `git clone` to download the project code to your computer.
|
||||
|
||||
You should also set up `roboflow/supervision` as an "upstream" remote (that is, tell git that the reference Supervision repository was the source of your fork of it):
|
||||
|
||||
```bash
|
||||
git remote add upstream https://github.com/roboflow/supervision.git
|
||||
git fetch upstream
|
||||
```
|
||||
|
||||
Move to a new branch using the `git checkout` command:
|
||||
|
||||
```bash
|
||||
git checkout -b <your_branch_name>
|
||||
git checkout -b <scope>/<your_branch_name> upstream/develop
|
||||
```
|
||||
|
||||
The name you choose for your branch should describe the change you want to make (i.e. `line-counter-docs`).
|
||||
The name you choose for your branch should describe the change you want to make and start with an appropriate prefix:
|
||||
|
||||
- `feat/`: for new features (e.g., `feat/line-counter`)
|
||||
- `fix/`: for bug fixes (e.g., `fix/memory-leak`)
|
||||
- `docs/`: for documentation changes (e.g., `docs/update-readme`)
|
||||
- `chore/`: for routine tasks, maintenance, or tooling changes (e.g., `chore/update-dependencies`)
|
||||
- `test/`: for adding or modifying tests (e.g., `test/add-unit-tests`)
|
||||
- `refactor/`: for code refactoring (e.g., `refactor/simplify-algorithm`)
|
||||
|
||||
Make any changes you want to the project code, then run the following commands to commit your changes:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Your commit message"
|
||||
git push -u origin main
|
||||
git add -A
|
||||
git commit -m "feat: add line counter functionality"
|
||||
git push -u origin <your_branch_name>
|
||||
```
|
||||
|
||||
## 🎨 Code quality
|
||||
Use conventional commit messages to clearly describe your changes. The format is:
|
||||
|
||||
### Pre-commit tool
|
||||
<type>[optional scope]: <description>
|
||||
|
||||
This project uses the [pre-commit](https://pre-commit.com/) tool to maintain code quality and consistency. Before submitting a pull request or making any commits, it is important to run the pre-commit tool to ensure that your changes meet the project's guidelines.
|
||||
|
||||
Furthermore, we have integrated a pre-commit GitHub Action into our workflow. This means that with every pull request opened, the pre-commit checks will be automatically enforced, streamlining the code review process and ensuring that all contributions adhere to our quality standards.
|
||||
|
||||
To run the pre-commit tool, follow these steps:
|
||||
|
||||
1. Install pre-commit by running the following command: `poetry install`. 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.
|
||||
|
||||
3. Run the command `pre-commit run --all-files`. This will execute the pre-commit hooks configured for this project against the modified files. If any issues are found, the pre-commit tool will provide feedback on how to resolve them. Make the necessary changes and re-run the pre-commit command until all issues are resolved.
|
||||
|
||||
4. You can also install pre-commit as a git hook by execute `pre-commit install`. Every time you made `git commit` pre-commit run automatically for you.
|
||||
|
||||
### Docstrings
|
||||
|
||||
All new functions and classes in `supervision` should include docstrings. This is a prerequisite for any new functions and classes to be added to the library.
|
||||
|
||||
`supervision` adheres to the [Google Python docstring style](https://google.github.io/styleguide/pyguide.html#383-functions-and-methods). Please refer to the style guide while writing docstrings for your contribution.
|
||||
|
||||
### Type checking
|
||||
|
||||
So far, **there is no type checking with mypy**. See [issue](https://github.com/roboflow-ai/template-python/issues/4).
|
||||
Common types include:
|
||||
- feat: A new feature
|
||||
- fix: A bug fix
|
||||
- docs: Documentation only changes
|
||||
- style: Changes that do not affect the meaning of the code (white-space, formatting, etc)
|
||||
- refactor: A code change that neither fixes a bug nor adds a feature
|
||||
- perf: A code change that improves performance
|
||||
- test: Adding missing tests or correcting existing tests
|
||||
- chore: Changes to the build process or auxiliary tools and libraries
|
||||
|
||||
Then, go back to your fork of the `supervision` repository, click "Pull Requests", and click "New Pull Request".
|
||||
|
||||
@@ -104,7 +123,75 @@ All pull requests will be reviewed by the maintainers of the project. We will pr
|
||||
|
||||
PRs must pass all tests and linting requirements before they can be merged.
|
||||
|
||||
## 📝 documentation
|
||||
## Installation for Contributors
|
||||
|
||||
Before starting your work on the project, set up your development environment:
|
||||
|
||||
1. Clone your fork of the project:
|
||||
```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
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
3. Install Poetry:
|
||||
|
||||
Using pip:
|
||||
```bash
|
||||
pip install -U pip setuptools
|
||||
pip install poetry
|
||||
```
|
||||
|
||||
Or using pipx (recommended for global installation):
|
||||
```bash
|
||||
pipx install poetry
|
||||
```
|
||||
|
||||
4. Install project dependencies:
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
|
||||
5. Run pytest to verify the setup:
|
||||
```bash
|
||||
poetry run pytest
|
||||
```
|
||||
|
||||
## 🎨 Code Style and Quality
|
||||
|
||||
### Pre-commit tool
|
||||
|
||||
This project uses the [pre-commit](https://pre-commit.com/) tool to maintain code quality and consistency. Before submitting a pull request or making any commits, it is important to run the pre-commit tool to ensure that your changes meet the project's guidelines.
|
||||
|
||||
Furthermore, we have integrated a pre-commit GitHub Action into our workflow. This means that with every pull request opened, the pre-commit checks will be automatically enforced, streamlining the code review process and ensuring that all contributions adhere to our quality standards.
|
||||
|
||||
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
|
||||
|
||||
2. Once pre-commit is installed, navigate to the project's root directory.
|
||||
|
||||
3. Run the command `pre-commit run --all-files`. This will execute the pre-commit hooks configured for this project against the modified files. If any issues are found, the pre-commit tool will provide feedback on how to resolve them. Make the necessary changes and re-run the pre-commit command until all issues are resolved.
|
||||
|
||||
4. You can also install pre-commit as a git hook by executing `pre-commit install`. Every time you do a `git commit` pre-commit run automatically for you.
|
||||
|
||||
### Docstrings
|
||||
|
||||
All new functions and classes in `supervision` should include docstrings. This is a prerequisite for any new functions and classes to be added to the library.
|
||||
|
||||
`supervision` adheres to the [Google Python docstring style](https://google.github.io/styleguide/pyguide.html#383-functions-and-methods). Please refer to the style guide while writing docstrings for your contribution.
|
||||
|
||||
### Type checking
|
||||
|
||||
So far, **there is no type checking with mypy**. See [issue](https://github.com/roboflow-ai/template-python/issues/4).
|
||||
|
||||
## 📝 Documentation
|
||||
|
||||
The `supervision` documentation is stored in a folder called `docs`. The project documentation is built using `mkdocs`.
|
||||
|
||||
@@ -112,13 +199,13 @@ To run the documentation, install the project requirements with `poetry install
|
||||
|
||||
You can learn more about mkdocs on the [mkdocs website](https://www.mkdocs.org/).
|
||||
|
||||
## 🧑🍳 cookbooks
|
||||
## 🧑🍳 Cookbooks
|
||||
|
||||
We are always looking for new examples and cookbooks to add to the `supervision`
|
||||
documentation. If you have a use case that you think would be helpful to others, please
|
||||
submit a PR with your example. Here are some guidelines for submitting a new example:
|
||||
|
||||
- Create a new notebook in the [`docs/nodebooks`](https://github.com/roboflow/supervision/tree/develop/docs/notebooks) folder.
|
||||
- Create a new notebook in the [`docs/notebooks`](https://github.com/roboflow/supervision/tree/develop/docs/notebooks) folder.
|
||||
- Add a link to the new notebook in [`docs/theme/cookbooks.html`](https://github.com/roboflow/supervision/blob/develop/docs/theme/cookbooks.html). Make sure to add the path to the new notebook, as well as a title, labels, author and supervision version.
|
||||
- Use the [Count Objects Crossing the Line](https://supervision.roboflow.com/develop/notebooks/count-objects-crossing-the-line/) example as a template for your new example.
|
||||
- Freeze the version of `supervision` you are using.
|
||||
@@ -126,10 +213,10 @@ submit a PR with your example. Here are some guidelines for submitting a new exa
|
||||
- Notebook should be self-contained. If you rely on external data ( videos, images, etc.) or libraries, include download and installation commands in the notebook.
|
||||
- Annotate the code with appropriate comments, including links to the documentation describing each of the tools you have used.
|
||||
|
||||
## 🧪 tests
|
||||
## 🧪 Tests
|
||||
|
||||
[`pytests`](https://docs.pytest.org/en/7.1.x/) is used to run our tests.
|
||||
|
||||
## 📄 license
|
||||
## 📄 License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under an [MIT license](https://github.com/roboflow/supervision/blob/develop/LICENSE.md).
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
[](https://badge.fury.io/py/supervision)
|
||||
[](https://pypistats.org/packages/supervision)
|
||||
[](https://snyk.io/advisor/python/supervision)
|
||||
[](https://github.com/roboflow/supervision/blob/main/LICENSE.md)
|
||||
[](https://badge.fury.io/py/supervision)
|
||||
[](https://colab.research.google.com/github/roboflow/supervision/blob/main/demo.ipynb)
|
||||
@@ -28,7 +29,7 @@
|
||||
|
||||
**We write your reusable computer vision tools.** Whether you need to load your dataset from your hard drive, draw detections on an image or video, or count how many detections are in a zone. You can count on us! 🤝
|
||||
|
||||
[](https://github.com/orgs/roboflow/projects/10)
|
||||
[](https://github.com/orgs/roboflow/projects)
|
||||
|
||||
## 💻 install
|
||||
|
||||
@@ -86,7 +87,7 @@ len(detections)
|
||||
|
||||
### annotators
|
||||
|
||||
Supervision offers a wide range of highly customizable [annotators](https://supervision.roboflow.com/latest/annotators/), allowing you to compose the perfect visualization for your use case.
|
||||
Supervision offers a wide range of highly customizable [annotators](https://supervision.roboflow.com/latest/detection/annotators/), allowing you to compose the perfect visualization for your use case.
|
||||
|
||||
```python
|
||||
import cv2
|
||||
@@ -95,8 +96,8 @@ import supervision as sv
|
||||
image = cv2.imread(...)
|
||||
detections = sv.Detections(...)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
annotated_frame = bounding_box_annotator.annotate(
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
annotated_frame = box_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections
|
||||
)
|
||||
@@ -106,22 +107,25 @@ https://github.com/roboflow/supervision/assets/26109316/691e219c-0565-4403-9218-
|
||||
|
||||
### datasets
|
||||
|
||||
Supervision provides a set of [utils](https://supervision.roboflow.com/latest/datasets/) that allow you to load, split, merge, and save datasets in one of the supported formats.
|
||||
Supervision provides a set of [utils](https://supervision.roboflow.com/latest/datasets/core/) that allow you to load, split, merge, and save datasets in one of the supported formats.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
from roboflow import Roboflow
|
||||
|
||||
dataset = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
project = Roboflow().workspace(<WORKSPACE_ID>).project(<PROJECT_ID>)
|
||||
dataset = project.version(<PROJECT_VERSION>).download("coco")
|
||||
|
||||
ds = sv.DetectionDataset.from_coco(
|
||||
images_directory_path=f"{dataset.location}/train",
|
||||
annotations_path=f"{dataset.location}/train/_annotations.coco.json",
|
||||
)
|
||||
|
||||
dataset.classes
|
||||
['dog', 'person']
|
||||
path, image, annotation = ds[0]
|
||||
# loads image on demand
|
||||
|
||||
len(dataset)
|
||||
# 1000
|
||||
for path, image, annotation in ds:
|
||||
# loads image on demand
|
||||
```
|
||||
|
||||
<details close>
|
||||
@@ -216,7 +220,7 @@ len(dataset)
|
||||
|
||||
## 🎬 tutorials
|
||||
|
||||
Want to learn how to use Supervision? Explore our [how-to guides](https://supervision.roboflow.com/develop/how_to/detect_and_annotate/), [end-to-end examples](https://github.com/roboflow/supervision/tree/develop/examples), and [cookbooks](https://supervision.roboflow.com/develop/cookbooks/)!
|
||||
Want to learn how to use Supervision? Explore our [how-to guides](https://supervision.roboflow.com/develop/how_to/detect_and_annotate/), [end-to-end examples](https://github.com/roboflow/supervision/tree/develop/examples), [cheatsheet](https://roboflow.github.io/cheatsheet-supervision/), and [cookbooks](https://supervision.roboflow.com/develop/cookbooks/)!
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
Vendored
+1478
-1342
File diff suppressed because one or more lines are too long
+287
-10
@@ -1,8 +1,290 @@
|
||||
### 0.23.0 <small>Aug 28, 2024</small>
|
||||
|
||||
- Added [#930](https://github.com/roboflow/supervision/pull/930): `IconAnnotator`, a [new annotator](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.IconAnnotator) that allows drawing icons on each detection. Useful if you want to draw a specific icon for each class.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
from inference import get_model
|
||||
|
||||
image = <SOURCE_IMAGE_PATH>
|
||||
icon_dog = <DOG_PNG_PATH>
|
||||
icon_cat = <CAT_PNG_PATH>
|
||||
|
||||
model = get_model(model_id="yolov8n-640")
|
||||
results = model.infer(image)[0]
|
||||
detections = sv.Detections.from_inference(results)
|
||||
|
||||
icon_paths = []
|
||||
for class_name in detections.data["class_name"]:
|
||||
if class_name == "dog":
|
||||
icon_paths.append(icon_dog)
|
||||
elif class_name == "cat":
|
||||
icon_paths.append(icon_cat)
|
||||
else:
|
||||
icon_paths.append("")
|
||||
|
||||
icon_annotator = sv.IconAnnotator()
|
||||
annotated_frame = icon_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections,
|
||||
icon_path=icon_paths
|
||||
)
|
||||
```
|
||||
|
||||
- Added [#1385](https://github.com/roboflow/supervision/pull/1385): [`BackgroundColorAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.BackgroundColorAnnotator), that draws an overlay on the background images of the detections.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
from inference import get_model
|
||||
|
||||
image = <SOURCE_IMAGE_PATH>
|
||||
|
||||
model = get_model(model_id="yolov8n-640")
|
||||
results = model.infer(image)[0]
|
||||
detections = sv.Detections.from_inference(results)
|
||||
|
||||
background_overlay_annotator = sv.BackgroundOverlayAnnotator()
|
||||
annotated_frame = background_overlay_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections
|
||||
)
|
||||
```
|
||||
|
||||
- Added [#1386](https://github.com/roboflow/supervision/pull/1386): Support for Transformers v5 functions in [`sv.Detections.from_transformers`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_transformers). This includes the `DetrImageProcessor` methods `post_process_object_detection`, `post_process_panoptic_segmentation`, `post_process_semantic_segmentation`, and `post_process_instance_segmentation`.
|
||||
|
||||
```python
|
||||
import torch
|
||||
import supervision as sv
|
||||
from PIL import Image
|
||||
from transformers import DetrImageProcessor, DetrForObjectDetection
|
||||
|
||||
processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50")
|
||||
model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50")
|
||||
|
||||
image = Image.open(<SOURCE_IMAGE_PATH>)
|
||||
inputs = processor(images=image, return_tensors="pt")
|
||||
|
||||
with torch.no_grad():
|
||||
outputs = model(**inputs)
|
||||
|
||||
width, height = image.size
|
||||
target_size = torch.tensor([[height, width]])
|
||||
results = processor.post_process_object_detection(
|
||||
outputs=outputs, target_sizes=target_size)[0]
|
||||
detections = sv.Detections.from_transformers(
|
||||
transformers_results=results,
|
||||
id2label=model.config.id2label)
|
||||
```
|
||||
|
||||
- Added [#1354](https://github.com/roboflow/supervision/pull/1354): Ultralytics SAM (Segment Anything Model) support in [`sv.Detections.from_ultralytics`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_ultralytics). [SAM2](https://sam2.metademolab.com/) was released during this update, and is already supported via [`sv.Detections.from_sam`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_sam).
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
from segment_anything import (
|
||||
sam_model_registry,
|
||||
SamAutomaticMaskGenerator
|
||||
)
|
||||
|
||||
sam_model_reg = sam_model_registry[MODEL_TYPE]
|
||||
sam = sam_model_reg(checkpoint=CHECKPOINT_PATH).to(device=DEVICE)
|
||||
mask_generator = SamAutomaticMaskGenerator(sam)
|
||||
sam_result = mask_generator.generate(IMAGE)
|
||||
detections = sv.Detections.from_sam(sam_result=sam_result)
|
||||
```
|
||||
|
||||
- Added [#1458](https://github.com/roboflow/supervision/pull/1458): `outline_color` options for [`TriangleAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.TriangleAnnotator) and [`DotAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.DotAnnotator).
|
||||
|
||||
- Added [#1409](https://github.com/roboflow/supervision/pull/1409): `text_color` option for [`VertexLabelAnnotator`](https://supervision.roboflow.com/latest/keypoint/annotators/#supervision.keypoint.annotators.VertexLabelAnnotator) keypoint annotator.
|
||||
|
||||
- Changed [#1434](https://github.com/roboflow/supervision/pull/1434): [`InferenceSlicer`](https://supervision.roboflow.com/latest/detection/tools/inference_slicer/) now features an `overlap_ratio_wh` parameter, making it easier to compute slice sizes when handling overlapping slices.
|
||||
|
||||
- Fix [#1448](https://github.com/roboflow/supervision/pull/1448): Various annotator type issues have been resolved, supporting expanded error handling.
|
||||
|
||||
- Fix [#1348](https://github.com/roboflow/supervision/pull/1348): Introduced a new method for [seeking to a specific video frame](https://supervision.roboflow.com/latest/utils/video/#supervision.utils.video.get_video_frames_generator), addressing cases where traditional seek methods were failing. It can be enabled with `iterative_seek=True`.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
for frame in sv.get_video_frames_generator(
|
||||
source_path=<SOURCE_VIDEO_PATH>,
|
||||
start=60,
|
||||
iterative_seek=True
|
||||
):
|
||||
...
|
||||
```
|
||||
|
||||
- Fix [#1424](https://github.com/roboflow/supervision/pull/1424): `plot_image` function now clearly indicates that the size is in inches.
|
||||
|
||||
!!! failure "Removed"
|
||||
|
||||
The `track_buffer`, `track_thresh`, and `match_thresh` parameters in [`ByteTrack`](trackers.md/#supervision.tracker.byte_tracker.core.ByteTrack) are deprecated and were removed as of `supervision-0.23.0`. Use `lost_track_buffer,` `track_activation_threshold`, and `minimum_matching_threshold` instead.
|
||||
|
||||
!!! failure "Removed"
|
||||
|
||||
The `triggering_position ` parameter in [`sv.PolygonZone`](detection/tools/polygon_zone.md/#supervision.detection.tools.polygon_zone.PolygonZone) was removed as of `supervision-0.23.0`. Use `triggering_anchors ` instead.
|
||||
|
||||
!!! failure "Deprecated"
|
||||
|
||||
`overlap_filter_strategy` in `InferenceSlicer.__init__` is deprecated and will be removed in `supervision-0.27.0`. Use `overlap_strategy` instead.
|
||||
|
||||
!!! failure "Deprecated"
|
||||
|
||||
`overlap_ratio_wh` in `InferenceSlicer.__init__` is deprecated and will be removed in `supervision-0.27.0`. Use `overlap_wh` instead.
|
||||
|
||||
### 0.22.0 <small>Jul 12, 2024</small>
|
||||
|
||||
- Added [#1326](https://github.com/roboflow/supervision/pull/1326): [`sv.DetectionsDataset`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset) and [`sv.ClassificationDataset`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.ClassificationDataset) allowing to load the images into memory only when necessary (lazy loading).
|
||||
|
||||
!!! failure "Deprecated"
|
||||
|
||||
Constructing `DetectionDataset` with parameter `images` as `Dict[str, np.ndarray]` is deprecated and will be removed in `supervision-0.26.0`. Please pass a list of paths `List[str]` instead.
|
||||
|
||||
!!! failure "Deprecated"
|
||||
|
||||
The `DetectionDataset.images` property is deprecated and will be removed in `supervision-0.26.0`. Please loop over images with `for path, image, annotation in dataset:`, as that does not require loading all images into memory.
|
||||
|
||||
```python
|
||||
import roboflow
|
||||
from roboflow import Roboflow
|
||||
import supervision as sv
|
||||
|
||||
roboflow.login()
|
||||
rf = Roboflow()
|
||||
|
||||
project = rf.workspace(<WORKSPACE_ID>).project(<PROJECT_ID>)
|
||||
dataset = project.version(<PROJECT_VERSION>).download("coco")
|
||||
|
||||
ds_train = sv.DetectionDataset.from_coco(
|
||||
images_directory_path=f"{dataset.location}/train",
|
||||
annotations_path=f"{dataset.location}/train/_annotations.coco.json",
|
||||
)
|
||||
|
||||
path, image, annotation = ds_train[0]
|
||||
# loads image on demand
|
||||
|
||||
for path, image, annotation in ds_train:
|
||||
# loads image on demand
|
||||
```
|
||||
|
||||
- Added [#1296](https://github.com/roboflow/supervision/pull/1296): [`sv.Detections.from_lmm`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_lmm) now supports parsing results from the [Florence 2](https://huggingface.co/microsoft/Florence-2-large) model, extending the capability to handle outputs from this Large Multimodal Model (LMM). This includes detailed object detection, OCR with region proposals, segmentation, and more. Find out more in our [Colab notebook](https://colab.research.google.com/github/roboflow-ai/notebooks/blob/main/notebooks/how-to-finetune-florence-2-on-detection-dataset.ipynb).
|
||||
|
||||
- Added [#1232](https://github.com/roboflow/supervision/pull/1232) to support keypoint detection with Mediapipe. Both [legacy](https://colab.research.google.com/github/googlesamples/mediapipe/blob/main/examples/pose_landmarker/python/%5BMediaPipe_Python_Tasks%5D_Pose_Landmarker.ipynb) and [modern](https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker/python) pipelines are supported. See [`sv.KeyPoints.from_mediapipe`](https://supervision.roboflow.com/latest/keypoint/core/#supervision.keypoint.core.KeyPoints.from_mediapipe) for more.
|
||||
|
||||
- Added [#1316](https://github.com/roboflow/supervision/pull/1316): [`sv.KeyPoints.from_mediapipe`](https://supervision.roboflow.com/latest/keypoint/core/#supervision.keypoint.core.KeyPoints.from_mediapipe) extended to support FaceMesh from Mediapipe. This enhancement allows for processing both face landmarks from `FaceLandmarker`, and legacy results from `FaceMesh`.
|
||||
|
||||
- Added [#1310](https://github.com/roboflow/supervision/pull/1310): [`sv.KeyPoints.from_detectron2`](https://supervision.roboflow.com/latest/keypoint/core/#supervision.keypoint.core.KeyPoints.from_detectron2) is a new `KeyPoints` method, adding support for extracting keypoints from the popular [Detectron 2](https://github.com/facebookresearch/detectron2) platform.
|
||||
|
||||
- Added [#1300](https://github.com/roboflow/supervision/pull/1300): [`sv.Detections.from_detectron2`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_detectron2) now supports segmentation models detectron2. The resulting masks can be used with [`sv.MaskAnnotator`](https://supervision.roboflow.com/latest/annotators/#supervision.annotators.core.MaskAnnotator) for displaying annotations.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
from detectron2 import model_zoo
|
||||
from detectron2.engine import DefaultPredictor
|
||||
from detectron2.config import get_cfg
|
||||
import cv2
|
||||
|
||||
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
||||
cfg = get_cfg()
|
||||
cfg.merge_from_file(model_zoo.get_config_file("COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml"))
|
||||
cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url("COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml")
|
||||
predictor = DefaultPredictor(cfg)
|
||||
|
||||
result = predictor(image)
|
||||
detections = sv.Detections.from_detectron2(result)
|
||||
|
||||
mask_annotator = sv.MaskAnnotator()
|
||||
annotated_frame = mask_annotator.annotate(scene=image.copy(), detections=detections)
|
||||
```
|
||||
|
||||
- Added [#1277](https://github.com/roboflow/supervision/pull/1277): if you provide a font that supports symbols of a language, [`sv.RichLabelAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.LabelAnnotator.annotate) will draw them on your images.
|
||||
- Various other annotators have been revised to ensure proper in-place functionality when used with `numpy` arrays. Additionally, we fixed a bug where `sv.ColorAnnotator` was filling boxes with solid color when used in-place.
|
||||
|
||||
```python
|
||||
import cv2
|
||||
import supervision as sv
|
||||
import
|
||||
|
||||
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
||||
|
||||
model = get_model(model_id="yolov8n-640")
|
||||
results = model.infer(image)[0]
|
||||
detections = sv.Detections.from_inference(results)
|
||||
|
||||
rich_label_annotator = sv.RichLabelAnnotator(font_path=<TTF_FONT_PATH>)
|
||||
annotated_image = rich_label_annotator.annotate(scene=image.copy(), detections=detections)
|
||||
```
|
||||
|
||||
- Added [#1227](https://github.com/roboflow/supervision/pull/1227): Added support for loading Oriented Bounding Boxes dataset in YOLO format.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
train_ds = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path="/content/dataset/train/images",
|
||||
annotations_directory_path="/content/dataset/train/labels",
|
||||
data_yaml_path="/content/dataset/data.yaml",
|
||||
is_obb=True
|
||||
)
|
||||
|
||||
_, image, detections in train_ds[0]
|
||||
|
||||
obb_annotator = OrientedBoxAnnotator()
|
||||
annotated_image = obb_annotator.annotate(scene=image.copy(), detections=detections)
|
||||
```
|
||||
|
||||
- Fixed [#1312](https://github.com/roboflow/supervision/pull/1312): Fixed [`CropAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.TraceAnnotator.annotate).
|
||||
|
||||
!!! failure "Removed"
|
||||
|
||||
`BoxAnnotator` was removed, however `BoundingBoxAnnotator` has been renamed to `BoxAnnotator`. Use a combination of [`BoxAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.BoxAnnotator) and [`LabelAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.LabelAnnotator) to simulate old `BoundingBox` behavior.
|
||||
|
||||
!!! failure "Deprecated"
|
||||
|
||||
The name `BoundingBoxAnnotator` has been deprecated and will be removed in `supervision-0.26.0`. It has been renamed to [`BoxAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.BoxAnnotator).
|
||||
|
||||
- Added [#975](https://github.com/roboflow/supervision/pull/975) 📝 New Cookbooks: serialize detections into [json](https://github.com/roboflow/supervision/blob/de896189b83a1f9434c0a37dd9192ee00d2a1283/docs/notebooks/serialise-detections-to-json.ipynb) and [csv](https://github.com/roboflow/supervision/blob/de896189b83a1f9434c0a37dd9192ee00d2a1283/docs/notebooks/serialise-detections-to-csv.ipynb).
|
||||
|
||||
- Added [#1290](https://github.com/roboflow/supervision/pull/1290): Mostly an internal change, our file utility function now support both `str` and `pathlib` paths.
|
||||
|
||||
- Added [#1340](https://github.com/roboflow/supervision/pull/1340): Two new methods for converting between bounding box formats - [`xywh_to_xyxy`](https://supervision.roboflow.com/latest/detection/utils/#supervision.detection.utils.xywh_to_xyxy) and [`xcycwh_to_xyxy`](https://supervision.roboflow.com/latest/detection/utils/#supervision.detection.utils.xcycwh_to_xyxy)
|
||||
|
||||
!!! failure "Removed"
|
||||
|
||||
`from_roboflow` method has been removed due to deprecation. Use [from_inference](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_inference) instead.
|
||||
|
||||
!!! failure "Removed"
|
||||
|
||||
`Color.white()` has been removed due to deprecation. Use `color.WHITE` instead.
|
||||
|
||||
!!! failure "Removed"
|
||||
|
||||
`Color.black()` has been removed due to deprecation. Use `color.BLACK` instead.
|
||||
|
||||
!!! failure "Removed"
|
||||
|
||||
`Color.red()` has been removed due to deprecation. Use `color.RED` instead.
|
||||
|
||||
!!! failure "Removed"
|
||||
|
||||
`Color.green()` has been removed due to deprecation. Use `color.GREEN` instead.
|
||||
|
||||
!!! failure "Removed"
|
||||
|
||||
`Color.blue()` has been removed due to deprecation. Use `color.BLUE` instead.
|
||||
|
||||
!!! failure "Removed"
|
||||
|
||||
`ColorPalette.default()` has been removed due to deprecation. Use [ColorPalette.DEFAULT](https://supervision.roboflow.com/latest/utils/draw/#supervision.draw.color.ColorPalette.DEFAULT) instead.
|
||||
|
||||
!!! failure "Removed"
|
||||
|
||||
`FPSMonitor.__call__` has been removed due to deprecation. Use the attribute [FPSMonitor.fps](https://supervision.roboflow.com/latest/utils/video/#supervision.utils.video.FPSMonitor.fps) instead.
|
||||
|
||||
### 0.21.0 <small>Jun 5, 2024</small>
|
||||
|
||||
- Added [#500](https://github.com/roboflow/supervision/pull/500): [`sv.Detections.with_nmm`](https://supervision.roboflow.com/develop/detection/core/#supervision.detection.core.Detections.with_nmm) to perform non-maximum merging on the current set of object detections.
|
||||
- Added [#500](https://github.com/roboflow/supervision/pull/500): [`sv.Detections.with_nmm`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.with_nmm) to perform non-maximum merging on the current set of object detections.
|
||||
|
||||
- Added [#1221](https://github.com/roboflow/supervision/pull/1221): [`sv.Detections.from_lmm`](https://supervision.roboflow.com/develop/detection/core/#supervision.detection.core.Detections.from_lmm) allowing to parse Large Multimodal Model (LMM) text result into [`sv.Detections`](https://supervision.roboflow.com/develop/detection/core/) object. For now `from_lmm` supports only [PaliGemma](https://colab.research.google.com/github/roboflow-ai/notebooks/blob/main/notebooks/how-to-finetune-paligemma-on-detection-dataset.ipynb) result parsing.
|
||||
- Added [#1221](https://github.com/roboflow/supervision/pull/1221): [`sv.Detections.from_lmm`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_lmm) allowing to parse Large Multimodal Model (LMM) text result into [`sv.Detections`](https://supervision.roboflow.com/latest/detection/core/) object. For now `from_lmm` supports only [PaliGemma](https://colab.research.google.com/github/roboflow-ai/notebooks/blob/main/notebooks/how-to-finetune-paligemma-on-detection-dataset.ipynb) result parsing.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
@@ -21,7 +303,7 @@ detections.class_id
|
||||
# array([0])
|
||||
```
|
||||
|
||||
- Added [#1236](https://github.com/roboflow/supervision/pull/1236): [`sv.VertexLabelAnnotator`](https://supervision.roboflow.com/develop/keypoint/annotators/#supervision.keypoint.annotators.EdgeAnnotator.annotate) allowing to annotate every vertex of a keypoint skeleton with custom text and color.
|
||||
- Added [#1236](https://github.com/roboflow/supervision/pull/1236): [`sv.VertexLabelAnnotator`](https://supervision.roboflow.com/latest/keypoint/annotators/#supervision.keypoint.annotators.EdgeAnnotator.annotate) allowing to annotate every vertex of a keypoint skeleton with custom text and color.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
@@ -164,8 +446,6 @@ with csv_sink:
|
||||
|
||||
- Added [#819](https://github.com/roboflow/supervision/pull/819): [`sv.JSONSink`](/0.19.0/detection/tools/save_detections/#supervision.detection.tools.csv_sink.JSONSink) allowing for the straightforward saving of image, video, or stream inference results in a `.json` file.
|
||||
|
||||
```python
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
@@ -213,7 +493,7 @@ annotated_frame = crop_annotator.annotate(
|
||||
|
||||
!!! failure "Deprecated"
|
||||
|
||||
The `track_buffer`, `track_thresh`, and `match_thresh` parameters in `sv.ByterTrack` are deprecated and will be removed in `supervision-0.23.0`. Use `lost_track_buffer,` `track_activation_threshold`, and `minimum_matching_threshold` instead.
|
||||
The `track_buffer`, `track_thresh`, and `match_thresh` parameters in `sv.ByteTrack` are deprecated and will be removed in `supervision-0.23.0`. Use `lost_track_buffer,` `track_activation_threshold`, and `minimum_matching_threshold` instead.
|
||||
|
||||
- Changed [#910](https://github.com/roboflow/supervision/pull/910): [`sv.PolygonZone`](/0.19.0/detection/tools/polygon_zone/#supervision.detection.tools.polygon_zone.PolygonZone) to now accept a list of specific box anchors that must be in zone for a detection to be counted.
|
||||
|
||||
@@ -285,7 +565,6 @@ ColorPalette(colors=[Color(r=68, g=1, b=84), Color(r=59, g=82, b=139), ...])
|
||||
|
||||
`sv.ColorPalette.default()` is deprecated and will be removed in `supervision-0.22.0`. Use `sv.ColorPalette.DEFAULT` instead.
|
||||
|
||||
|
||||
- Changed [#769](https://github.com/roboflow/supervision/pull/769): [`sv.ColorPalette.DEFAULT`](/0.18.0/draw/color/#colorpalette) value, giving users a more extensive set of annotation colors.
|
||||
|
||||
- Changed [#677](https://github.com/roboflow/supervision/pull/677): `sv.Detections.from_roboflow` to [`sv.Detections.from_inference`](/0.18.0/detection/core/#supervision.detection.core.Detections.from_inference) streamlining its functionality to be compatible with both the both [inference](https://github.com/roboflow/inference) pip package and the Robloflow [hosted API](https://docs.roboflow.com/deploy/hosted-api).
|
||||
@@ -294,7 +573,6 @@ ColorPalette(colors=[Color(r=68, g=1, b=84), Color(r=59, g=82, b=139), ...])
|
||||
|
||||
`Detections.from_roboflow()` is deprecated and will be removed in `supervision-0.22.0`. Use `Detections.from_inference` instead.
|
||||
|
||||
|
||||
- Fixed [#735](https://github.com/roboflow/supervision/pull/735): [`sv.LineZone`](/0.18.0/detection/tools/line_zone/#linezone) functionality to accurately update the counter when an object crosses a line from any direction, including from the side. This enhancement enables more precise tracking and analytics, such as calculating individual in/out counts for each lane on the road.
|
||||
|
||||
### 0.17.0 <small>December 06, 2023</small>
|
||||
@@ -387,13 +665,12 @@ ColorPalette(colors=[Color(r=68, g=1, b=84), Color(r=59, g=82, b=139), ...])
|
||||
|
||||
- Fixed [#477](https://github.com/roboflow/supervision/pull/477): Poetry env definition allowing proper local installation.
|
||||
|
||||
- Fixed [#430](https://github.com/roboflow/supervision/pull/430): [`sv.ByteTrack`](/0.16.0/trackers/#supervision.tracker.byte_tracker.core.ByteTrack) to return `np.array([], dtype=int)` when `svDetections` is empty.
|
||||
- Fixed [#430](https://github.com/roboflow/supervision/pull/430): [`sv.ByteTrack`](/0.16.0/trackers/#supervision.tracker.byte_tracker.core.ByteTrack) to return `np.array([], dtype=int)` when `svDetections` is empty.
|
||||
|
||||
!!! failure "Deprecated"
|
||||
|
||||
`sv.Detections.from_yolov8` and `sv.Classifications.from_yolov8` as those are now replaced by [`sv.Detections.from_ultralytics`](/0.16.0/detection/core/#supervision.detection.core.Detections.from_ultralytics) and [`sv.Classifications.from_ultralytics`](/0.16.0/classification/core/#supervision.classification.core.Classifications.from_ultralytics).
|
||||
|
||||
|
||||
### 0.15.0 <small>October 5, 2023</small>
|
||||
|
||||
- Added [#170](https://github.com/roboflow/supervision/pull/170): [`sv.BoundingBoxAnnotator`](/0.15.0/annotators/#supervision.annotators.core.BoundingBoxAnnotator) allowing to annotate images and videos with bounding boxes.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Datasets
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Datasets Utils
|
||||
|
||||
+30
-13
@@ -3,17 +3,34 @@ comments: true
|
||||
status: deprecated
|
||||
---
|
||||
|
||||
These features are phased out due to better alternatives or potential issues in future versions. Deprecated functionalities are supported for **three subsequent releases**, providing time for users to transition to updated methods.
|
||||
# Deprecated
|
||||
|
||||
- [`Detections.from_froboflow`](detection/core.md/#supervision.detection.core.Detections.from_roboflow) is deprecated and will be removed in `supervision-0.22.0`. Use [`Detections.from_inference`](detection/core.md/#supervision.detection.core.Detections.from_inference) instead.
|
||||
- The method `Color.white()` is deprecated and will be removed in `supervision-0.22.0`. Use the constant `Color.WHITE` instead.
|
||||
- The method `Color.black()` is deprecated and will be removed in `supervision-0.22.0`. Use the constant `Color.BLACK` instead.
|
||||
- The method `Color.red()` is deprecated and will be removed in `supervision-0.22.0`. Use the constant `Color.RED` instead.
|
||||
- The method `Color.green()` is deprecated and will be removed in `supervision-0.22.0`. Use the constant `Color.GREEN` instead.
|
||||
- The method `Color.blue()` is deprecated and will be removed in `supervision-0.22.0`. Use the constant `Color.BLUE` instead.
|
||||
- The method [`ColorPalette.default()`](draw/color.md/#supervision.draw.color.ColorPalette.default) is deprecated and will be removed in `supervision-0.22.0`. Use the constant [`ColorPalette.DEFAULT`](draw/color.md/#supervision.draw.color.ColorPalette.DEFAULT) instead.
|
||||
- `BoxAnnotator` is deprecated and will be removed in `supervision-0.22.0`. Use [`BoundingBoxAnnotator`](detection/annotators.md/#supervision.annotators.core.BoundingBoxAnnotator) and [`LabelAnnotator`](detection/annotators.md/#supervision.annotators.core.LabelAnnotator) instead.
|
||||
- The method [`FPSMonitor.__call__`](utils/video.md/#supervision.utils.video.FPSMonitor.__call__) is deprecated and will be removed in `supervision-0.22.0`. Use the attribute [`FPSMonitor.fps`](utils/video.md/#supervision.utils.video.FPSMonitor.fps) instead.
|
||||
- The `track_buffer`, `track_thresh`, and `match_thresh` parameters in [`ByterTrack`](trackers.md/#supervision.tracker.byte_tracker.core.ByteTrack) are deprecated and will be removed in `supervision-0.23.0`. Use `lost_track_buffer,` `track_activation_threshold`, and `minimum_matching_threshold` instead.
|
||||
- The `triggering_position ` parameter in [`sv.PolygonZone`](detection/tools/polygon_zone.md/#supervision.detection.tools.polygon_zone.PolygonZone) is deprecated and will be removed in `supervision-0.23.0`. Use `triggering_anchors ` instead.
|
||||
- The `frame_resolution_wh ` parameter in [`sv.PolygonZone`](detection/tools/polygon_zone.md/#supervision.detection.tools.polygon_zone.PolygonZone) is deprecated and will be removed in `supervision-0.24.0`.
|
||||
These features are phased out due to better alternatives or potential issues in future versions. Deprecated functionalities are supported for **five subsequent releases**, providing time for users to transition to updated methods.
|
||||
|
||||
- The `frame_resolution_wh ` parameter in [`sv.PolygonZone`](detection/tools/polygon_zone.md/#supervision.detection.tools.polygon_zone.PolygonZone) will be removed in `supervision-0.24.0`.
|
||||
|
||||
- Constructing [`DetectionDataset`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset) and [`ClassificationDataset`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.ClassificationDataset) with parameter `images` as `Dict[str, np.ndarray]` will be removed in `supervision-0.26.0`. Please pass a list of paths `List[str]` instead.
|
||||
- The `DetectionDataset.images` property will be removed in `supervision-0.26.0`. Please loop over images with `for path, image, annotation in dataset:`, as that does not require loading all images into memory.
|
||||
- `BoundingBoxAnnotator` has been renamed to `BoxAnnotator` after the old implementation of [`BoxAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.BoxAnnotator) has been removed. `BoundingBoxAnnotator` will be removed in `supervision-0.26.0`.
|
||||
|
||||
- `overlap_filter_strategy` in [`InferenceSlicer.__init__`](https://supervision.roboflow.com/latest/detection/tools/inference_slicer/) is deprecated and will be removed in `supervision-0.27.0`. Use `overlap_strategy` instead.
|
||||
- `overlap_ratio_wh` in [`InferenceSlicer.__init__`](https://supervision.roboflow.com/latest/detection/tools/inference_slicer/) is deprecated and will be removed in `supervision-0.27.0`. Use `overlap_wh` instead.
|
||||
|
||||
# Removed
|
||||
|
||||
### 0.23.0
|
||||
|
||||
- The `track_buffer`, `track_thresh`, and `match_thresh` parameters in [`ByteTrack`](trackers.md/#supervision.tracker.byte_tracker.core.ByteTrack) are deprecated and were removed as of `supervision-0.23.0`. Use `lost_track_buffer,` `track_activation_threshold`, and `minimum_matching_threshold` instead.
|
||||
- The `triggering_position ` parameter in [`sv.PolygonZone`](detection/tools/polygon_zone.md/#supervision.detection.tools.polygon_zone.PolygonZone) was removed as of `supervision-0.23.0`. Use `triggering_anchors ` instead.
|
||||
|
||||
### 0.22.0
|
||||
|
||||
- [`Detections.from_froboflow`](detection/core.md/#supervision.detection.core.Detections.from_roboflow) is removed as of `supervision-0.22.0`. Use [`Detections.from_inference`](detection/core.md/#supervision.detection.core.Detections.from_inference) instead.
|
||||
- The method `Color.white()` was removed as of `supervision-0.22.0`. Use the constant `Color.WHITE` instead.
|
||||
- The method `Color.black()` was removed as of `supervision-0.22.0`. Use the constant `Color.BLACK` instead.
|
||||
- The method `Color.red()` was removed as of `supervision-0.22.0`. Use the constant `Color.RED` instead.
|
||||
- The method `Color.green()` was removed as of `supervision-0.22.0`. Use the constant `Color.GREEN` instead.
|
||||
- The method `Color.blue()` was removed as of `supervision-0.22.0`. Use the constant `Color.BLUE` instead.
|
||||
- The method [`ColorPalette.default()`](draw/color.md/#supervision.draw.color.ColorPalette.default) was removed as of `supervision-0.22.0`. Use the constant [`ColorPalette.DEFAULT`](draw/color.md/#supervision.draw.color.ColorPalette.DEFAULT) instead.
|
||||
- `BoxAnnotator` was removed as of `supervision-0.22.0`, however `BoundingBoxAnnotator` was immediately renamed to `BoxAnnotator`. Use [`BoxAnnotator`](detection/annotators.md/#supervision.annotators.core.BoxAnnotator) and [`LabelAnnotator`](detection/annotators.md/#supervision.annotators.core.LabelAnnotator) instead of the old `BoxAnnotator`.
|
||||
- The method [`FPSMonitor.__call__`](utils/video.md/#supervision.utils.video.FPSMonitor.__call__) was removed as of `supervision-0.22.0`. Use the attribute [`FPSMonitor.fps`](utils/video.md/#supervision.utils.video.FPSMonitor.fps) instead.
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Annotators
|
||||
|
||||
=== "BoundingBox"
|
||||
=== "Box"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
@@ -12,8 +13,8 @@ comments: true
|
||||
image = ...
|
||||
detections = sv.Detections(...)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
annotated_frame = bounding_box_annotator.annotate(
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
annotated_frame = box_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections
|
||||
)
|
||||
@@ -299,10 +300,10 @@ comments: true
|
||||
]
|
||||
|
||||
rich_label_annotator = sv.RichLabelAnnotator(
|
||||
font_path=".../font.ttf",
|
||||
font_path="<TTF_FONT_PATH>",
|
||||
text_position=sv.Position.CENTER
|
||||
)
|
||||
annotated_frame = label_annotator.annotate(
|
||||
annotated_frame = rich_label_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections,
|
||||
labels=labels
|
||||
@@ -315,6 +316,33 @@ comments: true
|
||||
|
||||
</div>
|
||||
|
||||
=== "Icon"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
image = ...
|
||||
detections = sv.Detections(...)
|
||||
|
||||
icon_paths = [
|
||||
"<ICON_PATH>"
|
||||
for _ in detections
|
||||
]
|
||||
|
||||
icon_annotator = sv.IconAnnotator()
|
||||
annotated_frame = icon_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections,
|
||||
icon_path=icon_paths
|
||||
)
|
||||
```
|
||||
|
||||
<div class="result" markdown>
|
||||
|
||||
{ align=center width="800" }
|
||||
|
||||
</div>
|
||||
|
||||
=== "Crop"
|
||||
|
||||
```python
|
||||
@@ -383,7 +411,7 @@ comments: true
|
||||
trace_annotator = sv.TraceAnnotator()
|
||||
|
||||
video_info = sv.VideoInfo.from_video_path(video_path='...')
|
||||
frames_generator = get_video_frames_generator(source_path='...')
|
||||
frames_generator = sv.get_video_frames_generator(source_path='...')
|
||||
tracker = sv.ByteTrack()
|
||||
|
||||
with sv.VideoSink(target_path='...', video_info=video_info) as sink:
|
||||
@@ -414,7 +442,7 @@ comments: true
|
||||
heat_map_annotator = sv.HeatMapAnnotator()
|
||||
|
||||
video_info = sv.VideoInfo.from_video_path(video_path='...')
|
||||
frames_generator = get_video_frames_generator(source_path='...')
|
||||
frames_generator = sv.get_video_frames_generator(source_path='...')
|
||||
|
||||
with sv.VideoSink(target_path='...', video_info=video_info) as sink:
|
||||
for frame in frames_generator:
|
||||
@@ -432,11 +460,32 @@ comments: true
|
||||
|
||||
</div>
|
||||
|
||||
=== "Background Color"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
image = ...
|
||||
detections = sv.Detections(...)
|
||||
|
||||
background_overlay_annotator = sv.BackgroundOverlayAnnotator()
|
||||
annotated_frame = background_overlay_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections
|
||||
)
|
||||
```
|
||||
|
||||
<div class="result" markdown>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.annotators.core.BoundingBoxAnnotator">BoundingBoxAnnotator</a></h2>
|
||||
<h2><a href="#supervision.annotators.core.BoxAnnotator">BoxAnnotator</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.annotators.core.BoundingBoxAnnotator
|
||||
:::supervision.annotators.core.BoxAnnotator
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.annotators.core.RoundBoxAnnotator">RoundBoxAnnotator</a></h2>
|
||||
@@ -528,6 +577,12 @@ comments: true
|
||||
|
||||
:::supervision.annotators.core.RichLabelAnnotator
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.annotators.core.IconAnnotator">IconAnnotator</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.annotators.core.IconAnnotator
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.annotators.core.BlurAnnotator">BlurAnnotator</a></h2>
|
||||
</div>
|
||||
@@ -552,6 +607,12 @@ comments: true
|
||||
|
||||
:::supervision.annotators.core.CropAnnotator
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.annotators.core.BackgroundOverlayAnnotator">BackgroundOverlayAnnotator</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.annotators.core.BackgroundOverlayAnnotator
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.annotators.core.ColorLookup">ColorLookup</a></h2>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Double Detection Filter
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# Metrics
|
||||
# Legacy Metrics
|
||||
|
||||
Starting with `0.23.0`, a new metrics module is being introduced to supervision.
|
||||
Metrics here are part of the legacy evaluation API and will be deprecated in the future.
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.metrics.detection.ConfusionMatrix">ConfusionMatrix</a></h2>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
<div class="md-typeset">
|
||||
|
||||
@@ -76,6 +76,18 @@ comments: true
|
||||
|
||||
:::supervision.detection.utils.pad_boxes
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.xywh_to_xyxy">xywh_to_xyxy</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.utils.xywh_to_xyxy
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.xcycwh_to_xyxy">xcycwh_to_xyxy</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.utils.xcycwh_to_xyxy
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.contains_holes">contains_holes</a></h2>
|
||||
</div>
|
||||
|
||||
@@ -135,7 +135,7 @@ You can load predictions from other computer vision frameworks and libraries usi
|
||||
|
||||
## Annotate Image with Detections
|
||||
|
||||
Finally, we can annotate the image with the predictions. Since we are working with an object detection model, we will use the [`sv.BoundingBoxAnnotator`](/latest/annotators/#supervision.annotators.core.BoundingBoxAnnotator) and [`sv.LabelAnnotator`](/latest/annotators/#supervision.annotators.core.LabelAnnotator) classes.
|
||||
Finally, we can annotate the image with the predictions. Since we are working with an object detection model, we will use the [`sv.BoxAnnotator`](/latest/annotators/#supervision.annotators.core.BoxAnnotator) and [`sv.LabelAnnotator`](/latest/annotators/#supervision.annotators.core.LabelAnnotator) classes.
|
||||
|
||||
=== "Inference"
|
||||
|
||||
@@ -149,10 +149,10 @@ Finally, we can annotate the image with the predictions. Since we are working wi
|
||||
results = model.infer(image)[0]
|
||||
detections = sv.Detections.from_inference(results)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
annotated_image = bounding_box_annotator.annotate(
|
||||
annotated_image = box_annotator.annotate(
|
||||
scene=image, detections=detections)
|
||||
annotated_image = label_annotator.annotate(
|
||||
scene=annotated_image, detections=detections)
|
||||
@@ -170,10 +170,10 @@ Finally, we can annotate the image with the predictions. Since we are working wi
|
||||
results = model(image)[0]
|
||||
detections = sv.Detections.from_ultralytics(results)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
annotated_image = bounding_box_annotator.annotate(
|
||||
annotated_image = box_annotator.annotate(
|
||||
scene=image, detections=detections)
|
||||
annotated_image = label_annotator.annotate(
|
||||
scene=annotated_image, detections=detections)
|
||||
@@ -204,10 +204,10 @@ Finally, we can annotate the image with the predictions. Since we are working wi
|
||||
transformers_results=results,
|
||||
id2label=model.config.id2label)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
annotated_image = bounding_box_annotator.annotate(
|
||||
annotated_image = box_annotator.annotate(
|
||||
scene=image, detections=detections)
|
||||
annotated_image = label_annotator.annotate(
|
||||
scene=annotated_image, detections=detections)
|
||||
@@ -233,7 +233,7 @@ override this behavior by passing a list of custom `labels` to the `annotate` me
|
||||
results = model.infer(image)[0]
|
||||
detections = sv.Detections.from_inference(results)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
labels = [
|
||||
@@ -242,7 +242,7 @@ override this behavior by passing a list of custom `labels` to the `annotate` me
|
||||
in zip(detections['class_name'], detections.confidence)
|
||||
]
|
||||
|
||||
annotated_image = bounding_box_annotator.annotate(
|
||||
annotated_image = box_annotator.annotate(
|
||||
scene=image, detections=detections)
|
||||
annotated_image = label_annotator.annotate(
|
||||
scene=annotated_image, detections=detections, labels=labels)
|
||||
@@ -260,7 +260,7 @@ override this behavior by passing a list of custom `labels` to the `annotate` me
|
||||
results = model(image)[0]
|
||||
detections = sv.Detections.from_ultralytics(results)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
labels = [
|
||||
@@ -269,7 +269,7 @@ override this behavior by passing a list of custom `labels` to the `annotate` me
|
||||
in zip(detections['class_name'], detections.confidence)
|
||||
]
|
||||
|
||||
annotated_image = bounding_box_annotator.annotate(
|
||||
annotated_image = box_annotator.annotate(
|
||||
scene=image, detections=detections)
|
||||
annotated_image = label_annotator.annotate(
|
||||
scene=annotated_image, detections=detections, labels=labels)
|
||||
@@ -300,7 +300,7 @@ override this behavior by passing a list of custom `labels` to the `annotate` me
|
||||
transformers_results=results,
|
||||
id2label=model.config.id2label)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
labels = [
|
||||
@@ -309,7 +309,7 @@ override this behavior by passing a list of custom `labels` to the `annotate` me
|
||||
in zip(detections['class_name'], detections.confidence)
|
||||
]
|
||||
|
||||
annotated_image = bounding_box_annotator.annotate(
|
||||
annotated_image = box_annotator.annotate(
|
||||
scene=image, detections=detections)
|
||||
annotated_image = label_annotator.annotate(
|
||||
scene=annotated_image, detections=detections, labels=labels)
|
||||
@@ -322,7 +322,7 @@ override this behavior by passing a list of custom `labels` to the `annotate` me
|
||||
If you are running the segmentation model
|
||||
[`sv.MaskAnnotator`](/latest/annotators/#supervision.annotators.core.MaskAnnotator)
|
||||
is a drop-in replacement for
|
||||
[`sv.BoundingBoxAnnotator`](/latest/annotators/#supervision.annotators.core.BoundingBoxAnnotator)
|
||||
[`sv.BoxAnnotator`](/latest/annotators/#supervision.annotators.core.BoxAnnotator)
|
||||
that will allow you to draw masks instead of boxes.
|
||||
|
||||
=== "Inference"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Detect Small Objects
|
||||
@@ -32,10 +31,10 @@ size relative to the image resolution.
|
||||
results = model.infer(image)[0]
|
||||
detections = sv.Detections.from_inference(results)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
annotated_image = bounding_box_annotator.annotate(
|
||||
annotated_image = box_annotator.annotate(
|
||||
scene=image, detections=detections)
|
||||
annotated_image = label_annotator.annotate(
|
||||
scene=annotated_image, detections=detections)
|
||||
@@ -53,10 +52,10 @@ size relative to the image resolution.
|
||||
results = model(image)[0]
|
||||
detections = sv.Detections.from_ultralytics(results)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
annotated_image = bounding_box_annotator.annotate(
|
||||
annotated_image = box_annotator.annotate(
|
||||
scene=image, detections=detections)
|
||||
annotated_image = label_annotator.annotate(
|
||||
scene=annotated_image, detections=detections)
|
||||
@@ -85,7 +84,7 @@ size relative to the image resolution.
|
||||
outputs=outputs, target_sizes=target_size)[0]
|
||||
detections = sv.Detections.from_transformers(results)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
labels = [
|
||||
@@ -94,7 +93,7 @@ size relative to the image resolution.
|
||||
in detections.class_id
|
||||
]
|
||||
|
||||
annotated_image = bounding_box_annotator.annotate(
|
||||
annotated_image = box_annotator.annotate(
|
||||
scene=image, detections=detections)
|
||||
annotated_image = label_annotator.annotate(
|
||||
scene=annotated_image, detections=detections, labels=labels)
|
||||
@@ -120,10 +119,10 @@ is less effective for ultra-high-resolution images (4K and above).
|
||||
results = model.infer(image)[0]
|
||||
detections = sv.Detections.from_inference(results)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
annotated_image = bounding_box_annotator.annotate(
|
||||
annotated_image = box_annotator.annotate(
|
||||
scene=image, detections=detections)
|
||||
annotated_image = label_annotator.annotate(
|
||||
scene=annotated_image, detections=detections)
|
||||
@@ -141,10 +140,10 @@ is less effective for ultra-high-resolution images (4K and above).
|
||||
results = model(image, imgsz=1280)[0]
|
||||
detections = sv.Detections.from_ultralytics(results)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
annotated_image = bounding_box_annotator.annotate(
|
||||
annotated_image = box_annotator.annotate(
|
||||
scene=image, detections=detections)
|
||||
annotated_image = label_annotator.annotate(
|
||||
scene=annotated_image, detections=detections)
|
||||
@@ -180,10 +179,10 @@ objects within each, and aggregating the results.
|
||||
slicer = sv.InferenceSlicer(callback = callback)
|
||||
detections = slicer(image)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
annotated_image = bounding_box_annotator.annotate(
|
||||
annotated_image = box_annotator.annotate(
|
||||
scene=image, detections=detections)
|
||||
annotated_image = label_annotator.annotate(
|
||||
scene=annotated_image, detections=detections)
|
||||
@@ -207,10 +206,10 @@ objects within each, and aggregating the results.
|
||||
slicer = sv.InferenceSlicer(callback = callback)
|
||||
detections = slicer(image)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
annotated_image = bounding_box_annotator.annotate(
|
||||
annotated_image = box_annotator.annotate(
|
||||
scene=image, detections=detections)
|
||||
annotated_image = label_annotator.annotate(
|
||||
scene=annotated_image, detections=detections)
|
||||
@@ -248,7 +247,7 @@ objects within each, and aggregating the results.
|
||||
slicer = sv.InferenceSlicer(callback = callback)
|
||||
detections = slicer(image)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
labels = [
|
||||
@@ -257,7 +256,7 @@ objects within each, and aggregating the results.
|
||||
in detections.class_id
|
||||
]
|
||||
|
||||
annotated_image = bounding_box_annotator.annotate(
|
||||
annotated_image = box_annotator.annotate(
|
||||
scene=image, detections=detections)
|
||||
annotated_image = label_annotator.annotate(
|
||||
scene=annotated_image, detections=detections, labels=labels)
|
||||
|
||||
@@ -0,0 +1,454 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
With Supervision, you can load and manipulate classification, object detection, and
|
||||
segmentation datasets. This tutorial will walk you through how to load, split, merge,
|
||||
visualize, and augment datasets in Supervision.
|
||||
|
||||
## Download Dataset
|
||||
|
||||
In this tutorial, we will use a dataset from
|
||||
[Roboflow Universe](https://universe.roboflow.com/), a public repository of
|
||||
thousands of computer vision datasets. If you already have your dataset in
|
||||
[COCO](https://roboflow.com/formats/coco-json),
|
||||
[YOLO](https://roboflow.com/formats/yolov8-pytorch-txt),
|
||||
or [Pascal VOC](https://roboflow.com/formats/pascal-voc-xml) format, you can skip this
|
||||
section.
|
||||
|
||||
```bash
|
||||
pip install roboflow
|
||||
```
|
||||
|
||||
Next, log into your Roboflow account and download the dataset of your choice in the
|
||||
COCO, YOLO, or Pascal VOC format. You can customize the following code snippet with
|
||||
your workspace ID, project ID, and version number.
|
||||
|
||||
=== "COCO"
|
||||
|
||||
```python
|
||||
import roboflow
|
||||
|
||||
roboflow.login()
|
||||
|
||||
rf = roboflow.Roboflow()
|
||||
project = rf.workspace('<WORKSPACE_ID>').project('<PROJECT_ID>')
|
||||
dataset = project.version('<PROJECT_VERSION>').download("coco")
|
||||
```
|
||||
|
||||
=== "YOLO"
|
||||
|
||||
```python
|
||||
import roboflow
|
||||
|
||||
roboflow.login()
|
||||
|
||||
rf = roboflow.Roboflow()
|
||||
project = rf.workspace('<WORKSPACE_ID>').project('<PROJECT_ID>')
|
||||
dataset = project.version('<PROJECT_VERSION>').download("yolov8")
|
||||
```
|
||||
|
||||
=== "Pascal VOC"
|
||||
|
||||
```python
|
||||
import roboflow
|
||||
|
||||
roboflow.login()
|
||||
|
||||
rf = roboflow.Roboflow()
|
||||
project = rf.workspace('<WORKSPACE_ID>').project('<PROJECT_ID>')
|
||||
dataset = project.version('<PROJECT_VERSION>').download("voc")
|
||||
```
|
||||
|
||||
## Load Dataset
|
||||
|
||||
The Supervision library provides convenient functions to load datasets in various
|
||||
formats. If your dataset is already split into train, test, and valid subsets, you can
|
||||
load each of those as separate [`sv.DetectionDataset`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset)
|
||||
instances.
|
||||
|
||||
=== "COCO"
|
||||
|
||||
We can do so using the [`sv.DetectionDataset.from_coco`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.from_coco) to load annotations in [COCO](https://roboflow.com/formats/coco-json) format.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
ds_train = sv.DetectionDataset.from_coco(
|
||||
images_directory_path=f'{dataset.location}/train',
|
||||
annotations_path=f'{dataset.location}/train/_annotations.coco.json',
|
||||
)
|
||||
ds_valid = sv.DetectionDataset.from_coco(
|
||||
images_directory_path=f'{dataset.location}/valid',
|
||||
annotations_path=f'{dataset.location}/valid/_annotations.coco.json',
|
||||
)
|
||||
ds_test = sv.DetectionDataset.from_coco(
|
||||
images_directory_path=f'{dataset.location}/test',
|
||||
annotations_path=f'{dataset.location}/test/_annotations.coco.json',
|
||||
)
|
||||
|
||||
ds_train.classes
|
||||
# ['person', 'bicycle', 'car', ...]
|
||||
|
||||
len(ds_train), len(ds_valid), len(ds_test)
|
||||
# 800, 100, 100
|
||||
```
|
||||
|
||||
=== "YOLO"
|
||||
|
||||
We can do so using the [`sv.DetectionDataset.from_yolo`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.from_yolo) to load annotations in [YOLO](https://roboflow.com/formats/yolov8-pytorch-txt) format.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
ds_train = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=f'{dataset.location}/train/images',
|
||||
annotations_directory_path=f'{dataset.location}/train/labels',
|
||||
data_yaml_path=f'{dataset.location}/data.yaml'
|
||||
)
|
||||
ds_valid = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=f'{dataset.location}/valid/images',
|
||||
annotations_directory_path=f'{dataset.location}/valid/labels',
|
||||
data_yaml_path=f'{dataset.location}/data.yaml'
|
||||
)
|
||||
ds_test = 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'
|
||||
)
|
||||
|
||||
ds_train.classes
|
||||
# ['person', 'bicycle', 'car', ...]
|
||||
|
||||
len(ds_train), len(ds_valid), len(ds_test)
|
||||
# 800, 100, 100
|
||||
```
|
||||
|
||||
=== "Pascal VOC"
|
||||
|
||||
We can do so using the [`sv.DetectionDataset.from_pascal_voc`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.from_pascal_voc) to load annotations in [Pascal VOC](https://roboflow.com/formats/pascal-voc-xml) format.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
ds_train = sv.DetectionDataset.from_pascal_voc(
|
||||
images_directory_path=f'{dataset.location}/train/images',
|
||||
annotations_directory_path=f'{dataset.location}/train/labels'
|
||||
)
|
||||
ds_valid = sv.DetectionDataset.from_pascal_voc(
|
||||
images_directory_path=f'{dataset.location}/valid/images',
|
||||
annotations_directory_path=f'{dataset.location}/valid/labels'
|
||||
)
|
||||
ds_test = sv.DetectionDataset.from_pascal_voc(
|
||||
images_directory_path=f'{dataset.location}/test/images',
|
||||
annotations_directory_path=f'{dataset.location}/test/labels'
|
||||
)
|
||||
|
||||
ds_train.classes
|
||||
# ['person', 'bicycle', 'car', ...]
|
||||
|
||||
len(ds_train), len(ds_valid), len(ds_test)
|
||||
# 800, 100, 100
|
||||
```
|
||||
|
||||
## Split Dataset
|
||||
|
||||
If your dataset is not already split into train, test, and valid subsets, you can
|
||||
easily do so using the [`sv.DetectionDataset.split`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.split)
|
||||
method. We can split it as follows, ensuring a random shuffle of the data.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
ds = sv.DetectionDataset(...)
|
||||
|
||||
len(ds)
|
||||
# 1000
|
||||
|
||||
ds_train, ds = ds.split(split_ratio=0.8, shuffle=True)
|
||||
ds_valid, ds_test = ds.split(split_ratio=0.5, shuffle=True)
|
||||
|
||||
len(ds_train), len(ds_valid), len(ds_test)
|
||||
# 800, 100, 100
|
||||
```
|
||||
|
||||
## Merge Dataset
|
||||
|
||||
If you have multiple datasets that you would like to merge, you can do so using the
|
||||
[`sv.DetectionDataset.merge`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.merge)
|
||||
method.
|
||||
|
||||
=== "COCO"
|
||||
|
||||
```{ .py hl_lines="22-28" }
|
||||
import supervision as sv
|
||||
|
||||
ds_train = sv.DetectionDataset.from_coco(
|
||||
images_directory_path=f'{dataset.location}/train',
|
||||
annotations_path=f'{dataset.location}/train/_annotations.coco.json',
|
||||
)
|
||||
ds_valid = sv.DetectionDataset.from_coco(
|
||||
images_directory_path=f'{dataset.location}/valid',
|
||||
annotations_path=f'{dataset.location}/valid/_annotations.coco.json',
|
||||
)
|
||||
ds_test = sv.DetectionDataset.from_coco(
|
||||
images_directory_path=f'{dataset.location}/test',
|
||||
annotations_path=f'{dataset.location}/test/_annotations.coco.json',
|
||||
)
|
||||
|
||||
ds_train.classes
|
||||
# ['person', 'bicycle', 'car', ...]
|
||||
|
||||
len(ds_train), len(ds_valid), len(ds_test)
|
||||
# 800, 100, 100
|
||||
|
||||
ds = sv.DetectionDataset.merge([ds_train, ds_valid, ds_test])
|
||||
|
||||
ds.classes
|
||||
# ['person', 'bicycle', 'car', ...]
|
||||
|
||||
len(ds)
|
||||
# 1000
|
||||
```
|
||||
|
||||
=== "YOLO"
|
||||
|
||||
```{ .py hl_lines="25-31" }
|
||||
import supervision as sv
|
||||
|
||||
ds_train = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=f'{dataset.location}/train/images',
|
||||
annotations_directory_path=f'{dataset.location}/train/labels',
|
||||
data_yaml_path=f'{dataset.location}/data.yaml'
|
||||
)
|
||||
ds_valid = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=f'{dataset.location}/valid/images',
|
||||
annotations_directory_path=f'{dataset.location}/valid/labels',
|
||||
data_yaml_path=f'{dataset.location}/data.yaml'
|
||||
)
|
||||
ds_test = 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'
|
||||
)
|
||||
|
||||
ds_train.classes
|
||||
# ['person', 'bicycle', 'car', ...]
|
||||
|
||||
len(ds_train), len(ds_valid), len(ds_test)
|
||||
# 800, 100, 100
|
||||
|
||||
ds = sv.DetectionDataset.merge([ds_train, ds_valid, ds_test])
|
||||
|
||||
ds.classes
|
||||
# ['person', 'bicycle', 'car', ...]
|
||||
|
||||
len(ds)
|
||||
# 1000
|
||||
```
|
||||
|
||||
=== "Pascal VOC"
|
||||
|
||||
```{ .py hl_lines="22-28" }
|
||||
import supervision as sv
|
||||
|
||||
ds_train = sv.DetectionDataset.from_pascal_voc(
|
||||
images_directory_path=f'{dataset.location}/train/images',
|
||||
annotations_directory_path=f'{dataset.location}/train/labels'
|
||||
)
|
||||
ds_valid = sv.DetectionDataset.from_pascal_voc(
|
||||
images_directory_path=f'{dataset.location}/valid/images',
|
||||
annotations_directory_path=f'{dataset.location}/valid/labels'
|
||||
)
|
||||
ds_test = sv.DetectionDataset.from_pascal_voc(
|
||||
images_directory_path=f'{dataset.location}/test/images',
|
||||
annotations_directory_path=f'{dataset.location}/test/labels'
|
||||
)
|
||||
|
||||
ds_train.classes
|
||||
# ['person', 'bicycle', 'car', ...]
|
||||
|
||||
len(ds_train), len(ds_valid), len(ds_test)
|
||||
# 800, 100, 100
|
||||
|
||||
ds = sv.DetectionDataset.merge([ds_train, ds_valid, ds_test])
|
||||
|
||||
ds.classes
|
||||
# ['person', 'bicycle', 'car', ...]
|
||||
|
||||
len(ds)
|
||||
# 1000
|
||||
```
|
||||
|
||||
## Iterate over Dataset
|
||||
|
||||
There are two ways to loop over a `sv.DetectionDataset`: using a direct
|
||||
[for loop](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.__iter__)
|
||||
called on the `sv.DetectionDataset` instance or loading `sv.DetectionDataset` entries
|
||||
[by index](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.__getitem__).
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
ds = sv.DetectionDataset(...)
|
||||
|
||||
# Option 1
|
||||
for image_path, image, annotations in ds:
|
||||
... # Process each image and its annotations
|
||||
|
||||
# Option 2
|
||||
for idx in range(len(ds)):
|
||||
image_path, image, annotations = ds[idx]
|
||||
... # Process the image and annotations at index `idx`
|
||||
```
|
||||
|
||||
## Visualize Dataset
|
||||
|
||||
The Supervision library provides tools for easily visualizing your detection dataset.
|
||||
You can create a grid of annotated images to quickly inspect your data and labels.
|
||||
First, initialize the [`sv.BoxAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.BoxAnnotator)
|
||||
and [`sv.LabelAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.LabelAnnotator).
|
||||
Then, iterate through a subset of the dataset (e.g., the first 25 images), drawing
|
||||
bounding boxes and class labels on each image. Finally, combine the annotated images
|
||||
into a grid for display.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
ds = sv.DetectionDataset(...)
|
||||
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
annotated_images = []
|
||||
for i in range(16):
|
||||
_, image, annotations = ds[i]
|
||||
|
||||
labels = [ds.classes[class_id] for class_id in annotations.class_id]
|
||||
|
||||
annotated_image = image.copy()
|
||||
annotated_image = box_annotator.annotate(annotated_image, annotations)
|
||||
annotated_image = label_annotator.annotate(annotated_image, annotations, labels)
|
||||
annotated_images.append(annotated_image)
|
||||
|
||||
grid = sv.create_tiles(
|
||||
annotated_images,
|
||||
grid_size=(4, 4),
|
||||
single_tile_size=(400, 400),
|
||||
tile_padding_color=sv.Color.WHITE,
|
||||
tile_margin_color=sv.Color.WHITE
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Save Dataset
|
||||
|
||||
=== "COCO"
|
||||
|
||||
We can do so using the [`sv.DetectionDataset.as_coco`](https://supervision.roboflow.com/datasets/#supervision.dataset.core.DetectionDataset.as_coco) method to save annotations in [COCO](https://roboflow.com/formats/coco-json) format.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
ds = sv.DetectionDataset(...)
|
||||
|
||||
ds.as_coco(
|
||||
images_directory_path='<IMAGE_DIRECTORY_PATH>',
|
||||
annotations_path='<ANNOTATIONS_PATH>'
|
||||
)
|
||||
```
|
||||
|
||||
=== "YOLO"
|
||||
|
||||
We can do so using the [`sv.DetectionDataset.as_yolo`](https://supervision.roboflow.com/datasets/#supervision.dataset.core.DetectionDataset.as_yolo) method to save annotations in [YOLO](https://roboflow.com/formats/yolov8-pytorch-txt) format.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
ds = sv.DetectionDataset(...)
|
||||
|
||||
ds.as_yolo(
|
||||
images_directory_path='<IMAGE_DIRECTORY_PATH>',
|
||||
annotations_directory_path='<ANNOTATIONS_DIRECTORY_PATH>',
|
||||
data_yaml_path='<DATA_YAML_PATH>'
|
||||
)
|
||||
```
|
||||
|
||||
=== "Pascal VOC"
|
||||
|
||||
We can do so using the [`sv.DetectionDataset.as_pascal_voc`](https://supervision.roboflow.com/datasets/#supervision.dataset.core.DetectionDataset.as_pascal_voc) method to save annotations in [Pascal VOC](https://roboflow.com/formats/pascal-voc-xml) format.
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
ds = sv.DetectionDataset(...)
|
||||
|
||||
ds.as_pascal_voc(
|
||||
images_directory_path='<IMAGE_DIRECTORY_PATH>',
|
||||
annotations_directory_path='<ANNOTATIONS_DIRECTORY_PATH>'
|
||||
)
|
||||
```
|
||||
|
||||
## Augment Dataset
|
||||
|
||||
In this section, we'll explore using Supervision in combination with Albumentations to
|
||||
augment our dataset. Data augmentation is a common technique in computer vision to
|
||||
increase the size and diversity of training datasets, leading to improved model
|
||||
performance and generalization.
|
||||
|
||||
```bash
|
||||
pip install augmentation
|
||||
```
|
||||
|
||||
Albumentations provides a flexible and powerful API for image augmentation. The core of
|
||||
the library is the [`Compose`](https://albumentations.ai/docs/api_reference/full_reference/?h=compose#albumentations.core.composition.Compose)
|
||||
class, which allows you to chain multiple image transformations together. Each
|
||||
transformation is defined using a dedicated class, such as
|
||||
[`HorizontalFlip`](https://albumentations.ai/docs/api_reference/full_reference/?h=horizontalflip#albumentations.augmentations.geometric.transforms.HorizontalFlip),
|
||||
[`RandomBrightnessContrast`](https://albumentations.ai/docs/api_reference/full_reference/?h=horizontalflip#albumentations.augmentations.transforms.RandomBrightnessContrast),
|
||||
or [`Perspective`](https://albumentations.ai/docs/api_reference/full_reference/?h=horizontalflip#albumentations.augmentations.geometric.transforms.Perspective).
|
||||
|
||||
```python
|
||||
import albumentations as A
|
||||
|
||||
augmentation = A.Compose(
|
||||
transforms=[
|
||||
A.Perspective(p=0.1),
|
||||
A.HorizontalFlip(p=0.5),
|
||||
A.RandomBrightnessContrast(p=0.5)
|
||||
],
|
||||
bbox_params=A.BboxParams(
|
||||
format='pascal_voc',
|
||||
label_fields=['category']
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
The key is to set `format='pascal_voc'`, which corresponds to the
|
||||
`[x_min, y_min, x_max, y_max]` bounding box format used in Supervision.
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
from dataclasses import replace
|
||||
|
||||
ds = sv.DetectionDataset(...)
|
||||
|
||||
_, original_image, original_annotations = ds[0]
|
||||
|
||||
output = augmentation(
|
||||
image=original_image,
|
||||
bboxes=original_annotations.xyxy,
|
||||
category=original_annotations.class_id
|
||||
)
|
||||
|
||||
augmented_image = output['image']
|
||||
augmented_annotations = replace(
|
||||
original_annotations,
|
||||
xyxy=np.array(output['bboxes']),
|
||||
class_id=np.array(output['category'])
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
@@ -32,7 +32,7 @@ download_assets(VideoAssets.PEOPLE_WALKING)
|
||||
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,
|
||||
Supervision is versatile and compatible with various models. Check this
|
||||
[link](latest/how_to/detect_and_annotate/#load-predictions-into-supervision)
|
||||
[link](/latest/how_to/detect_and_annotate/#load-predictions-into-supervision)
|
||||
for guidance on how to plug in other models.
|
||||
|
||||
We will define a `callback` function, which will process each frame of the video
|
||||
|
||||
@@ -146,4 +146,13 @@ You can install `supervision` in a
|
||||
|
||||
Master the techniques to selectively filter and focus on objects within a specific zone
|
||||
|
||||
- **Cheatsheet**
|
||||
|
||||
***
|
||||
|
||||
Access a quick reference guide to the most common `supervision` functions
|
||||
|
||||
[:octicons-arrow-right-24: Cheatsheet](https://roboflow.github.io/cheatsheet-supervision/)
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@@ -28,16 +28,15 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}, {});
|
||||
|
||||
|
||||
async function renderCard(element, elementIndex) {
|
||||
const url = element.getAttribute('data-url');
|
||||
const name = element.getAttribute('data-name');
|
||||
const labels = element.getAttribute('data-labels');
|
||||
const version = element.getAttribute('data-version');
|
||||
const authors = element.getAttribute('data-author');
|
||||
async function renderCard(element, elementIndex) {
|
||||
const name = element.getAttribute('data-name');
|
||||
const labels = element.getAttribute('data-labels');
|
||||
const version = element.getAttribute('data-version');
|
||||
const authors = element.getAttribute('data-author');
|
||||
|
||||
const labelHTML = labels ? labels.split(',').filter(label => label !== '').map((label, index) => {
|
||||
const color = labelToColor[label.trim()];
|
||||
return `
|
||||
const labelHTML = labels ? labels.split(',').filter(label => label !== '').map((label, index) => {
|
||||
const color = labelToColor[label.trim()];
|
||||
return `
|
||||
<span
|
||||
class="label non-selectable-text"
|
||||
style="background-color: ${color}"
|
||||
@@ -45,18 +44,18 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
${label.trim()}
|
||||
</span>
|
||||
`;
|
||||
}).join(' ') : '';
|
||||
}).join(' ') : '';
|
||||
|
||||
const authorArray = authors.split(',');
|
||||
const authorDataArray = await Promise.all(authorArray.map(async (author) => {
|
||||
const response = await fetch(`https://api.github.com/users/${author.trim()}`);
|
||||
return await response.json();
|
||||
}));
|
||||
const authorArray = authors.split(',');
|
||||
const authorDataArray = await Promise.all(authorArray.map(async (author) => {
|
||||
const response = await fetch(`https://api.github.com/users/${author.trim()}`);
|
||||
return await response.json();
|
||||
}));
|
||||
|
||||
let authorAvatarsHTML = authorDataArray.map((authorData, index) => {
|
||||
const marginLeft = index === 0 ? '0' : '-10px';
|
||||
const zIndex = 4 - index;
|
||||
return `
|
||||
let authorAvatarsHTML = authorDataArray.map((authorData, index) => {
|
||||
const marginLeft = index === 0 ? '0' : '-10px';
|
||||
const zIndex = 4 - index;
|
||||
return `
|
||||
<div
|
||||
class="author-container"
|
||||
data-login="${authorData.login}-${elementIndex}"
|
||||
@@ -75,10 +74,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}).join('');
|
||||
|
||||
let authorNamesHTML = authorDataArray.map(
|
||||
authorData => `
|
||||
let authorNamesHTML = authorDataArray.map(
|
||||
authorData => `
|
||||
<span
|
||||
class="author-name"
|
||||
data-login="${authorData.login}-${elementIndex}"
|
||||
@@ -88,54 +87,52 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
${authorData.login}
|
||||
</a>
|
||||
</span>`
|
||||
).join(', ');
|
||||
).join(', ');
|
||||
|
||||
let authorsHTML = `
|
||||
let authorsHTML = `
|
||||
<div class="authors">
|
||||
${authorAvatarsHTML}
|
||||
<div class="author-names">${authorNamesHTML}</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
|
||||
element.innerText = `
|
||||
<a style="text-decoration: none; color: inherit;" href="${url}">
|
||||
<div style="flex-direction: column; height: 100%; display: flex;
|
||||
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji; background: ${theme.background}; font-size: 14px; line-height: 1.5; color: ${theme.color}">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span style="font-weight: 700; font-size: 1rem; color: ${theme.linkColor};">
|
||||
${name}
|
||||
</span>
|
||||
</div>
|
||||
${authorsHTML}
|
||||
<div style="font-size: 12px; color: ${theme.color}; display: flex; flex: 0; justify-content: space-between">
|
||||
element.innerText = `
|
||||
<div style="flex-direction: column; height: 100%; display: flex;
|
||||
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji; background: ${theme.background}; font-size: 14px; line-height: 1.5; color: ${theme.color}">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<img src="/assets/supervision-lenny.png" aria-label="stars" width="20" height="20" role="img" />
|
||||
|
||||
<span style="margin-left: 4px">${version}</span>
|
||||
<span style="font-weight: 700; font-size: 1rem; color: ${theme.linkColor};">
|
||||
${name}
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; flex-wrap: wrap">
|
||||
${labelHTML}
|
||||
${authorsHTML}
|
||||
<div style="font-size: 12px; color: ${theme.color}; display: flex; flex: 0; justify-content: space-between">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<img src="/assets/supervision-lenny.png" aria-label="stars" width="20" height="20" role="img" />
|
||||
|
||||
<span style="margin-left: 4px">${version}</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; flex-wrap: wrap">
|
||||
${labelHTML}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
`
|
||||
`;
|
||||
|
||||
let sanitizedHTML = DOMPurify.sanitize(element.innerText);
|
||||
element.innerHTML = sanitizedHTML;
|
||||
let sanitizedHTML = DOMPurify.sanitize(element.innerText);
|
||||
element.innerHTML = sanitizedHTML;
|
||||
|
||||
document.querySelectorAll('.author-name').forEach(element => {
|
||||
element.addEventListener('mouseenter', function() {
|
||||
const login = this.getAttribute('data-login');
|
||||
document.querySelector(`.author-container[data-login="${login}"]`).classList.add('hover');
|
||||
document.querySelectorAll('.author-name').forEach(element => {
|
||||
element.addEventListener('mouseenter', function () {
|
||||
const login = this.getAttribute('data-login');
|
||||
document.querySelector(`.author-container[data-login="${login}"]`).classList.add('hover');
|
||||
});
|
||||
|
||||
element.addEventListener('mouseleave', function () {
|
||||
const login = this.getAttribute('data-login');
|
||||
document.querySelector(`.author-container[data-login="${login}"]`).classList.remove('hover');
|
||||
});
|
||||
});
|
||||
|
||||
element.addEventListener('mouseleave', function() {
|
||||
const login = this.getAttribute('data-login');
|
||||
document.querySelector(`.author-container[data-login="${login}"]`).classList.remove('hover');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
repoCards.forEach((element, index) => {
|
||||
renderCard(element, index);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Keypoint Detection
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Mean Average Precision
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.metrics.mean_average_precision.MeanAveragePrecision">MeanAveragePrecision</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.metrics.mean_average_precision.MeanAveragePrecision
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.metrics.mean_average_precision.MeanAveragePrecisionResult">MeanAveragePrecisionResult</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.metrics.mean_average_precision.MeanAveragePrecisionResult
|
||||
Vendored
+357
-224
File diff suppressed because one or more lines are too long
+745
File diff suppressed because one or more lines are too long
+779
File diff suppressed because one or more lines are too long
@@ -19,6 +19,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.custom-grid a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.custom-grid a:hover {
|
||||
color: inherit; /* Ensure color does not change on hover */
|
||||
}
|
||||
|
||||
.repo-card {
|
||||
background: radial-gradient(at right top, #A351FB25, #A5F9EA25);
|
||||
border-radius: 0.5rem;
|
||||
|
||||
Vendored
+39
-8
@@ -11,14 +11,45 @@
|
||||
<h1>Supervision Cookbooks</h1>
|
||||
</div>
|
||||
<div class="custom-grid">
|
||||
<p class="card repo-card" data-url="/develop/notebooks/quickstart" data-name="Supervision Quickstart" data-labels="ANNOTATOR,DETECTION,SAM" data-version="v0.18.0" data-author="SkalskiP,onuralpszr"></p>
|
||||
<p class="card repo-card" data-url="/develop/notebooks/count-objects-crossing-the-line" data-name="Count Objects Crossing the Line" data-labels="ANNOTATORS,LINE ZONE,TRACKING" data-version="v0.18.0" data-author="SkalskiP"></p>
|
||||
<p class="card repo-card" data-url="/develop/notebooks/zero-shot-object-detection-with-yolo-world" data-name="Zero-Shot Object Detection with YOLO-World" data-labels="ANNOTATORS,DETECTION,INFERENCE" data-version="v0.19.0" data-author="SkalskiP"></p>
|
||||
<p class="card repo-card" data-url="/develop/notebooks/download-supervision-assets" data-name="Downloading Supervision Assets" data-labels="ASSETS" data-version="v0.18.0" data-author="nickherrig"></p>
|
||||
<p class="card repo-card" data-url="/develop/notebooks/annotate-video-with-detections" data-name="Annotate Video with Detections" data-labels="INFERENCE,YOLOV8" data-version="v0.18.0" data-author="nickherrig"></p>
|
||||
<p class="card repo-card" data-url="/develop/notebooks/object-tracking" data-name="Object Tracking" data-labels="TRACKING, ANNOTATOR" data-version="v0.18.0" data-author="nickherrig"></p>
|
||||
<p class="card repo-card" data-url="/develop/notebooks/occupancy_analytics" data-name="Analyzing Zone Occupancy" data-labels="ANNOTATOR,DETECTION,ZONES" data-version="v0.19.0" data-author="stellasphere"></p>
|
||||
<p class="card repo-card" data-url="/develop/notebooks/evaluating-alignment-of-text-to-image-diffusion-models" data-name="Evaluating Alignment of Text-to-image Diffusion Models" data-labels="ANNOTATORS,YOLO WORLD" data-version="v0.19.0rc5" data-author="iamhatesz"></p>
|
||||
<a href="/develop/notebooks/quickstart"> <p class="card repo-card" data-name="Supervision Quickstart" data-labels="ANNOTATOR,DETECTION,SAM"
|
||||
data-version="v0.18.0" data-author="SkalskiP,onuralpszr"></p>
|
||||
</a>
|
||||
<a href="/develop/notebooks/count-objects-crossing-the-line">
|
||||
<p class="card repo-card" data-name="Count Objects Crossing the Line"
|
||||
data-labels="ANNOTATORS,LINE ZONE,TRACKING" data-version="v0.18.0" data-author="SkalskiP"></p>
|
||||
</a>
|
||||
<a href="/develop/notebooks/zero-shot-object-detection-with-yolo-world">
|
||||
<p class="card repo-card" data-name="Zero-Shot Object Detection with YOLO-World"
|
||||
data-labels="ANNOTATORS,DETECTION,INFERENCE" data-version="v0.19.0" data-author="SkalskiP"></p>
|
||||
</a>
|
||||
<a href="/develop/notebooks/download-supervision-assets">
|
||||
<p class="card repo-card" data-name="Downloading Supervision Assets" data-labels="ASSETS" data-version="v0.18.0"
|
||||
data-author="nickherrig"></p>
|
||||
</a>
|
||||
<a href="/develop/notebooks/annotate-video-with-detections">
|
||||
<p class="card repo-card" data-name="Annotate Video with Detections" data-labels="INFERENCE,YOLOV8"
|
||||
data-version="v0.18.0" data-author="nickherrig"></p>
|
||||
</a>
|
||||
<a href="/develop/notebooks/object-tracking">
|
||||
<p class="card repo-card" data-name="Object Tracking" data-labels="TRACKING, ANNOTATOR" data-version="v0.18.0"
|
||||
data-author="nickherrig"></p>
|
||||
</a>
|
||||
<a href="/develop/notebooks/occupancy_analytics">
|
||||
<p class="card repo-card" data-name="Analyzing Zone Occupancy" data-labels="ANNOTATOR,DETECTION,ZONES"
|
||||
data-version="v0.19.0" data-author="stellasphere"></p>
|
||||
</a>
|
||||
<a href="/develop/notebooks/evaluating-alignment-of-text-to-image-diffusion-models">
|
||||
<p class="card repo-card" data-name="Evaluating Alignment of Text-to-image Diffusion Models"
|
||||
data-labels="ANNOTATORS,YOLO WORLD" data-version="v0.19.0rc5" data-author="iamhatesz"></p>
|
||||
</a>
|
||||
<a href="/develop/notebooks/serialise-detections-to-csv">
|
||||
<p class="card repo-card" data-name="Serialise Detections to a CSV File"
|
||||
data-labels="DETECTIONS,CSV SINK,INFERENCE" data-version="v0.21.0" data-author="onuralpszr"></p>
|
||||
</a>
|
||||
<a href="/develop/notebooks/serialise-detections-to-json">
|
||||
<p class="card repo-card" data-name="Serialise Detections to a JSON File"
|
||||
data-labels="DETECTIONS,JSON SINK,INFERENCE" data-version="v0.21.0" data-author="onuralpszr"></p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Image Utils
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Video Utils
|
||||
|
||||
@@ -11,7 +11,7 @@ from tqdm import tqdm
|
||||
|
||||
import supervision as sv
|
||||
|
||||
COLORS = sv.ColorPalette.default()
|
||||
COLORS = sv.ColorPalette.DEFAULT
|
||||
|
||||
|
||||
def load_zones_config(file_path: str) -> List[np.ndarray]:
|
||||
@@ -75,7 +75,7 @@ def detect(
|
||||
frame (np.ndarray): The frame to process, expected to be a NumPy array.
|
||||
model (RoboflowInferenceModel): The Inference model used for processing the
|
||||
frame.
|
||||
confidence_threshold (float, optional): The confidence threshold for filtering
|
||||
confidence_threshold (float): The confidence threshold for filtering
|
||||
detections. Default is 0.5.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -9,7 +9,7 @@ from ultralytics import YOLO
|
||||
|
||||
import supervision as sv
|
||||
|
||||
COLORS = sv.ColorPalette.default()
|
||||
COLORS = sv.ColorPalette.DEFAULT
|
||||
|
||||
|
||||
def load_zones_config(file_path: str) -> List[np.ndarray]:
|
||||
@@ -72,7 +72,7 @@ def detect(
|
||||
Args:
|
||||
frame (np.ndarray): The frame to process, expected to be a NumPy array.
|
||||
model (YOLO): The YOLO model used for processing the frame.
|
||||
confidence_threshold (float, optional): The confidence threshold for filtering
|
||||
confidence_threshold (float): The confidence threshold for filtering
|
||||
detections. Default is 0.5.
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -20,9 +20,9 @@ def heatmap_and_track(
|
||||
iou_threshold: float = 0.5,
|
||||
heatmap_alpha: float = 0.5,
|
||||
radius: int = 25,
|
||||
track_threshold: float = 0.35,
|
||||
track_activation_threshold: float = 0.35,
|
||||
track_seconds: int = 5,
|
||||
match_threshold: float = 0.99,
|
||||
minimum_matching_threshold: float = 0.99,
|
||||
) -> None:
|
||||
### instantiate model
|
||||
model = YOLO(source_weights_path)
|
||||
@@ -47,9 +47,9 @@ def heatmap_and_track(
|
||||
|
||||
### tracker config
|
||||
byte_tracker = sv.ByteTrack(
|
||||
track_thresh=track_threshold,
|
||||
track_buffer=track_seconds * fps,
|
||||
match_thresh=match_threshold,
|
||||
track_activation_threshold=track_activation_threshold,
|
||||
lost_track_buffer=track_seconds * fps,
|
||||
minimum_matching_threshold=minimum_matching_threshold,
|
||||
frame_rate=fps,
|
||||
)
|
||||
|
||||
@@ -175,7 +175,7 @@ if __name__ == "__main__":
|
||||
iou_threshold=args.iou_threshold,
|
||||
heatmap_alpha=args.heatmap_alpha,
|
||||
radius=args.radius,
|
||||
track_threshold=args.track_threshold,
|
||||
track_activation_threshold=args.track_threshold,
|
||||
track_seconds=args.track_seconds,
|
||||
match_threshold=args.match_threshold,
|
||||
minimum_matching_threshold=args.match_threshold,
|
||||
)
|
||||
|
||||
@@ -95,14 +95,14 @@ if __name__ == "__main__":
|
||||
model = get_roboflow_model(model_id=args.model_id, api_key=args.roboflow_api_key)
|
||||
|
||||
byte_track = sv.ByteTrack(
|
||||
frame_rate=video_info.fps, track_thresh=args.confidence_threshold
|
||||
frame_rate=video_info.fps, track_activation_threshold=args.confidence_threshold
|
||||
)
|
||||
|
||||
thickness = sv.calculate_optimal_line_thickness(
|
||||
resolution_wh=video_info.resolution_wh
|
||||
)
|
||||
text_scale = sv.calculate_optimal_text_scale(resolution_wh=video_info.resolution_wh)
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator(thickness=thickness)
|
||||
box_annotator = sv.BoxAnnotator(thickness=thickness)
|
||||
label_annotator = sv.LabelAnnotator(
|
||||
text_scale=text_scale,
|
||||
text_thickness=thickness,
|
||||
@@ -154,7 +154,7 @@ if __name__ == "__main__":
|
||||
annotated_frame = trace_annotator.annotate(
|
||||
scene=annotated_frame, detections=detections
|
||||
)
|
||||
annotated_frame = bounding_box_annotator.annotate(
|
||||
annotated_frame = box_annotator.annotate(
|
||||
scene=annotated_frame, detections=detections
|
||||
)
|
||||
annotated_frame = label_annotator.annotate(
|
||||
|
||||
@@ -73,14 +73,14 @@ if __name__ == "__main__":
|
||||
model = YOLO("yolov8x.pt")
|
||||
|
||||
byte_track = sv.ByteTrack(
|
||||
frame_rate=video_info.fps, track_thresh=args.confidence_threshold
|
||||
frame_rate=video_info.fps, track_activation_threshold=args.confidence_threshold
|
||||
)
|
||||
|
||||
thickness = sv.calculate_optimal_line_thickness(
|
||||
resolution_wh=video_info.resolution_wh
|
||||
)
|
||||
text_scale = sv.calculate_optimal_text_scale(resolution_wh=video_info.resolution_wh)
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator(thickness=thickness)
|
||||
box_annotator = sv.BoxAnnotator(thickness=thickness)
|
||||
label_annotator = sv.LabelAnnotator(
|
||||
text_scale=text_scale,
|
||||
text_thickness=thickness,
|
||||
@@ -132,7 +132,7 @@ if __name__ == "__main__":
|
||||
annotated_frame = trace_annotator.annotate(
|
||||
scene=annotated_frame, detections=detections
|
||||
)
|
||||
annotated_frame = bounding_box_annotator.annotate(
|
||||
annotated_frame = box_annotator.annotate(
|
||||
scene=annotated_frame, detections=detections
|
||||
)
|
||||
annotated_frame = label_annotator.annotate(
|
||||
|
||||
@@ -74,14 +74,14 @@ if __name__ == "__main__":
|
||||
model = models.get(Models.YOLO_NAS_L, pretrained_weights="coco")
|
||||
|
||||
byte_track = sv.ByteTrack(
|
||||
frame_rate=video_info.fps, track_thresh=args.confidence_threshold
|
||||
frame_rate=video_info.fps, track_activation_threshold=args.confidence_threshold
|
||||
)
|
||||
|
||||
thickness = sv.calculate_optimal_line_thickness(
|
||||
resolution_wh=video_info.resolution_wh
|
||||
)
|
||||
text_scale = sv.calculate_optimal_text_scale(resolution_wh=video_info.resolution_wh)
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator(thickness=thickness)
|
||||
box_annotator = sv.BoxAnnotator(thickness=thickness)
|
||||
label_annotator = sv.LabelAnnotator(
|
||||
text_scale=text_scale,
|
||||
text_thickness=thickness,
|
||||
@@ -132,7 +132,7 @@ if __name__ == "__main__":
|
||||
annotated_frame = trace_annotator.annotate(
|
||||
scene=annotated_frame, detections=detections
|
||||
)
|
||||
annotated_frame = bounding_box_annotator.annotate(
|
||||
annotated_frame = box_annotator.annotate(
|
||||
scene=annotated_frame, detections=detections
|
||||
)
|
||||
annotated_frame = label_annotator.annotate(
|
||||
|
||||
@@ -22,7 +22,7 @@ class FPSBasedTimer:
|
||||
"""Initializes the FPSBasedTimer with the specified frames per second rate.
|
||||
|
||||
Args:
|
||||
fps (int, optional): The frame rate of the video stream. Defaults to 30.
|
||||
fps (int): The frame rate of the video stream. Defaults to 30.
|
||||
"""
|
||||
self.fps = fps
|
||||
self.frame_id = 0
|
||||
|
||||
@@ -93,7 +93,7 @@ class VideoProcessor:
|
||||
self.zones_in = initiate_polygon_zones(ZONE_IN_POLYGONS, [sv.Position.CENTER])
|
||||
self.zones_out = initiate_polygon_zones(ZONE_OUT_POLYGONS, [sv.Position.CENTER])
|
||||
|
||||
self.bounding_box_annotator = sv.BoundingBoxAnnotator(color=COLORS)
|
||||
self.box_annotator = sv.BoxAnnotator(color=COLORS)
|
||||
self.label_annotator = sv.LabelAnnotator(
|
||||
color=COLORS, text_color=sv.Color.BLACK
|
||||
)
|
||||
@@ -134,9 +134,7 @@ class VideoProcessor:
|
||||
|
||||
labels = [f"#{tracker_id}" for tracker_id in detections.tracker_id]
|
||||
annotated_frame = self.trace_annotator.annotate(annotated_frame, detections)
|
||||
annotated_frame = self.bounding_box_annotator.annotate(
|
||||
annotated_frame, detections
|
||||
)
|
||||
annotated_frame = self.box_annotator.annotate(annotated_frame, detections)
|
||||
annotated_frame = self.label_annotator.annotate(
|
||||
annotated_frame, detections, labels
|
||||
)
|
||||
|
||||
@@ -90,7 +90,7 @@ class VideoProcessor:
|
||||
self.zones_in = initiate_polygon_zones(ZONE_IN_POLYGONS, [sv.Position.CENTER])
|
||||
self.zones_out = initiate_polygon_zones(ZONE_OUT_POLYGONS, [sv.Position.CENTER])
|
||||
|
||||
self.bounding_box_annotator = sv.BoundingBoxAnnotator(color=COLORS)
|
||||
self.box_annotator = sv.BoxAnnotator(color=COLORS)
|
||||
self.label_annotator = sv.LabelAnnotator(
|
||||
color=COLORS, text_color=sv.Color.BLACK
|
||||
)
|
||||
@@ -131,9 +131,7 @@ class VideoProcessor:
|
||||
|
||||
labels = [f"#{tracker_id}" for tracker_id in detections.tracker_id]
|
||||
annotated_frame = self.trace_annotator.annotate(annotated_frame, detections)
|
||||
annotated_frame = self.bounding_box_annotator.annotate(
|
||||
annotated_frame, detections
|
||||
)
|
||||
annotated_frame = self.box_annotator.annotate(annotated_frame, detections)
|
||||
annotated_frame = self.label_annotator.annotate(
|
||||
annotated_frame, detections, labels
|
||||
)
|
||||
|
||||
+55
-48
@@ -33,7 +33,6 @@ extra_css:
|
||||
- stylesheets/extra.css
|
||||
- stylesheets/cookbooks-card.css
|
||||
|
||||
|
||||
nav:
|
||||
- Supervision: index.md
|
||||
- Learn:
|
||||
@@ -42,51 +41,53 @@ nav:
|
||||
- Filter Detections: how_to/filter_detections.md
|
||||
- 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
|
||||
|
||||
- Reference - Code API:
|
||||
- Detection and Segmentation:
|
||||
- Core: detection/core.md
|
||||
- Annotators: detection/annotators.md
|
||||
- Metrics: detection/metrics.md
|
||||
- Double Detection Filter: detection/double_detection_filter.md
|
||||
- Utils: detection/utils.md
|
||||
- Keypoint Detection:
|
||||
- Core: keypoint/core.md
|
||||
- Annotators: keypoint/annotators.md
|
||||
- Classification:
|
||||
- Core: classification/core.md
|
||||
- Tools:
|
||||
- Line Zone: detection/tools/line_zone.md
|
||||
- Polygon Zone: detection/tools/polygon_zone.md
|
||||
- Inference Slicer: detection/tools/inference_slicer.md
|
||||
- Detection Smoother: detection/tools/smoother.md
|
||||
- Save Detections: detection/tools/save_detections.md
|
||||
- Trackers: trackers.md
|
||||
- Datasets:
|
||||
- Core: datasets/core.md
|
||||
- Utils: datasets/utils.md
|
||||
- Utils:
|
||||
- Video: utils/video.md
|
||||
- Image: utils/image.md
|
||||
- Iterables: utils/iterables.md
|
||||
- Notebook: utils/notebook.md
|
||||
- File: utils/file.md
|
||||
- Draw: utils/draw.md
|
||||
- Geometry: utils/geometry.md
|
||||
- Assets: assets.md
|
||||
- Detection and Segmentation:
|
||||
- Core: detection/core.md
|
||||
- Annotators: detection/annotators.md
|
||||
- Double Detection Filter: detection/double_detection_filter.md
|
||||
- Utils: detection/utils.md
|
||||
- Keypoint Detection:
|
||||
- Core: keypoint/core.md
|
||||
- Annotators: keypoint/annotators.md
|
||||
- Classification:
|
||||
- Core: classification/core.md
|
||||
- Tools:
|
||||
- Line Zone: detection/tools/line_zone.md
|
||||
- Polygon Zone: detection/tools/polygon_zone.md
|
||||
- Inference Slicer: detection/tools/inference_slicer.md
|
||||
- Detection Smoother: detection/tools/smoother.md
|
||||
- Save Detections: detection/tools/save_detections.md
|
||||
- Trackers: trackers.md
|
||||
- Datasets:
|
||||
- Core: datasets/core.md
|
||||
- Utils: datasets/utils.md
|
||||
- Metrics:
|
||||
- mAP: metrics/mean_average_precision.md
|
||||
- Legacy Metrics: detection/metrics.md
|
||||
- Utils:
|
||||
- Video: utils/video.md
|
||||
- Image: utils/image.md
|
||||
- Iterables: utils/iterables.md
|
||||
- Notebook: utils/notebook.md
|
||||
- File: utils/file.md
|
||||
- Draw: utils/draw.md
|
||||
- Geometry: utils/geometry.md
|
||||
- Assets: assets.md
|
||||
- Cookbooks: cookbooks.md
|
||||
- Cheatsheet: https://roboflow.github.io/cheatsheet-supervision/
|
||||
- Contribute:
|
||||
- Contributing: contributing.md
|
||||
- Code of Conduct: code_of_conduct.md
|
||||
- License: license.md
|
||||
- Contributing: contributing.md
|
||||
- Code of Conduct: code_of_conduct.md
|
||||
- License: license.md
|
||||
- Release Notes:
|
||||
- Changelog: changelog.md
|
||||
- Deprecated: deprecated.md
|
||||
|
||||
|
||||
- Changelog: changelog.md
|
||||
- Deprecated: deprecated.md
|
||||
|
||||
theme:
|
||||
name: 'material'
|
||||
name: "material"
|
||||
icon:
|
||||
edit: material/pencil
|
||||
logo: assets/supervision-lenny.png
|
||||
@@ -103,19 +104,18 @@ theme:
|
||||
palette:
|
||||
# Palette for light mode
|
||||
- scheme: default
|
||||
primary: 'custom'
|
||||
primary: "custom"
|
||||
toggle:
|
||||
icon: material/brightness-7
|
||||
name: Switch to dark mode
|
||||
|
||||
# Palette toggle for dark mode
|
||||
- scheme: slate
|
||||
primary: 'custom'
|
||||
primary: "custom"
|
||||
toggle:
|
||||
icon: material/brightness-4
|
||||
name: Switch to light mode
|
||||
|
||||
|
||||
font:
|
||||
text: Roboto
|
||||
code: Roboto Mono
|
||||
@@ -126,10 +126,10 @@ theme:
|
||||
plugins:
|
||||
- search
|
||||
- mkdocs-jupyter:
|
||||
kernel_name: python3
|
||||
execute: false
|
||||
include_source: True
|
||||
include_requirejs: true
|
||||
kernel_name: python3
|
||||
execute: false
|
||||
include_source: True
|
||||
include_requirejs: true
|
||||
- mkdocstrings:
|
||||
default_handler: python
|
||||
handlers:
|
||||
@@ -153,8 +153,6 @@ plugins:
|
||||
- git-revision-date-localized:
|
||||
enable_creation_date: true
|
||||
|
||||
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- pymdownx.details
|
||||
@@ -178,3 +176,12 @@ extra_javascript:
|
||||
- "javascripts/init_kapa_widget.js"
|
||||
- "javascripts/cookbooks-card.js"
|
||||
- "https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.8/purify.min.js"
|
||||
|
||||
# Messages shown during document build
|
||||
# Reference: https://www.mkdocs.org/user-guide/configuration/#validation
|
||||
# Values: [warn, info, ignore]
|
||||
validation:
|
||||
nav:
|
||||
absolute_links: ignore
|
||||
links:
|
||||
absolute_links: ignore
|
||||
|
||||
Generated
+1430
-1175
File diff suppressed because it is too large
Load Diff
+93
-23
@@ -1,18 +1,26 @@
|
||||
[tool.poetry]
|
||||
name = "supervision"
|
||||
version = "0.21.0"
|
||||
version = "0.23.0"
|
||||
description = "A set of easy-to-use utils that will come in handy in any Computer Vision project"
|
||||
authors = ["Piotr Skalski <piotr.skalski92@gmail.com>"]
|
||||
maintainers = ["Piotr Skalski <piotr.skalski92@gmail.com>"]
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
packages = [{include = "supervision"}]
|
||||
packages = [{ include = "supervision" }]
|
||||
homepage = "https://github.com/roboflow/supervision"
|
||||
repository = "https://github.com/roboflow/supervision"
|
||||
documentation = "https://github.com/roboflow/supervision/blob/main/README.md"
|
||||
keywords = ["machine-learning", "deep-learning", "vision", "ML", "DL", "AI", "Roboflow"]
|
||||
keywords = [
|
||||
"machine-learning",
|
||||
"deep-learning",
|
||||
"vision",
|
||||
"ML",
|
||||
"DL",
|
||||
"AI",
|
||||
"Roboflow",
|
||||
]
|
||||
|
||||
classifiers=[
|
||||
classifiers = [
|
||||
'Development Status :: 4 - Beta',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: Education',
|
||||
@@ -27,33 +35,40 @@ classifiers=[
|
||||
'Typing :: Typed',
|
||||
'Operating System :: Microsoft :: Windows',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Operating System :: MacOS'
|
||||
'Operating System :: MacOS',
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
numpy = ">=1.21.2"
|
||||
numpy = [
|
||||
{ version = ">=1.21.2,<1.23.3", python = "<=3.10" },
|
||||
{ version = ">=1.23.3", python = ">3.10" },
|
||||
]
|
||||
scipy = [
|
||||
{ version = "1.10.0", python = "<3.9" },
|
||||
{ version = "^1.10.0", python = ">=3.9" }
|
||||
{ version = "^1.10.0", python = ">=3.9" },
|
||||
]
|
||||
matplotlib = ">=3.6.0"
|
||||
pyyaml = ">=5.3"
|
||||
defusedxml = "^0.7.1"
|
||||
pillow = ">=9.4"
|
||||
opencv-python = { version = ">=4.5.5.64", optional = true }
|
||||
opencv-python-headless = ">=4.5.5.64"
|
||||
requests = { version = ">=2.26.0,<=2.32.3", optional = true }
|
||||
tqdm = { version = ">=4.62.3,<=4.66.4", optional = true }
|
||||
pillow = ">=9.4"
|
||||
tqdm = { version = ">=4.62.3,<=4.66.5", 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 }
|
||||
|
||||
[tool.poetry.extras]
|
||||
desktop = ["opencv-python"]
|
||||
assets = ["requests","tqdm"]
|
||||
assets = ["requests", "tqdm"]
|
||||
metrics = ["pandas", "pandas-stubs"]
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
twine = ">=4.0.2,<6.0.0"
|
||||
twine = "^5.1.1"
|
||||
pytest = ">=7.2.2,<9.0.0"
|
||||
wheel = ">=0.40,<0.44"
|
||||
wheel = ">=0.40,<0.45"
|
||||
build = ">=0.10,<1.3"
|
||||
ruff = ">=0.1.0"
|
||||
mypy = "^1.4.1"
|
||||
@@ -63,11 +78,14 @@ 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" },
|
||||
]
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
mkdocs-material = {extras = ["imaging"], version = "^9.5.5"}
|
||||
mkdocstrings = {extras = ["python"], version = ">=0.20,<0.26"}
|
||||
mkdocs-material = { extras = ["imaging"], version = "^9.5.5" }
|
||||
mkdocstrings = { extras = ["python"], version = "^0.25.2" }
|
||||
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
|
||||
@@ -75,11 +93,15 @@ mkdocs-jupyter = "^0.24.3"
|
||||
mkdocs-git-committers-plugin-2 = "^2.2.3"
|
||||
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.isort]
|
||||
line_length = 88
|
||||
profile = "black"
|
||||
|
||||
[tool.bandit]
|
||||
target = ["test", "supervision"]
|
||||
@@ -89,7 +111,6 @@ tests = ["B201", "B301", "B318", "B314", "B303", "B413", "B412", "B410"]
|
||||
check = true
|
||||
imports = ["cv2", "supervision"]
|
||||
|
||||
|
||||
[tool.black]
|
||||
target-version = ["py38"]
|
||||
line-length = 88
|
||||
@@ -145,10 +166,55 @@ indent-width = 4
|
||||
|
||||
[tool.ruff.lint]
|
||||
# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
|
||||
select = ["E", "F"]
|
||||
select = ["E", "F", "I", "A", "Q", "W"]
|
||||
ignore = []
|
||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"]
|
||||
fixable = [
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G",
|
||||
"I",
|
||||
"N",
|
||||
"Q",
|
||||
"S",
|
||||
"T",
|
||||
"W",
|
||||
"ANN",
|
||||
"ARG",
|
||||
"BLE",
|
||||
"COM",
|
||||
"DJ",
|
||||
"DTZ",
|
||||
"EM",
|
||||
"ERA",
|
||||
"EXE",
|
||||
"FBT",
|
||||
"ICN",
|
||||
"INP",
|
||||
"ISC",
|
||||
"NPY",
|
||||
"PD",
|
||||
"PGH",
|
||||
"PIE",
|
||||
"PL",
|
||||
"PT",
|
||||
"PTH",
|
||||
"PYI",
|
||||
"RET",
|
||||
"RSE",
|
||||
"RUF",
|
||||
"SIM",
|
||||
"SLF",
|
||||
"TCH",
|
||||
"TID",
|
||||
"TRY",
|
||||
"UP",
|
||||
"YTT",
|
||||
]
|
||||
unfixable = []
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
@@ -163,12 +229,16 @@ docstring-quotes = "double"
|
||||
convention = "google"
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"__init__.py" = ["E402","F401"]
|
||||
"__init__.py" = ["E402", "F401"]
|
||||
|
||||
[tool.ruff.lint.mccabe]
|
||||
# Flag errors (`C901`) whenever the complexity level exceeds 5.
|
||||
max-complexity = 20
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
order-by-type = true
|
||||
no-sections = false
|
||||
|
||||
[tool.ruff.format]
|
||||
# Like Black, use double quotes for strings.
|
||||
quote-style = "double"
|
||||
@@ -186,7 +256,7 @@ line-ending = "auto"
|
||||
include-package-data = false
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
exclude = ["docs*", "test*","examples*"]
|
||||
exclude = ["docs*", "test*", "examples*"]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Release Process
|
||||
|
||||
This doc outlines how supervision is released into production.
|
||||
|
||||
It assumes you already have the code changes, as well as a draft of the release notes.
|
||||
|
||||
1. Make sure you have all required changes were merged into `develop`.
|
||||
2. Create and merge a PR, merging `develop` into `main`, containing:
|
||||
- A commit that updates the project version in `pyproject.toml`.
|
||||
- All changes made during the release.
|
||||
3. Tag the commit with the new supervision version.
|
||||
- make sure to pull from `main` !
|
||||
- Verify that the latest merge commits exists. `git log`.
|
||||
- Run `git tag x.y.z`, with your version
|
||||
- Check with `git log`.
|
||||
- Run `git push origin --tags`
|
||||
- Upon pushing the tag, the [PyPi](https://pypi.org/project/supervision/) should update to the new version. Check this!
|
||||
4. Open and merge a PR, merging `main` into `develop`.
|
||||
5. Update the docs by running the [Supervision Release Documentation Workflow 📚](https://github.com/roboflow/supervision/actions/workflows/publish-release-docs.yml) workflow from GitHub.
|
||||
- Select the `main` branch from the dropdown.
|
||||
6. Create a release on GitHub.
|
||||
- Go to releases
|
||||
- Assign the release notes to the tag created in step 3.
|
||||
- Publish the release.
|
||||
@@ -7,8 +7,10 @@ except importlib_metadata.PackageNotFoundError:
|
||||
__version__ = "development"
|
||||
|
||||
from supervision.annotators.core import (
|
||||
BackgroundOverlayAnnotator,
|
||||
BlurAnnotator,
|
||||
BoundingBoxAnnotator,
|
||||
BoxAnnotator,
|
||||
BoxCornerAnnotator,
|
||||
CircleAnnotator,
|
||||
ColorAnnotator,
|
||||
@@ -17,6 +19,7 @@ from supervision.annotators.core import (
|
||||
EllipseAnnotator,
|
||||
HaloAnnotator,
|
||||
HeatMapAnnotator,
|
||||
IconAnnotator,
|
||||
LabelAnnotator,
|
||||
MaskAnnotator,
|
||||
OrientedBoxAnnotator,
|
||||
@@ -36,7 +39,6 @@ from supervision.dataset.core import (
|
||||
DetectionDataset,
|
||||
)
|
||||
from supervision.dataset.utils import mask_to_rle, rle_to_mask
|
||||
from supervision.detection.annotate import BoxAnnotator
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.line_zone import LineZone, LineZoneAnnotator
|
||||
from supervision.detection.lmm import LMM
|
||||
@@ -67,6 +69,8 @@ from supervision.detection.utils import (
|
||||
polygon_to_mask,
|
||||
polygon_to_xyxy,
|
||||
scale_boxes,
|
||||
xcycwh_to_xyxy,
|
||||
xywh_to_xyxy,
|
||||
)
|
||||
from supervision.draw.color import Color, ColorPalette
|
||||
from supervision.draw.utils import (
|
||||
|
||||
+504
-108
File diff suppressed because it is too large
Load Diff
+383
-151
@@ -3,8 +3,9 @@ from __future__ import annotations
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterator, List, Optional, Tuple
|
||||
from typing import Dict, Iterator, List, Optional, Tuple, Union
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
@@ -31,9 +32,10 @@ from supervision.dataset.utils import (
|
||||
train_test_split,
|
||||
)
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.utils.internal import deprecated, warn_deprecated
|
||||
from supervision.utils.iterables import find_duplicates
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseDataset(ABC):
|
||||
@abstractmethod
|
||||
def __len__(self) -> int:
|
||||
@@ -41,35 +43,103 @@ class BaseDataset(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def split(
|
||||
self, split_ratio=0.8, random_state=None, shuffle: bool = True
|
||||
self,
|
||||
split_ratio: float = 0.8,
|
||||
random_state: Optional[int] = None,
|
||||
shuffle: bool = True,
|
||||
) -> Tuple[BaseDataset, BaseDataset]:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class DetectionDataset(BaseDataset):
|
||||
"""
|
||||
Dataclass containing information about object detection dataset.
|
||||
Contains information about a detection dataset. Handles lazy image loading
|
||||
and annotation retrieval, dataset splitting, conversions into multiple
|
||||
formats.
|
||||
|
||||
Attributes:
|
||||
classes (List[str]): List containing dataset class names.
|
||||
images (Dict[str, np.ndarray]): Dictionary mapping image name to image.
|
||||
images (Union[List[str], Dict[str, np.ndarray]]):
|
||||
Accepts a list of image paths, or dictionaries of loaded cv2 images
|
||||
with paths as keys. If you pass a list of paths, the dataset will
|
||||
lazily load images on demand, which is much more memory-efficient.
|
||||
annotations (Dict[str, Detections]): Dictionary mapping
|
||||
image name to annotations.
|
||||
image path to annotations. The dictionary keys match
|
||||
match the keys in `images` or entries in the list of
|
||||
image paths.
|
||||
"""
|
||||
|
||||
classes: List[str]
|
||||
images: Dict[str, np.ndarray]
|
||||
annotations: Dict[str, Detections]
|
||||
def __init__(
|
||||
self,
|
||||
classes: List[str],
|
||||
images: Union[List[str], Dict[str, np.ndarray]],
|
||||
annotations: Dict[str, Detections],
|
||||
) -> None:
|
||||
self.classes = classes
|
||||
|
||||
if set(images) != set(annotations):
|
||||
raise ValueError(
|
||||
"The keys of the images and annotations dictionaries must match."
|
||||
)
|
||||
self.annotations = annotations
|
||||
|
||||
# Eliminate duplicates while preserving order
|
||||
self.image_paths = list(dict.fromkeys(images))
|
||||
|
||||
self._images_in_memory: Dict[str, np.ndarray] = {}
|
||||
if isinstance(images, dict):
|
||||
self._images_in_memory = images
|
||||
warn_deprecated(
|
||||
"Passing a `Dict[str, np.ndarray]` into `DetectionDataset` is "
|
||||
"deprecated and will be removed in `supervision-0.26.0`. Use "
|
||||
"a list of paths `List[str]` instead."
|
||||
)
|
||||
# TODO: when supervision-0.26.0 is released, and Dict[str, np.ndarray]
|
||||
# for images is no longer supported, also simplify the rest of
|
||||
# the code. E.g. list(images) is no longer needed, and merge can
|
||||
# be simplified.
|
||||
|
||||
@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:
|
||||
return self._images_in_memory[image_path]
|
||||
return cv2.imread(image_path)
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""
|
||||
Return the number of images in the dataset.
|
||||
return len(self._images_in_memory) or len(self.image_paths)
|
||||
|
||||
Returns:
|
||||
int: The number of images.
|
||||
def __getitem__(self, i: int) -> Tuple[str, np.ndarray, Detections]:
|
||||
"""
|
||||
return len(self.images)
|
||||
Returns:
|
||||
Tuple[str, np.ndarray, Detections]: The image path, image data,
|
||||
and its corresponding annotation at index i.
|
||||
"""
|
||||
image_path = self.image_paths[i]
|
||||
image = self._get_image(image_path)
|
||||
annotation = self.annotations[image_path]
|
||||
return image_path, image, annotation
|
||||
|
||||
def __iter__(self) -> Iterator[Tuple[str, np.ndarray, Detections]]:
|
||||
"""
|
||||
@@ -77,40 +147,51 @@ class DetectionDataset(BaseDataset):
|
||||
|
||||
Yields:
|
||||
Iterator[Tuple[str, np.ndarray, Detections]]:
|
||||
An iterator that yields tuples containing the image name,
|
||||
An iterator that yields tuples containing the image path,
|
||||
the image data, and its corresponding annotation.
|
||||
"""
|
||||
for image_name, image in self.images.items():
|
||||
yield image_name, image, self.annotations.get(image_name, None)
|
||||
for i in range(len(self)):
|
||||
image_path, image, annotation = self[i]
|
||||
yield image_path, image, annotation
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other) -> bool:
|
||||
if not isinstance(other, DetectionDataset):
|
||||
return False
|
||||
|
||||
if set(self.classes) != set(other.classes):
|
||||
return False
|
||||
|
||||
for key in self.images:
|
||||
if not np.array_equal(self.images[key], other.images[key]):
|
||||
return False
|
||||
if not self.annotations[key] == other.annotations[key]:
|
||||
if self.image_paths != other.image_paths:
|
||||
return False
|
||||
|
||||
if self._images_in_memory or other._images_in_memory:
|
||||
if not np.array_equal(
|
||||
list(self._images_in_memory.values()),
|
||||
list(other._images_in_memory.values()),
|
||||
):
|
||||
return False
|
||||
|
||||
if self.annotations != other.annotations:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def split(
|
||||
self, split_ratio=0.8, random_state=None, shuffle: bool = True
|
||||
self,
|
||||
split_ratio: float = 0.8,
|
||||
random_state: Optional[int] = None,
|
||||
shuffle: bool = True,
|
||||
) -> Tuple[DetectionDataset, DetectionDataset]:
|
||||
"""
|
||||
Splits the dataset into two parts (training and testing)
|
||||
using the provided split_ratio.
|
||||
|
||||
Args:
|
||||
split_ratio (float, optional): The ratio of the training
|
||||
split_ratio (float): The ratio of the training
|
||||
set to the entire dataset.
|
||||
random_state (int, optional): The seed for the random number generator.
|
||||
random_state (Optional[int]): The seed for the random number generator.
|
||||
This is used for reproducibility.
|
||||
shuffle (bool, optional): Whether to shuffle the data before splitting.
|
||||
shuffle (bool): Whether to shuffle the data before splitting.
|
||||
|
||||
Returns:
|
||||
Tuple[DetectionDataset, DetectionDataset]: A tuple containing
|
||||
@@ -127,26 +208,129 @@ class DetectionDataset(BaseDataset):
|
||||
```
|
||||
"""
|
||||
|
||||
image_names = list(self.images.keys())
|
||||
train_names, test_names = train_test_split(
|
||||
data=image_names,
|
||||
train_paths, test_paths = train_test_split(
|
||||
data=self.image_paths,
|
||||
train_ratio=split_ratio,
|
||||
random_state=random_state,
|
||||
shuffle=shuffle,
|
||||
)
|
||||
|
||||
train_input: Union[List[str], Dict[str, np.ndarray]]
|
||||
test_input: Union[List[str], Dict[str, np.ndarray]]
|
||||
if self._images_in_memory:
|
||||
train_input = {path: self._images_in_memory[path] for path in train_paths}
|
||||
test_input = {path: self._images_in_memory[path] for path in test_paths}
|
||||
else:
|
||||
train_input = train_paths
|
||||
test_input = test_paths
|
||||
train_annotations = {path: self.annotations[path] for path in train_paths}
|
||||
test_annotations = {path: self.annotations[path] for path in test_paths}
|
||||
|
||||
train_dataset = DetectionDataset(
|
||||
classes=self.classes,
|
||||
images={name: self.images[name] for name in train_names},
|
||||
annotations={name: self.annotations[name] for name in train_names},
|
||||
images=train_input,
|
||||
annotations=train_annotations,
|
||||
)
|
||||
test_dataset = DetectionDataset(
|
||||
classes=self.classes,
|
||||
images={name: self.images[name] for name in test_names},
|
||||
annotations={name: self.annotations[name] for name in test_names},
|
||||
images=test_input,
|
||||
annotations=test_annotations,
|
||||
)
|
||||
return train_dataset, test_dataset
|
||||
|
||||
@classmethod
|
||||
def merge(cls, dataset_list: List[DetectionDataset]) -> DetectionDataset:
|
||||
"""
|
||||
Merge a list of `DetectionDataset` objects into a single
|
||||
`DetectionDataset` object.
|
||||
|
||||
This method takes a list of `DetectionDataset` objects and combines
|
||||
their respective fields (`classes`, `images`,
|
||||
`annotations`) into a single `DetectionDataset` object.
|
||||
|
||||
Args:
|
||||
dataset_list (List[DetectionDataset]): A list of `DetectionDataset`
|
||||
objects to merge.
|
||||
|
||||
Returns:
|
||||
(DetectionDataset): A single `DetectionDataset` object containing
|
||||
the merged data from the input list.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
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_merged = sv.DetectionDataset.merge([ds_1, ds_2])
|
||||
len(ds_merged)
|
||||
# 300
|
||||
ds_merged.classes
|
||||
# ['cat', 'dog', 'person']
|
||||
```
|
||||
"""
|
||||
|
||||
def is_in_memory(dataset: DetectionDataset) -> bool:
|
||||
return len(dataset._images_in_memory) > 0 or len(dataset.image_paths) == 0
|
||||
|
||||
def is_lazy(dataset: DetectionDataset) -> bool:
|
||||
return len(dataset._images_in_memory) == 0
|
||||
|
||||
all_in_memory = all([is_in_memory(dataset) for dataset in dataset_list])
|
||||
all_lazy = all([is_lazy(dataset) for dataset in dataset_list])
|
||||
if not all_in_memory and not all_lazy:
|
||||
raise ValueError(
|
||||
"Merging lazy and in-memory DetectionDatasets is not supported."
|
||||
)
|
||||
|
||||
images_in_memory = {}
|
||||
for dataset in dataset_list:
|
||||
images_in_memory.update(dataset._images_in_memory)
|
||||
|
||||
image_paths = list(
|
||||
chain.from_iterable(dataset.image_paths for dataset in dataset_list)
|
||||
)
|
||||
image_paths_unique = list(dict.fromkeys(image_paths))
|
||||
if len(image_paths) != len(image_paths_unique):
|
||||
duplicates = find_duplicates(image_paths)
|
||||
raise ValueError(
|
||||
f"Image paths {duplicates} are not unique across datasets."
|
||||
)
|
||||
image_paths = image_paths_unique
|
||||
|
||||
classes = merge_class_lists(
|
||||
class_lists=[dataset.classes for dataset in dataset_list]
|
||||
)
|
||||
|
||||
annotations = {}
|
||||
for dataset in dataset_list:
|
||||
annotations.update(dataset.annotations)
|
||||
for dataset in dataset_list:
|
||||
class_index_mapping = build_class_index_mapping(
|
||||
source_classes=dataset.classes, target_classes=classes
|
||||
)
|
||||
for image_path in dataset.image_paths:
|
||||
annotations[image_path] = map_detections_class_id(
|
||||
source_to_target_mapping=class_index_mapping,
|
||||
detections=annotations[image_path],
|
||||
)
|
||||
|
||||
return cls(
|
||||
classes=classes,
|
||||
images=images_in_memory or image_paths,
|
||||
annotations=annotations,
|
||||
)
|
||||
|
||||
def as_pascal_voc(
|
||||
self,
|
||||
images_directory_path: Optional[str] = None,
|
||||
@@ -180,25 +364,22 @@ class DetectionDataset(BaseDataset):
|
||||
"""
|
||||
if images_directory_path:
|
||||
save_dataset_images(
|
||||
images_directory_path=images_directory_path, images=self.images
|
||||
dataset=self,
|
||||
images_directory_path=images_directory_path,
|
||||
)
|
||||
if annotations_directory_path:
|
||||
Path(annotations_directory_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for image_path, image in self.images.items():
|
||||
detections = self.annotations[image_path]
|
||||
|
||||
if annotations_directory_path:
|
||||
for image_path, image, annotations in self:
|
||||
annotation_name = Path(image_path).stem
|
||||
annotations_path = os.path.join(
|
||||
annotations_directory_path, f"{annotation_name}.xml"
|
||||
)
|
||||
image_name = Path(image_path).name
|
||||
pascal_voc_xml = detections_to_pascal_voc(
|
||||
detections=detections,
|
||||
detections=annotations,
|
||||
classes=self.classes,
|
||||
filename=image_name,
|
||||
image_shape=image.shape,
|
||||
image_shape=image.shape, # type: ignore
|
||||
min_image_area_percentage=min_image_area_percentage,
|
||||
max_image_area_percentage=max_image_area_percentage,
|
||||
approximation_percentage=approximation_percentage,
|
||||
@@ -221,7 +402,7 @@ class DetectionDataset(BaseDataset):
|
||||
images_directory_path (str): Path to the directory containing the images.
|
||||
annotations_directory_path (str): Path to the directory
|
||||
containing the PASCAL VOC XML annotations.
|
||||
force_masks (bool, optional): If True, forces masks to
|
||||
force_masks (bool): If True, forces masks to
|
||||
be loaded for all annotations, regardless of whether they are present.
|
||||
|
||||
Returns:
|
||||
@@ -251,13 +432,15 @@ class DetectionDataset(BaseDataset):
|
||||
```
|
||||
"""
|
||||
|
||||
classes, images, annotations = load_pascal_voc_annotations(
|
||||
classes, image_paths, annotations = load_pascal_voc_annotations(
|
||||
images_directory_path=images_directory_path,
|
||||
annotations_directory_path=annotations_directory_path,
|
||||
force_masks=force_masks,
|
||||
)
|
||||
|
||||
return DetectionDataset(classes=classes, images=images, annotations=annotations)
|
||||
return DetectionDataset(
|
||||
classes=classes, images=image_paths, annotations=annotations
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_yolo(
|
||||
@@ -266,6 +449,7 @@ class DetectionDataset(BaseDataset):
|
||||
annotations_directory_path: str,
|
||||
data_yaml_path: str,
|
||||
force_masks: bool = False,
|
||||
is_obb: bool = False,
|
||||
) -> DetectionDataset:
|
||||
"""
|
||||
Creates a Dataset instance from YOLO formatted data.
|
||||
@@ -277,9 +461,12 @@ class DetectionDataset(BaseDataset):
|
||||
containing the YOLO annotation files.
|
||||
data_yaml_path (str): The path to the data
|
||||
YAML file containing class information.
|
||||
force_masks (bool, optional): If True, forces
|
||||
force_masks (bool): If True, forces
|
||||
masks to be loaded for all annotations,
|
||||
regardless of whether they are present.
|
||||
is_obb (bool): If True, loads the annotations in OBB format.
|
||||
OBB annotations are defined as `[class_id, x, y, x, y, x, y, x, y]`,
|
||||
where pairs of [x, y] are box corners.
|
||||
|
||||
Returns:
|
||||
DetectionDataset: A DetectionDataset instance
|
||||
@@ -307,13 +494,16 @@ class DetectionDataset(BaseDataset):
|
||||
# ['dog', 'person']
|
||||
```
|
||||
"""
|
||||
classes, images, annotations = load_yolo_annotations(
|
||||
classes, image_paths, annotations = load_yolo_annotations(
|
||||
images_directory_path=images_directory_path,
|
||||
annotations_directory_path=annotations_directory_path,
|
||||
data_yaml_path=data_yaml_path,
|
||||
force_masks=force_masks,
|
||||
is_obb=is_obb,
|
||||
)
|
||||
return DetectionDataset(
|
||||
classes=classes, images=image_paths, annotations=annotations
|
||||
)
|
||||
return DetectionDataset(classes=classes, images=images, annotations=annotations)
|
||||
|
||||
def as_yolo(
|
||||
self,
|
||||
@@ -354,13 +544,12 @@ class DetectionDataset(BaseDataset):
|
||||
"""
|
||||
if images_directory_path is not None:
|
||||
save_dataset_images(
|
||||
images_directory_path=images_directory_path, images=self.images
|
||||
dataset=self, images_directory_path=images_directory_path
|
||||
)
|
||||
if annotations_directory_path is not None:
|
||||
save_yolo_annotations(
|
||||
dataset=self,
|
||||
annotations_directory_path=annotations_directory_path,
|
||||
images=self.images,
|
||||
annotations=self.annotations,
|
||||
min_image_area_percentage=min_image_area_percentage,
|
||||
max_image_area_percentage=max_image_area_percentage,
|
||||
approximation_percentage=approximation_percentage,
|
||||
@@ -382,7 +571,7 @@ class DetectionDataset(BaseDataset):
|
||||
images_directory_path (str): The path to the
|
||||
directory containing the images.
|
||||
annotations_path (str): The path to the json annotation files.
|
||||
force_masks (bool, optional): If True,
|
||||
force_masks (bool): If True,
|
||||
forces masks to be loaded for all annotations,
|
||||
regardless of whether they are present.
|
||||
|
||||
@@ -464,117 +653,151 @@ class DetectionDataset(BaseDataset):
|
||||
"""
|
||||
if images_directory_path is not None:
|
||||
save_dataset_images(
|
||||
images_directory_path=images_directory_path, images=self.images
|
||||
dataset=self, images_directory_path=images_directory_path
|
||||
)
|
||||
if annotations_path is not None:
|
||||
save_coco_annotations(
|
||||
dataset=self,
|
||||
annotation_path=annotations_path,
|
||||
images=self.images,
|
||||
annotations=self.annotations,
|
||||
classes=self.classes,
|
||||
min_image_area_percentage=min_image_area_percentage,
|
||||
max_image_area_percentage=max_image_area_percentage,
|
||||
approximation_percentage=approximation_percentage,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def merge(cls, dataset_list: List[DetectionDataset]) -> DetectionDataset:
|
||||
"""
|
||||
Merge a list of `DetectionDataset` objects into a single
|
||||
`DetectionDataset` object.
|
||||
|
||||
This method takes a list of `DetectionDataset` objects and combines
|
||||
their respective fields (`classes`, `images`,
|
||||
`annotations`) into a single `DetectionDataset` object.
|
||||
|
||||
Args:
|
||||
dataset_list (List[DetectionDataset]): A list of `DetectionDataset`
|
||||
objects to merge.
|
||||
|
||||
Returns:
|
||||
(DetectionDataset): A single `DetectionDataset` object containing
|
||||
the merged data from the input list.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
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_merged = sv.DetectionDataset.merge([ds_1, ds_2])
|
||||
len(ds_merged)
|
||||
# 300
|
||||
ds_merged.classes
|
||||
# ['cat', 'dog', 'person']
|
||||
```
|
||||
"""
|
||||
merged_images, merged_annotations = {}, {}
|
||||
class_lists = [dataset.classes for dataset in dataset_list]
|
||||
merged_classes = merge_class_lists(class_lists=class_lists)
|
||||
|
||||
for dataset in dataset_list:
|
||||
class_index_mapping = build_class_index_mapping(
|
||||
source_classes=dataset.classes, target_classes=merged_classes
|
||||
)
|
||||
for image_name, image, detections in dataset:
|
||||
if image_name in merged_annotations:
|
||||
raise ValueError(
|
||||
f"Image name {image_name} is not unique across datasets."
|
||||
)
|
||||
|
||||
merged_images[image_name] = image
|
||||
merged_annotations[image_name] = map_detections_class_id(
|
||||
source_to_target_mapping=class_index_mapping,
|
||||
detections=detections,
|
||||
)
|
||||
|
||||
return cls(
|
||||
classes=merged_classes, images=merged_images, annotations=merged_annotations
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClassificationDataset(BaseDataset):
|
||||
"""
|
||||
Dataclass containing information about a classification dataset.
|
||||
Contains information about a classification dataset, handles lazy image
|
||||
loading, dataset splitting.
|
||||
|
||||
Attributes:
|
||||
classes (List[str]): List containing dataset class names.
|
||||
images (Dict[str, np.ndarray]): Dictionary mapping image name to image.
|
||||
annotations (Dict[str, Detections]): Dictionary mapping
|
||||
images (Union[List[str], Dict[str, np.ndarray]]):
|
||||
List of image paths or dictionary mapping image name to image data.
|
||||
annotations (Dict[str, Classifications]): Dictionary mapping
|
||||
image name to annotations.
|
||||
"""
|
||||
|
||||
classes: List[str]
|
||||
images: Dict[str, np.ndarray]
|
||||
annotations: Dict[str, Classifications]
|
||||
def __init__(
|
||||
self,
|
||||
classes: List[str],
|
||||
images: Union[List[str], Dict[str, np.ndarray]],
|
||||
annotations: Dict[str, Classifications],
|
||||
) -> None:
|
||||
self.classes = classes
|
||||
|
||||
if set(images) != set(annotations):
|
||||
raise ValueError(
|
||||
"The keys of the images and annotations dictionaries must match."
|
||||
)
|
||||
self.annotations = annotations
|
||||
|
||||
# Eliminate duplicates while preserving order
|
||||
self.image_paths = list(dict.fromkeys(images))
|
||||
|
||||
self._images_in_memory: Dict[str, np.ndarray] = {}
|
||||
if isinstance(images, dict):
|
||||
self._images_in_memory = images
|
||||
warn_deprecated(
|
||||
"Passing a `Dict[str, np.ndarray]` into `ClassificationDataset` is "
|
||||
"deprecated and will be removed in a future release. Use "
|
||||
"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:
|
||||
return self._images_in_memory[image_path]
|
||||
return cv2.imread(image_path)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.images)
|
||||
return len(self._images_in_memory) or len(self.image_paths)
|
||||
|
||||
def __getitem__(self, i: int) -> Tuple[str, np.ndarray, Classifications]:
|
||||
"""
|
||||
Returns:
|
||||
Tuple[str, np.ndarray, Classifications]: The image path, image data,
|
||||
and its corresponding annotation at index i.
|
||||
"""
|
||||
image_path = self.image_paths[i]
|
||||
image = self._get_image(image_path)
|
||||
annotation = self.annotations[image_path]
|
||||
return image_path, image, annotation
|
||||
|
||||
def __iter__(self) -> Iterator[Tuple[str, np.ndarray, Classifications]]:
|
||||
"""
|
||||
Iterate over the images and annotations in the dataset.
|
||||
|
||||
Yields:
|
||||
Iterator[Tuple[str, np.ndarray, Detections]]:
|
||||
An iterator that yields tuples containing the image path,
|
||||
the image data, and its corresponding annotation.
|
||||
"""
|
||||
for i in range(len(self)):
|
||||
image_path, image, annotation = self[i]
|
||||
yield image_path, image, annotation
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if not isinstance(other, ClassificationDataset):
|
||||
return False
|
||||
|
||||
if set(self.classes) != set(other.classes):
|
||||
return False
|
||||
|
||||
if self.image_paths != other.image_paths:
|
||||
return False
|
||||
|
||||
if self._images_in_memory or other._images_in_memory:
|
||||
if not np.array_equal(
|
||||
list(self._images_in_memory.values()),
|
||||
list(other._images_in_memory.values()),
|
||||
):
|
||||
return False
|
||||
|
||||
if self.annotations != other.annotations:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def split(
|
||||
self, split_ratio=0.8, random_state=None, shuffle: bool = True
|
||||
self,
|
||||
split_ratio: float = 0.8,
|
||||
random_state: Optional[int] = None,
|
||||
shuffle: bool = True,
|
||||
) -> Tuple[ClassificationDataset, ClassificationDataset]:
|
||||
"""
|
||||
Splits the dataset into two parts (training and testing)
|
||||
using the provided split_ratio.
|
||||
|
||||
Args:
|
||||
split_ratio (float, optional): The ratio of the training
|
||||
split_ratio (float): The ratio of the training
|
||||
set to the entire dataset.
|
||||
random_state (int, optional): The seed for the
|
||||
random_state (Optional[int]): The seed for the
|
||||
random number generator. This is used for reproducibility.
|
||||
shuffle (bool, optional): Whether to shuffle the data before splitting.
|
||||
shuffle (bool): Whether to shuffle the data before splitting.
|
||||
|
||||
Returns:
|
||||
Tuple[ClassificationDataset, ClassificationDataset]: A tuple containing
|
||||
@@ -590,24 +813,35 @@ class ClassificationDataset(BaseDataset):
|
||||
# (700, 300)
|
||||
```
|
||||
"""
|
||||
image_names = list(self.images.keys())
|
||||
train_names, test_names = train_test_split(
|
||||
data=image_names,
|
||||
train_paths, test_paths = train_test_split(
|
||||
data=self.image_paths,
|
||||
train_ratio=split_ratio,
|
||||
random_state=random_state,
|
||||
shuffle=shuffle,
|
||||
)
|
||||
|
||||
train_input: Union[List[str], Dict[str, np.ndarray]]
|
||||
test_input: Union[List[str], Dict[str, np.ndarray]]
|
||||
if self._images_in_memory:
|
||||
train_input = {path: self._images_in_memory[path] for path in train_paths}
|
||||
test_input = {path: self._images_in_memory[path] for path in test_paths}
|
||||
else:
|
||||
train_input = train_paths
|
||||
test_input = test_paths
|
||||
train_annotations = {path: self.annotations[path] for path in train_paths}
|
||||
test_annotations = {path: self.annotations[path] for path in test_paths}
|
||||
|
||||
train_dataset = ClassificationDataset(
|
||||
classes=self.classes,
|
||||
images={name: self.images[name] for name in train_names},
|
||||
annotations={name: self.annotations[name] for name in train_names},
|
||||
images=train_input,
|
||||
annotations=train_annotations,
|
||||
)
|
||||
test_dataset = ClassificationDataset(
|
||||
classes=self.classes,
|
||||
images={name: self.images[name] for name in test_names},
|
||||
annotations={name: self.annotations[name] for name in test_names},
|
||||
images=test_input,
|
||||
annotations=test_annotations,
|
||||
)
|
||||
|
||||
return train_dataset, test_dataset
|
||||
|
||||
def as_folder_structure(self, root_directory_path: str) -> None:
|
||||
@@ -623,18 +857,16 @@ class ClassificationDataset(BaseDataset):
|
||||
for class_name in self.classes:
|
||||
os.makedirs(os.path.join(root_directory_path, class_name), exist_ok=True)
|
||||
|
||||
for image_path in self.images:
|
||||
classification = self.annotations[image_path]
|
||||
image = self.images[image_path]
|
||||
image_name = Path(image_path).name
|
||||
for image_save_path, image, annotation in self:
|
||||
image_name = Path(image_save_path).name
|
||||
class_id = (
|
||||
classification.class_id[0]
|
||||
if classification.confidence is None
|
||||
else classification.get_top_k(1)[0][0]
|
||||
annotation.class_id[0]
|
||||
if annotation.confidence is None
|
||||
else annotation.get_top_k(1)[0][0]
|
||||
)
|
||||
class_name = self.classes[class_id]
|
||||
image_path = os.path.join(root_directory_path, class_name, image_name)
|
||||
cv2.imwrite(image_path, image)
|
||||
image_save_path = os.path.join(root_directory_path, class_name, image_name)
|
||||
cv2.imwrite(image_save_path, image)
|
||||
|
||||
@classmethod
|
||||
def from_folder_structure(cls, root_directory_path: str) -> ClassificationDataset:
|
||||
@@ -667,7 +899,7 @@ class ClassificationDataset(BaseDataset):
|
||||
classes = os.listdir(root_directory_path)
|
||||
classes = sorted(set(classes))
|
||||
|
||||
images = {}
|
||||
image_paths = []
|
||||
annotations = {}
|
||||
|
||||
for class_name in classes:
|
||||
@@ -675,13 +907,13 @@ class ClassificationDataset(BaseDataset):
|
||||
|
||||
for image in os.listdir(os.path.join(root_directory_path, class_name)):
|
||||
image_path = str(os.path.join(root_directory_path, class_name, image))
|
||||
images[image_path] = cv2.imread(image_path)
|
||||
image_paths.append(image_path)
|
||||
annotations[image_path] = Classifications(
|
||||
class_id=np.array([class_id]),
|
||||
)
|
||||
|
||||
return cls(
|
||||
classes=classes,
|
||||
images=images,
|
||||
images=image_paths,
|
||||
annotations=annotations,
|
||||
)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
from typing import TYPE_CHECKING, Dict, List, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
@@ -21,6 +20,9 @@ from supervision.detection.utils import (
|
||||
)
|
||||
from supervision.utils.file import read_json_file, save_json_file
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from supervision.dataset.core import DetectionDataset
|
||||
|
||||
|
||||
def coco_categories_to_classes(coco_categories: List[dict]) -> List[str]:
|
||||
return [
|
||||
@@ -161,7 +163,7 @@ def load_coco_annotations(
|
||||
images_directory_path: str,
|
||||
annotations_path: str,
|
||||
force_masks: bool = False,
|
||||
) -> Tuple[List[str], Dict[str, np.ndarray], Dict[str, Detections]]:
|
||||
) -> Tuple[List[str], List[str], Dict[str, Detections]]:
|
||||
coco_data = read_json_file(file_path=annotations_path)
|
||||
classes = coco_categories_to_classes(coco_categories=coco_data["categories"])
|
||||
class_index_mapping = build_coco_class_index_mapping(
|
||||
@@ -172,7 +174,7 @@ def load_coco_annotations(
|
||||
coco_annotations=coco_data["annotations"]
|
||||
)
|
||||
|
||||
images = {}
|
||||
images = []
|
||||
annotations = {}
|
||||
|
||||
for coco_image in coco_images:
|
||||
@@ -184,7 +186,6 @@ def load_coco_annotations(
|
||||
image_annotations = coco_annotations_groups.get(coco_image["id"], [])
|
||||
image_path = os.path.join(images_directory_path, image_name)
|
||||
|
||||
image = cv2.imread(image_path)
|
||||
annotation = coco_annotations_to_detections(
|
||||
image_annotations=image_annotations,
|
||||
resolution_wh=(image_width, image_height),
|
||||
@@ -195,23 +196,20 @@ def load_coco_annotations(
|
||||
detections=annotation,
|
||||
)
|
||||
|
||||
images[image_path] = image
|
||||
images.append(image_path)
|
||||
annotations[image_path] = annotation
|
||||
|
||||
return classes, images, annotations
|
||||
|
||||
|
||||
def save_coco_annotations(
|
||||
dataset: "DetectionDataset",
|
||||
annotation_path: str,
|
||||
images: Dict[str, np.ndarray],
|
||||
annotations: Dict[str, Detections],
|
||||
classes: List[str],
|
||||
min_image_area_percentage: float = 0.0,
|
||||
max_image_area_percentage: float = 1.0,
|
||||
approximation_percentage: float = 0.75,
|
||||
) -> None:
|
||||
Path(annotation_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
info = {}
|
||||
licenses = [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -222,10 +220,10 @@ def save_coco_annotations(
|
||||
|
||||
coco_annotations = []
|
||||
coco_images = []
|
||||
coco_categories = classes_to_coco_categories(classes=classes)
|
||||
coco_categories = classes_to_coco_categories(classes=dataset.classes)
|
||||
|
||||
image_id, annotation_id = 1, 1
|
||||
for image_path, image in images.items():
|
||||
for image_path, image, annotation in dataset:
|
||||
image_height, image_width, _ = image.shape
|
||||
image_name = f"{Path(image_path).stem}{Path(image_path).suffix}"
|
||||
coco_image = {
|
||||
@@ -238,10 +236,8 @@ def save_coco_annotations(
|
||||
}
|
||||
|
||||
coco_images.append(coco_image)
|
||||
detections = annotations[image_path]
|
||||
|
||||
coco_annotation, annotation_id = detections_to_coco_annotations(
|
||||
detections=detections,
|
||||
detections=annotation,
|
||||
image_id=image_id,
|
||||
annotation_id=annotation_id,
|
||||
min_image_area_percentage=min_image_area_percentage,
|
||||
@@ -253,7 +249,7 @@ def save_coco_annotations(
|
||||
image_id += 1
|
||||
|
||||
annotation_dict = {
|
||||
"info": info,
|
||||
"info": {},
|
||||
"licenses": licenses,
|
||||
"categories": coco_categories,
|
||||
"images": coco_images,
|
||||
|
||||
@@ -5,7 +5,8 @@ from xml.etree.ElementTree import Element, SubElement
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from defusedxml.ElementTree import fromstring, parse, tostring
|
||||
from defusedxml.ElementTree import parse, tostring
|
||||
from defusedxml.minidom import parseString
|
||||
|
||||
from supervision.dataset.utils import approximate_mask_with_polygons
|
||||
from supervision.detection.core import Detections
|
||||
@@ -129,8 +130,7 @@ def detections_to_pascal_voc(
|
||||
annotation.append(next_object)
|
||||
|
||||
# Generate XML string
|
||||
xml_string = fromstring(tostring(annotation)).toprettyxml(indent=" ")
|
||||
|
||||
xml_string = parseString(tostring(annotation)).toprettyxml(indent=" ")
|
||||
return xml_string
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ def load_pascal_voc_annotations(
|
||||
images_directory_path: str,
|
||||
annotations_directory_path: str,
|
||||
force_masks: bool = False,
|
||||
) -> Tuple[List[str], Dict[str, np.ndarray], Dict[str, Detections]]:
|
||||
) -> Tuple[List[str], List[str], Dict[str, Detections]]:
|
||||
"""
|
||||
Loads PASCAL VOC XML annotations and returns the image name,
|
||||
a Detections instance, and a list of class names.
|
||||
@@ -147,48 +147,43 @@ def load_pascal_voc_annotations(
|
||||
images_directory_path (str): The path to the directory containing the images.
|
||||
annotations_directory_path (str): The path to the directory containing the
|
||||
PASCAL VOC annotation files.
|
||||
force_masks (bool, optional): If True, forces masks to be loaded for all
|
||||
force_masks (bool): If True, forces masks to be loaded for all
|
||||
annotations, regardless of whether they are present.
|
||||
|
||||
Returns:
|
||||
Tuple[List[str], Dict[str, np.ndarray], Dict[str, Detections]]: A tuple
|
||||
containing a list of class names,
|
||||
a dictionary with image names as keys and
|
||||
images as values, and a dictionary with image names as
|
||||
keys and corresponding Detections instances as values.
|
||||
Tuple[List[str], List[str], Dict[str, Detections]]: A tuple with a list
|
||||
of class names, a list of paths to images, and a dictionary with image
|
||||
paths as keys and corresponding Detections instances as values.
|
||||
"""
|
||||
|
||||
image_paths = list_files_with_extensions(
|
||||
directory=images_directory_path, extensions=["jpg", "jpeg", "png"]
|
||||
)
|
||||
image_paths = [
|
||||
str(path)
|
||||
for path in list_files_with_extensions(
|
||||
directory=images_directory_path, extensions=["jpg", "jpeg", "png"]
|
||||
)
|
||||
]
|
||||
|
||||
classes = []
|
||||
images = {}
|
||||
classes: List[str] = []
|
||||
annotations = {}
|
||||
|
||||
for image_path in image_paths:
|
||||
image_name = Path(image_path).stem
|
||||
image_path = str(image_path)
|
||||
image = cv2.imread(image_path)
|
||||
|
||||
annotation_path = os.path.join(annotations_directory_path, f"{image_name}.xml")
|
||||
image_stem = Path(image_path).stem
|
||||
annotation_path = os.path.join(annotations_directory_path, f"{image_stem}.xml")
|
||||
if not os.path.exists(annotation_path):
|
||||
images[image_path] = image
|
||||
annotations[image_path] = Detections.empty()
|
||||
continue
|
||||
|
||||
tree = parse(annotation_path)
|
||||
root = tree.getroot()
|
||||
|
||||
image = cv2.imread(image_path)
|
||||
resolution_wh = (image.shape[1], image.shape[0])
|
||||
annotation, classes = detections_from_xml_obj(
|
||||
root, classes, resolution_wh, force_masks
|
||||
)
|
||||
|
||||
images[image_path] = image
|
||||
annotations[image_path] = annotation
|
||||
|
||||
return classes, images, annotations
|
||||
return classes, image_paths, annotations
|
||||
|
||||
|
||||
def detections_from_xml_obj(
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from supervision.config import ORIENTED_BOX_COORDINATES
|
||||
from supervision.dataset.utils import approximate_mask_with_polygons
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.utils import polygon_to_mask, polygon_to_xyxy
|
||||
@@ -16,6 +17,9 @@ from supervision.utils.file import (
|
||||
save_yaml_file,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from supervision.dataset.core import DetectionDataset
|
||||
|
||||
|
||||
def _parse_box(values: List[str]) -> np.ndarray:
|
||||
x_center, y_center, width, height = values
|
||||
@@ -70,12 +74,15 @@ def _image_name_to_annotation_name(image_name: str) -> str:
|
||||
|
||||
|
||||
def yolo_annotations_to_detections(
|
||||
lines: List[str], resolution_wh: Tuple[int, int], with_masks: bool
|
||||
lines: List[str],
|
||||
resolution_wh: Tuple[int, int],
|
||||
with_masks: bool,
|
||||
is_obb: bool = False,
|
||||
) -> Detections:
|
||||
if len(lines) == 0:
|
||||
return Detections.empty()
|
||||
|
||||
class_id, relative_xyxy, relative_polygon = [], [], []
|
||||
class_id, relative_xyxy, relative_polygon, relative_xyxyxyxy = [], [], [], []
|
||||
w, h = resolution_wh
|
||||
for line in lines:
|
||||
values = line.split()
|
||||
@@ -88,21 +95,30 @@ def yolo_annotations_to_detections(
|
||||
elif len(values) > 5:
|
||||
polygon = _parse_polygon(values=values[1:])
|
||||
relative_xyxy.append(polygon_to_xyxy(polygon=polygon))
|
||||
if is_obb:
|
||||
relative_xyxyxyxy.append(np.array(values[1:]))
|
||||
if with_masks:
|
||||
relative_polygon.append(polygon)
|
||||
|
||||
class_id = np.array(class_id, dtype=int)
|
||||
relative_xyxy = np.array(relative_xyxy, dtype=np.float32)
|
||||
xyxy = relative_xyxy * np.array([w, h, w, h], dtype=np.float32)
|
||||
data = {}
|
||||
|
||||
if is_obb:
|
||||
relative_xyxyxyxy = np.array(relative_xyxyxyxy, dtype=np.float32)
|
||||
xyxyxyxy = relative_xyxyxyxy.reshape(-1, 4, 2)
|
||||
xyxyxyxy *= np.array([w, h], dtype=np.float32)
|
||||
data[ORIENTED_BOX_COORDINATES] = xyxyxyxy
|
||||
|
||||
if not with_masks:
|
||||
return Detections(class_id=class_id, xyxy=xyxy)
|
||||
return Detections(class_id=class_id, xyxy=xyxy, data=data)
|
||||
|
||||
polygons = [
|
||||
(polygon * np.array(resolution_wh)).astype(int) for polygon in relative_polygon
|
||||
]
|
||||
mask = _polygons_to_masks(polygons=polygons, resolution_wh=resolution_wh)
|
||||
return Detections(class_id=class_id, xyxy=xyxy, mask=mask)
|
||||
return Detections(class_id=class_id, xyxy=xyxy, data=data, mask=mask)
|
||||
|
||||
|
||||
def load_yolo_annotations(
|
||||
@@ -110,7 +126,8 @@ def load_yolo_annotations(
|
||||
annotations_directory_path: str,
|
||||
data_yaml_path: str,
|
||||
force_masks: bool = False,
|
||||
) -> Tuple[List[str], Dict[str, np.ndarray], Dict[str, Detections]]:
|
||||
is_obb: bool = False,
|
||||
) -> Tuple[List[str], List[str], Dict[str, Detections]]:
|
||||
"""
|
||||
Loads YOLO annotations and returns class names, images,
|
||||
and their corresponding detections.
|
||||
@@ -121,34 +138,36 @@ def load_yolo_annotations(
|
||||
containing the YOLO annotation files.
|
||||
data_yaml_path (str): The path to the data
|
||||
YAML file containing class information.
|
||||
force_masks (bool, optional): If True, forces masks to be loaded
|
||||
force_masks (bool): If True, forces masks to be loaded
|
||||
for all annotations, regardless of whether they are present.
|
||||
is_obb (bool): If True, loads the annotations in OBB format.
|
||||
OBB annotations are defined as `[class_id, x, y, x, y, x, y, x, y]`,
|
||||
where pairs of [x, y] are box corners.
|
||||
|
||||
Returns:
|
||||
Tuple[List[str], Dict[str, np.ndarray], Dict[str, Detections]]:
|
||||
Tuple[List[str], List[str], Dict[str, Detections]]:
|
||||
A tuple containing a list of class names, a dictionary with
|
||||
image names as keys and images as values, and a dictionary
|
||||
with image names as keys and corresponding Detections instances as values.
|
||||
"""
|
||||
image_paths = list_files_with_extensions(
|
||||
directory=images_directory_path, extensions=["jpg", "jpeg", "png"]
|
||||
)
|
||||
image_paths = [
|
||||
str(path)
|
||||
for path in list_files_with_extensions(
|
||||
directory=images_directory_path, extensions=["jpg", "jpeg", "png"]
|
||||
)
|
||||
]
|
||||
|
||||
classes = _extract_class_names(file_path=data_yaml_path)
|
||||
images = {}
|
||||
annotations = {}
|
||||
|
||||
for image_path in image_paths:
|
||||
image_stem = Path(image_path).stem
|
||||
image_path = str(image_path)
|
||||
image = cv2.imread(image_path)
|
||||
|
||||
annotation_path = os.path.join(annotations_directory_path, f"{image_stem}.txt")
|
||||
if not os.path.exists(annotation_path):
|
||||
images[image_path] = image
|
||||
annotations[image_path] = Detections.empty()
|
||||
continue
|
||||
|
||||
image = cv2.imread(image_path)
|
||||
lines = read_txt_file(file_path=annotation_path, skip_empty=True)
|
||||
h, w, _ = image.shape
|
||||
resolution_wh = (w, h)
|
||||
@@ -156,12 +175,13 @@ def load_yolo_annotations(
|
||||
with_masks = _with_mask(lines=lines)
|
||||
with_masks = force_masks if force_masks else with_masks
|
||||
annotation = yolo_annotations_to_detections(
|
||||
lines=lines, resolution_wh=resolution_wh, with_masks=with_masks
|
||||
lines=lines,
|
||||
resolution_wh=resolution_wh,
|
||||
with_masks=with_masks,
|
||||
is_obb=is_obb,
|
||||
)
|
||||
|
||||
images[image_path] = image
|
||||
annotations[image_path] = annotation
|
||||
return classes, images, annotations
|
||||
return classes, image_paths, annotations
|
||||
|
||||
|
||||
def object_to_yolo(
|
||||
@@ -195,6 +215,9 @@ def detections_to_yolo_annotations(
|
||||
) -> List[str]:
|
||||
annotation = []
|
||||
for xyxy, mask, _, class_id, _, _ in detections:
|
||||
if class_id is None:
|
||||
raise ValueError("Class ID is required for YOLO annotations.")
|
||||
|
||||
if mask is not None:
|
||||
polygons = approximate_mask_with_polygons(
|
||||
mask=mask,
|
||||
@@ -220,24 +243,22 @@ def detections_to_yolo_annotations(
|
||||
|
||||
|
||||
def save_yolo_annotations(
|
||||
dataset: "DetectionDataset",
|
||||
annotations_directory_path: str,
|
||||
images: Dict[str, np.ndarray],
|
||||
annotations: Dict[str, Detections],
|
||||
min_image_area_percentage: float = 0.0,
|
||||
max_image_area_percentage: float = 1.0,
|
||||
approximation_percentage: float = 0.75,
|
||||
) -> None:
|
||||
Path(annotations_directory_path).mkdir(parents=True, exist_ok=True)
|
||||
for image_path, image in images.items():
|
||||
detections = annotations[image_path]
|
||||
for image_path, image, annotation in dataset:
|
||||
image_name = Path(image_path).name
|
||||
yolo_annotations_name = _image_name_to_annotation_name(image_name=image_name)
|
||||
yolo_annotations_path = os.path.join(
|
||||
annotations_directory_path, yolo_annotations_name
|
||||
)
|
||||
lines = detections_to_yolo_annotations(
|
||||
detections=detections,
|
||||
image_shape=image.shape,
|
||||
detections=annotation,
|
||||
image_shape=image.shape, # type: ignore
|
||||
min_image_area_percentage=min_image_area_percentage,
|
||||
max_image_area_percentage=max_image_area_percentage,
|
||||
approximation_percentage=approximation_percentage,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import copy
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple, TypeVar, Union
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, TypeVar, Union
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
@@ -15,6 +16,9 @@ from supervision.detection.utils import (
|
||||
mask_to_polygons,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from supervision.dataset.core import DetectionDataset
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@@ -59,6 +63,7 @@ def merge_class_lists(class_lists: List[List[str]]) -> List[str]:
|
||||
def build_class_index_mapping(
|
||||
source_classes: List[str], target_classes: List[str]
|
||||
) -> Dict[int, int]:
|
||||
"""Returns the index map of source classes -> target classes."""
|
||||
index_mapping = {}
|
||||
|
||||
for i, class_name in enumerate(source_classes):
|
||||
@@ -94,14 +99,16 @@ def map_detections_class_id(
|
||||
|
||||
|
||||
def save_dataset_images(
|
||||
images_directory_path: str, images: Dict[str, np.ndarray]
|
||||
dataset: "DetectionDataset", images_directory_path: str
|
||||
) -> None:
|
||||
Path(images_directory_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for image_path, image in images.items():
|
||||
image_name = Path(image_path).name
|
||||
target_image_path = os.path.join(images_directory_path, image_name)
|
||||
cv2.imwrite(target_image_path, image)
|
||||
for image_path in dataset.image_paths:
|
||||
final_path = os.path.join(images_directory_path, Path(image_path).name)
|
||||
if image_path in dataset._images_in_memory:
|
||||
image = dataset._images_in_memory[image_path]
|
||||
cv2.imwrite(final_path, image)
|
||||
else:
|
||||
shutil.copyfile(image_path, final_path)
|
||||
|
||||
|
||||
def train_test_split(
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import cv2
|
||||
|
||||
from supervision.annotators.base import ImageType
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.draw.color import Color, ColorPalette
|
||||
from supervision.utils.conversion import convert_for_annotation_method
|
||||
from supervision.utils.internal import deprecated
|
||||
|
||||
|
||||
class BoxAnnotator:
|
||||
"""
|
||||
A class for drawing bounding boxes on an image using detections provided.
|
||||
|
||||
Attributes:
|
||||
color (Union[Color, ColorPalette]): The color to draw the bounding box,
|
||||
can be a single color or a color palette
|
||||
thickness (int): The thickness of the bounding box lines, default is 2
|
||||
text_color (Color): The color of the text on the bounding box, default is white
|
||||
text_scale (float): The scale of the text on the bounding box, default is 0.5
|
||||
text_thickness (int): The thickness of the text on the bounding box,
|
||||
default is 1
|
||||
text_padding (int): The padding around the text on the bounding box,
|
||||
default is 5
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
color: Union[Color, ColorPalette] = ColorPalette.DEFAULT,
|
||||
thickness: int = 2,
|
||||
text_color: Color = Color.BLACK,
|
||||
text_scale: float = 0.5,
|
||||
text_thickness: int = 1,
|
||||
text_padding: int = 10,
|
||||
):
|
||||
self.color: Union[Color, ColorPalette] = color
|
||||
self.thickness: int = thickness
|
||||
self.text_color: Color = text_color
|
||||
self.text_scale: float = text_scale
|
||||
self.text_thickness: int = text_thickness
|
||||
self.text_padding: int = text_padding
|
||||
|
||||
@deprecated(
|
||||
"`BoxAnnotator` is deprecated and will be removed in "
|
||||
"`supervision-0.22.0`. Use `BoundingBoxAnnotator` and `LabelAnnotator` instead"
|
||||
)
|
||||
@convert_for_annotation_method
|
||||
def annotate(
|
||||
self,
|
||||
scene: ImageType,
|
||||
detections: Detections,
|
||||
labels: Optional[List[str]] = None,
|
||||
skip_label: bool = False,
|
||||
) -> ImageType:
|
||||
"""
|
||||
Draws bounding boxes on the frame using the detections provided.
|
||||
|
||||
Args:
|
||||
scene (ImageType): The image on which the bounding boxes will be drawn.
|
||||
`ImageType` is a flexible type, accepting either `numpy.ndarray`
|
||||
or `PIL.Image.Image`.
|
||||
detections (Detections): The detections for which the
|
||||
bounding boxes will be drawn
|
||||
labels (Optional[List[str]]): An optional list of labels
|
||||
corresponding to each detection. If `labels` are not provided,
|
||||
corresponding `class_id` will be used as label.
|
||||
skip_label (bool): Is set to `True`, skips bounding box label annotation.
|
||||
Returns:
|
||||
ImageType: The image with the bounding boxes drawn on it, matching the
|
||||
type of `scene` (`numpy.ndarray` or `PIL.Image.Image`)
|
||||
|
||||
Example:
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
classes = ['person', ...]
|
||||
image = ...
|
||||
detections = sv.Detections(...)
|
||||
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
labels = [
|
||||
f"{classes[class_id]} {confidence:0.2f}"
|
||||
for _, _, confidence, class_id, _ in detections
|
||||
]
|
||||
annotated_frame = box_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections,
|
||||
labels=labels
|
||||
)
|
||||
```
|
||||
"""
|
||||
font = cv2.FONT_HERSHEY_SIMPLEX
|
||||
for i in range(len(detections)):
|
||||
x1, y1, x2, y2 = detections.xyxy[i].astype(int)
|
||||
class_id = (
|
||||
detections.class_id[i] if detections.class_id is not None else None
|
||||
)
|
||||
idx = class_id if class_id is not None else i
|
||||
color = (
|
||||
self.color.by_idx(idx)
|
||||
if isinstance(self.color, ColorPalette)
|
||||
else self.color
|
||||
)
|
||||
cv2.rectangle(
|
||||
img=scene,
|
||||
pt1=(x1, y1),
|
||||
pt2=(x2, y2),
|
||||
color=color.as_bgr(),
|
||||
thickness=self.thickness,
|
||||
)
|
||||
if skip_label:
|
||||
continue
|
||||
|
||||
text = (
|
||||
f"{class_id}"
|
||||
if (labels is None or len(detections) != len(labels))
|
||||
else labels[i]
|
||||
)
|
||||
|
||||
text_width, text_height = cv2.getTextSize(
|
||||
text=text,
|
||||
fontFace=font,
|
||||
fontScale=self.text_scale,
|
||||
thickness=self.text_thickness,
|
||||
)[0]
|
||||
|
||||
text_x = x1 + self.text_padding
|
||||
text_y = y1 - self.text_padding
|
||||
|
||||
text_background_x1 = x1
|
||||
text_background_y1 = y1 - 2 * self.text_padding - text_height
|
||||
|
||||
text_background_x2 = x1 + 2 * self.text_padding + text_width
|
||||
text_background_y2 = y1
|
||||
|
||||
cv2.rectangle(
|
||||
img=scene,
|
||||
pt1=(text_background_x1, text_background_y1),
|
||||
pt2=(text_background_x2, text_background_y2),
|
||||
color=color.as_bgr(),
|
||||
thickness=cv2.FILLED,
|
||||
)
|
||||
cv2.putText(
|
||||
img=scene,
|
||||
text=text,
|
||||
org=(text_x, text_y),
|
||||
fontFace=font,
|
||||
fontScale=self.text_scale,
|
||||
color=self.text_color.as_rgb(),
|
||||
thickness=self.text_thickness,
|
||||
lineType=cv2.LINE_AA,
|
||||
)
|
||||
return scene
|
||||
@@ -7,12 +7,22 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
|
||||
import numpy as np
|
||||
|
||||
from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES
|
||||
from supervision.detection.lmm import LMM, from_paligemma, validate_lmm_and_kwargs
|
||||
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,
|
||||
mask_non_max_suppression,
|
||||
)
|
||||
from supervision.detection.tools.transformers import (
|
||||
process_transformers_detection_result,
|
||||
process_transformers_v4_segmentation_result,
|
||||
process_transformers_v5_segmentation_result,
|
||||
)
|
||||
from supervision.detection.utils import (
|
||||
box_iou_batch,
|
||||
calculate_masks_centroids,
|
||||
@@ -25,7 +35,7 @@ from supervision.detection.utils import (
|
||||
xywh_to_xyxy,
|
||||
)
|
||||
from supervision.geometry.core import Position
|
||||
from supervision.utils.internal import deprecated, get_instance_variables
|
||||
from supervision.utils.internal import get_instance_variables
|
||||
from supervision.validators import validate_detections_fields
|
||||
|
||||
|
||||
@@ -239,10 +249,6 @@ class Detections:
|
||||
results = model(image)[0]
|
||||
detections = sv.Detections.from_ultralytics(results)
|
||||
```
|
||||
|
||||
!!! tip
|
||||
|
||||
Class names values can be accessed using `detections["class_name"]`.
|
||||
""" # noqa: E501 // docs
|
||||
|
||||
if hasattr(ultralytics_results, "obb") and ultralytics_results.obb is not None:
|
||||
@@ -262,6 +268,14 @@ class Detections:
|
||||
},
|
||||
)
|
||||
|
||||
if hasattr(ultralytics_results, "boxes") and ultralytics_results.boxes is None:
|
||||
masks = extract_ultralytics_masks(ultralytics_results)
|
||||
return cls(
|
||||
xyxy=mask_to_xyxy(masks),
|
||||
mask=masks,
|
||||
class_id=np.arange(len(ultralytics_results)),
|
||||
)
|
||||
|
||||
class_id = ultralytics_results.boxes.cls.cpu().numpy().astype(int)
|
||||
class_names = np.array([ultralytics_results.names[i] for i in class_id])
|
||||
return cls(
|
||||
@@ -433,15 +447,19 @@ class Detections:
|
||||
cls, transformers_results: dict, id2label: Optional[Dict[int, str]] = None
|
||||
) -> Detections:
|
||||
"""
|
||||
Creates a Detections instance from object detection or segmentation
|
||||
Creates a Detections instance from object detection or panoptic, semantic
|
||||
and instance segmentation
|
||||
[Transformer](https://github.com/huggingface/transformers) inference result.
|
||||
|
||||
Args:
|
||||
transformers_results (dict): The output of Transformers model inference. A
|
||||
dictionary containing the `scores`, `labels`, `boxes` and `masks` keys.
|
||||
transformers_results (Union[dict, torch.Tensor]): Inference results from
|
||||
your Transformers model. This can be either a dictionary containing
|
||||
valuable outputs like `scores`, `labels`, `boxes`, `masks`,
|
||||
`segments_info`, and `segmentation`, or a `torch.Tensor` holding a
|
||||
segmentation map where values represent class IDs.
|
||||
id2label (Optional[Dict[int, str]]): A dictionary mapping class IDs to
|
||||
class names. If provided, the resulting Detections object will contain
|
||||
`class_name` data field with the class names.
|
||||
labels, typically part of the `transformers` model configuration. If
|
||||
provided, the resulting dictionary will include class names.
|
||||
|
||||
Returns:
|
||||
Detections: A new Detections object.
|
||||
@@ -472,36 +490,28 @@ class Detections:
|
||||
id2label=model.config.id2label
|
||||
)
|
||||
```
|
||||
|
||||
!!! tip
|
||||
|
||||
Class names values can be accessed using `detections["class_name"]`.
|
||||
""" # noqa: E501 // docs
|
||||
|
||||
class_ids = transformers_results["labels"].cpu().detach().numpy().astype(int)
|
||||
data = {}
|
||||
if id2label is not None:
|
||||
class_names = np.array([id2label[class_id] for class_id in class_ids])
|
||||
data[CLASS_NAME_DATA_FIELD] = class_names
|
||||
if (
|
||||
transformers_results.__class__.__name__ == "Tensor"
|
||||
or "segmentation" in transformers_results
|
||||
):
|
||||
return cls(
|
||||
**process_transformers_v5_segmentation_result(
|
||||
transformers_results, id2label
|
||||
)
|
||||
)
|
||||
|
||||
if "masks" in transformers_results or "png_string" in transformers_results:
|
||||
return cls(
|
||||
**process_transformers_v4_segmentation_result(
|
||||
transformers_results, id2label
|
||||
)
|
||||
)
|
||||
|
||||
if "boxes" in transformers_results:
|
||||
return cls(
|
||||
xyxy=transformers_results["boxes"].cpu().detach().numpy(),
|
||||
confidence=transformers_results["scores"].cpu().detach().numpy(),
|
||||
class_id=class_ids,
|
||||
data=data,
|
||||
)
|
||||
elif "masks" in transformers_results:
|
||||
masks = transformers_results["masks"].cpu().detach().numpy().astype(bool)
|
||||
return cls(
|
||||
xyxy=mask_to_xyxy(masks),
|
||||
mask=masks,
|
||||
confidence=transformers_results["scores"].cpu().detach().numpy(),
|
||||
class_id=class_ids,
|
||||
data=data,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Only object detection and semantic segmentation results are supported."
|
||||
**process_transformers_detection_result(transformers_results, id2label)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -540,6 +550,9 @@ class Detections:
|
||||
return cls(
|
||||
xyxy=detectron2_results["instances"].pred_boxes.tensor.cpu().numpy(),
|
||||
confidence=detectron2_results["instances"].scores.cpu().numpy(),
|
||||
mask=detectron2_results["instances"].pred_masks.cpu().numpy()
|
||||
if hasattr(detectron2_results["instances"], "pred_masks")
|
||||
else None,
|
||||
class_id=detectron2_results["instances"]
|
||||
.pred_classes.cpu()
|
||||
.numpy()
|
||||
@@ -575,10 +588,6 @@ class Detections:
|
||||
result = model.infer(image)[0]
|
||||
detections = sv.Detections.from_inference(result)
|
||||
```
|
||||
|
||||
!!! tip
|
||||
|
||||
Class names values can be accessed using `detections["class_name"]`.
|
||||
"""
|
||||
with suppress(AttributeError):
|
||||
roboflow_result = roboflow_result.dict(exclude_none=True, by_alias=True)
|
||||
@@ -600,45 +609,6 @@ class Detections:
|
||||
data=data,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@deprecated(
|
||||
"`Detections.from_roboflow` is deprecated and will be removed in "
|
||||
"`supervision-0.22.0`. Use `Detections.from_inference` instead."
|
||||
)
|
||||
def from_roboflow(cls, roboflow_result: Union[dict, Any]) -> Detections:
|
||||
"""
|
||||
!!! failure "Deprecated"
|
||||
|
||||
`Detections.from_roboflow` is deprecated and will be removed in
|
||||
`supervision-0.22.0`. Use `Detections.from_inference` instead.
|
||||
|
||||
Create a Detections object from the [Roboflow](https://roboflow.com/)
|
||||
API inference result or the [Inference](https://inference.roboflow.com/)
|
||||
package results.
|
||||
|
||||
Args:
|
||||
roboflow_result (dict): The result from the
|
||||
Roboflow API containing predictions.
|
||||
|
||||
Returns:
|
||||
(Detections): A Detections object containing the bounding boxes, class IDs,
|
||||
and confidences of the predictions.
|
||||
|
||||
Example:
|
||||
```python
|
||||
import cv2
|
||||
import supervision as sv
|
||||
from inference import get_model
|
||||
|
||||
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
||||
model = get_model(model_id="yolov8s-640")
|
||||
|
||||
result = model.infer(image)[0]
|
||||
detections = sv.Detections.from_roboflow(result)
|
||||
```
|
||||
"""
|
||||
return cls.from_inference(roboflow_result)
|
||||
|
||||
@classmethod
|
||||
def from_sam(cls, sam_result: List[dict]) -> Detections:
|
||||
"""
|
||||
@@ -678,7 +648,7 @@ class Detections:
|
||||
if np.asarray(xywh).shape[0] == 0:
|
||||
return cls.empty()
|
||||
|
||||
xyxy = xywh_to_xyxy(boxes_xywh=xywh)
|
||||
xyxy = xywh_to_xyxy(xywh=xywh)
|
||||
return cls(xyxy=xyxy, mask=mask)
|
||||
|
||||
@classmethod
|
||||
@@ -811,7 +781,9 @@ class Detections:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_lmm(cls, lmm: Union[LMM, str], result: str, **kwargs) -> Detections:
|
||||
def from_lmm(
|
||||
cls, lmm: Union[LMM, str], result: Union[str, dict], **kwargs
|
||||
) -> Detections:
|
||||
"""
|
||||
Creates a Detections object from the given result string based on the specified
|
||||
Large Multimodal Model (LMM).
|
||||
@@ -847,13 +819,28 @@ class Detections:
|
||||
# array([0])
|
||||
```
|
||||
"""
|
||||
lmm = validate_lmm_and_kwargs(lmm, kwargs)
|
||||
lmm = validate_lmm_parameters(lmm, result, kwargs)
|
||||
|
||||
if lmm == LMM.PALIGEMMA:
|
||||
assert isinstance(result, str)
|
||||
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)
|
||||
xyxy, labels, mask, xyxyxyxy = from_florence_2(result, **kwargs)
|
||||
if len(xyxy) == 0:
|
||||
return cls.empty()
|
||||
|
||||
data = {}
|
||||
if labels is not None:
|
||||
data[CLASS_NAME_DATA_FIELD] = labels
|
||||
if xyxyxyxy is not None:
|
||||
data[ORIENTED_BOX_COORDINATES] = xyxyxyxy
|
||||
|
||||
return cls(xyxy=xyxy, mask=mask, data=data)
|
||||
|
||||
raise ValueError(f"Unsupported LMM: {lmm}")
|
||||
|
||||
@classmethod
|
||||
@@ -1171,10 +1158,10 @@ class Detections:
|
||||
from a segmentation model, the IoU mask is applied. Otherwise, box IoU is used.
|
||||
|
||||
Args:
|
||||
threshold (float, optional): The intersection-over-union threshold
|
||||
threshold (float): The intersection-over-union threshold
|
||||
to use for non-maximum suppression. I'm the lower the value the more
|
||||
restrictive the NMS becomes. Defaults to 0.5.
|
||||
class_agnostic (bool, optional): Whether to perform class-agnostic
|
||||
class_agnostic (bool): Whether to perform class-agnostic
|
||||
non-maximum suppression. If True, the class_id of each detection
|
||||
will be ignored. Defaults to False.
|
||||
|
||||
@@ -1226,9 +1213,9 @@ class Detections:
|
||||
Perform non-maximum merging on the current set of object detections.
|
||||
|
||||
Args:
|
||||
threshold (float, optional): The intersection-over-union threshold
|
||||
threshold (float): The intersection-over-union threshold
|
||||
to use for non-maximum merging. Defaults to 0.5.
|
||||
class_agnostic (bool, optional): Whether to perform class-agnostic
|
||||
class_agnostic (bool): Whether to perform class-agnostic
|
||||
non-maximum merging. If True, the class_id of each detection
|
||||
will be ignored. Defaults to False.
|
||||
|
||||
|
||||
@@ -4,17 +4,43 @@ 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"
|
||||
|
||||
|
||||
REQUIRED_ARGUMENTS: Dict[LMM, List[str]] = {LMM.PALIGEMMA: ["resolution_wh"]}
|
||||
RESULT_TYPES: Dict[LMM, type] = {LMM.PALIGEMMA: str, LMM.FLORENCE_2: dict}
|
||||
|
||||
ALLOWED_ARGUMENTS: Dict[LMM, List[str]] = {LMM.PALIGEMMA: ["resolution_wh", "classes"]}
|
||||
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_and_kwargs(lmm: Union[LMM, str], kwargs: Dict[str, Any]) -> LMM:
|
||||
def validate_lmm_parameters(
|
||||
lmm: Union[LMM, str], result: Any, kwargs: Dict[str, Any]
|
||||
) -> LMM:
|
||||
if isinstance(lmm, str):
|
||||
try:
|
||||
lmm = LMM(lmm.lower())
|
||||
@@ -23,6 +49,11 @@ def validate_lmm_and_kwargs(lmm: Union[LMM, str], kwargs: Dict[str, Any]) -> LMM
|
||||
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:
|
||||
@@ -57,3 +88,97 @@ def from_paligemma(
|
||||
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 = list(result.keys())[0]
|
||||
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}"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import List, Union
|
||||
|
||||
@@ -53,9 +55,9 @@ def mask_non_max_suppression(
|
||||
masks (np.ndarray): 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.
|
||||
iou_threshold (float, optional): The intersection-over-union threshold
|
||||
iou_threshold (float): The intersection-over-union threshold
|
||||
to use for non-maximum suppression.
|
||||
mask_dimension (int, optional): The dimension to which the masks should be
|
||||
mask_dimension (int): The dimension to which the masks should be
|
||||
resized before computing IOU values. Defaults to 640.
|
||||
|
||||
Returns:
|
||||
@@ -101,7 +103,7 @@ def box_non_max_suppression(
|
||||
predictions (np.ndarray): An array of object detection predictions in
|
||||
the format of `(x_min, y_min, x_max, y_max, score)`
|
||||
or `(x_min, y_min, x_max, y_max, score, class)`.
|
||||
iou_threshold (float, optional): The intersection-over-union threshold
|
||||
iou_threshold (float): The intersection-over-union threshold
|
||||
to use for non-maximum suppression.
|
||||
|
||||
Returns:
|
||||
@@ -156,7 +158,7 @@ def group_overlapping_boxes(
|
||||
predictions (npt.NDArray[np.float64]): An array of shape `(n, 5)` containing
|
||||
the bounding boxes coordinates in format `[x1, y1, x2, y2]`
|
||||
and the confidence scores.
|
||||
iou_threshold (float, optional): The intersection-over-union threshold
|
||||
iou_threshold (float): The intersection-over-union threshold
|
||||
to use for non-maximum suppression. Defaults to 0.5.
|
||||
|
||||
Returns:
|
||||
@@ -200,7 +202,7 @@ def box_non_max_merge(
|
||||
containing the bounding boxes coordinates in format `[x1, y1, x2, y2]`,
|
||||
the confidence scores and class_ids. Omit class_id column to allow
|
||||
detections of different classes to be merged.
|
||||
iou_threshold (float, optional): The intersection-over-union threshold
|
||||
iou_threshold (float): The intersection-over-union threshold
|
||||
to use for non-maximum suppression. Defaults to 0.5.
|
||||
|
||||
Returns:
|
||||
@@ -248,16 +250,21 @@ class OverlapFilter(Enum):
|
||||
NON_MAX_SUPPRESSION = "non_max_suppression"
|
||||
NON_MAX_MERGE = "non_max_merge"
|
||||
|
||||
@classmethod
|
||||
def list(cls):
|
||||
return list(map(lambda c: c.value, cls))
|
||||
|
||||
def validate_overlap_filter(
|
||||
strategy: Union[OverlapFilter, str],
|
||||
) -> OverlapFilter:
|
||||
if isinstance(strategy, str):
|
||||
try:
|
||||
strategy = OverlapFilter(strategy.lower())
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"Invalid strategy value: {strategy}. Must be one of "
|
||||
f"{[e.value for e in OverlapFilter]}"
|
||||
)
|
||||
return strategy
|
||||
@classmethod
|
||||
def from_value(cls, value: Union[OverlapFilter, str]) -> OverlapFilter:
|
||||
if isinstance(value, cls):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
value = value.lower()
|
||||
try:
|
||||
return cls(value)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid value: {value}. Must be one of {cls.list()}")
|
||||
raise ValueError(
|
||||
f"Invalid value type: {type(value)}. Must be an instance of "
|
||||
f"{cls.__name__} or str."
|
||||
)
|
||||
|
||||
@@ -4,11 +4,16 @@ from typing import Callable, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from supervision.config import ORIENTED_BOX_COORDINATES
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.overlap_filter import OverlapFilter, validate_overlap_filter
|
||||
from supervision.detection.utils import move_boxes, move_masks
|
||||
from supervision.detection.overlap_filter import OverlapFilter
|
||||
from supervision.detection.utils import move_boxes, move_masks, move_oriented_boxes
|
||||
from supervision.utils.image import crop_image
|
||||
from supervision.utils.internal import SupervisionWarnings
|
||||
from supervision.utils.internal import (
|
||||
SupervisionWarnings,
|
||||
deprecated_parameter,
|
||||
warn_deprecated,
|
||||
)
|
||||
|
||||
|
||||
def move_detections(
|
||||
@@ -28,6 +33,10 @@ def move_detections(
|
||||
(sv.Detections) repositioned Detections object.
|
||||
"""
|
||||
detections.xyxy = move_boxes(xyxy=detections.xyxy, offset=offset)
|
||||
if ORIENTED_BOX_COORDINATES in detections.data:
|
||||
detections.data[ORIENTED_BOX_COORDINATES] = move_oriented_boxes(
|
||||
xyxyxyxy=detections.data[ORIENTED_BOX_COORDINATES], offset=offset
|
||||
)
|
||||
if detections.mask is not None:
|
||||
if resolution_wh is None:
|
||||
raise ValueError(
|
||||
@@ -49,11 +58,16 @@ class InferenceSlicer:
|
||||
slice, and then merging the detections.
|
||||
|
||||
Args:
|
||||
slice_wh (Tuple[int, int]): Dimensions of each slice in the format
|
||||
`(width, height)`.
|
||||
overlap_ratio_wh (Tuple[float, float]): Overlap ratio between consecutive
|
||||
slices in the format `(width_ratio, height_ratio)`.
|
||||
overlap_filter_strategy (Union[OverlapFilter, str]): Strategy for
|
||||
slice_wh (Tuple[int, int]): Dimensions of each slice measured in pixels. The
|
||||
tuple should be in the format `(width, height)`.
|
||||
overlap_ratio_wh (Optional[Tuple[float, float]]): A tuple representing the
|
||||
desired overlap ratio for width and height between consecutive slices.
|
||||
Each value should be in the range [0, 1), where 0 means no overlap and
|
||||
a value close to 1 means high overlap.
|
||||
overlap_wh (Optional[Tuple[int, int]]): A tuple representing the desired
|
||||
overlap for width and height between consecutive slices measured in pixels.
|
||||
Each value should be greater than or equal to 0.
|
||||
overlap_filter (Union[OverlapFilter, str]): Strategy for
|
||||
filtering or merging overlapping detections in slices.
|
||||
iou_threshold (float): Intersection over Union (IoU) threshold
|
||||
used when filtering by overlap.
|
||||
@@ -68,23 +82,37 @@ class InferenceSlicer:
|
||||
not a multiple of the slice's width or height minus the overlap.
|
||||
"""
|
||||
|
||||
@deprecated_parameter(
|
||||
old_parameter="overlap_filter_strategy",
|
||||
new_parameter="overlap_filter",
|
||||
map_function=lambda x: x,
|
||||
warning_message="`{old_parameter}` in `{function_name}` is deprecated and will "
|
||||
"be removed in `supervision-0.27.0`. Use '{new_parameter}' "
|
||||
"instead.",
|
||||
)
|
||||
def __init__(
|
||||
self,
|
||||
callback: Callable[[np.ndarray], Detections],
|
||||
slice_wh: Tuple[int, int] = (320, 320),
|
||||
overlap_ratio_wh: Tuple[float, float] = (0.2, 0.2),
|
||||
overlap_filter_strategy: Union[
|
||||
OverlapFilter, str
|
||||
] = OverlapFilter.NON_MAX_SUPPRESSION,
|
||||
overlap_ratio_wh: Optional[Tuple[float, float]] = (0.2, 0.2),
|
||||
overlap_wh: Optional[Tuple[int, int]] = None,
|
||||
overlap_filter: Union[OverlapFilter, str] = OverlapFilter.NON_MAX_SUPPRESSION,
|
||||
iou_threshold: float = 0.5,
|
||||
thread_workers: int = 1,
|
||||
):
|
||||
overlap_filter_strategy = validate_overlap_filter(overlap_filter_strategy)
|
||||
if overlap_ratio_wh is not None:
|
||||
warn_deprecated(
|
||||
"`overlap_ratio_wh` in `InferenceSlicer.__init__` is deprecated and "
|
||||
"will be removed in `supervision-0.27.0`. Use `overlap_wh` instead."
|
||||
)
|
||||
|
||||
self._validate_overlap(overlap_ratio_wh, overlap_wh)
|
||||
self.overlap_ratio_wh = overlap_ratio_wh
|
||||
self.overlap_wh = overlap_wh
|
||||
|
||||
self.slice_wh = slice_wh
|
||||
self.overlap_ratio_wh = overlap_ratio_wh
|
||||
self.iou_threshold = iou_threshold
|
||||
self.overlap_filter_strategy = overlap_filter_strategy
|
||||
self.overlap_filter = OverlapFilter.from_value(overlap_filter)
|
||||
self.callback = callback
|
||||
self.thread_workers = thread_workers
|
||||
|
||||
@@ -129,6 +157,7 @@ class InferenceSlicer:
|
||||
resolution_wh=resolution_wh,
|
||||
slice_wh=self.slice_wh,
|
||||
overlap_ratio_wh=self.overlap_ratio_wh,
|
||||
overlap_wh=self.overlap_wh,
|
||||
)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=self.thread_workers) as executor:
|
||||
@@ -139,15 +168,15 @@ class InferenceSlicer:
|
||||
detections_list.append(future.result())
|
||||
|
||||
merged = Detections.merge(detections_list=detections_list)
|
||||
if self.overlap_filter_strategy == OverlapFilter.NONE:
|
||||
if self.overlap_filter == OverlapFilter.NONE:
|
||||
return merged
|
||||
elif self.overlap_filter_strategy == OverlapFilter.NON_MAX_SUPPRESSION:
|
||||
elif self.overlap_filter == OverlapFilter.NON_MAX_SUPPRESSION:
|
||||
return merged.with_nms(threshold=self.iou_threshold)
|
||||
elif self.overlap_filter_strategy == OverlapFilter.NON_MAX_MERGE:
|
||||
elif self.overlap_filter == OverlapFilter.NON_MAX_MERGE:
|
||||
return merged.with_nmm(threshold=self.iou_threshold)
|
||||
else:
|
||||
warnings.warn(
|
||||
f"Invalid overlap filter strategy: {self.overlap_filter_strategy}",
|
||||
f"Invalid overlap filter strategy: {self.overlap_filter}",
|
||||
category=SupervisionWarnings,
|
||||
)
|
||||
return merged
|
||||
@@ -177,7 +206,8 @@ class InferenceSlicer:
|
||||
def _generate_offset(
|
||||
resolution_wh: Tuple[int, int],
|
||||
slice_wh: Tuple[int, int],
|
||||
overlap_ratio_wh: Tuple[float, float],
|
||||
overlap_ratio_wh: Optional[Tuple[float, float]],
|
||||
overlap_wh: Optional[Tuple[int, int]],
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Generate offset coordinates for slicing an image based on the given resolution,
|
||||
@@ -186,12 +216,15 @@ class InferenceSlicer:
|
||||
Args:
|
||||
resolution_wh (Tuple[int, int]): A tuple representing the width and height
|
||||
of the image to be sliced.
|
||||
slice_wh (Tuple[int, int]): A tuple representing the desired width and
|
||||
height of each slice.
|
||||
overlap_ratio_wh (Tuple[float, float]): A tuple representing the desired
|
||||
overlap ratio for width and height between consecutive slices. Each
|
||||
value should be in the range [0, 1), where 0 means no overlap and a
|
||||
value close to 1 means high overlap.
|
||||
slice_wh (Tuple[int, int]): Dimensions of each slice measured in pixels. The
|
||||
tuple should be in the format `(width, height)`.
|
||||
overlap_ratio_wh (Optional[Tuple[float, float]]): A tuple representing the
|
||||
desired overlap ratio for width and height between consecutive slices.
|
||||
Each value should be in the range [0, 1), where 0 means no overlap and
|
||||
a value close to 1 means high overlap.
|
||||
overlap_wh (Optional[Tuple[int, int]]): A tuple representing the desired
|
||||
overlap for width and height between consecutive slices measured in
|
||||
pixels. Each value should be greater than or equal to 0.
|
||||
|
||||
Returns:
|
||||
np.ndarray: An array of shape `(n, 4)` containing coordinates for each
|
||||
@@ -206,10 +239,19 @@ class InferenceSlicer:
|
||||
"""
|
||||
slice_width, slice_height = slice_wh
|
||||
image_width, image_height = resolution_wh
|
||||
overlap_ratio_width, overlap_ratio_height = overlap_ratio_wh
|
||||
overlap_width = (
|
||||
overlap_wh[0]
|
||||
if overlap_wh is not None
|
||||
else int(overlap_ratio_wh[0] * slice_width)
|
||||
)
|
||||
overlap_height = (
|
||||
overlap_wh[1]
|
||||
if overlap_wh is not None
|
||||
else int(overlap_ratio_wh[1] * slice_height)
|
||||
)
|
||||
|
||||
width_stride = slice_width - int(overlap_ratio_width * slice_width)
|
||||
height_stride = slice_height - int(overlap_ratio_height * slice_height)
|
||||
width_stride = slice_width - overlap_width
|
||||
height_stride = slice_height - overlap_height
|
||||
|
||||
ws = np.arange(0, image_width, width_stride)
|
||||
hs = np.arange(0, image_height, height_stride)
|
||||
@@ -221,3 +263,32 @@ class InferenceSlicer:
|
||||
offsets = np.stack([xmin, ymin, xmax, ymax], axis=-1).reshape(-1, 4)
|
||||
|
||||
return offsets
|
||||
|
||||
@staticmethod
|
||||
def _validate_overlap(
|
||||
overlap_ratio_wh: Optional[Tuple[float, float]],
|
||||
overlap_wh: Optional[Tuple[int, int]],
|
||||
) -> None:
|
||||
if overlap_ratio_wh is not None and overlap_wh is not None:
|
||||
raise ValueError(
|
||||
"Both `overlap_ratio_wh` and `overlap_wh` cannot be provided. "
|
||||
"Please provide only one of them."
|
||||
)
|
||||
if overlap_ratio_wh is None and overlap_wh is None:
|
||||
raise ValueError(
|
||||
"Either `overlap_ratio_wh` or `overlap_wh` must be provided. "
|
||||
"Please provide one of them."
|
||||
)
|
||||
|
||||
if overlap_ratio_wh is not None:
|
||||
if not (0 <= overlap_ratio_wh[0] < 1 and 0 <= overlap_ratio_wh[1] < 1):
|
||||
raise ValueError(
|
||||
"Overlap ratios must be in the range [0, 1). "
|
||||
f"Received: {overlap_ratio_wh}"
|
||||
)
|
||||
if overlap_wh is not None:
|
||||
if not (overlap_wh[0] >= 0 and overlap_wh[1] >= 0):
|
||||
raise ValueError(
|
||||
"Overlap values must be greater than or equal to 0. "
|
||||
f"Received: {overlap_wh}"
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ from supervision.draw.color import Color
|
||||
from supervision.draw.utils import draw_polygon, draw_text
|
||||
from supervision.geometry.core import Position
|
||||
from supervision.geometry.utils import get_polygon_center
|
||||
from supervision.utils.internal import SupervisionWarnings, deprecated_parameter
|
||||
from supervision.utils.internal import SupervisionWarnings
|
||||
|
||||
|
||||
class PolygonZone:
|
||||
@@ -30,14 +30,6 @@ class PolygonZone:
|
||||
mask (np.ndarray): The 2D bool mask for the polygon zone
|
||||
"""
|
||||
|
||||
@deprecated_parameter(
|
||||
old_parameter="triggering_position",
|
||||
new_parameter="triggering_anchors",
|
||||
map_function=lambda x: [x],
|
||||
warning_message="`{old_parameter}` in `{function_name}` is deprecated and will "
|
||||
"be remove in `supervision-0.23.0`. Use '{new_parameter}' "
|
||||
"instead.",
|
||||
)
|
||||
def __init__(
|
||||
self,
|
||||
polygon: npt.NDArray[np.int64],
|
||||
@@ -147,7 +139,7 @@ class PolygonZoneAnnotator:
|
||||
|
||||
Parameters:
|
||||
scene (np.ndarray): The image on which the polygon zone will be annotated
|
||||
label (Optional[str]): An optional label for the count of detected objects
|
||||
label (Optional[str]): A label for the count of detected objects
|
||||
within the polygon zone (default: None)
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -41,7 +41,7 @@ class DetectionsSmoother:
|
||||
tracker = sv.ByteTrack(frame_rate=video_info.fps)
|
||||
smoother = sv.DetectionsSmoother()
|
||||
|
||||
annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
|
||||
with sv.VideoSink(<TARGET_FILE_PATH>, video_info=video_info) as sink:
|
||||
for frame in frame_generator:
|
||||
@@ -50,7 +50,7 @@ class DetectionsSmoother:
|
||||
detections = tracker.update_with_detections(detections)
|
||||
detections = smoother.update_with_detections(detections)
|
||||
|
||||
annotated_frame = bounding_box_annotator.annotate(frame.copy(), detections)
|
||||
annotated_frame = box_annotator.annotate(frame.copy(), detections)
|
||||
sink.write_frame(annotated_frame)
|
||||
```
|
||||
""" # noqa: E501 // docs
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import io
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from supervision.config import CLASS_NAME_DATA_FIELD
|
||||
from supervision.detection.utils import mask_to_xyxy
|
||||
|
||||
|
||||
def process_transformers_detection_result(
|
||||
detection_result: dict, id2label: Optional[Dict[int, str]]
|
||||
) -> dict:
|
||||
"""
|
||||
Process the result of Transformers object detection functions such as
|
||||
`post_process` (v4) and `post_process_detection` (v5).
|
||||
|
||||
Args:
|
||||
detection_result (dict): Dictionary containing detection results with keys
|
||||
'boxes', 'labels', and 'scores'.
|
||||
id2label (Optional[Dict[int, str]]): A dictionary mapping class IDs to labels,
|
||||
typically part of the `transformers` model configuration. If provided, the
|
||||
resulting dictionary will include class names.
|
||||
|
||||
Returns:
|
||||
dict: Processed detection result including bounding boxes, confidence scores,
|
||||
class IDs, and data.
|
||||
"""
|
||||
class_ids = detection_result["labels"].cpu().detach().numpy().astype(int)
|
||||
data = append_class_names_to_data(class_ids, id2label, {})
|
||||
|
||||
return dict(
|
||||
xyxy=detection_result["boxes"].cpu().detach().numpy(),
|
||||
confidence=detection_result["scores"].cpu().detach().numpy(),
|
||||
class_id=class_ids,
|
||||
data=data,
|
||||
)
|
||||
|
||||
|
||||
def process_transformers_v4_segmentation_result(
|
||||
segmentation_result: dict, id2label: Optional[Dict[int, str]]
|
||||
) -> dict:
|
||||
"""
|
||||
Process the result of Transformers segmentation functions such as
|
||||
`post_process_panoptic`, `post_process_segmentation`, and `post_process_instance`
|
||||
(v4).
|
||||
|
||||
Args:
|
||||
segmentation_result (dict): Dictionary containing segmentation results with keys
|
||||
'masks', 'labels', and 'scores'.
|
||||
id2label (Optional[Dict[int, str]]): A dictionary mapping class IDs to labels,
|
||||
typically part of the `transformers` model configuration. If provided, the
|
||||
resulting dictionary will include class names.
|
||||
|
||||
Returns:
|
||||
dict: Processed segmentation result including bounding boxes, masks, confidence
|
||||
scores, class IDs, and data.
|
||||
"""
|
||||
if "png_string" in segmentation_result:
|
||||
return process_transformers_v4_panoptic_segmentation_result(
|
||||
segmentation_result, id2label
|
||||
)
|
||||
else:
|
||||
boxes = None
|
||||
if "boxes" in segmentation_result:
|
||||
boxes = segmentation_result["boxes"].cpu().detach().numpy()
|
||||
masks = segmentation_result["masks"].cpu().detach().numpy().astype(bool)
|
||||
class_ids = segmentation_result["labels"].cpu().detach().numpy().astype(int)
|
||||
|
||||
return dict(
|
||||
xyxy=boxes if boxes is not None else mask_to_xyxy(masks),
|
||||
mask=np.squeeze(masks, axis=1) if boxes is not None else masks,
|
||||
confidence=segmentation_result["scores"].cpu().detach().numpy(),
|
||||
class_id=class_ids,
|
||||
data=append_class_names_to_data(class_ids, id2label, {}),
|
||||
)
|
||||
|
||||
|
||||
def process_transformers_v5_segmentation_result(
|
||||
segmentation_result: dict, id2label: Optional[Dict[int, str]]
|
||||
) -> dict:
|
||||
"""
|
||||
Process the result of Transformers segmentation functions such as
|
||||
`post_process_semantic_segmentation`, `post_process_instance_segmentation`, and
|
||||
`post_process_panoptic_segmentation` (v5).
|
||||
|
||||
Args:
|
||||
segmentation_result (Union[dict, np.ndarray]): Either a dictionary containing
|
||||
segmentation results or an ndarray representing a segmentation map.
|
||||
id2label (Optional[Dict[int, str]]): A dictionary mapping class IDs to labels,
|
||||
typically part of the `transformers` model configuration. If provided, the
|
||||
resulting dictionary will include class names.
|
||||
|
||||
Returns:
|
||||
dict: Processed segmentation result including bounding boxes, masks, confidence
|
||||
scores, class IDs, and data.
|
||||
"""
|
||||
if segmentation_result.__class__.__name__ == "Tensor":
|
||||
segmentation_array = segmentation_result.cpu().detach().numpy()
|
||||
return process_transformers_v5_panoptic_segmentation_result(
|
||||
segmentation_array, id2label
|
||||
)
|
||||
|
||||
return process_transformers_v5_semantic_or_instance_segmentation_result(
|
||||
segmentation_result, id2label
|
||||
)
|
||||
|
||||
|
||||
def process_transformers_v5_semantic_or_instance_segmentation_result(
|
||||
segmentation_result: dict, id2label: Optional[Dict[int, str]]
|
||||
) -> dict:
|
||||
"""
|
||||
Process the result of Transformers segmentation functions such as
|
||||
`post_process_semantic_segmentation` and `post_process_instance_segmentation` (v5).
|
||||
|
||||
Args:
|
||||
segmentation_result (dict): Dictionary containing segmentation results with keys
|
||||
`segments_info` and `segmentation`.
|
||||
id2label (Optional[Dict[int, str]]): A dictionary mapping class IDs to labels,
|
||||
typically part of the `transformers` model configuration. If provided, the
|
||||
resulting dictionary will include class names.
|
||||
|
||||
Returns:
|
||||
dict: Processed segmentation result including bounding boxes, masks, confidence
|
||||
scores, class IDs, and data.
|
||||
"""
|
||||
segments_info = segmentation_result["segments_info"]
|
||||
scores = np.array([segment["score"] for segment in segments_info])
|
||||
class_ids = np.array([segment["label_id"] for segment in segments_info])
|
||||
segmentation_array = segmentation_result["segmentation"].cpu().detach().numpy()
|
||||
masks = np.array(
|
||||
[segmentation_array == segment["id"] for segment in segments_info]
|
||||
).astype(bool)
|
||||
data = append_class_names_to_data(class_ids, id2label, {})
|
||||
|
||||
return dict(
|
||||
xyxy=mask_to_xyxy(masks),
|
||||
mask=masks,
|
||||
confidence=scores,
|
||||
class_id=class_ids,
|
||||
data=data,
|
||||
)
|
||||
|
||||
|
||||
def process_transformers_v4_panoptic_segmentation_result(
|
||||
segmentation_result: dict, id2label: Optional[Dict[int, str]]
|
||||
) -> dict:
|
||||
"""
|
||||
Process the result of the Transformers function `post_process_panoptic` (v4).
|
||||
|
||||
Args:
|
||||
segmentation_result (dict): Dictionary containing segmentation results with keys
|
||||
such as 'png_string' and 'segments_info'.
|
||||
id2label (Optional[Dict[int, str]]): A dictionary mapping class IDs to labels,
|
||||
typically part of the `transformers` model configuration. If provided, the
|
||||
resulting dictionary will include class names.
|
||||
|
||||
Returns:
|
||||
dict: Processed segmentation result including bounding boxes, masks,
|
||||
class IDs, and data.
|
||||
"""
|
||||
segments_info = segmentation_result["segments_info"]
|
||||
png_string = segmentation_result["png_string"]
|
||||
class_ids = np.array([segment["category_id"] for segment in segments_info])
|
||||
segmentation_array = png_string_to_segmentation_array(png_string=png_string)
|
||||
masks = np.array(
|
||||
[segmentation_array == segment["id"] for segment in segments_info]
|
||||
).astype(bool)
|
||||
data = append_class_names_to_data(class_ids, id2label, {})
|
||||
|
||||
return dict(
|
||||
xyxy=mask_to_xyxy(masks),
|
||||
mask=masks,
|
||||
class_id=class_ids,
|
||||
data=data,
|
||||
)
|
||||
|
||||
|
||||
def process_transformers_v5_panoptic_segmentation_result(
|
||||
segmentation_array: np.ndarray, id2label: Optional[Dict[int, str]]
|
||||
) -> dict:
|
||||
"""
|
||||
Process the result of the Transformers function
|
||||
`post_process_panoptic_segmentation` (v5).
|
||||
|
||||
Args:
|
||||
segmentation_array (np.ndarray): Segmentation array.
|
||||
id2label (Optional[Dict[int, str]]): A dictionary mapping class IDs to labels,
|
||||
typically part of the `transformers` model configuration. If provided, the
|
||||
resulting dictionary will include class names.
|
||||
|
||||
Returns:
|
||||
dict: Processed segmentation result including bounding boxes, masks,
|
||||
class IDs, and data.
|
||||
"""
|
||||
class_ids = np.unique(segmentation_array)
|
||||
masks = np.stack(
|
||||
[segmentation_array == class_id for class_id in class_ids], axis=0
|
||||
).astype(bool)
|
||||
data = append_class_names_to_data(class_ids, id2label, {})
|
||||
return dict(xyxy=mask_to_xyxy(masks), mask=masks, class_id=class_ids, data=data)
|
||||
|
||||
|
||||
def png_string_to_segmentation_array(png_string: bytes) -> np.ndarray:
|
||||
"""
|
||||
Convert a PNG byte string to a label mask array.
|
||||
|
||||
Args:
|
||||
png_string (bytes): A byte string representing the PNG image.
|
||||
|
||||
Returns:
|
||||
np.ndarray: A label mask array with shape (H, W), where H and W
|
||||
are the height and width of the image. Each unique value in the array
|
||||
represents a different object or category.
|
||||
"""
|
||||
image = Image.open(io.BytesIO(png_string))
|
||||
mask = np.array(image, dtype=np.uint8)
|
||||
return mask[:, :, 0]
|
||||
|
||||
|
||||
def append_class_names_to_data(
|
||||
class_ids: np.ndarray,
|
||||
id2label: Optional[Dict[int, str]],
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Helper function to create or append to a data dictionary with class names if
|
||||
available.
|
||||
|
||||
Args:
|
||||
class_ids (np.ndarray): Array of class IDs.
|
||||
id2label (Optional[Dict[int, str]]): A dictionary mapping class IDs to labels,
|
||||
typically part of the `transformers` model configuration. If provided, the
|
||||
resulting dictionary will include class names.
|
||||
data (Optional[Dict[str, Any]]): An existing data dictionary to append to.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Dictionary containing class names if id2label is provided.
|
||||
"""
|
||||
if data is None:
|
||||
data = {}
|
||||
|
||||
if id2label is not None:
|
||||
class_names = np.array([id2label[class_id] for class_id in class_ids])
|
||||
data[CLASS_NAME_DATA_FIELD] = class_names
|
||||
|
||||
return data
|
||||
@@ -106,7 +106,7 @@ def mask_iou_batch(
|
||||
Args:
|
||||
masks_true (np.ndarray): 3D `np.ndarray` representing ground-truth masks.
|
||||
masks_detection (np.ndarray): 3D `np.ndarray` representing detection masks.
|
||||
memory_limit (int, optional): memory limit in MB, default is 1024 * 5 MB (5GB).
|
||||
memory_limit (int): memory limit in MB, default is 1024 * 5 MB (5GB).
|
||||
|
||||
Returns:
|
||||
np.ndarray: Pairwise IoU of masks from `masks_true` and `masks_detection`.
|
||||
@@ -228,10 +228,78 @@ def pad_boxes(xyxy: np.ndarray, px: int, py: Optional[int] = None) -> np.ndarray
|
||||
return result
|
||||
|
||||
|
||||
def xywh_to_xyxy(boxes_xywh: np.ndarray) -> np.ndarray:
|
||||
xyxy = boxes_xywh.copy()
|
||||
xyxy[:, 2] = boxes_xywh[:, 0] + boxes_xywh[:, 2]
|
||||
xyxy[:, 3] = boxes_xywh[:, 1] + boxes_xywh[:, 3]
|
||||
def xywh_to_xyxy(xywh: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Converts bounding box coordinates from `(x, y, width, height)`
|
||||
format to `(x_min, y_min, x_max, y_max)` format.
|
||||
|
||||
Args:
|
||||
xywh (np.ndarray): A numpy array of shape `(N, 4)` where each row
|
||||
corresponds to a bounding box in the format `(x, y, width, height)`.
|
||||
|
||||
Returns:
|
||||
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)`.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
|
||||
xywh = np.array([
|
||||
[10, 20, 30, 40],
|
||||
[15, 25, 35, 45]
|
||||
])
|
||||
|
||||
sv.xywh_to_xyxy(xywh=xywh)
|
||||
# array([
|
||||
# [10, 20, 40, 60],
|
||||
# [15, 25, 50, 70]
|
||||
# ])
|
||||
```
|
||||
"""
|
||||
xyxy = xywh.copy()
|
||||
xyxy[:, 2] = xywh[:, 0] + xywh[:, 2]
|
||||
xyxy[:, 3] = xywh[:, 1] + xywh[:, 3]
|
||||
return xyxy
|
||||
|
||||
|
||||
def xcycwh_to_xyxy(xcycwh: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Converts bounding box coordinates from `(center_x, center_y, width, height)`
|
||||
format to `(x_min, y_min, x_max, y_max)` format.
|
||||
|
||||
Args:
|
||||
xcycwh (np.ndarray): A numpy array of shape `(N, 4)` where each row
|
||||
corresponds to a bounding box in the format `(center_x, center_y, width,
|
||||
height)`.
|
||||
|
||||
Returns:
|
||||
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)`.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
|
||||
xcycwh = np.array([
|
||||
[50, 50, 20, 30],
|
||||
[30, 40, 10, 15]
|
||||
])
|
||||
|
||||
sv.xcycwh_to_xyxy(xcycwh=xcycwh)
|
||||
# array([
|
||||
# [40, 35, 60, 65],
|
||||
# [25, 32.5, 35, 47.5]
|
||||
# ])
|
||||
```
|
||||
"""
|
||||
xyxy = xcycwh.copy()
|
||||
xyxy[:, 0] = xcycwh[:, 0] - xcycwh[:, 2] / 2
|
||||
xyxy[:, 1] = xcycwh[:, 1] - xcycwh[:, 3] / 2
|
||||
xyxy[:, 2] = xcycwh[:, 0] + xcycwh[:, 2] / 2
|
||||
xyxy[:, 3] = xcycwh[:, 1] + xcycwh[:, 3] / 2
|
||||
return xyxy
|
||||
|
||||
|
||||
@@ -528,6 +596,61 @@ def move_boxes(
|
||||
return xyxy + np.hstack([offset, offset])
|
||||
|
||||
|
||||
def move_oriented_boxes(
|
||||
xyxyxyxy: npt.NDArray[np.float64], offset: npt.NDArray[np.int32]
|
||||
) -> npt.NDArray[np.float64]:
|
||||
"""
|
||||
Parameters:
|
||||
xyxyxyxy (npt.NDArray[np.float64]): An array of shape `(n, 4, 2)` containing the
|
||||
oriented bounding boxes coordinates in format
|
||||
`[[x1, y1], [x2, y2], [x3, y3], [x3, y3]]`
|
||||
offset (np.array): An array of shape `(2,)` containing offset values in format
|
||||
is `[dx, dy]`.
|
||||
|
||||
Returns:
|
||||
npt.NDArray[np.float64]: Repositioned bounding boxes.
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
|
||||
xyxyxyxy = np.array([
|
||||
[
|
||||
[20, 10],
|
||||
[10, 20],
|
||||
[20, 30],
|
||||
[30, 20]
|
||||
],
|
||||
[
|
||||
[30 ,30],
|
||||
[20, 40],
|
||||
[30, 50],
|
||||
[40, 40]
|
||||
]
|
||||
])
|
||||
offset = np.array([5, 5])
|
||||
|
||||
sv.move_oriented_boxes(xyxy=xyxy, offset=offset)
|
||||
# array([
|
||||
# [
|
||||
# [25, 15],
|
||||
# [15, 25],
|
||||
# [25, 35],
|
||||
# [35, 25]
|
||||
# ],
|
||||
# [
|
||||
# [35, 35],
|
||||
# [25, 45],
|
||||
# [35, 55],
|
||||
# [45, 45]
|
||||
# ]
|
||||
# ])
|
||||
```
|
||||
"""
|
||||
return xyxyxyxy + offset
|
||||
|
||||
|
||||
def move_masks(
|
||||
masks: npt.NDArray[np.bool_],
|
||||
offset: npt.NDArray[np.int32],
|
||||
|
||||
+10
-73
@@ -5,7 +5,7 @@ from typing import List, Tuple, Union
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from supervision.utils.internal import classproperty, deprecated
|
||||
from supervision.utils.internal import classproperty
|
||||
|
||||
DEFAULT_COLOR_PALETTE = [
|
||||
"A351FB",
|
||||
@@ -255,46 +255,6 @@ class Color:
|
||||
def ROBOFLOW(cls) -> Color:
|
||||
return Color.from_hex("#A351FB")
|
||||
|
||||
@classmethod
|
||||
@deprecated(
|
||||
"`Color.white()` is deprecated and will be removed in "
|
||||
"`supervision-0.22.0`. Use `Color.WHITE` instead."
|
||||
)
|
||||
def white(cls) -> Color:
|
||||
return Color.from_hex(color_hex="#ffffff")
|
||||
|
||||
@classmethod
|
||||
@deprecated(
|
||||
"`Color.black()` is deprecated and will be removed in "
|
||||
"`supervision-0.22.0`. Use `Color.BLACK` instead."
|
||||
)
|
||||
def black(cls) -> Color:
|
||||
return Color.from_hex(color_hex="#000000")
|
||||
|
||||
@classmethod
|
||||
@deprecated(
|
||||
"`Color.red()` is deprecated and will be removed in "
|
||||
"`supervision-0.22.0`. Use `Color.RED` instead."
|
||||
)
|
||||
def red(cls) -> Color:
|
||||
return Color.from_hex(color_hex="#ff0000")
|
||||
|
||||
@classmethod
|
||||
@deprecated(
|
||||
"`Color.green()` is deprecated and will be removed in "
|
||||
"`supervision-0.22.0`. Use `Color.GREEN` instead."
|
||||
)
|
||||
def green(cls) -> Color:
|
||||
return Color.from_hex(color_hex="#00ff00")
|
||||
|
||||
@classmethod
|
||||
@deprecated(
|
||||
"`Color.blue()` is deprecated and will be removed in "
|
||||
"`supervision-0.22.0`. Use `Color.BLUE` instead."
|
||||
)
|
||||
def blue(cls) -> Color:
|
||||
return Color.from_hex(color_hex="#0000ff")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ColorPalette:
|
||||
@@ -346,33 +306,6 @@ class ColorPalette:
|
||||
def LEGACY(cls) -> ColorPalette:
|
||||
return ColorPalette.from_hex(color_hex_list=LEGACY_COLOR_PALETTE)
|
||||
|
||||
@classmethod
|
||||
@deprecated(
|
||||
"`ColorPalette.default()` is deprecated and will be removed in "
|
||||
"`supervision-0.22.0`. Use `Color.DEFAULT` instead."
|
||||
)
|
||||
def default(cls) -> ColorPalette:
|
||||
"""
|
||||
!!! failure "Deprecated"
|
||||
|
||||
`ColorPalette.default()` is deprecated and will be removed in
|
||||
`supervision-0.22.0`. Use `Color.DEFAULT` instead.
|
||||
|
||||
Returns a default color palette.
|
||||
|
||||
Returns:
|
||||
ColorPalette: A ColorPalette instance with default colors.
|
||||
|
||||
Example:
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
sv.ColorPalette.default()
|
||||
# ColorPalette(colors=[Color(r=255, g=64, b=64), Color(r=255, g=161, b=160), ...])
|
||||
```
|
||||
""" # noqa: E501 // docs
|
||||
return ColorPalette.from_hex(color_hex_list=DEFAULT_COLOR_PALETTE)
|
||||
|
||||
@classmethod
|
||||
def from_hex(cls, color_hex_list: List[str]) -> ColorPalette:
|
||||
"""
|
||||
@@ -419,11 +352,15 @@ class ColorPalette:
|
||||
supervision-annotator-examples/visualized_color_palette.png)
|
||||
""" # noqa: E501 // docs
|
||||
mpl_palette = plt.get_cmap(palette_name, color_count)
|
||||
colors = [
|
||||
Color(int(r * 255), int(g * 255), int(b * 255))
|
||||
for r, g, b, _ in mpl_palette.colors
|
||||
]
|
||||
return cls(colors)
|
||||
|
||||
if hasattr(mpl_palette, "colors"):
|
||||
colors = mpl_palette.colors
|
||||
else:
|
||||
colors = [mpl_palette(i / (color_count - 1)) for i in range(color_count)]
|
||||
|
||||
return cls(
|
||||
[Color(int(r * 255), int(g * 255), int(b * 255)) for r, g, b, _ in colors]
|
||||
)
|
||||
|
||||
def by_idx(self, idx: int) -> Color:
|
||||
"""
|
||||
|
||||
@@ -142,7 +142,7 @@ def draw_polygon(
|
||||
scene (np.ndarray): The scene to draw the polygon on.
|
||||
polygon (np.ndarray): The polygon to be drawn, given as a list of vertices.
|
||||
color (Color): The color of the polygon.
|
||||
thickness (int, optional): The thickness of the polygon lines, by default 2.
|
||||
thickness (int): The thickness of the polygon lines, by default 2.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The scene with the polygon drawn on it.
|
||||
@@ -172,14 +172,14 @@ def draw_text(
|
||||
text (str): The text to be drawn.
|
||||
text_anchor (Point): The anchor point for the text, represented as a
|
||||
Point object with x and y attributes.
|
||||
text_color (Color, optional): The color of the text. Defaults to black.
|
||||
text_scale (float, optional): The scale of the text. Defaults to 0.5.
|
||||
text_thickness (int, optional): The thickness of the text. Defaults to 1.
|
||||
text_padding (int, optional): The amount of padding to add around the text
|
||||
text_color (Color): The color of the text. Defaults to black.
|
||||
text_scale (float): The scale of the text. Defaults to 0.5.
|
||||
text_thickness (int): The thickness of the text. Defaults to 1.
|
||||
text_padding (int): The amount of padding to add around the text
|
||||
when drawing a rectangle in the background. Defaults to 10.
|
||||
text_font (int, optional): The font to use for the text.
|
||||
text_font (int): The font to use for the text.
|
||||
Defaults to cv2.FONT_HERSHEY_SIMPLEX.
|
||||
background_color (Color, optional): The color of the background rectangle,
|
||||
background_color (Optional[Color]): The color of the background rectangle,
|
||||
if one is to be drawn. Defaults to None.
|
||||
|
||||
Returns:
|
||||
@@ -295,8 +295,8 @@ def calculate_optimal_text_scale(resolution_wh: Tuple[int, int]) -> float:
|
||||
Calculate font scale based on the resolution of an image.
|
||||
|
||||
Parameters:
|
||||
resolution_wh (Tuple[int, int]): A tuple representing the width and height
|
||||
of the image.
|
||||
resolution_wh (Tuple[int, int]): A tuple representing the width and height
|
||||
of the image.
|
||||
|
||||
Returns:
|
||||
float: The calculated font scale factor.
|
||||
|
||||
@@ -11,7 +11,7 @@ from supervision.draw.color import Color
|
||||
from supervision.draw.utils import draw_rounded_rectangle
|
||||
from supervision.keypoint.core import KeyPoints
|
||||
from supervision.keypoint.skeletons import SKELETONS_BY_VERTEX_COUNT
|
||||
from supervision.utils.conversion import convert_for_annotation_method
|
||||
from supervision.utils.conversion import ensure_cv2_image_for_annotation
|
||||
|
||||
|
||||
class BaseKeyPointAnnotator(ABC):
|
||||
@@ -34,14 +34,14 @@ class VertexAnnotator(BaseKeyPointAnnotator):
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
color (Color, optional): The color to use for annotating key points.
|
||||
radius (int, optional): The radius of the circles used to represent the key
|
||||
color (Color): The color to use for annotating key points.
|
||||
radius (int): The radius of the circles used to represent the key
|
||||
points.
|
||||
"""
|
||||
self.color = color
|
||||
self.radius = radius
|
||||
|
||||
@convert_for_annotation_method
|
||||
@ensure_cv2_image_for_annotation
|
||||
def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType:
|
||||
"""
|
||||
Annotates the given scene with skeleton vertices based on the provided key
|
||||
@@ -78,6 +78,7 @@ class VertexAnnotator(BaseKeyPointAnnotator):
|
||||

|
||||
"""
|
||||
assert isinstance(scene, np.ndarray)
|
||||
if len(key_points) == 0:
|
||||
return scene
|
||||
|
||||
@@ -108,8 +109,8 @@ class EdgeAnnotator(BaseKeyPointAnnotator):
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
color (Color, optional): The color to use for the edges.
|
||||
thickness (int, optional): The thickness of the edges.
|
||||
color (Color): The color to use for the edges.
|
||||
thickness (int): The thickness of the edges.
|
||||
edges (Optional[List[Tuple[int, int]]]): The edges to draw.
|
||||
If set to `None`, will attempt to select automatically.
|
||||
"""
|
||||
@@ -117,7 +118,7 @@ class EdgeAnnotator(BaseKeyPointAnnotator):
|
||||
self.thickness = thickness
|
||||
self.edges = edges
|
||||
|
||||
@convert_for_annotation_method
|
||||
@ensure_cv2_image_for_annotation
|
||||
def annotate(self, scene: ImageType, key_points: KeyPoints) -> ImageType:
|
||||
"""
|
||||
Annotates the given scene by drawing lines between specified key points to form
|
||||
@@ -155,6 +156,7 @@ class EdgeAnnotator(BaseKeyPointAnnotator):
|
||||

|
||||
"""
|
||||
assert isinstance(scene, np.ndarray)
|
||||
if len(key_points) == 0:
|
||||
return scene
|
||||
|
||||
@@ -194,7 +196,7 @@ class VertexLabelAnnotator:
|
||||
def __init__(
|
||||
self,
|
||||
color: Union[Color, List[Color]] = Color.ROBOFLOW,
|
||||
text_color: Color = Color.WHITE,
|
||||
text_color: Union[Color, List[Color]] = Color.WHITE,
|
||||
text_scale: float = 0.5,
|
||||
text_thickness: int = 1,
|
||||
text_padding: int = 10,
|
||||
@@ -202,25 +204,30 @@ class VertexLabelAnnotator:
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
color (Union[Color, List[Color]], optional): The color to use for each
|
||||
color (Union[Color, List[Color]]): The color to use for each
|
||||
keypoint label. If a list is provided, the colors will be used in order
|
||||
for each keypoint.
|
||||
text_color (Color, optional): The color to use for the labels.
|
||||
text_scale (float, optional): The scale of the text.
|
||||
text_thickness (int, optional): The thickness of the text.
|
||||
text_padding (int, optional): The padding around the text.
|
||||
border_radius (int, optional): The radius of the rounded corners of the
|
||||
text_color (Union[Color, List[Color]]): The color to use
|
||||
for the labels. If a list is provided, the colors will be used in order
|
||||
for each keypoint.
|
||||
text_scale (float): The scale of the text.
|
||||
text_thickness (int): The thickness of the text.
|
||||
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.
|
||||
"""
|
||||
self.border_radius: int = border_radius
|
||||
self.color: Union[Color, List[Color]] = color
|
||||
self.text_color: Color = text_color
|
||||
self.text_color: Union[Color, List[Color]] = text_color
|
||||
self.text_scale: float = text_scale
|
||||
self.text_thickness: int = text_thickness
|
||||
self.text_padding: int = text_padding
|
||||
|
||||
def annotate(
|
||||
self, scene: ImageType, key_points: KeyPoints, labels: List[str] = None
|
||||
self,
|
||||
scene: ImageType,
|
||||
key_points: KeyPoints,
|
||||
labels: Optional[List[str]] = None,
|
||||
) -> ImageType:
|
||||
"""
|
||||
A class that draws labels of skeleton vertices on images. It uses specified key
|
||||
@@ -232,7 +239,7 @@ class VertexLabelAnnotator:
|
||||
`PIL.Image.Image`.
|
||||
key_points (KeyPoints): A collection of key points where each key point
|
||||
consists of x and y coordinates.
|
||||
labels (List[str], optional): A list of labels to be displayed on the
|
||||
labels (Optional[List[str]]): A list of labels to be displayed on the
|
||||
annotated image. If not provided, keypoint indices will be used.
|
||||
|
||||
Returns:
|
||||
@@ -303,6 +310,7 @@ class VertexLabelAnnotator:
|
||||

|
||||
"""
|
||||
assert isinstance(scene, np.ndarray)
|
||||
font = cv2.FONT_HERSHEY_SIMPLEX
|
||||
|
||||
skeletons_count, points_count, _ = key_points.xy.shape
|
||||
@@ -321,12 +329,19 @@ class VertexLabelAnnotator:
|
||||
skeletons_count=skeletons_count,
|
||||
)
|
||||
|
||||
text_colors = self.preprocess_and_validate_colors(
|
||||
colors=self.text_color,
|
||||
points_count=points_count,
|
||||
skeletons_count=skeletons_count,
|
||||
)
|
||||
|
||||
labels = self.preprocess_and_validate_labels(
|
||||
labels=labels, points_count=points_count, skeletons_count=skeletons_count
|
||||
)
|
||||
|
||||
anchors = anchors[mask]
|
||||
colors = colors[mask]
|
||||
text_colors = text_colors[mask]
|
||||
labels = labels[mask]
|
||||
|
||||
xyxy = np.array(
|
||||
@@ -344,7 +359,9 @@ class VertexLabelAnnotator:
|
||||
|
||||
xyxy_padded = pad_boxes(xyxy=xyxy, px=self.text_padding)
|
||||
|
||||
for text, color, box, box_padded in zip(labels, colors, xyxy, xyxy_padded):
|
||||
for text, color, text_color, box, box_padded in zip(
|
||||
labels, colors, text_colors, xyxy, xyxy_padded
|
||||
):
|
||||
draw_rounded_rectangle(
|
||||
scene=scene,
|
||||
rect=Rect.from_xyxy(box_padded),
|
||||
@@ -354,10 +371,10 @@ class VertexLabelAnnotator:
|
||||
cv2.putText(
|
||||
img=scene,
|
||||
text=text,
|
||||
org=(box[0], box[1] + self.text_padding),
|
||||
org=(box[0], box[3]),
|
||||
fontFace=font,
|
||||
fontScale=self.text_scale,
|
||||
color=self.text_color.as_rgb(),
|
||||
color=text_color.as_bgr(),
|
||||
thickness=self.text_thickness,
|
||||
lineType=cv2.LINE_AA,
|
||||
)
|
||||
@@ -389,7 +406,7 @@ class VertexLabelAnnotator:
|
||||
@staticmethod
|
||||
def preprocess_and_validate_labels(
|
||||
labels: Optional[List[str]], points_count: int, skeletons_count: int
|
||||
) -> np.array:
|
||||
) -> np.ndarray:
|
||||
if labels and len(labels) != points_count:
|
||||
raise ValueError(
|
||||
f"Number of labels ({len(labels)}) must match number of key points "
|
||||
@@ -405,7 +422,7 @@ class VertexLabelAnnotator:
|
||||
colors: Optional[Union[Color, List[Color]]],
|
||||
points_count: int,
|
||||
skeletons_count: int,
|
||||
) -> np.array:
|
||||
) -> np.ndarray:
|
||||
if isinstance(colors, list) and len(colors) != points_count:
|
||||
raise ValueError(
|
||||
f"Number of colors ({len(colors)}) must match number of key points "
|
||||
|
||||
+291
-52
@@ -18,12 +18,13 @@ class KeyPoints:
|
||||
The `sv.KeyPoints` class in the Supervision library standardizes results from
|
||||
various keypoint detection and pose estimation models into a consistent format. This
|
||||
class simplifies data manipulation and filtering, providing a uniform API for
|
||||
integration with Supervision annotators.
|
||||
integration with Supervision [keypoints annotators](/keypoint/annotators).
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
Use [`sv.KeyPoints.from_ultralytics`](/keypoint/core/#supervision.keypoint.core.KeyPoints.from_ultralytics)
|
||||
method, which accepts model results.
|
||||
method, which accepts [YOLOv8](https://github.com/ultralytics/ultralytics)
|
||||
pose result.
|
||||
|
||||
```python
|
||||
import cv2
|
||||
@@ -32,20 +33,72 @@ class KeyPoints:
|
||||
|
||||
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
||||
model = YOLO('yolov8s-pose.pt')
|
||||
|
||||
result = model(image)[0]
|
||||
key_points = sv.KeyPoints.from_ultralytics(result)
|
||||
```
|
||||
|
||||
=== "Inference"
|
||||
|
||||
Use [`sv.KeyPoints.from_inference`](/keypoint/core/#supervision.keypoint.core.KeyPoints.from_inference)
|
||||
method, which accepts [Inference](https://inference.roboflow.com/) pose result.
|
||||
|
||||
```python
|
||||
import cv2
|
||||
import supervision as sv
|
||||
from inference import get_model
|
||||
|
||||
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
||||
model = get_model(model_id=<POSE_MODEL_ID>, api_key=<ROBOFLOW_API_KEY>)
|
||||
|
||||
result = model.infer(image)[0]
|
||||
key_points = sv.KeyPoints.from_inference(result)
|
||||
```
|
||||
|
||||
=== "MediaPipe"
|
||||
|
||||
Use [`sv.KeyPoints.from_mediapipe`](/keypoint/core/#supervision.keypoint.core.KeyPoints.from_mediapipe)
|
||||
method, which accepts [MediaPipe](https://github.com/google-ai-edge/mediapipe)
|
||||
pose result.
|
||||
|
||||
```python
|
||||
import cv2
|
||||
import mediapipe as mp
|
||||
import supervision as sv
|
||||
|
||||
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
||||
image_height, image_width, _ = image.shape
|
||||
mediapipe_image = mp.Image(
|
||||
image_format=mp.ImageFormat.SRGB,
|
||||
data=cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
|
||||
options = mp.tasks.vision.PoseLandmarkerOptions(
|
||||
base_options=mp.tasks.BaseOptions(
|
||||
model_asset_path="pose_landmarker_heavy.task"
|
||||
),
|
||||
running_mode=mp.tasks.vision.RunningMode.IMAGE,
|
||||
num_poses=2)
|
||||
|
||||
PoseLandmarker = mp.tasks.vision.PoseLandmarker
|
||||
with PoseLandmarker.create_from_options(options) as landmarker:
|
||||
pose_landmarker_result = landmarker.detect(mediapipe_image)
|
||||
|
||||
key_points = sv.KeyPoints.from_mediapipe(
|
||||
pose_landmarker_result, (image_width, image_height))
|
||||
```
|
||||
|
||||
Attributes:
|
||||
xy (np.ndarray): An array of shape `(n, 2)` containing
|
||||
the bounding boxes coordinates in format `[x1, y1]`
|
||||
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,)` containing the confidence scores of the keypoint keypoints.
|
||||
`(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 keypoint keypoints.
|
||||
`(n,)` containing the class ids of the detected objects.
|
||||
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.
|
||||
is either a NumPy array or a list of corresponding data of length `n`
|
||||
(one entry per detected object).
|
||||
""" # noqa: E501 // docs
|
||||
|
||||
xy: npt.NDArray[np.float32]
|
||||
@@ -63,7 +116,7 @@ class KeyPoints:
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""
|
||||
Returns the number of keypoints in the keypoints object.
|
||||
Returns the number of keypoints in the `sv.KeyPoints` object.
|
||||
"""
|
||||
return len(self.xy)
|
||||
|
||||
@@ -81,7 +134,7 @@ class KeyPoints:
|
||||
]:
|
||||
"""
|
||||
Iterates over the Keypoint object and yield a tuple of
|
||||
`(xy, confidence, class_id, data)` for each keypoint detection.
|
||||
`(xy, confidence, class_id, data)` for each object detection.
|
||||
"""
|
||||
for i in range(len(self.xy)):
|
||||
yield (
|
||||
@@ -106,18 +159,17 @@ class KeyPoints:
|
||||
"""
|
||||
Create a `sv.KeyPoints` object from the [Roboflow](https://roboflow.com/)
|
||||
API inference result or the [Inference](https://inference.roboflow.com/)
|
||||
package results. When a keypoint detection model is used, this method
|
||||
extracts the keypoint coordinates, class IDs, confidences, and class names.
|
||||
package results.
|
||||
|
||||
Args:
|
||||
inference_result (dict, any): The result from the
|
||||
Roboflow API or Inference package containing predictions with keypoints.
|
||||
|
||||
Returns:
|
||||
(KeyPoints): A KeyPoints object containing the keypoint coordinates,
|
||||
class IDs, and confidences of each keypoint.
|
||||
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
|
||||
@@ -151,8 +203,6 @@ class KeyPoints:
|
||||
"You can retrieve it like so: inference_result = model.infer(image)[0]"
|
||||
)
|
||||
|
||||
# Unpack the result if received from inference.get_model,
|
||||
# rather than inference_sdk.InferenceHTTPClient
|
||||
with suppress(AttributeError):
|
||||
inference_result = inference_result.dict(exclude_none=True, by_alias=True)
|
||||
|
||||
@@ -185,20 +235,147 @@ class KeyPoints:
|
||||
data=data,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_mediapipe(
|
||||
cls, mediapipe_results, resolution_wh: Tuple[int, int]
|
||||
) -> KeyPoints:
|
||||
"""
|
||||
Creates a `sv.KeyPoints` instance from a
|
||||
[MediaPipe](https://github.com/google-ai-edge/mediapipe)
|
||||
pose landmark detection inference result.
|
||||
|
||||
Args:
|
||||
mediapipe_results (Union[PoseLandmarkerResult, FaceLandmarkerResult, SolutionOutputs]):
|
||||
The output results from Mediapipe. It support pose and face landmarks
|
||||
from `PoseLandmaker`, `FaceLandmarker` and the legacy ones
|
||||
from `Pose` and `FaceMesh`.
|
||||
resolution_wh (Tuple[int, int]): A tuple of the form `(width, height)`
|
||||
representing the resolution of the frame.
|
||||
|
||||
Returns:
|
||||
A `sv.KeyPoints` object containing the keypoint coordinates and
|
||||
confidences of each keypoint.
|
||||
|
||||
!!! tip
|
||||
Before you start, download model bundles from the
|
||||
[MediaPipe website](https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker/index#models).
|
||||
|
||||
Examples:
|
||||
```python
|
||||
import cv2
|
||||
import mediapipe as mp
|
||||
import supervision as sv
|
||||
|
||||
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
||||
image_height, image_width, _ = image.shape
|
||||
mediapipe_image = mp.Image(
|
||||
image_format=mp.ImageFormat.SRGB,
|
||||
data=cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
|
||||
options = mp.tasks.vision.PoseLandmarkerOptions(
|
||||
base_options=mp.tasks.BaseOptions(
|
||||
model_asset_path="pose_landmarker_heavy.task"
|
||||
),
|
||||
running_mode=mp.tasks.vision.RunningMode.IMAGE,
|
||||
num_poses=2)
|
||||
|
||||
PoseLandmarker = mp.tasks.vision.PoseLandmarker
|
||||
with PoseLandmarker.create_from_options(options) as landmarker:
|
||||
pose_landmarker_result = landmarker.detect(mediapipe_image)
|
||||
|
||||
key_points = sv.KeyPoints.from_mediapipe(
|
||||
pose_landmarker_result, (image_width, image_height))
|
||||
```
|
||||
|
||||
```python
|
||||
import cv2
|
||||
import mediapipe as mp
|
||||
import supervision as sv
|
||||
|
||||
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
||||
image_height, image_width, _ = image.shape
|
||||
mediapipe_image = mp.Image(
|
||||
image_format=mp.ImageFormat.SRGB,
|
||||
data=cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
|
||||
options = mp.tasks.vision.FaceLandmarkerOptions(
|
||||
base_options=mp.tasks.BaseOptions(
|
||||
model_asset_path="face_landmarker.task"
|
||||
),
|
||||
output_face_blendshapes=True,
|
||||
output_facial_transformation_matrixes=True,
|
||||
num_faces=2)
|
||||
|
||||
FaceLandmarker = mp.tasks.vision.FaceLandmarker
|
||||
with FaceLandmarker.create_from_options(options) as landmarker:
|
||||
face_landmarker_result = landmarker.detect(mediapipe_image)
|
||||
|
||||
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
|
||||
if not isinstance(mediapipe_results.pose_landmarks, list):
|
||||
if mediapipe_results.pose_landmarks is None:
|
||||
results = []
|
||||
else:
|
||||
results = [
|
||||
[
|
||||
landmark
|
||||
for landmark in mediapipe_results.pose_landmarks.landmark
|
||||
]
|
||||
]
|
||||
elif hasattr(mediapipe_results, "face_landmarks"):
|
||||
results = mediapipe_results.face_landmarks
|
||||
elif hasattr(mediapipe_results, "multi_face_landmarks"):
|
||||
if mediapipe_results.multi_face_landmarks is None:
|
||||
results = []
|
||||
else:
|
||||
results = [
|
||||
face_landmark.landmark
|
||||
for face_landmark in mediapipe_results.multi_face_landmarks
|
||||
]
|
||||
|
||||
if len(results) == 0:
|
||||
return cls.empty()
|
||||
|
||||
xy = []
|
||||
confidence = []
|
||||
for pose in results:
|
||||
prediction_xy = []
|
||||
prediction_confidence = []
|
||||
for landmark in pose:
|
||||
keypoint_xy = [
|
||||
landmark.x * resolution_wh[0],
|
||||
landmark.y * resolution_wh[1],
|
||||
]
|
||||
prediction_xy.append(keypoint_xy)
|
||||
prediction_confidence.append(landmark.visibility)
|
||||
|
||||
xy.append(prediction_xy)
|
||||
confidence.append(prediction_confidence)
|
||||
|
||||
return cls(
|
||||
xy=np.array(xy, dtype=np.float32),
|
||||
confidence=np.array(confidence, dtype=np.float32),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_ultralytics(cls, ultralytics_results) -> KeyPoints:
|
||||
"""
|
||||
Creates a KeyPoints instance from a
|
||||
[YOLOv8](https://github.com/ultralytics/ultralytics) inference result.
|
||||
Creates a `sv.KeyPoints` instance from a
|
||||
[YOLOv8](https://github.com/ultralytics/ultralytics) pose inference result.
|
||||
|
||||
Args:
|
||||
ultralytics_results (ultralytics.engine.results.Keypoints):
|
||||
The output Results instance from YOLOv8
|
||||
|
||||
Returns:
|
||||
KeyPoints: A new KeyPoints object.
|
||||
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
|
||||
@@ -206,8 +383,9 @@ class KeyPoints:
|
||||
|
||||
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
||||
model = YOLO('yolov8s-pose.pt')
|
||||
|
||||
result = model(image)[0]
|
||||
keypoints = sv.KeyPoints.from_ultralytics(result)
|
||||
key_points = sv.KeyPoints.from_ultralytics(result)
|
||||
```
|
||||
"""
|
||||
if ultralytics_results.keypoints.xy.numel() == 0:
|
||||
@@ -224,16 +402,18 @@ class KeyPoints:
|
||||
@classmethod
|
||||
def from_yolo_nas(cls, yolo_nas_results) -> KeyPoints:
|
||||
"""
|
||||
Create a KeyPoints instance from a YOLO NAS results.
|
||||
Create a `sv.KeyPoints` instance from a [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS-POSE.md)
|
||||
pose inference results.
|
||||
|
||||
Args:
|
||||
yolo_nas_results (ImagePoseEstimationPrediction):
|
||||
The output object from YOLO NAS.
|
||||
yolo_nas_results (ImagePoseEstimationPrediction): The output object from
|
||||
YOLO NAS.
|
||||
|
||||
Returns:
|
||||
KeyPoints: A new KeyPoints object.
|
||||
A `sv.KeyPoints` object containing the keypoint coordinates, class IDs,
|
||||
and class names, and confidences of each keypoint.
|
||||
|
||||
Example:
|
||||
Examples:
|
||||
```python
|
||||
import cv2
|
||||
import torch
|
||||
@@ -243,13 +423,13 @@ class KeyPoints:
|
||||
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
||||
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
yolo_nas = super_gradients.training.models.get(
|
||||
model = super_gradients.training.models.get(
|
||||
"yolo_nas_pose_s", pretrained_weights="coco_pose").to(device)
|
||||
|
||||
results = yolo_nas.predict(image, conf=0.1)
|
||||
keypoints = sv.KeyPoints.from_yolo_nas(results)
|
||||
results = model.predict(image, conf=0.1)
|
||||
key_points = sv.KeyPoints.from_yolo_nas(results)
|
||||
```
|
||||
"""
|
||||
""" # noqa: E501 // docs
|
||||
if len(yolo_nas_results.prediction.poses) == 0:
|
||||
return cls.empty()
|
||||
|
||||
@@ -278,38 +458,96 @@ class KeyPoints:
|
||||
data=data,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_detectron2(cls, detectron2_results) -> KeyPoints:
|
||||
"""
|
||||
Create a `sv.KeyPoints` object from the
|
||||
[Detectron2](https://github.com/facebookresearch/detectron2) inference result.
|
||||
|
||||
Args:
|
||||
detectron2_results: The output of a
|
||||
Detectron2 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.
|
||||
|
||||
Example:
|
||||
```python
|
||||
import cv2
|
||||
import supervision as sv
|
||||
from detectron2.engine import DefaultPredictor
|
||||
from detectron2.config import get_cfg
|
||||
|
||||
|
||||
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
||||
cfg = get_cfg()
|
||||
cfg.merge_from_file(<CONFIG_PATH>)
|
||||
cfg.MODEL.WEIGHTS = <WEIGHTS_PATH>
|
||||
predictor = DefaultPredictor(cfg)
|
||||
|
||||
result = predictor(image)
|
||||
keypoints = sv.KeyPoints.from_detectron2(result)
|
||||
```
|
||||
"""
|
||||
|
||||
if hasattr(detectron2_results["instances"], "pred_keypoints"):
|
||||
if detectron2_results["instances"].pred_keypoints.cpu().numpy().size == 0:
|
||||
return cls.empty()
|
||||
return cls(
|
||||
xy=detectron2_results["instances"]
|
||||
.pred_keypoints.cpu()
|
||||
.numpy()[:, :, :2],
|
||||
confidence=detectron2_results["instances"]
|
||||
.pred_keypoints.cpu()
|
||||
.numpy()[:, :, 2],
|
||||
class_id=detectron2_results["instances"]
|
||||
.pred_classes.cpu()
|
||||
.numpy()
|
||||
.astype(int),
|
||||
)
|
||||
else:
|
||||
return cls.empty()
|
||||
|
||||
def __getitem__(
|
||||
self, index: Union[int, slice, List[int], np.ndarray, str]
|
||||
) -> Union[KeyPoints, List, np.ndarray, None]:
|
||||
"""
|
||||
Get a subset of the KeyPoints object or access an item from its data field.
|
||||
Get a subset of the `sv.KeyPoints` object or access an item from its data field.
|
||||
|
||||
When provided with an integer, slice, list of integers, or a numpy array, this
|
||||
method returns a new KeyPoints object that represents a subset of the original
|
||||
keypoints. When provided with a string, it accesses the corresponding item in
|
||||
the data dictionary.
|
||||
method returns a new `sv.KeyPoints` object that represents a subset of the
|
||||
original `sv.KeyPoints`. When provided with a string, it accesses the
|
||||
corresponding item in the data dictionary.
|
||||
|
||||
Args:
|
||||
index (Union[int, slice, List[int], np.ndarray, str]): The index, indices,
|
||||
or key to access a subset of the KeyPoints or an item from the data.
|
||||
or key to access a subset of the `sv.KeyPoints` or an item from the
|
||||
data.
|
||||
|
||||
Returns:
|
||||
Union[KeyPoints, Any]: A subset of the KeyPoints object or an item from
|
||||
the data field.
|
||||
A subset of the `sv.KeyPoints` object or an item from the data field.
|
||||
|
||||
Example:
|
||||
Examples:
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
keypoints = sv.KeyPoints()
|
||||
key_points = sv.KeyPoints()
|
||||
|
||||
first_detection = keypoints[0]
|
||||
first_10_keypoints = keypoints[0:10]
|
||||
some_keypoints = keypoints[[0, 2, 4]]
|
||||
class_0_keypoints = keypoints[keypoints.class_id == 0]
|
||||
high_confidence_keypoints = keypoints[keypoints.confidence > 0.5]
|
||||
# access the first keypoint using an integer index
|
||||
key_points[0]
|
||||
|
||||
feature_vector = keypoints['feature_vector']
|
||||
# access the first 10 keypoints using index slice
|
||||
key_points[0:10]
|
||||
|
||||
# access selected keypoints using a list of indices
|
||||
key_points[[0, 2, 4]]
|
||||
|
||||
# access keypoints with selected class_id
|
||||
key_points[key_points.class_id == 0]
|
||||
|
||||
# access keypoints with confidence greater than 0.5
|
||||
key_points[key_points.confidence > 0.5]
|
||||
```
|
||||
"""
|
||||
if isinstance(index, str):
|
||||
@@ -325,13 +563,13 @@ class KeyPoints:
|
||||
|
||||
def __setitem__(self, key: str, value: Union[np.ndarray, List]):
|
||||
"""
|
||||
Set a value in the data dictionary of the KeyPoints object.
|
||||
Set a value in the data dictionary of the `sv.KeyPoints` object.
|
||||
|
||||
Args:
|
||||
key (str): The key in the data dictionary to set.
|
||||
value (Union[np.ndarray, List]): The value to set for the key.
|
||||
|
||||
Example:
|
||||
Examples:
|
||||
```python
|
||||
import cv2
|
||||
import supervision as sv
|
||||
@@ -343,7 +581,7 @@ class KeyPoints:
|
||||
result = model(image)[0]
|
||||
keypoints = sv.KeyPoints.from_ultralytics(result)
|
||||
|
||||
keypoints['names'] = [
|
||||
keypoints['class_name'] = [
|
||||
model.model.names[class_id]
|
||||
for class_id
|
||||
in keypoints.class_id
|
||||
@@ -364,12 +602,13 @@ class KeyPoints:
|
||||
Create an empty Keypoints object with no keypoints.
|
||||
|
||||
Returns:
|
||||
(KeyPoints): An empty Keypoints object.
|
||||
An empty `sv.KeyPoints` object.
|
||||
|
||||
Example:
|
||||
Examples:
|
||||
```python
|
||||
from supervision import Keypoints
|
||||
empty_keypoints = Keypoints.empty()
|
||||
import supervision as sv
|
||||
|
||||
key_points = sv.KeyPoints.empty()
|
||||
```
|
||||
"""
|
||||
return cls(xy=np.empty((0, 0, 2), dtype=np.float32))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
from supervision.metrics.core import CLASS_ID_NONE, Metric, MetricTarget
|
||||
from supervision.metrics.mean_average_precision import (
|
||||
MeanAveragePrecision,
|
||||
MeanAveragePrecisionResult,
|
||||
)
|
||||
from supervision.metrics.utils.object_size import (
|
||||
ObjectSizeCategory,
|
||||
get_detection_size_category,
|
||||
get_object_size_category,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
CLASS_ID_NONE = -1
|
||||
"""Used by metrics module as class ID, when none is present"""
|
||||
|
||||
|
||||
class Metric(ABC):
|
||||
"""
|
||||
The base class for all supervision metrics.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def update(self, *args, **kwargs) -> "Metric":
|
||||
"""
|
||||
Add data to the metric, without computing the result.
|
||||
Return the metric itself to allow method chaining.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Reset internal metric state.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def compute(self, *args, **kwargs) -> Any:
|
||||
"""
|
||||
Compute the metric from the internal state and return the result.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class MetricTarget(Enum):
|
||||
"""
|
||||
Specifies what type of detection is used to compute the metric.
|
||||
|
||||
* BOXES: xyxy bounding boxes
|
||||
* MASKS: Binary masks
|
||||
* ORIENTED_BOUNDING_BOXES: Oriented bounding boxes (OBB)
|
||||
"""
|
||||
|
||||
BOXES = "boxes"
|
||||
MASKS = "masks"
|
||||
ORIENTED_BOUNDING_BOXES = "obb"
|
||||
@@ -391,11 +391,10 @@ class ConfusionMatrix:
|
||||
```
|
||||
"""
|
||||
predictions, targets = [], []
|
||||
for img_name, img in dataset.images.items():
|
||||
predictions_batch = callback(img)
|
||||
for _, image, annotation in dataset:
|
||||
predictions_batch = callback(image)
|
||||
predictions.append(predictions_batch)
|
||||
targets_batch = dataset.annotations[img_name]
|
||||
targets.append(targets_batch)
|
||||
targets.append(annotation)
|
||||
return cls.from_detections(
|
||||
predictions=predictions,
|
||||
targets=targets,
|
||||
@@ -604,11 +603,10 @@ class MeanAveragePrecision:
|
||||
```
|
||||
"""
|
||||
predictions, targets = [], []
|
||||
for img_name, img in dataset.images.items():
|
||||
predictions_batch = callback(img)
|
||||
for _, image, annotation in dataset:
|
||||
predictions_batch = callback(image)
|
||||
predictions.append(predictions_batch)
|
||||
targets_batch = dataset.annotations[img_name]
|
||||
targets.append(targets_batch)
|
||||
targets.append(annotation)
|
||||
return cls.from_detections(
|
||||
predictions=predictions,
|
||||
targets=targets,
|
||||
@@ -666,12 +664,12 @@ class MeanAveragePrecision:
|
||||
np.array([[1.0, 1.0, 2.0, 2.0, 2, 0.8]])
|
||||
]
|
||||
|
||||
mean_average_precison = sv.MeanAveragePrecision.from_tensors(
|
||||
mean_average_precision = sv.MeanAveragePrecision.from_tensors(
|
||||
predictions=predictions,
|
||||
targets=targets,
|
||||
)
|
||||
|
||||
print(mean_average_precison.map50_95)
|
||||
print(mean_average_precision.map50_95)
|
||||
# 0.6649
|
||||
```
|
||||
"""
|
||||
@@ -808,7 +806,7 @@ class MeanAveragePrecision:
|
||||
prediction_confidence (np.ndarray): Objectness value from 0-1.
|
||||
prediction_class_ids (np.ndarray): Predicted object classes.
|
||||
true_class_ids (np.ndarray): True object classes.
|
||||
eps (float, optional): Small value to prevent division by zero.
|
||||
eps (float): Small value to prevent division by zero.
|
||||
|
||||
Returns:
|
||||
np.ndarray: Average precision for different IoU levels.
|
||||
|
||||
@@ -0,0 +1,562 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, List, Optional, 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
|
||||
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 MeanAveragePrecision(Metric):
|
||||
def __init__(
|
||||
self,
|
||||
metric_target: MetricTarget = MetricTarget.BOXES,
|
||||
class_agnostic: bool = False,
|
||||
):
|
||||
"""
|
||||
Initialize the Mean Average Precision metric.
|
||||
|
||||
Args:
|
||||
metric_target (MetricTarget): The type of detection data to use.
|
||||
class_agnostic (bool): Whether to treat all data as a single class.
|
||||
"""
|
||||
self._metric_target = metric_target
|
||||
if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
|
||||
raise NotImplementedError(
|
||||
"Mean Average Precision is not implemented for oriented bounding boxes."
|
||||
)
|
||||
|
||||
self._class_agnostic = class_agnostic
|
||||
|
||||
self._predictions_list: List[Detections] = []
|
||||
self._targets_list: List[Detections] = []
|
||||
|
||||
def reset(self) -> None:
|
||||
self._predictions_list = []
|
||||
self._targets_list = []
|
||||
|
||||
def update(
|
||||
self,
|
||||
predictions: Union[Detections, List[Detections]],
|
||||
targets: Union[Detections, List[Detections]],
|
||||
) -> MeanAveragePrecision:
|
||||
"""
|
||||
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 ground-truth detections.
|
||||
|
||||
Returns:
|
||||
(MeanAveragePrecision): 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,
|
||||
) -> MeanAveragePrecisionResult:
|
||||
"""
|
||||
Calculate Mean Average Precision based on predicted and ground-truth
|
||||
detections at different threshold.
|
||||
|
||||
Args:
|
||||
predictions (List[np.ndarray]): Each element of the list describes
|
||||
a single image and has `shape = (M, 6)` where `M` is
|
||||
the number of detected objects. Each row is expected to be
|
||||
in `(x_min, y_min, x_max, y_max, class, conf)` format.
|
||||
targets (List[np.ndarray]): Each element of the list describes a single
|
||||
image and has `shape = (N, 5)` where `N` is the
|
||||
number of ground-truth objects. Each row is expected to be in
|
||||
`(x_min, y_min, x_max, y_max, class)` format.
|
||||
Returns:
|
||||
(MeanAveragePrecision): New instance of MeanAveragePrecision.
|
||||
|
||||
Example:
|
||||
```python
|
||||
import supervision as sv
|
||||
from supervision.metrics import MeanAveragePrecision
|
||||
|
||||
predictions = sv.Detections(...)
|
||||
targets = sv.Detections(...)
|
||||
|
||||
map_metric = MeanAveragePrecision()
|
||||
map_result = map_metric.update(predictions, targets).compute()
|
||||
|
||||
print(map_result)
|
||||
print(map_result.map50_95)
|
||||
map_result.plot()
|
||||
```
|
||||
"""
|
||||
result = self._compute(self._predictions_list, self._targets_list)
|
||||
|
||||
small_predictions = []
|
||||
small_targets = []
|
||||
for predictions, targets in zip(self._predictions_list, self._targets_list):
|
||||
small_predictions.append(
|
||||
self._filter_detections_by_size(predictions, ObjectSizeCategory.SMALL)
|
||||
)
|
||||
small_targets.append(
|
||||
self._filter_detections_by_size(targets, ObjectSizeCategory.SMALL)
|
||||
)
|
||||
result.small_objects = self._compute(small_predictions, small_targets)
|
||||
|
||||
medium_predictions = []
|
||||
medium_targets = []
|
||||
for predictions, targets in zip(self._predictions_list, self._targets_list):
|
||||
medium_predictions.append(
|
||||
self._filter_detections_by_size(predictions, ObjectSizeCategory.MEDIUM)
|
||||
)
|
||||
medium_targets.append(
|
||||
self._filter_detections_by_size(targets, ObjectSizeCategory.MEDIUM)
|
||||
)
|
||||
result.medium_objects = self._compute(medium_predictions, medium_targets)
|
||||
|
||||
large_predictions = []
|
||||
large_targets = []
|
||||
for predictions, targets in zip(self._predictions_list, self._targets_list):
|
||||
large_predictions.append(
|
||||
self._filter_detections_by_size(predictions, ObjectSizeCategory.LARGE)
|
||||
)
|
||||
large_targets.append(
|
||||
self._filter_detections_by_size(targets, ObjectSizeCategory.LARGE)
|
||||
)
|
||||
result.large_objects = self._compute(large_predictions, large_targets)
|
||||
|
||||
return result
|
||||
|
||||
def _compute(
|
||||
self,
|
||||
predictions_list: List[Detections],
|
||||
targets_list: List[Detections],
|
||||
) -> MeanAveragePrecisionResult:
|
||||
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)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"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,
|
||||
)
|
||||
)
|
||||
|
||||
# Compute average precisions if any matches exist
|
||||
if stats:
|
||||
concatenated_stats = [np.concatenate(items, 0) for items in zip(*stats)]
|
||||
average_precisions = self._average_precisions_per_class(*concatenated_stats)
|
||||
map50 = average_precisions[:, 0].mean()
|
||||
map75 = average_precisions[:, 5].mean()
|
||||
map50_95 = average_precisions.mean()
|
||||
else:
|
||||
map50, map75, map50_95 = 0, 0, 0
|
||||
average_precisions = np.empty((0, len(iou_thresholds)), dtype=np.float32)
|
||||
|
||||
return MeanAveragePrecisionResult(
|
||||
iou_thresholds=iou_thresholds,
|
||||
map50_95=map50_95,
|
||||
map50=map50,
|
||||
map75=map75,
|
||||
per_class_ap50_95=average_precisions,
|
||||
metric_target=self._metric_target,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def compute_average_precision(recall: np.ndarray, precision: np.ndarray) -> float:
|
||||
"""
|
||||
Compute the average precision using 101-point interpolation (COCO), given
|
||||
the recall and precision curves.
|
||||
|
||||
Args:
|
||||
recall (np.ndarray): The recall curve.
|
||||
precision (np.ndarray): The precision curve.
|
||||
|
||||
Returns:
|
||||
(float): Average precision.
|
||||
"""
|
||||
extended_recall = np.concatenate(([0.0], recall, [1.0]))
|
||||
extended_precision = np.concatenate(([1.0], precision, [0.0]))
|
||||
max_accumulated_precision = np.flip(
|
||||
np.maximum.accumulate(np.flip(extended_precision))
|
||||
)
|
||||
interpolated_recall_levels = np.linspace(0, 1, 101)
|
||||
interpolated_precision = np.interp(
|
||||
interpolated_recall_levels, extended_recall, max_accumulated_precision
|
||||
)
|
||||
average_precision = np.trapz(interpolated_precision, interpolated_recall_levels)
|
||||
return average_precision
|
||||
|
||||
@staticmethod
|
||||
def _match_detection_batch(
|
||||
predictions_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 _average_precisions_per_class(
|
||||
matches: np.ndarray,
|
||||
prediction_confidence: np.ndarray,
|
||||
prediction_class_ids: np.ndarray,
|
||||
true_class_ids: np.ndarray,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Compute the average precision, given the recall and precision curves.
|
||||
Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
|
||||
|
||||
Args:
|
||||
matches (np.ndarray): True positives.
|
||||
prediction_confidence (np.ndarray): Objectness value from 0-1.
|
||||
prediction_class_ids (np.ndarray): Predicted object classes.
|
||||
true_class_ids (np.ndarray): True object classes.
|
||||
eps (float, optional): Small value to prevent division by zero.
|
||||
|
||||
Returns:
|
||||
(np.ndarray): Average precision for different IoU levels.
|
||||
"""
|
||||
eps = 1e-16
|
||||
|
||||
sorted_indices = np.argsort(-prediction_confidence)
|
||||
matches = matches[sorted_indices]
|
||||
prediction_class_ids = prediction_class_ids[sorted_indices]
|
||||
|
||||
unique_classes, class_counts = np.unique(true_class_ids, return_counts=True)
|
||||
num_classes = unique_classes.shape[0]
|
||||
|
||||
average_precisions = np.zeros((num_classes, matches.shape[1]))
|
||||
|
||||
for class_idx, class_id in enumerate(unique_classes):
|
||||
is_class = prediction_class_ids == class_id
|
||||
total_true = class_counts[class_idx]
|
||||
total_prediction = is_class.sum()
|
||||
|
||||
if total_prediction == 0 or total_true == 0:
|
||||
continue
|
||||
|
||||
false_positives = (1 - matches[is_class]).cumsum(0)
|
||||
true_positives = matches[is_class].cumsum(0)
|
||||
true_negatives = total_true - true_positives
|
||||
|
||||
recall = true_positives / (true_positives + true_negatives + eps)
|
||||
precision = true_positives / (true_positives + false_positives)
|
||||
|
||||
for iou_level_idx in range(matches.shape[1]):
|
||||
average_precisions[class_idx, iou_level_idx] = (
|
||||
MeanAveragePrecision.compute_average_precision(
|
||||
recall[:, iou_level_idx], precision[:, iou_level_idx]
|
||||
)
|
||||
)
|
||||
|
||||
return average_precisions
|
||||
|
||||
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:
|
||||
if obb := detections.data.get(ORIENTED_BOX_COORDINATES):
|
||||
return np.ndarray(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, 8), 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
|
||||
|
||||
|
||||
@dataclass
|
||||
class MeanAveragePrecisionResult:
|
||||
iou_thresholds: np.ndarray
|
||||
"""Array of IoU thresholds used in the calculations"""
|
||||
map50_95: float
|
||||
"""Mean Average Precision over IoU thresholds from 0.5 to 0.95"""
|
||||
|
||||
map50: float
|
||||
"""Mean Average Precision at IoU threshold of 0.5"""
|
||||
|
||||
map75: float
|
||||
"""Mean Average Precision at IoU threshold of 0.75"""
|
||||
|
||||
per_class_ap50_95: np.ndarray
|
||||
"""Average precision for each class at different IoU thresholds"""
|
||||
|
||||
metric_target: MetricTarget
|
||||
"""
|
||||
Defines the type of data used for the metric - boxes, masks or
|
||||
oriented bounding boxes.
|
||||
"""
|
||||
|
||||
small_objects: Optional[MeanAveragePrecisionResult] = None
|
||||
"""Mean Average Precision results for small objects"""
|
||||
|
||||
medium_objects: Optional[MeanAveragePrecisionResult] = None
|
||||
"""Mean Average Precision results for medium objects"""
|
||||
|
||||
large_objects: Optional[MeanAveragePrecisionResult] = None
|
||||
"""Mean Average Precision results for large objects"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Format the mAP results as a pretty string.
|
||||
|
||||
Example:
|
||||
```python
|
||||
print(map_result)
|
||||
```
|
||||
"""
|
||||
|
||||
out_str = (
|
||||
f"{self.__class__.__name__}:\n"
|
||||
f"iou_thresholds: {self.iou_thresholds}\n"
|
||||
f"map50_95: {self.map50_95:.4f}\n"
|
||||
f"map50: {self.map50:.4f}\n"
|
||||
f"map75: {self.map75:.4f}\n"
|
||||
f"per_class_ap50_95:"
|
||||
)
|
||||
|
||||
for class_id, ap in enumerate(self.per_class_ap50_95):
|
||||
out_str += f"\n {class_id}: {ap}"
|
||||
|
||||
indent = " "
|
||||
if self.small_objects is not None:
|
||||
indented_str = indent + str(self.small_objects).replace("\n", f"\n{indent}")
|
||||
out_str += f"\nSmall objects:\n{indented_str}"
|
||||
if self.medium_objects is not None:
|
||||
indented_str = indent + str(self.medium_objects).replace(
|
||||
"\n", f"\n{indent}"
|
||||
)
|
||||
out_str += f"\nMedium objects:\n{indented_str}"
|
||||
if self.large_objects is not None:
|
||||
indented_str = indent + str(self.large_objects).replace("\n", f"\n{indent}")
|
||||
out_str += f"\nLarge objects:\n{indented_str}"
|
||||
|
||||
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 = {
|
||||
"mAP_50_95": self.map50_95,
|
||||
"mAP_50": self.map50,
|
||||
"mAP_75": self.map75,
|
||||
}
|
||||
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
|
||||
|
||||
# Average precisions are currently not included in the DataFrame.
|
||||
|
||||
return pd.DataFrame(
|
||||
pandas_data,
|
||||
index=[0],
|
||||
)
|
||||
|
||||
def plot(self):
|
||||
"""
|
||||
Plot the mAP results.
|
||||
"""
|
||||
|
||||
labels = ["mAP_50_95", "mAP_50", "mAP_75"]
|
||||
values = [self.map50_95, self.map50, self.map75]
|
||||
colors = [LEGACY_COLOR_PALETTE[0]] * 3
|
||||
|
||||
if self.small_objects is not None:
|
||||
labels += [
|
||||
"small_objects_mAP_50_95",
|
||||
"small_objects_mAP_50",
|
||||
"small_objects_mAP_75",
|
||||
]
|
||||
values += [
|
||||
self.small_objects.map50_95,
|
||||
self.small_objects.map50,
|
||||
self.small_objects.map75,
|
||||
]
|
||||
colors += [LEGACY_COLOR_PALETTE[3]] * 3
|
||||
|
||||
if self.medium_objects is not None:
|
||||
labels += [
|
||||
"medium_objects_mAP_50_95",
|
||||
"medium_objects_mAP_50",
|
||||
"medium_objects_mAP_75",
|
||||
]
|
||||
values += [
|
||||
self.medium_objects.map50_95,
|
||||
self.medium_objects.map50,
|
||||
self.medium_objects.map75,
|
||||
]
|
||||
colors += [LEGACY_COLOR_PALETTE[2]] * 3
|
||||
|
||||
if self.large_objects is not None:
|
||||
labels += [
|
||||
"large_objects_mAP_50_95",
|
||||
"large_objects_mAP_50",
|
||||
"large_objects_mAP_75",
|
||||
]
|
||||
values += [
|
||||
self.large_objects.map50_95,
|
||||
self.large_objects.map50,
|
||||
self.large_objects.map75,
|
||||
]
|
||||
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")
|
||||
ax.set_title("Mean Average Precision", 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()
|
||||
@@ -0,0 +1,300 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, List, Optional, Set, Tuple
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from supervision.config import ORIENTED_BOX_COORDINATES
|
||||
from supervision.metrics.core import CLASS_ID_NONE, MetricTarget
|
||||
from supervision.metrics.utils.object_size import (
|
||||
ObjectSizeCategory,
|
||||
get_object_size_category,
|
||||
)
|
||||
from supervision.metrics.utils.utils import unify_pad_masks_shape
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from supervision.detection.core import Detections
|
||||
|
||||
|
||||
class MetricData:
|
||||
"""
|
||||
A container for detection contents, decouple from `Detections`.
|
||||
"""
|
||||
|
||||
def __init__(self, metric_target: MetricTarget, class_agnostic: bool = False):
|
||||
self._metric_target = metric_target
|
||||
self._class_agnostic = class_agnostic
|
||||
self._content_list: List[npt.NDArray] = []
|
||||
self._confidence_list: List[npt.NDArray[np.float32]] = []
|
||||
self._class_id_list: List[npt.NDArray[np.int_]] = []
|
||||
|
||||
def update(self, detections: Detections):
|
||||
"""Add new detections to the store."""
|
||||
if detections.is_empty() or len(detections) == 0:
|
||||
return
|
||||
|
||||
# Relies on Detections to ensure that member vars are equal in length
|
||||
# or None/empty.
|
||||
self._validate_new_entry(detections)
|
||||
|
||||
new_content = self._detections_content(detections)
|
||||
self._validate_shape(new_content)
|
||||
self._content_list.append(new_content)
|
||||
|
||||
if detections.confidence is not None:
|
||||
self._confidence_list.append(detections.confidence)
|
||||
|
||||
if detections.class_id is not None:
|
||||
if self._class_agnostic:
|
||||
self._class_id_list.append(
|
||||
np.full(len(detections.class_id), CLASS_ID_NONE, dtype=int)
|
||||
)
|
||||
else:
|
||||
self._class_id_list.append(detections.class_id)
|
||||
|
||||
def get_classes(self) -> Set[int]:
|
||||
"""Return all class IDs."""
|
||||
if self._class_agnostic:
|
||||
return {CLASS_ID_NONE}
|
||||
return set.union(*[set(class_id) for class_id in self._class_id_list])
|
||||
|
||||
def get(
|
||||
self, class_id: Optional[int] = None, size_category=ObjectSizeCategory.ANY
|
||||
) -> Tuple[npt.NDArray, npt.NDArray, npt.NDArray]:
|
||||
"""
|
||||
Get the contents, class_ids and confidences, optionally filtered by
|
||||
class_id and/or size.
|
||||
|
||||
Args:
|
||||
class_id (Optional[int]): The class ID for the data to retrieve
|
||||
size_category (ObjectSizeCategory): The size of the objects to retrieve.
|
||||
|
||||
Returns:
|
||||
(np.ndarray): Boxes, masks or obb, all stacked in a single array.
|
||||
"""
|
||||
if self._metric_target in [
|
||||
MetricTarget.BOXES,
|
||||
MetricTarget.ORIENTED_BOUNDING_BOXES,
|
||||
]:
|
||||
self._merge_boxes()
|
||||
if len(self._content_list) == 0:
|
||||
content = self._make_empty_content()
|
||||
else:
|
||||
content = self._content_list[0]
|
||||
|
||||
elif self._metric_target == MetricTarget.MASKS:
|
||||
self._merge_masks()
|
||||
if len(self._content_list) == 0:
|
||||
content = self._make_empty_content()
|
||||
else:
|
||||
content = self._content_list[0]
|
||||
|
||||
size_mask = np.full(len(content), True)
|
||||
if size_category != ObjectSizeCategory.ANY:
|
||||
sizes = get_object_size_category(content, self._metric_target)
|
||||
size_mask = sizes == size_category.value
|
||||
|
||||
class_mask = np.full(len(content), True)
|
||||
if class_id is not None:
|
||||
self._merge_class_id()
|
||||
class_ids = self._class_id_list[0]
|
||||
class_mask = class_ids == class_id
|
||||
|
||||
content = content[(size_mask & class_mask)]
|
||||
|
||||
self._merge_class_id()
|
||||
if len(self._class_id_list) == 0:
|
||||
class_ids = np.array([], dtype=int)
|
||||
else:
|
||||
class_ids = self._class_id_list[0]
|
||||
if len(class_ids) > 0:
|
||||
class_ids = class_ids[(size_mask & class_mask)]
|
||||
|
||||
self._merge_confidence()
|
||||
if len(self._confidence_list) == 0:
|
||||
confidences = np.array([], dtype=float)
|
||||
else:
|
||||
confidences = self._confidence_list[0]
|
||||
if len(confidences) > 0:
|
||||
confidences = confidences[(size_mask & class_mask)]
|
||||
|
||||
return content, class_ids, confidences
|
||||
|
||||
def get_class_id(self) -> npt.NDArray[np.int_]:
|
||||
self._merge_class_id()
|
||||
if len(self._class_id_list) == 0:
|
||||
return np.array([], dtype=int)
|
||||
return self._class_id_list[0]
|
||||
|
||||
def get_confidence(self) -> npt.NDArray[np.float32]:
|
||||
self._merge_confidence()
|
||||
if len(self._confidence_list) == 0:
|
||||
return np.array([], dtype=float)
|
||||
return self._confidence_list[0]
|
||||
|
||||
def _detections_content(self, detections: Detections) -> npt.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:
|
||||
if obb := detections.data.get(ORIENTED_BOX_COORDINATES):
|
||||
return np.ndarray(obb, dtype=np.float32)
|
||||
return self._make_empty_content()
|
||||
raise ValueError(f"Invalid metric target: {self._metric_target}")
|
||||
|
||||
def _make_empty_content(self) -> npt.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, 8), dtype=np.float32)
|
||||
raise ValueError(f"Invalid metric target: {self._metric_target}")
|
||||
|
||||
def _validate_new_entry(self, detections: Detections) -> None:
|
||||
if (
|
||||
len(self._content_list) == 0
|
||||
or detections.is_empty()
|
||||
or len(detections) == 0
|
||||
):
|
||||
return
|
||||
|
||||
is_self_class_empty = len(self._class_id_list) == 0
|
||||
is_self_confidence_empty = len(self._confidence_list) == 0
|
||||
is_detection_class_empty = (
|
||||
detections.class_id is None or len(detections.class_id) == 0
|
||||
)
|
||||
is_detection_confidence_empty = (
|
||||
detections.confidence is None or len(detections.confidence) == 0
|
||||
)
|
||||
|
||||
if is_self_confidence_empty and not is_detection_confidence_empty:
|
||||
raise ValueError(
|
||||
"Previously stored detections without confidence,"
|
||||
" but new detections have it."
|
||||
)
|
||||
if is_self_class_empty and not is_detection_class_empty:
|
||||
raise ValueError(
|
||||
"Previously stored detections without class ID,"
|
||||
" but new detections have it."
|
||||
)
|
||||
if not is_self_class_empty and is_detection_class_empty:
|
||||
raise ValueError(
|
||||
"Started storing detections with class ID, but"
|
||||
" new detections do not have it."
|
||||
)
|
||||
if not is_self_confidence_empty and is_detection_confidence_empty:
|
||||
raise ValueError(
|
||||
"Started storing detections with confidence,"
|
||||
" but new detections do not have it."
|
||||
)
|
||||
|
||||
def _validate_shape(self, data: npt.NDArray) -> None:
|
||||
if self._metric_target == MetricTarget.BOXES:
|
||||
if len(data.shape) != 2 or data.shape[1] != 4:
|
||||
raise ValueError(f"Invalid xyxy shape: {data.shape}. Expected: (N, 4)")
|
||||
elif self._metric_target == MetricTarget.MASKS:
|
||||
if len(data.shape) != 3:
|
||||
raise ValueError(
|
||||
f"Invalid mask shape: {data.shape}. Expected: (N, H, W)"
|
||||
)
|
||||
elif self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
|
||||
if len(data.shape) != 2 or data.shape[1] != 8:
|
||||
raise ValueError(f"Invalid obb shape: {data.shape}. Expected: (N, 8)")
|
||||
else:
|
||||
raise ValueError(f"Invalid metric target: {self._metric_target}")
|
||||
|
||||
def _merge_boxes(self):
|
||||
"""Merge all boxes into a single array."""
|
||||
if self._metric_target not in (
|
||||
MetricTarget.BOXES,
|
||||
MetricTarget.ORIENTED_BOUNDING_BOXES,
|
||||
):
|
||||
raise ValueError("Invalid metric target for merging boxes")
|
||||
if len(self._content_list) < 2:
|
||||
return
|
||||
self._content_list = [np.vstack(self._content_list)]
|
||||
|
||||
def _merge_masks(self):
|
||||
"""Merge all masks into a single array."""
|
||||
if self._metric_target != MetricTarget.MASKS:
|
||||
raise ValueError("Invalid metric target for merging masks")
|
||||
if len(self._content_list) < 2:
|
||||
return
|
||||
|
||||
mew_mask_list = unify_pad_masks_shape(*self._content_list)
|
||||
self._content_list = [np.vstack(mew_mask_list)]
|
||||
|
||||
def _merge_class_id(self):
|
||||
"""Merge all class IDs into a single array."""
|
||||
if len(self._class_id_list) < 2:
|
||||
return
|
||||
self._class_id_list = [np.hstack(self._class_id_list)]
|
||||
|
||||
def _merge_confidence(self):
|
||||
"""Merge all confidences into a single array."""
|
||||
if len(self._confidence_list) < 2:
|
||||
return
|
||||
self._confidence_list = [np.hstack(self._confidence_list)]
|
||||
|
||||
|
||||
class MetricDataStore:
|
||||
"""
|
||||
Stores internal data for metrics.
|
||||
|
||||
Provides a class-agnostic way to access it.
|
||||
"""
|
||||
|
||||
def __init__(self, metric_target: MetricTarget, class_agnostic: bool = False):
|
||||
self._metric_target = metric_target
|
||||
self._class_agnostic = class_agnostic
|
||||
self._data_1: MetricData
|
||||
self._data_2: MetricData
|
||||
self.reset()
|
||||
|
||||
def reset(self) -> None:
|
||||
self._data_1 = MetricData(self._metric_target, self._class_agnostic)
|
||||
self._data_2 = MetricData(self._metric_target, self._class_agnostic)
|
||||
|
||||
def update(self, data_1: Detections, data_2: Detections) -> None:
|
||||
"""
|
||||
Add new data to the store.
|
||||
|
||||
Use sv.Detections.empty() if only one set of data is available.
|
||||
"""
|
||||
self._data_1.update(data_1)
|
||||
self._data_2.update(data_2)
|
||||
|
||||
def get(
|
||||
self,
|
||||
class_id: Optional[int] = None,
|
||||
size_category: ObjectSizeCategory = ObjectSizeCategory.ANY,
|
||||
) -> Tuple[
|
||||
Tuple[npt.NDArray, npt.NDArray, npt.NDArray],
|
||||
Tuple[npt.NDArray, npt.NDArray, npt.NDArray],
|
||||
]:
|
||||
"""
|
||||
Get the data for both sets, optionally filtered by class_id and/or size.
|
||||
"""
|
||||
|
||||
data_1, class_id_1, confidence_1 = self._data_1.get(class_id, size_category)
|
||||
data_2, class_id_2, confidence_2 = self._data_2.get(class_id, size_category)
|
||||
|
||||
if self._metric_target == MetricTarget.MASKS:
|
||||
data_1, data_2 = unify_pad_masks_shape(data_1, data_2)
|
||||
|
||||
return (
|
||||
(data_1, class_id_1, confidence_1),
|
||||
(data_2, class_id_2, confidence_2),
|
||||
)
|
||||
|
||||
def get_classes(self) -> Set[int]:
|
||||
"""Return all class IDs."""
|
||||
return self._data_1.get_classes() | self._data_2.get_classes()
|
||||
@@ -0,0 +1,155 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from supervision.config import ORIENTED_BOX_COORDINATES
|
||||
from supervision.metrics.core import MetricTarget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from supervision.detection.core import Detections
|
||||
|
||||
SIZE_THRESHOLDS = (32**2, 96**2)
|
||||
|
||||
|
||||
class ObjectSizeCategory(Enum):
|
||||
ANY = -1
|
||||
SMALL = 1
|
||||
MEDIUM = 2
|
||||
LARGE = 3
|
||||
|
||||
|
||||
def get_object_size_category(
|
||||
data: npt.NDArray, metric_target: MetricTarget
|
||||
) -> npt.NDArray[np.int_]:
|
||||
"""
|
||||
Get the size category of an object. Distinguish based on the metric target.
|
||||
|
||||
Args:
|
||||
data (np.ndarray): The object data, shaped (N, ...).
|
||||
metric_target (MetricTarget): Determines whether boxes, masks or
|
||||
oriented bounding boxes are used.
|
||||
|
||||
Returns:
|
||||
(np.ndarray) The size category of each object, matching
|
||||
the enum values of ObjectSizeCategory. Shaped (N,).
|
||||
"""
|
||||
if metric_target == MetricTarget.BOXES:
|
||||
return get_bbox_size_category(data)
|
||||
if metric_target == MetricTarget.MASKS:
|
||||
return get_mask_size_category(data)
|
||||
if metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
|
||||
return get_obb_size_category(data)
|
||||
raise ValueError("Invalid metric type")
|
||||
|
||||
|
||||
def get_bbox_size_category(xyxy: npt.NDArray[np.float32]) -> npt.NDArray[np.int_]:
|
||||
"""
|
||||
Get the size category of a bounding boxes array.
|
||||
|
||||
Args:
|
||||
xyxy (np.ndarray): The bounding boxes array shaped (N, 4).
|
||||
|
||||
Returns:
|
||||
(np.ndarray) The size category of each bounding box, matching
|
||||
the enum values of ObjectSizeCategory. Shaped (N,).
|
||||
"""
|
||||
if len(xyxy.shape) != 2 or xyxy.shape[1] != 4:
|
||||
raise ValueError("Bounding boxes must be shaped (N, 4)")
|
||||
|
||||
width = xyxy[:, 2] - xyxy[:, 0]
|
||||
height = xyxy[:, 3] - xyxy[:, 1]
|
||||
areas = width * height
|
||||
|
||||
result = np.full(areas.shape, ObjectSizeCategory.ANY.value)
|
||||
SM, LG = SIZE_THRESHOLDS
|
||||
result[areas < SM] = ObjectSizeCategory.SMALL.value
|
||||
result[(areas >= SM) & (areas < LG)] = ObjectSizeCategory.MEDIUM.value
|
||||
result[areas >= LG] = ObjectSizeCategory.LARGE.value
|
||||
return result
|
||||
|
||||
|
||||
def get_mask_size_category(mask: npt.NDArray[np.bool_]) -> npt.NDArray[np.int_]:
|
||||
"""
|
||||
Get the size category of detection masks.
|
||||
|
||||
Args:
|
||||
mask (np.ndarray): The mask array shaped (N, H, W).
|
||||
|
||||
Returns:
|
||||
(np.ndarray) The size category of each mask, matching
|
||||
the enum values of ObjectSizeCategory. Shaped (N,).
|
||||
"""
|
||||
if len(mask.shape) != 3:
|
||||
raise ValueError("Masks must be shaped (N, H, W)")
|
||||
|
||||
areas = np.sum(mask, axis=(1, 2))
|
||||
|
||||
result = np.full(areas.shape, ObjectSizeCategory.ANY.value)
|
||||
SM, LG = SIZE_THRESHOLDS
|
||||
result[areas < SM] = ObjectSizeCategory.SMALL.value
|
||||
result[(areas >= SM) & (areas < LG)] = ObjectSizeCategory.MEDIUM.value
|
||||
result[areas >= LG] = ObjectSizeCategory.LARGE.value
|
||||
return result
|
||||
|
||||
|
||||
def get_obb_size_category(xyxyxyxy: npt.NDArray[np.float32]) -> npt.NDArray[np.int_]:
|
||||
"""
|
||||
Get the size category of a oriented bounding boxes array.
|
||||
|
||||
Args:
|
||||
xyxyxyxy (np.ndarray): The bounding boxes array shaped (N, 8).
|
||||
|
||||
Returns:
|
||||
(np.ndarray) The size category of each bounding box, matching
|
||||
the enum values of ObjectSizeCategory. Shaped (N,).
|
||||
"""
|
||||
if len(xyxyxyxy.shape) != 2 or xyxyxyxy.shape[1] != 8:
|
||||
raise ValueError("Oriented bounding boxes must be shaped (N, 8)")
|
||||
|
||||
# Shoelace formula
|
||||
x1, y1, x2, y2, x3, y3, x4, y4 = xyxyxyxy.T
|
||||
areas = 0.5 * np.abs(
|
||||
(x1 * y2 + x2 * y3 + x3 * y4 + x4 * y1)
|
||||
- (x2 * y1 + x3 * y2 + x4 * y3 + x1 * y4)
|
||||
)
|
||||
|
||||
result = np.full(areas.shape, ObjectSizeCategory.ANY.value)
|
||||
SM, LG = SIZE_THRESHOLDS
|
||||
result[areas < SM] = ObjectSizeCategory.SMALL.value
|
||||
result[(areas >= SM) & (areas < LG)] = ObjectSizeCategory.MEDIUM.value
|
||||
result[areas >= LG] = ObjectSizeCategory.LARGE.value
|
||||
return result
|
||||
|
||||
|
||||
def get_detection_size_category(
|
||||
detections: Detections, metric_target: MetricTarget = MetricTarget.BOXES
|
||||
) -> npt.NDArray[np.int_]:
|
||||
"""
|
||||
Get the size category of a detections object.
|
||||
|
||||
Args:
|
||||
xyxyxyxy (np.ndarray): The bounding boxes array shaped (N, 8).
|
||||
metric_target (MetricTarget): Determines whether boxes, masks or
|
||||
oriented bounding boxes are used.
|
||||
|
||||
Returns:
|
||||
(np.ndarray) The size category of each bounding box, matching
|
||||
the enum values of ObjectSizeCategory. Shaped (N,).
|
||||
"""
|
||||
if metric_target == MetricTarget.BOXES:
|
||||
return get_bbox_size_category(detections.xyxy)
|
||||
if metric_target == MetricTarget.MASKS:
|
||||
if detections.mask is None:
|
||||
raise ValueError("Detections mask is not available")
|
||||
return get_mask_size_category(detections.mask)
|
||||
if metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
|
||||
if detections.data.get(ORIENTED_BOX_COORDINATES) is None:
|
||||
raise ValueError("Detections oriented bounding boxes are not available")
|
||||
return get_obb_size_category(
|
||||
np.array(detections.data[ORIENTED_BOX_COORDINATES])
|
||||
)
|
||||
raise ValueError("Invalid metric type")
|
||||
@@ -0,0 +1,70 @@
|
||||
from typing import List, Tuple
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
|
||||
def ensure_pandas_installed():
|
||||
try:
|
||||
import pandas # noqa
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"`metrics` extra is required to run the function."
|
||||
" Run `pip install 'supervision[metrics]'` or"
|
||||
" `poetry add supervision -E metrics`"
|
||||
)
|
||||
|
||||
|
||||
def pad_mask(mask: npt.NDArray, new_shape: Tuple[int, int]) -> npt.NDArray:
|
||||
"""Pad a mask to a new shape, inserting zeros on the right and bottom."""
|
||||
if len(mask.shape) != 3:
|
||||
raise ValueError(f"Invalid mask shape: {mask.shape}. Expected: (N, H, W)")
|
||||
|
||||
new_mask = np.pad(
|
||||
mask,
|
||||
(
|
||||
(0, 0),
|
||||
(0, new_shape[0] - mask.shape[1]),
|
||||
(0, new_shape[1] - mask.shape[2]),
|
||||
),
|
||||
mode="constant",
|
||||
constant_values=0,
|
||||
)
|
||||
|
||||
return new_mask
|
||||
|
||||
|
||||
def unify_pad_masks_shape(
|
||||
*masks: npt.NDArray[np.bool_],
|
||||
) -> List[npt.NDArray[np.bool_]]:
|
||||
"""
|
||||
Given any number of (N, H, W) mask objects, return copies of the
|
||||
same (H, W), padded to the largest dimensions.
|
||||
|
||||
Args:
|
||||
*masks (np.ndarray): The mask arrays to unify. Each shaped (_, H, W),
|
||||
|
||||
Returns:
|
||||
List[np.ndarray]: The masks, padded to the largest dimensions.
|
||||
Each list element shaped (_, H_max, W_max)
|
||||
"""
|
||||
new_h = 0
|
||||
new_w = 0
|
||||
for mask in masks:
|
||||
if len(mask.shape) != 3:
|
||||
raise ValueError(f"Invalid mask shape: {mask.shape}. Expected: (N, H, W)")
|
||||
|
||||
_, h, w = mask.shape
|
||||
new_h = max(new_h, h)
|
||||
new_w = max(new_w, w)
|
||||
|
||||
results = []
|
||||
for mask in masks:
|
||||
results.append(pad_mask(mask, (new_h, new_w)))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def len0_like(data: npt.NDArray) -> npt.NDArray:
|
||||
"""Create an empty array with the same shape as input, but with 0 rows."""
|
||||
return np.empty((0, *data.shape[1:]), dtype=data.dtype)
|
||||
@@ -7,7 +7,6 @@ from supervision.detection.utils import box_iou_batch
|
||||
from supervision.tracker.byte_tracker import matching
|
||||
from supervision.tracker.byte_tracker.basetrack import BaseTrack, TrackState
|
||||
from supervision.tracker.byte_tracker.kalman_filter import KalmanFilter
|
||||
from supervision.utils.internal import deprecated_parameter
|
||||
|
||||
|
||||
class STrack(BaseTrack):
|
||||
@@ -197,48 +196,24 @@ class ByteTrack:
|
||||
</video>
|
||||
|
||||
Parameters:
|
||||
track_activation_threshold (float, optional): Detection confidence threshold
|
||||
track_activation_threshold (float): Detection confidence threshold
|
||||
for track activation. Increasing track_activation_threshold improves accuracy
|
||||
and stability but might miss true detections. Decreasing it increases
|
||||
completeness but risks introducing noise and instability.
|
||||
lost_track_buffer (int, optional): Number of frames to buffer when a track is lost.
|
||||
lost_track_buffer (int): Number of frames to buffer when a track is lost.
|
||||
Increasing lost_track_buffer enhances occlusion handling, significantly
|
||||
reducing the likelihood of track fragmentation or disappearance caused
|
||||
by brief detection gaps.
|
||||
minimum_matching_threshold (float, optional): Threshold for matching tracks with detections.
|
||||
minimum_matching_threshold (float): Threshold for matching tracks with detections.
|
||||
Increasing minimum_matching_threshold improves accuracy but risks fragmentation.
|
||||
Decreasing it improves completeness but risks false positives and drift.
|
||||
frame_rate (int, optional): The frame rate of the video.
|
||||
minimum_consecutive_frames (int, optional): Number of consecutive frames that an object must
|
||||
frame_rate (int): The frame rate of the video.
|
||||
minimum_consecutive_frames (int): Number of consecutive frames that an object must
|
||||
be tracked before it is considered a 'valid' track.
|
||||
Increasing minimum_consecutive_frames prevents the creation of accidental tracks from
|
||||
false detection or double detection, but risks missing shorter tracks.
|
||||
""" # noqa: E501 // docs
|
||||
|
||||
@deprecated_parameter(
|
||||
old_parameter="track_buffer",
|
||||
new_parameter="lost_track_buffer",
|
||||
map_function=lambda x: x,
|
||||
warning_message="`{old_parameter}` in `{function_name}` is deprecated and will "
|
||||
"be remove in `supervision-0.23.0`. Use '{new_parameter}' "
|
||||
"instead.",
|
||||
)
|
||||
@deprecated_parameter(
|
||||
old_parameter="track_thresh",
|
||||
new_parameter="track_activation_threshold",
|
||||
map_function=lambda x: x,
|
||||
warning_message="`{old_parameter}` in `{function_name}` is deprecated and will "
|
||||
"be remove in `supervision-0.23.0`. Use '{new_parameter}' "
|
||||
"instead.",
|
||||
)
|
||||
@deprecated_parameter(
|
||||
old_parameter="match_thresh",
|
||||
new_parameter="minimum_matching_threshold",
|
||||
map_function=lambda x: x,
|
||||
warning_message="`{old_parameter}` in `{function_name}` is deprecated and will "
|
||||
"be remove in `supervision-0.23.0`. Use '{new_parameter}' "
|
||||
"instead.",
|
||||
)
|
||||
def __init__(
|
||||
self,
|
||||
track_activation_threshold: float = 0.25,
|
||||
@@ -276,7 +251,7 @@ class ByteTrack:
|
||||
model = YOLO(<MODEL_PATH>)
|
||||
tracker = sv.ByteTrack()
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
label_annotator = sv.LabelAnnotator()
|
||||
|
||||
def callback(frame: np.ndarray, index: int) -> np.ndarray:
|
||||
@@ -286,7 +261,7 @@ class ByteTrack:
|
||||
|
||||
labels = [f"#{tracker_id}" for tracker_id in detections.tracker_id]
|
||||
|
||||
annotated_frame = bounding_box_annotator.annotate(
|
||||
annotated_frame = box_annotator.annotate(
|
||||
scene=frame.copy(), detections=detections)
|
||||
annotated_frame = label_annotator.annotate(
|
||||
scene=annotated_frame, detections=detections, labels=labels)
|
||||
|
||||
@@ -8,11 +8,13 @@ from PIL import Image
|
||||
from supervision.annotators.base import ImageType
|
||||
|
||||
|
||||
def convert_for_annotation_method(annotate_func):
|
||||
def ensure_cv2_image_for_annotation(annotate_func):
|
||||
"""
|
||||
Decorates `BaseAnnotator.annotate` implementations, converts scene to
|
||||
an image type used internally by the annotators, converts back when annotation
|
||||
is complete.
|
||||
|
||||
Assumes the annotators modify the scene in-place.
|
||||
"""
|
||||
|
||||
@wraps(annotate_func)
|
||||
@@ -21,19 +23,22 @@ def convert_for_annotation_method(annotate_func):
|
||||
return annotate_func(self, scene, *args, **kwargs)
|
||||
|
||||
if isinstance(scene, Image.Image):
|
||||
scene = pillow_to_cv2(scene)
|
||||
annotated = annotate_func(self, scene, *args, **kwargs)
|
||||
return cv2_to_pillow(image=annotated)
|
||||
scene_np = pillow_to_cv2(scene)
|
||||
annotated_np = annotate_func(self, scene_np, *args, **kwargs)
|
||||
scene.paste(cv2_to_pillow(annotated_np))
|
||||
return scene
|
||||
|
||||
raise ValueError(f"Unsupported image type: {type(scene)}")
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def convert_for_image_processing(image_processing_fun):
|
||||
def ensure_cv2_image_for_processing(image_processing_fun):
|
||||
"""
|
||||
Decorates image processing functions that accept np.ndarray, converting `image` to
|
||||
np.ndarray, converts back when processing is complete.
|
||||
|
||||
Assumes the annotators do NOT modify the scene in-place.
|
||||
"""
|
||||
|
||||
@wraps(image_processing_fun)
|
||||
@@ -44,13 +49,37 @@ def convert_for_image_processing(image_processing_fun):
|
||||
if isinstance(image, Image.Image):
|
||||
scene = pillow_to_cv2(image)
|
||||
annotated = image_processing_fun(scene, *args, **kwargs)
|
||||
return cv2_to_pillow(image=annotated)
|
||||
return cv2_to_pillow(annotated)
|
||||
|
||||
raise ValueError(f"Unsupported image type: {type(image)}")
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def ensure_pil_image_for_annotation(annotate_func):
|
||||
"""
|
||||
Decorates image processing functions that accept np.ndarray, converting `image` to
|
||||
PIL image, converts back when processing is complete.
|
||||
|
||||
Assumes the annotators modify the scene in-place.
|
||||
"""
|
||||
|
||||
@wraps(annotate_func)
|
||||
def wrapper(self, scene: ImageType, *args, **kwargs):
|
||||
if isinstance(scene, np.ndarray):
|
||||
scene_pil = cv2_to_pillow(scene)
|
||||
annotated_pil = annotate_func(self, scene_pil, *args, **kwargs)
|
||||
np.copyto(scene, pillow_to_cv2(annotated_pil))
|
||||
return scene
|
||||
|
||||
if isinstance(scene, Image.Image):
|
||||
return annotate_func(self, scene, *args, **kwargs)
|
||||
|
||||
raise ValueError(f"Unsupported image type: {type(scene)}")
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def images_to_cv2(images: List[ImageType]) -> List[np.ndarray]:
|
||||
"""
|
||||
Converts images provided either as Pillow images or OpenCV
|
||||
@@ -67,7 +96,7 @@ def images_to_cv2(images: List[ImageType]) -> List[np.ndarray]:
|
||||
result = []
|
||||
for image in images:
|
||||
if issubclass(type(image), Image.Image):
|
||||
image = pillow_to_cv2(image=image)
|
||||
image = pillow_to_cv2(image)
|
||||
result.append(image)
|
||||
return result
|
||||
|
||||
|
||||
+19
-19
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Union
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import yaml
|
||||
@@ -57,20 +57,20 @@ def list_files_with_extensions(
|
||||
return files_with_extensions
|
||||
|
||||
|
||||
def read_txt_file(file_path: str, skip_empty: bool = False) -> List[str]:
|
||||
def read_txt_file(file_path: Union[str, Path], skip_empty: bool = False) -> List[str]:
|
||||
"""
|
||||
Read a text file and return a list of strings without newline characters.
|
||||
Optionally skip empty lines.
|
||||
|
||||
Args:
|
||||
file_path (str): The path to the text file.
|
||||
file_path (Union[str, Path]): The file path as a string or Path object.
|
||||
skip_empty (bool): If True, skip lines that are empty or contain only
|
||||
whitespace. Default is False.
|
||||
|
||||
Returns:
|
||||
List[str]: A list of strings representing the lines in the text file.
|
||||
"""
|
||||
with open(file_path, "r") as file:
|
||||
with open(str(file_path), "r") as file:
|
||||
if skip_empty:
|
||||
lines = [line.rstrip("\n") for line in file if line.strip()]
|
||||
else:
|
||||
@@ -79,71 +79,71 @@ def read_txt_file(file_path: str, skip_empty: bool = False) -> List[str]:
|
||||
return lines
|
||||
|
||||
|
||||
def save_text_file(lines: List[str], file_path: str):
|
||||
def save_text_file(lines: List[str], file_path: Union[str, Path]) -> None:
|
||||
"""
|
||||
Write a list of strings to a text file, each string on a new line.
|
||||
|
||||
Args:
|
||||
lines (List[str]): The list of strings to be written to the file.
|
||||
file_path (str): The path to the text file.
|
||||
file_path (Union[str, Path]): The file path as a string or Path object.
|
||||
"""
|
||||
with open(file_path, "w") as file:
|
||||
with open(str(file_path), "w") as file:
|
||||
for line in lines:
|
||||
file.write(line + "\n")
|
||||
|
||||
|
||||
def read_json_file(file_path: str) -> dict:
|
||||
def read_json_file(file_path: Union[str, Path]) -> Dict:
|
||||
"""
|
||||
Read a json file and return a dict.
|
||||
|
||||
Args:
|
||||
file_path (str): The path to the json file.
|
||||
file_path (Union[str, Path]): The file path as a string or Path object.
|
||||
|
||||
Returns:
|
||||
dict: A dict of annotations information
|
||||
"""
|
||||
with open(file_path, "r") as file:
|
||||
with open(str(file_path), "r") as file:
|
||||
data = json.load(file)
|
||||
return data
|
||||
|
||||
|
||||
def save_json_file(data: dict, file_path: str, indent: int = 3) -> None:
|
||||
def save_json_file(data: dict, file_path: Union[str, Path], indent: int = 3) -> None:
|
||||
"""
|
||||
Write a dict to a json file.
|
||||
|
||||
Args:
|
||||
indent:
|
||||
data (dict): dict with unique keys and value as pair.
|
||||
file_path (str): The path to the json file.
|
||||
file_path (Union[str, Path]): The file path as a string or Path object.
|
||||
"""
|
||||
with open(file_path, "w") as fp:
|
||||
with open(str(file_path), "w") as fp:
|
||||
json.dump(data, fp, cls=NumpyJsonEncoder, indent=indent)
|
||||
|
||||
|
||||
def read_yaml_file(file_path: str) -> dict:
|
||||
def read_yaml_file(file_path: Union[str, Path]) -> Dict:
|
||||
"""
|
||||
Read a yaml file and return a dict.
|
||||
|
||||
Args:
|
||||
file_path (str): The path to the yaml file.
|
||||
file_path (Union[str, Path]): The file path as a string or Path object.
|
||||
|
||||
Returns:
|
||||
dict: A dict of content information
|
||||
"""
|
||||
with open(file_path, "r") as file:
|
||||
with open(str(file_path), "r") as file:
|
||||
data = yaml.safe_load(file)
|
||||
return data
|
||||
|
||||
|
||||
def save_yaml_file(data: dict, file_path: str) -> None:
|
||||
def save_yaml_file(data: dict, file_path: Union[str, Path]) -> None:
|
||||
"""
|
||||
Save a dict to a json file.
|
||||
|
||||
Args:
|
||||
indent:
|
||||
data (dict): dict with unique keys and value as pair.
|
||||
file_path (str): The path to the json file.
|
||||
file_path (Union[str, Path]): The file path as a string or Path object.
|
||||
"""
|
||||
|
||||
with open(file_path, "w") as outfile:
|
||||
with open(str(file_path), "w") as outfile:
|
||||
yaml.dump(data, outfile, sort_keys=False, default_flow_style=None)
|
||||
|
||||
+33
-13
@@ -14,8 +14,8 @@ from supervision.draw.color import Color, unify_to_bgr
|
||||
from supervision.draw.utils import calculate_optimal_text_scale, draw_text
|
||||
from supervision.geometry.core import Point
|
||||
from supervision.utils.conversion import (
|
||||
convert_for_image_processing,
|
||||
cv2_to_pillow,
|
||||
ensure_cv2_image_for_processing,
|
||||
images_to_cv2,
|
||||
)
|
||||
from supervision.utils.iterables import create_batches, fill
|
||||
@@ -25,7 +25,7 @@ RelativePosition = Literal["top", "bottom"]
|
||||
MAX_COLUMNS_FOR_SINGLE_ROW_GRID = 3
|
||||
|
||||
|
||||
@convert_for_image_processing
|
||||
@ensure_cv2_image_for_processing
|
||||
def crop_image(
|
||||
image: ImageType,
|
||||
xyxy: Union[npt.NDArray[int], List[int], Tuple[int, int, int, int]],
|
||||
@@ -86,7 +86,7 @@ def crop_image(
|
||||
return image[y_min:y_max, x_min:x_max]
|
||||
|
||||
|
||||
@convert_for_image_processing
|
||||
@ensure_cv2_image_for_processing
|
||||
def scale_image(image: ImageType, scale_factor: float) -> ImageType:
|
||||
"""
|
||||
Scales the given image based on the given scale factor.
|
||||
@@ -143,7 +143,7 @@ def scale_image(image: ImageType, scale_factor: float) -> ImageType:
|
||||
return cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
|
||||
@convert_for_image_processing
|
||||
@ensure_cv2_image_for_processing
|
||||
def resize_image(
|
||||
image: ImageType,
|
||||
resolution_wh: Tuple[int, int],
|
||||
@@ -158,7 +158,7 @@ def resize_image(
|
||||
accepting either `numpy.ndarray` or `PIL.Image.Image`.
|
||||
resolution_wh (Tuple[int, int]): The target resolution as
|
||||
`(width, height)`.
|
||||
keep_aspect_ratio (bool, optional): Flag to maintain the image's original
|
||||
keep_aspect_ratio (bool): Flag to maintain the image's original
|
||||
aspect ratio. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
@@ -216,7 +216,7 @@ def resize_image(
|
||||
return cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
|
||||
@convert_for_image_processing
|
||||
@ensure_cv2_image_for_processing
|
||||
def letterbox_image(
|
||||
image: ImageType,
|
||||
resolution_wh: Tuple[int, int],
|
||||
@@ -270,6 +270,7 @@ def letterbox_image(
|
||||
|
||||
{ align=center width="800" }
|
||||
""" # noqa E501 // docs
|
||||
assert isinstance(image, np.ndarray)
|
||||
color = unify_to_bgr(color=color)
|
||||
resized_image = resize_image(
|
||||
image=image, resolution_wh=resolution_wh, keep_aspect_ratio=True
|
||||
@@ -279,7 +280,7 @@ def letterbox_image(
|
||||
padding_bottom = resolution_wh[1] - height_new - padding_top
|
||||
padding_left = (resolution_wh[0] - width_new) // 2
|
||||
padding_right = resolution_wh[0] - width_new - padding_left
|
||||
return cv2.copyMakeBorder(
|
||||
image_with_borders = cv2.copyMakeBorder(
|
||||
resized_image,
|
||||
padding_top,
|
||||
padding_bottom,
|
||||
@@ -289,6 +290,14 @@ def letterbox_image(
|
||||
value=color,
|
||||
)
|
||||
|
||||
if image.shape[2] == 4:
|
||||
image[:padding_top, :, 3] = 0
|
||||
image[height_new - padding_bottom :, :, 3] = 0
|
||||
image[:, :padding_left, 3] = 0
|
||||
image[:, width_new - padding_right :, 3] = 0
|
||||
|
||||
return image_with_borders
|
||||
|
||||
|
||||
def overlay_image(
|
||||
image: npt.NDArray[np.uint8],
|
||||
@@ -341,9 +350,20 @@ def overlay_image(
|
||||
crop_x_max = image_width - max((anchor_x + image_width) - scene_width, 0)
|
||||
crop_y_max = image_height - max((anchor_y + image_height) - scene_height, 0)
|
||||
|
||||
image[y_min:y_max, x_min:x_max] = overlay[
|
||||
crop_y_min:crop_y_max, crop_x_min:crop_x_max
|
||||
]
|
||||
if overlay.shape[2] == 4:
|
||||
b, g, r, alpha = cv2.split(
|
||||
overlay[crop_y_min:crop_y_max, crop_x_min:crop_x_max]
|
||||
)
|
||||
alpha = alpha[:, :, None] / 255.0
|
||||
overlay_color = cv2.merge((b, g, r))
|
||||
|
||||
roi = image[y_min:y_max, x_min:x_max]
|
||||
roi[:] = roi * (1 - alpha) + overlay_color * alpha
|
||||
image[y_min:y_max, x_min:x_max] = roi
|
||||
else:
|
||||
image[y_min:y_max, x_min:x_max] = overlay[
|
||||
crop_y_min:crop_y_max, crop_x_min:crop_x_max
|
||||
]
|
||||
|
||||
return image
|
||||
|
||||
@@ -360,9 +380,9 @@ class ImageSink:
|
||||
|
||||
Args:
|
||||
target_dir_path (str): The target directory where images will be saved.
|
||||
overwrite (bool, optional): Whether to overwrite the existing directory.
|
||||
overwrite (bool): Whether to overwrite the existing directory.
|
||||
Defaults to False.
|
||||
image_name_pattern (str, optional): The image file name pattern.
|
||||
image_name_pattern (str): The image file name pattern.
|
||||
Defaults to "image_{:05d}.png".
|
||||
|
||||
Examples:
|
||||
@@ -399,7 +419,7 @@ class ImageSink:
|
||||
Args:
|
||||
image (np.ndarray): The image to be saved. The image must be in BGR color
|
||||
format.
|
||||
image_name (str, optional): The name to use for the saved image.
|
||||
image_name (Optional[str]): The name to use for the saved image.
|
||||
If not provided, a name will be
|
||||
generated using the `image_name_pattern`.
|
||||
"""
|
||||
|
||||
@@ -2,7 +2,7 @@ import functools
|
||||
import inspect
|
||||
import os
|
||||
import warnings
|
||||
from typing import Any, Callable, Set
|
||||
from typing import Any, Callable, Generic, Optional, Set, TypeVar
|
||||
|
||||
|
||||
class SupervisionWarnings(Warning):
|
||||
@@ -31,6 +31,16 @@ else:
|
||||
warnings.simplefilter("always", SupervisionWarnings)
|
||||
|
||||
|
||||
def warn_deprecated(message: str):
|
||||
"""
|
||||
Issue a warning that a function is deprecated.
|
||||
|
||||
Args:
|
||||
message (str): The message to display when the function is called.
|
||||
"""
|
||||
warnings.warn(message, category=SupervisionWarnings, stacklevel=2)
|
||||
|
||||
|
||||
def deprecated_parameter(
|
||||
old_parameter: str,
|
||||
new_parameter: str,
|
||||
@@ -46,9 +56,9 @@ def deprecated_parameter(
|
||||
Parameters:
|
||||
old_parameter (str): The name of the deprecated parameter.
|
||||
new_parameter (str): The name of the parameter that should be used instead.
|
||||
map_function (Callable, optional): A function used to map the value of the old
|
||||
map_function (Callable): A function used to map the value of the old
|
||||
parameter to the new parameter. Defaults to the identity function.
|
||||
warning_message (str, optional): The warning message to be displayed when the
|
||||
warning_message (str): The warning message to be displayed when the
|
||||
deprecated parameter is used. Defaults to a generic warning message with
|
||||
placeholders for the old parameter, new parameter, and function name.
|
||||
**message_kwargs: Additional keyword arguments that can be used to customize
|
||||
@@ -82,15 +92,13 @@ def deprecated_parameter(
|
||||
else:
|
||||
function_name = func.__name__
|
||||
|
||||
warnings.warn(
|
||||
warn_deprecated(
|
||||
message=warning_message.format(
|
||||
function_name=function_name,
|
||||
old_parameter=old_parameter,
|
||||
new_parameter=new_parameter,
|
||||
**message_kwargs,
|
||||
),
|
||||
category=SupervisionWarnings,
|
||||
stacklevel=2,
|
||||
)
|
||||
)
|
||||
|
||||
kwargs[new_parameter] = map_function(kwargs.pop(old_parameter))
|
||||
@@ -106,11 +114,7 @@ def deprecated(reason: str):
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
warnings.warn(
|
||||
f"{func.__name__} is deprecated: {reason}",
|
||||
category=SupervisionWarnings,
|
||||
stacklevel=2,
|
||||
)
|
||||
warn_deprecated(f"{func.__name__} is deprecated: {reason}")
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
@@ -118,7 +122,10 @@ def deprecated(reason: str):
|
||||
return decorator
|
||||
|
||||
|
||||
class classproperty(property):
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class classproperty(Generic[T]):
|
||||
"""
|
||||
A decorator that combines @classmethod and @property.
|
||||
It allows a method to be accessed as a property of the class,
|
||||
@@ -130,17 +137,27 @@ class classproperty(property):
|
||||
...
|
||||
"""
|
||||
|
||||
def __get__(self, owner_self: object, owner_cls: type) -> object:
|
||||
def __init__(self, fget: Callable[..., T]):
|
||||
"""
|
||||
Args:
|
||||
The function that is called when the property is accessed.
|
||||
"""
|
||||
self.fget = fget
|
||||
|
||||
def __get__(self, owner_self: Any, owner_cls: Optional[type] = None) -> T:
|
||||
"""
|
||||
Override the __get__ method to return the result of the function call.
|
||||
|
||||
Args:
|
||||
owner_self: The instance through which the attribute was accessed, or None.
|
||||
owner_cls: The class through which the attribute was accessed.
|
||||
owner_self: The instance through which the attribute was accessed, or None.
|
||||
Irrelevant for class properties.
|
||||
owner_cls: The class through which the attribute was accessed.
|
||||
|
||||
Returns:
|
||||
The result of calling the function stored in 'fget' with 'owner_cls'.
|
||||
The result of calling the function stored in 'fget' with 'owner_cls'.
|
||||
"""
|
||||
if self.fget is None:
|
||||
raise AttributeError("unreadable attribute")
|
||||
return self.fget(owner_cls)
|
||||
|
||||
|
||||
|
||||
@@ -68,3 +68,17 @@ def fill(sequence: List[V], desired_size: int, content: V) -> List[V]:
|
||||
missing_size = max(0, desired_size - len(sequence))
|
||||
sequence.extend([content] * missing_size)
|
||||
return sequence
|
||||
|
||||
|
||||
def find_duplicates(sequence: List) -> List:
|
||||
"""
|
||||
Find all duplicate elements in the input sequence.
|
||||
"""
|
||||
seen = set()
|
||||
duplicates = set()
|
||||
for element in sequence:
|
||||
if element in seen:
|
||||
duplicates.add(element)
|
||||
else:
|
||||
seen.add(element)
|
||||
return list(duplicates)
|
||||
|
||||
@@ -17,7 +17,7 @@ def plot_image(
|
||||
Args:
|
||||
image (ImageType): The frame to be displayed ImageType
|
||||
is a flexible type, accepting either `numpy.ndarray` or `PIL.Image.Image`.
|
||||
size (Tuple[int, int]): The size of the plot.
|
||||
size (Tuple[int, int]): The size of the plot in inches.
|
||||
cmap (str): the colormap to use for single channel images.
|
||||
|
||||
Examples:
|
||||
|
||||
+25
-25
@@ -8,8 +8,6 @@ from typing import Callable, Generator, Optional, Tuple
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from supervision.utils.internal import deprecated
|
||||
|
||||
|
||||
@dataclass
|
||||
class VideoInfo:
|
||||
@@ -21,7 +19,7 @@ class VideoInfo:
|
||||
width (int): width of the video in pixels
|
||||
height (int): height of the video in pixels
|
||||
fps (int): frames per second of the video
|
||||
total_frames (int, optional): total number of frames in the video,
|
||||
total_frames (Optional[int]): total number of frames in the video,
|
||||
default is None
|
||||
|
||||
Examples:
|
||||
@@ -118,7 +116,9 @@ class VideoSink:
|
||||
self.__writer.release()
|
||||
|
||||
|
||||
def _validate_and_setup_video(source_path: str, start: int, end: Optional[int]):
|
||||
def _validate_and_setup_video(
|
||||
source_path: str, start: int, end: Optional[int], iterative_seek: bool = False
|
||||
):
|
||||
video = cv2.VideoCapture(source_path)
|
||||
if not video.isOpened():
|
||||
raise Exception(f"Could not open video at {source_path}")
|
||||
@@ -127,12 +127,25 @@ def _validate_and_setup_video(source_path: str, start: int, end: Optional[int]):
|
||||
raise Exception("Requested frames are outbound")
|
||||
start = max(start, 0)
|
||||
end = min(end, total_frames) if end is not None else total_frames
|
||||
video.set(cv2.CAP_PROP_POS_FRAMES, start)
|
||||
|
||||
if iterative_seek:
|
||||
while start > 0:
|
||||
success = video.grab()
|
||||
if not success:
|
||||
break
|
||||
start -= 1
|
||||
elif start > 0:
|
||||
video.set(cv2.CAP_PROP_POS_FRAMES, start)
|
||||
|
||||
return video, start, end
|
||||
|
||||
|
||||
def get_video_frames_generator(
|
||||
source_path: str, stride: int = 1, start: int = 0, end: Optional[int] = None
|
||||
source_path: str,
|
||||
stride: int = 1,
|
||||
start: int = 0,
|
||||
end: Optional[int] = None,
|
||||
iterative_seek: bool = False,
|
||||
) -> Generator[np.ndarray, None, None]:
|
||||
"""
|
||||
Get a generator that yields the frames of the video.
|
||||
@@ -145,6 +158,9 @@ def get_video_frames_generator(
|
||||
video should generate frames
|
||||
end (Optional[int]): Indicates the ending position at which video
|
||||
should stop generating frames. If None, video will be read to the end.
|
||||
iterative_seek (bool): If True, the generator will seek to the
|
||||
`start` frame by grabbing each frame, which is much slower. This is a
|
||||
workaround for videos that don't open at all when you set the `start` value.
|
||||
|
||||
Returns:
|
||||
(Generator[np.ndarray, None, None]): A generator that yields the
|
||||
@@ -158,7 +174,9 @@ def get_video_frames_generator(
|
||||
...
|
||||
```
|
||||
"""
|
||||
video, start, end = _validate_and_setup_video(source_path, start, end)
|
||||
video, start, end = _validate_and_setup_video(
|
||||
source_path, start, end, iterative_seek
|
||||
)
|
||||
frame_position = start
|
||||
while True:
|
||||
success, frame = video.read()
|
||||
@@ -239,24 +257,6 @@ class FPSMonitor:
|
||||
""" # noqa: E501 // docs
|
||||
self.all_timestamps = deque(maxlen=sample_size)
|
||||
|
||||
@deprecated(
|
||||
"`FPSMonitor.__call__` is deprecated and will be removed in "
|
||||
"`supervision-0.22.0`. Use `FPSMonitor.fps` instead."
|
||||
)
|
||||
def __call__(self) -> float:
|
||||
"""
|
||||
!!! failure "Deprecated"
|
||||
|
||||
`FPSMonitor.__call__` is deprecated and will be removed in
|
||||
`supervision-0.22.0`. Use `FPSMonitor.fps` instead.
|
||||
|
||||
Computes and returns the average FPS based on the stored time stamps.
|
||||
|
||||
Returns:
|
||||
float: The average FPS. Returns 0.0 if no time stamps are stored.
|
||||
"""
|
||||
return self.fps
|
||||
|
||||
@property
|
||||
def fps(self) -> float:
|
||||
"""
|
||||
|
||||
@@ -62,8 +62,7 @@ def validate_keypoint_confidence(confidence: Any, n: int, m: int) -> None:
|
||||
if not is_valid:
|
||||
raise ValueError(
|
||||
f"confidence must be a 1D np.ndarray with shape {expected_shape}, but "
|
||||
"got"
|
||||
f"shape {actual_shape}"
|
||||
f"got shape {actual_shape}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
from test.test_utils import mock_detections
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
@@ -7,6 +6,7 @@ import pytest
|
||||
|
||||
from supervision.annotators.utils import ColorLookup, resolve_color_idx
|
||||
from supervision.detection.core import Detections
|
||||
from test.test_utils import mock_detections
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
from test.test_utils import mock_detections
|
||||
from typing import List, Optional
|
||||
|
||||
import defusedxml.ElementTree as ET
|
||||
import numpy as np
|
||||
import pytest
|
||||
from defusedxml import ElementTree
|
||||
|
||||
from supervision.dataset.formats.pascal_voc import (
|
||||
detections_from_xml_obj,
|
||||
object_to_pascal_voc,
|
||||
parse_polygon_points,
|
||||
)
|
||||
from test.test_utils import mock_detections
|
||||
|
||||
|
||||
def are_xml_elements_equal(elem1, elem2):
|
||||
@@ -36,7 +36,7 @@ def are_xml_elements_equal(elem1, elem2):
|
||||
np.array([0, 0, 10, 10]),
|
||||
"test",
|
||||
None,
|
||||
ET.fromstring(
|
||||
ElementTree.fromstring(
|
||||
"""<object><name>test</name><bndbox><xmin>1</xmin><ymin>1</ymin>
|
||||
<xmax>11</xmax><ymax>11</ymax></bndbox></object>"""
|
||||
),
|
||||
@@ -46,7 +46,7 @@ def are_xml_elements_equal(elem1, elem2):
|
||||
np.array([0, 0, 10, 10]),
|
||||
"test",
|
||||
np.array([[0, 0], [10, 0], [10, 10], [0, 10]]),
|
||||
ET.fromstring(
|
||||
ElementTree.fromstring(
|
||||
"""<object><name>test</name><bndbox><xmin>1</xmin><ymin>1</ymin>
|
||||
<xmax>11</xmax><ymax>11</ymax>
|
||||
</bndbox><polygon><x1>1</x1><y1>1</y1><x2>11</x2>
|
||||
@@ -73,7 +73,7 @@ def test_object_to_pascal_voc(
|
||||
"polygon_element, expected_result, exception",
|
||||
[
|
||||
(
|
||||
ET.fromstring(
|
||||
ElementTree.fromstring(
|
||||
"""<polygon><x1>0</x1><y1>0</y1><x2>10</x2><y2>0</y2><x3>10</x3>
|
||||
<y3>10</y3><x4>0</x4><y4>10</y4></polygon>"""
|
||||
),
|
||||
@@ -160,6 +160,6 @@ def test_detections_from_xml_obj(
|
||||
xml_string, classes, resolution_wh, force_masks, expected_result, exception
|
||||
):
|
||||
with exception:
|
||||
root = ET.fromstring(xml_string)
|
||||
root = ElementTree.fromstring(xml_string)
|
||||
result, _ = detections_from_xml_obj(root, classes, resolution_wh, force_masks)
|
||||
assert result == expected_result
|
||||
|
||||
@@ -13,7 +13,7 @@ from supervision.dataset.formats.yolo import (
|
||||
from supervision.detection.core import Detections
|
||||
|
||||
|
||||
def _mock_simple_mask(resolution_wh: Tuple[int, int], box: List[int]) -> np.array:
|
||||
def _mock_simple_mask(resolution_wh: Tuple[int, int], box: List[int]) -> np.ndarray:
|
||||
x_min, y_min, x_max, y_max = box
|
||||
mask = np.full(resolution_wh, False, dtype=bool)
|
||||
mask[y_min:y_max, x_min:x_max] = True
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
from test.test_utils import mock_detections
|
||||
from typing import List, Optional
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from supervision import DetectionDataset
|
||||
from test.test_utils import mock_detections
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
from test.test_utils import mock_detections
|
||||
from typing import Dict, List, Optional, Tuple, TypeVar
|
||||
|
||||
import numpy as np
|
||||
@@ -15,6 +14,7 @@ from supervision.dataset.utils import (
|
||||
rle_to_mask,
|
||||
train_test_split,
|
||||
)
|
||||
from test.test_utils import mock_detections
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
from test.test_utils import mock_detections
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
@@ -7,6 +6,7 @@ import pytest
|
||||
|
||||
from supervision.detection.core import Detections, merge_inner_detection_object_pair
|
||||
from supervision.geometry.core import Position
|
||||
from test.test_utils import mock_detections
|
||||
|
||||
PREDICTIONS = np.array(
|
||||
[
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import csv
|
||||
import os
|
||||
from test.test_utils import mock_detections
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pytest
|
||||
|
||||
import supervision as sv
|
||||
from test.test_utils import mock_detections
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import json
|
||||
import os
|
||||
from test.test_utils import mock_detections
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pytest
|
||||
|
||||
import supervision as sv
|
||||
from test.test_utils import mock_detections
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
from test.test_utils import mock_detections
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from supervision import LineZone
|
||||
from supervision.geometry.core import Point, Position, Vector
|
||||
from test.test_utils import mock_detections
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from supervision.detection.lmm import from_florence_2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"florence_result, resolution_wh, expected_results, exception",
|
||||
[
|
||||
( # Object detection: empty
|
||||
{"<OD>": {"bboxes": [], "labels": []}},
|
||||
(10, 10),
|
||||
(np.array([], dtype=np.float32), np.array([]), None, None),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
( # Object detection: two detections
|
||||
{
|
||||
"<OD>": {
|
||||
"bboxes": [[4, 4, 6, 6], [5, 5, 7, 7]],
|
||||
"labels": ["car", "door"],
|
||||
}
|
||||
},
|
||||
(10, 10),
|
||||
(
|
||||
np.array([[4, 4, 6, 6], [5, 5, 7, 7]], dtype=np.float32),
|
||||
np.array(["car", "door"]),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
( # Caption: unsupported
|
||||
{"<CAPTION>": "A green car parked in front of a yellow building."},
|
||||
(10, 10),
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
( # Detailed Caption: unsupported
|
||||
{
|
||||
"<DETAILED_CAPTION>": "The image shows a blue Volkswagen Beetle parked "
|
||||
"in front of a yellow building with two brown doors, surrounded by "
|
||||
"trees and a clear blue sky."
|
||||
},
|
||||
(10, 10),
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
( # More Detailed Caption: unsupported
|
||||
{
|
||||
"<MORE_DETAILED_CAPTION>": "The image shows a vintage Volkswagen "
|
||||
"Beetle car parked on a "
|
||||
"cobblestone street in front of a yellow building with two wooden "
|
||||
"doors. The car is painted in a bright turquoise color and has a "
|
||||
"white stripe running along the side. It has two doors on either side "
|
||||
"of the car, one on top of the other, and a small window on the "
|
||||
"front. The building appears to be old and dilapidated, with peeling "
|
||||
"paint and crumbling walls. The sky is blue and there are trees in "
|
||||
"the background."
|
||||
},
|
||||
(10, 10),
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
( # Caption to Phrase Grounding: empty
|
||||
{"<CAPTION_TO_PHRASE_GROUNDING>": {"bboxes": [], "labels": []}},
|
||||
(10, 10),
|
||||
(np.array([], dtype=np.float32), np.array([]), None, None),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
( # Caption to Phrase Grounding: two detections
|
||||
{
|
||||
"<CAPTION_TO_PHRASE_GROUNDING>": {
|
||||
"bboxes": [[4, 4, 6, 6], [5, 5, 7, 7]],
|
||||
"labels": ["a green car", "a yellow building"],
|
||||
}
|
||||
},
|
||||
(10, 10),
|
||||
(
|
||||
np.array([[4, 4, 6, 6], [5, 5, 7, 7]], dtype=np.float32),
|
||||
np.array(["a green car", "a yellow building"]),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
( # Dense Region caption: empty
|
||||
{"<DENSE_REGION_CAPTION>": {"bboxes": [], "labels": []}},
|
||||
(10, 10),
|
||||
(np.array([], dtype=np.float32), np.array([]), None, None),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
( # Caption to Phrase Grounding: two detections
|
||||
{
|
||||
"<DENSE_REGION_CAPTION>": {
|
||||
"bboxes": [[4, 4, 6, 6], [5, 5, 7, 7]],
|
||||
"labels": ["a green car", "a yellow building"],
|
||||
}
|
||||
},
|
||||
(10, 10),
|
||||
(
|
||||
np.array([[4, 4, 6, 6], [5, 5, 7, 7]], dtype=np.float32),
|
||||
np.array(["a green car", "a yellow building"]),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
( # Region proposal
|
||||
{
|
||||
"<REGION_PROPOSAL>": {
|
||||
"bboxes": [[4, 4, 6, 6], [5, 5, 7, 7]],
|
||||
"labels": ["", ""],
|
||||
}
|
||||
},
|
||||
(10, 10),
|
||||
(
|
||||
np.array([[4, 4, 6, 6], [5, 5, 7, 7]], dtype=np.float32),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
( # Referring Expression Segmentation
|
||||
{
|
||||
"<REFERRING_EXPRESSION_SEGMENTATION>": {
|
||||
"polygons": [[[1, 1, 2, 1, 2, 2, 1, 2]]],
|
||||
"labels": [""],
|
||||
}
|
||||
},
|
||||
(10, 10),
|
||||
(
|
||||
np.array([[1.0, 1.0, 2.0, 2.0]], dtype=np.float32),
|
||||
None,
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 1, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 1, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
None,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
( # Referring Expression Segmentation
|
||||
{
|
||||
"<REFERRING_EXPRESSION_SEGMENTATION>": {
|
||||
"polygons": [[[1, 1, 2, 1, 2, 2, 1, 2]]],
|
||||
"labels": [""],
|
||||
}
|
||||
},
|
||||
(10, 10),
|
||||
(
|
||||
np.array([[1.0, 1.0, 2.0, 2.0]], dtype=np.float32),
|
||||
None,
|
||||
np.array(
|
||||
[
|
||||
[
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 1, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 1, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
]
|
||||
],
|
||||
dtype=bool,
|
||||
),
|
||||
None,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
( # OCR: unsupported
|
||||
{"<OCR>": "A"},
|
||||
(10, 10),
|
||||
None,
|
||||
pytest.raises(ValueError),
|
||||
),
|
||||
( # OCR with Region: obb boxes
|
||||
{
|
||||
"<OCR_WITH_REGION>": {
|
||||
"quad_boxes": [[2, 2, 6, 4, 5, 6, 1, 5], [4, 4, 5, 5, 4, 6, 3, 5]],
|
||||
"labels": ["some text", "other text"],
|
||||
}
|
||||
},
|
||||
(10, 10),
|
||||
(
|
||||
np.array([[1, 2, 6, 6], [3, 4, 5, 6]], dtype=np.float32),
|
||||
np.array(["some text", "other text"]),
|
||||
None,
|
||||
np.array(
|
||||
[[[2, 2], [6, 4], [5, 6], [1, 5]], [[4, 4], [5, 5], [4, 6], [3, 5]]]
|
||||
),
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
( # Open Vocabulary Detection
|
||||
{
|
||||
"<OPEN_VOCABULARY_DETECTION>": {
|
||||
"bboxes": [[4, 4, 6, 6], [5, 5, 7, 7]],
|
||||
"bboxes_labels": ["cat", "cat"],
|
||||
"polygon": [],
|
||||
"polygons_labels": [],
|
||||
}
|
||||
},
|
||||
(10, 10),
|
||||
(
|
||||
np.array([[4, 4, 6, 6], [5, 5, 7, 7]], dtype=np.float32),
|
||||
np.array(["cat", "cat"]),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
( # Region to Category: empty
|
||||
{"<REGION_TO_CATEGORY>": "No object detected."},
|
||||
(10, 10),
|
||||
(np.empty((0, 4), dtype=np.float32), np.array([]), None, None),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
( # Region to Category: detected
|
||||
{"<REGION_TO_CATEGORY>": "some object<loc_300><loc_400><loc_500><loc_600>"},
|
||||
(10, 10),
|
||||
(
|
||||
np.array([[3, 4, 5, 6]], dtype=np.float32),
|
||||
np.array(["some object"]),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
( # Region to Description: empty
|
||||
{"<REGION_TO_DESCRIPTION>": "No object detected."},
|
||||
(10, 10),
|
||||
(np.empty((0, 4), dtype=np.float32), np.array([]), None, None),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
( # Region to Description: detected
|
||||
{"<REGION_TO_DESCRIPTION>": "descr<loc_300><loc_400><loc_500><loc_600>"},
|
||||
(10, 10),
|
||||
(
|
||||
np.array([[3, 4, 5, 6]], dtype=np.float32),
|
||||
np.array(["descr"]),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
DoesNotRaise(),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_florence_2(
|
||||
florence_result: dict,
|
||||
resolution_wh: Tuple[int, int],
|
||||
expected_results: Tuple[
|
||||
np.ndarray, Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray]
|
||||
],
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
with exception:
|
||||
result = from_florence_2(florence_result, resolution_wh)
|
||||
np.testing.assert_array_equal(result[0], expected_results[0])
|
||||
if expected_results[1] is None:
|
||||
assert result[1] is None
|
||||
else:
|
||||
np.testing.assert_array_equal(result[1], expected_results[1])
|
||||
if expected_results[2] is None:
|
||||
assert result[2] is None
|
||||
else:
|
||||
np.testing.assert_array_equal(result[2], expected_results[2])
|
||||
if expected_results[3] is None:
|
||||
assert result[3] is None
|
||||
else:
|
||||
np.testing.assert_array_equal(result[3], expected_results[3])
|
||||
@@ -1,10 +1,10 @@
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
from test.test_utils import mock_detections
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import supervision as sv
|
||||
from test.test_utils import mock_detections
|
||||
|
||||
DETECTION_BOXES = np.array(
|
||||
[
|
||||
|
||||
@@ -17,6 +17,8 @@ from supervision.detection.utils import (
|
||||
move_boxes,
|
||||
process_roboflow_result,
|
||||
scale_boxes,
|
||||
xcycwh_to_xyxy,
|
||||
xywh_to_xyxy,
|
||||
)
|
||||
|
||||
TEST_MASK = np.zeros((1, 1000, 1000), dtype=bool)
|
||||
@@ -1090,3 +1092,49 @@ def test_contains_multiple_segments(
|
||||
with exception:
|
||||
result = contains_multiple_segments(mask=mask, connectivity=connectivity)
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"xywh, expected_result",
|
||||
[
|
||||
(np.array([[10, 20, 30, 40]]), np.array([[10, 20, 40, 60]])), # standard case
|
||||
(np.array([[0, 0, 0, 0]]), np.array([[0, 0, 0, 0]])), # zero size bounding box
|
||||
(
|
||||
np.array([[50, 50, 100, 100]]),
|
||||
np.array([[50, 50, 150, 150]]),
|
||||
), # large bounding box
|
||||
(
|
||||
np.array([[-10, -20, 30, 40]]),
|
||||
np.array([[-10, -20, 20, 20]]),
|
||||
), # negative coordinates
|
||||
(np.array([[50, 50, 0, 30]]), np.array([[50, 50, 50, 80]])), # zero width
|
||||
(np.array([[50, 50, 20, 0]]), np.array([[50, 50, 70, 50]])), # zero height
|
||||
(np.array([]).reshape(0, 4), np.array([]).reshape(0, 4)), # empty array
|
||||
],
|
||||
)
|
||||
def test_xywh_to_xyxy(xywh: np.ndarray, expected_result: np.ndarray) -> None:
|
||||
result = xywh_to_xyxy(xywh)
|
||||
np.testing.assert_array_equal(result, expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"xcycwh, expected_result",
|
||||
[
|
||||
(np.array([[50, 50, 20, 30]]), np.array([[40, 35, 60, 65]])), # standard case
|
||||
(np.array([[0, 0, 0, 0]]), np.array([[0, 0, 0, 0]])), # zero size bounding box
|
||||
(
|
||||
np.array([[50, 50, 100, 100]]),
|
||||
np.array([[0, 0, 100, 100]]),
|
||||
), # large bounding box centered at (50, 50)
|
||||
(
|
||||
np.array([[-10, -10, 20, 30]]),
|
||||
np.array([[-20, -25, 0, 5]]),
|
||||
), # negative coordinates
|
||||
(np.array([[50, 50, 0, 30]]), np.array([[50, 35, 50, 65]])), # zero width
|
||||
(np.array([[50, 50, 20, 0]]), np.array([[40, 50, 60, 50]])), # zero height
|
||||
(np.array([]).reshape(0, 4), np.array([]).reshape(0, 4)), # empty array
|
||||
],
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from contextlib import ExitStack as DoesNotRaise
|
||||
from test.test_utils import assert_almost_equal, mock_detections
|
||||
from typing import Optional, Union
|
||||
|
||||
import numpy as np
|
||||
@@ -11,6 +10,7 @@ from supervision.metrics.detection import (
|
||||
MeanAveragePrecision,
|
||||
detections_to_tensor,
|
||||
)
|
||||
from test.test_utils import assert_almost_equal, mock_detections
|
||||
|
||||
CLASSES = np.arange(80)
|
||||
NUM_CLASSES = len(CLASSES)
|
||||
|
||||
+3
-2
@@ -1,13 +1,14 @@
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.keypoint.core import KeyPoints
|
||||
|
||||
|
||||
def mock_detections(
|
||||
xyxy: List[List[float]],
|
||||
xyxy: npt.NDArray[np.float32],
|
||||
mask: Optional[List[np.ndarray]] = None,
|
||||
confidence: Optional[List[float]] = None,
|
||||
class_id: Optional[List[int]] = None,
|
||||
@@ -32,7 +33,7 @@ def mock_detections(
|
||||
|
||||
|
||||
def mock_keypoints(
|
||||
xy: List[List[float]],
|
||||
xy: npt.NDArray[np.float32],
|
||||
confidence: Optional[List[float]] = None,
|
||||
class_id: Optional[List[int]] = None,
|
||||
data: Optional[Dict[str, List[Any]]] = None,
|
||||
|
||||
@@ -11,7 +11,7 @@ ALL_IMAGES_LIST = [os.path.join(ASSETS_DIR, f"{i}.jpg") for i in range(1, 6)]
|
||||
|
||||
|
||||
@fixture(scope="function")
|
||||
def empty_opencv_image() -> np.ndarray:
|
||||
def empty_cv2_image() -> np.ndarray:
|
||||
return np.zeros((128, 128, 3), dtype=np.uint8)
|
||||
|
||||
|
||||
|
||||
@@ -2,28 +2,28 @@ import numpy as np
|
||||
from PIL import Image, ImageChops
|
||||
|
||||
from supervision.utils.conversion import (
|
||||
convert_for_image_processing,
|
||||
cv2_to_pillow,
|
||||
ensure_cv2_image_for_processing,
|
||||
images_to_cv2,
|
||||
pillow_to_cv2,
|
||||
)
|
||||
|
||||
|
||||
def test_convert_for_image_processing_when_pillow_image_submitted(
|
||||
empty_opencv_image: np.ndarray, empty_pillow_image: Image.Image
|
||||
def test_ensure_cv2_image_for_processing_when_pillow_image_submitted(
|
||||
empty_cv2_image: np.ndarray, empty_pillow_image: Image.Image
|
||||
) -> None:
|
||||
# given
|
||||
param_a_value = 3
|
||||
param_b_value = "some"
|
||||
|
||||
@convert_for_image_processing
|
||||
@ensure_cv2_image_for_processing
|
||||
def my_custom_processing_function(
|
||||
image: np.ndarray,
|
||||
param_a: int,
|
||||
param_b: str,
|
||||
) -> np.ndarray:
|
||||
assert np.allclose(
|
||||
image, empty_opencv_image
|
||||
image, empty_cv2_image
|
||||
), "Expected conversion to OpenCV image to happen"
|
||||
assert (
|
||||
param_a == param_a_value
|
||||
@@ -48,21 +48,21 @@ def test_convert_for_image_processing_when_pillow_image_submitted(
|
||||
)
|
||||
|
||||
|
||||
def test_convert_for_image_processing_when_opencv_image_submitted(
|
||||
empty_opencv_image: np.ndarray,
|
||||
def test_ensure_cv2_image_for_processing_when_cv2_image_submitted(
|
||||
empty_cv2_image: np.ndarray,
|
||||
) -> None:
|
||||
# given
|
||||
param_a_value = 3
|
||||
param_b_value = "some"
|
||||
|
||||
@convert_for_image_processing
|
||||
@ensure_cv2_image_for_processing
|
||||
def my_custom_processing_function(
|
||||
image: np.ndarray,
|
||||
param_a: int,
|
||||
param_b: str,
|
||||
) -> np.ndarray:
|
||||
assert np.allclose(
|
||||
image, empty_opencv_image
|
||||
image, empty_cv2_image
|
||||
), "Expected conversion to OpenCV image to happen"
|
||||
assert (
|
||||
param_a == param_a_value
|
||||
@@ -74,22 +74,20 @@ def test_convert_for_image_processing_when_opencv_image_submitted(
|
||||
|
||||
# when
|
||||
result = my_custom_processing_function(
|
||||
empty_opencv_image,
|
||||
empty_cv2_image,
|
||||
param_a_value,
|
||||
param_b=param_b_value,
|
||||
)
|
||||
|
||||
# then
|
||||
assert (
|
||||
result is empty_opencv_image
|
||||
), "Expected to return OpenCV image without changes"
|
||||
assert result is empty_cv2_image, "Expected to return OpenCV image without changes"
|
||||
|
||||
|
||||
def test_cv2_to_pillow(
|
||||
empty_opencv_image: np.ndarray, empty_pillow_image: Image.Image
|
||||
empty_cv2_image: np.ndarray, empty_pillow_image: Image.Image
|
||||
) -> None:
|
||||
# when
|
||||
result = cv2_to_pillow(image=empty_opencv_image)
|
||||
result = cv2_to_pillow(image=empty_cv2_image)
|
||||
|
||||
# then
|
||||
difference = ImageChops.difference(result, empty_pillow_image)
|
||||
@@ -99,14 +97,14 @@ def test_cv2_to_pillow(
|
||||
|
||||
|
||||
def test_pillow_to_cv2(
|
||||
empty_opencv_image: np.ndarray, empty_pillow_image: Image.Image
|
||||
empty_cv2_image: np.ndarray, empty_pillow_image: Image.Image
|
||||
) -> None:
|
||||
# when
|
||||
result = pillow_to_cv2(image=empty_pillow_image)
|
||||
|
||||
# then
|
||||
assert np.allclose(
|
||||
result, empty_opencv_image
|
||||
result, empty_cv2_image
|
||||
), "Conversion to OpenCV image expected not to change the content of image"
|
||||
|
||||
|
||||
@@ -119,10 +117,10 @@ def test_images_to_cv2_when_empty_input_provided() -> None:
|
||||
|
||||
|
||||
def test_images_to_cv2_when_only_cv2_images_provided(
|
||||
empty_opencv_image: np.ndarray,
|
||||
empty_cv2_image: np.ndarray,
|
||||
) -> None:
|
||||
# given
|
||||
images = [empty_opencv_image] * 5
|
||||
images = [empty_cv2_image] * 5
|
||||
|
||||
# when
|
||||
result = images_to_cv2(images=images)
|
||||
@@ -131,13 +129,13 @@ def test_images_to_cv2_when_only_cv2_images_provided(
|
||||
assert len(result) == 5, "Expected the same number of output element as input ones"
|
||||
for result_element in result:
|
||||
assert (
|
||||
result_element is empty_opencv_image
|
||||
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(
|
||||
empty_pillow_image: Image.Image,
|
||||
empty_opencv_image: np.ndarray,
|
||||
empty_cv2_image: np.ndarray,
|
||||
) -> None:
|
||||
# given
|
||||
images = [empty_pillow_image] * 5
|
||||
@@ -149,16 +147,16 @@ def test_images_to_cv2_when_only_pillow_images_provided(
|
||||
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_opencv_image
|
||||
result_element, empty_cv2_image
|
||||
), "Output images expected to be equal to empty OpenCV image"
|
||||
|
||||
|
||||
def test_images_to_cv2_when_mixed_input_provided(
|
||||
empty_pillow_image: Image.Image,
|
||||
empty_opencv_image: np.ndarray,
|
||||
empty_cv2_image: np.ndarray,
|
||||
) -> None:
|
||||
# given
|
||||
images = [empty_pillow_image, empty_opencv_image]
|
||||
images = [empty_pillow_image, empty_cv2_image]
|
||||
|
||||
# when
|
||||
result = images_to_cv2(images=images)
|
||||
@@ -166,8 +164,8 @@ 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_opencv_image
|
||||
result[0], empty_cv2_image
|
||||
), "PIL image should be converted to OpenCV one, equal to example empty image"
|
||||
assert (
|
||||
result[1] is empty_opencv_image
|
||||
result[1] is empty_cv2_image
|
||||
), "Expected CV images not to be touched by conversion"
|
||||
|
||||
Reference in New Issue
Block a user