Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58e27f44f1 | ||
|
|
953ecba283 | ||
|
|
e1e00ffc52 | ||
|
|
b01b321eca | ||
|
|
c9dfdbe301 | ||
|
|
38a1d3ad90 | ||
|
|
68d668a1f2 | ||
|
|
8f61898964 | ||
|
|
02ed59b27b | ||
|
|
72d0b9b914 | ||
|
|
db0a078280 | ||
|
|
63f5bfd4f3 | ||
|
|
61ababd0b5 | ||
|
|
f8b6d3cf9a | ||
|
|
73ed766e97 | ||
|
|
d3d1bfc86c | ||
|
|
55d7c8daf3 | ||
|
|
66067cc0ac | ||
|
|
448a0ab7c0 | ||
|
|
f207eaca31 | ||
|
|
51e2f9e462 | ||
|
|
8755bc8391 | ||
|
|
eb0e50a2f9 | ||
|
|
d6aa72c0f2 | ||
|
|
2be5008b3c | ||
|
|
b09559a900 | ||
|
|
ef89d8ceff | ||
|
|
0b931e11cd | ||
|
|
0bc52bf6fe | ||
|
|
5af3eab3d0 | ||
|
|
56c269a1ba | ||
|
|
52c9ec0def | ||
|
|
892bd87b6c | ||
|
|
23023e9764 | ||
|
|
3789f694c4 | ||
|
|
865cdd3bd1 | ||
|
|
121190be50 | ||
|
|
f01126fabb | ||
|
|
125a858654 | ||
|
|
1d4b61f20c | ||
|
|
edda8dea0b | ||
|
|
3b95c1d8bf | ||
|
|
1d77868080 | ||
|
|
eebd57a731 | ||
|
|
1860fdb0a4 | ||
|
|
31a3cca5d5 | ||
|
|
b45bee0761 | ||
|
|
103669bf0f | ||
|
|
5b39c81035 | ||
|
|
78f17cba2e | ||
|
|
36f0755767 | ||
|
|
9ddbad2300 | ||
|
|
c3b9db0258 | ||
|
|
dec29200a9 | ||
|
|
20c5f8da3d | ||
|
|
39d0e65592 | ||
|
|
58a9588a53 | ||
|
|
9522ac9a27 | ||
|
|
363aa0629e | ||
|
|
f499430aa3 | ||
|
|
06d1640700 | ||
|
|
d50c51fc52 | ||
|
|
e63611deb3 | ||
|
|
c81aec9d1b | ||
|
|
4a95aaa2c4 | ||
|
|
fedc1a47e0 | ||
|
|
db0d965e8d | ||
|
|
d4eb957201 | ||
|
|
4722459bce | ||
|
|
156dbd5046 | ||
|
|
ebd5e0661b | ||
|
|
d6d3213f62 | ||
|
|
7b22cfa741 | ||
|
|
356a86a469 | ||
|
|
c1444c6d14 | ||
|
|
5f86dbe29b | ||
|
|
6fbabeefff | ||
|
|
60c5292781 | ||
|
|
f8b60eab87 | ||
|
|
d88d8ed58b | ||
|
|
9bbd81e6ec | ||
|
|
93190b2bb6 | ||
|
|
a29d9599d7 | ||
|
|
787bbdac96 | ||
|
|
1b6fd01f7e | ||
|
|
65f606e197 | ||
|
|
0d86159de1 | ||
|
|
c211453def | ||
|
|
943a21412e | ||
|
|
f221019208 | ||
|
|
d01e76a9c7 | ||
|
|
5ea6d95fe0 | ||
|
|
270b5f564f | ||
|
|
47a7f929a2 | ||
|
|
130308c28b | ||
|
|
504fc59b68 | ||
|
|
856c72eb67 | ||
|
|
fef9f10c03 | ||
|
|
f8397bdce9 | ||
|
|
5d9d7446b8 | ||
|
|
e10caa2215 | ||
|
|
6551fd6c29 | ||
|
|
fa8378e50c | ||
|
|
7de1f5855d | ||
|
|
d208250123 | ||
|
|
2759beb293 | ||
|
|
9a56513371 | ||
|
|
7f85963314 | ||
|
|
6713523e9b | ||
|
|
3c79677039 | ||
|
|
d85ceb0c23 | ||
|
|
9d1549228f | ||
|
|
c94cfe2031 | ||
|
|
dc07f13dec | ||
|
|
fdda7254a3 | ||
|
|
81ee4f1c2c | ||
|
|
85e053eae6 | ||
|
|
887bb7c2cb | ||
|
|
e3eeb5f2cc | ||
|
|
a34f0e9597 | ||
|
|
6acfdbf502 | ||
|
|
1a870b9b1b | ||
|
|
784191a099 | ||
|
|
c9fa3d613f | ||
|
|
4644f21340 | ||
|
|
9be2fdf5c4 | ||
|
|
c532758693 | ||
|
|
14fc6c2dca | ||
|
|
ebdada92bd | ||
|
|
7ad0bc361b | ||
|
|
f307f981f6 | ||
|
|
79858ad9d3 | ||
|
|
1c30493ee0 | ||
|
|
2961045177 | ||
|
|
1f0c278c4e | ||
|
|
32d22efedc | ||
|
|
128358f183 | ||
|
|
ef2b604df6 | ||
|
|
1b7344d6cc | ||
|
|
685929fbd9 | ||
|
|
95add3679d | ||
|
|
9d2dc08e34 | ||
|
|
420b832e8e | ||
|
|
d25016e428 | ||
|
|
b9e9adaab3 | ||
|
|
96b8d53123 | ||
|
|
00cba6e8cf | ||
|
|
0ddb62d024 | ||
|
|
d9b297a1d1 | ||
|
|
4919b84dd7 | ||
|
|
3cf9634907 | ||
|
|
cbd62ef741 | ||
|
|
30f75bd3a9 | ||
|
|
c9159f9f24 | ||
|
|
d5efa5dbac | ||
|
|
6e07b17422 | ||
|
|
ef52dfff05 | ||
|
|
ade8976eaf | ||
|
|
abec8da828 | ||
|
|
675d3e58bc | ||
|
|
c30c9f4b4a | ||
|
|
9203116468 | ||
|
|
a9fe5ccd9a | ||
|
|
f5c50013b6 | ||
|
|
a063f0aa2b | ||
|
|
23ce82f48d | ||
|
|
ea4bc3f875 | ||
|
|
cceecb095b | ||
|
|
a20d784e25 | ||
|
|
ce53f6c99d | ||
|
|
2f942d296a | ||
|
|
06f6286452 | ||
|
|
b987f98795 | ||
|
|
f20a3149c8 | ||
|
|
26987d7cf5 | ||
|
|
370e80d7ec | ||
|
|
8a912c5dde | ||
|
|
323e75844f | ||
|
|
9410f033bb | ||
|
|
b25f0b1ac2 | ||
|
|
e52989280a | ||
|
|
621f8e4d29 | ||
|
|
7a1c3149fd | ||
|
|
5467db4447 | ||
|
|
96bdcb3dea | ||
|
|
54278ea1f0 | ||
|
|
4e9aec6d2a | ||
|
|
db79350caa | ||
|
|
a6e0177cb9 | ||
|
|
d3968822a7 | ||
|
|
b06d979525 | ||
|
|
1ed782560d | ||
|
|
3c41c9721d | ||
|
|
e805212ae6 | ||
|
|
80567df1ea | ||
|
|
fab89d2374 | ||
|
|
a937fa5100 | ||
|
|
418ec5bad2 | ||
|
|
9b52d62bde | ||
|
|
a298ceb18c | ||
|
|
c7c02bb4de | ||
|
|
c1e249e2bb | ||
|
|
a7debec87d | ||
|
|
b4b941fe1b | ||
|
|
3ddc4bd007 | ||
|
|
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 | ||
|
|
2e55149d43 | ||
|
|
fb25fda6ef | ||
|
|
838b8c345a | ||
|
|
1f21354f02 | ||
|
|
6c03451279 | ||
|
|
ab983bba27 | ||
|
|
48a1f04060 | ||
|
|
4356a90fc9 | ||
|
|
5cda6e339e | ||
|
|
d6ebc433a7 | ||
|
|
cd2fd0d723 | ||
|
|
3653755996 | ||
|
|
e6686ba71d | ||
|
|
3b5b8709e5 | ||
|
|
aed3feba14 | ||
|
|
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 | ||
|
|
86d15c6c90 | ||
|
|
6984d72f4a | ||
|
|
6f5b477ac0 | ||
|
|
b19511968d | ||
|
|
165c77d830 | ||
|
|
558581943b | ||
|
|
8acbc422c9 | ||
|
|
9995771604 | ||
|
|
bcc1bc1ff2 | ||
|
|
cce69d893e | ||
|
|
3e2fe835db | ||
|
|
47f1c44e20 | ||
|
|
420543534d | ||
|
|
1ea9345845 | ||
|
|
6ca09ccbfc | ||
|
|
c1e4d79c56 | ||
|
|
7f78f93407 | ||
|
|
05ceca70ef | ||
|
|
e8f8c38f39 | ||
|
|
0739e24b25 | ||
|
|
560760993a | ||
|
|
859f0258bc | ||
|
|
2f644c52e3 | ||
|
|
a82c37c7fa | ||
|
|
fc2710c9c9 | ||
|
|
29161a485e | ||
|
|
523d218dd4 | ||
|
|
46bd267079 | ||
|
|
d025791805 | ||
|
|
7e4fc8a485 | ||
|
|
26d70219bf | ||
|
|
daf13442f3 | ||
|
|
ac6dd7bca4 | ||
|
|
a6b985ff4b | ||
|
|
4fea0031ba | ||
|
|
490a341d5d | ||
|
|
2c3fa27929 | ||
|
|
51c7ebdf60 | ||
|
|
2653f5a090 | ||
|
|
cbec7f8580 | ||
|
|
a7c00ca5da | ||
|
|
2a5dc6757c | ||
|
|
d309d74f37 | ||
|
|
b85035c45e | ||
|
|
687f8d9621 | ||
|
|
668f2f063f | ||
|
|
f93c712dca | ||
|
|
88f995803f | ||
|
|
71164fd0c1 | ||
|
|
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}"
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Supervision Test Releases to PyPi
|
||||
name: Publish Supervision Pre-Releases to PyPI and TestPyPI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -9,9 +9,11 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
build-and-publish-pre-release-pypi:
|
||||
name: Build and publish to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
@@ -30,14 +32,11 @@ jobs:
|
||||
python -m pip install --upgrade build twine
|
||||
python -m build
|
||||
twine check --strict dist/*
|
||||
- name: 🚀 Publish distribution to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: ${{ secrets.PYPI_USERNAME }}
|
||||
password: ${{ secrets.PYPI_PASSWORD }}
|
||||
|
||||
- name: 🚀 Publish to PyPi
|
||||
uses: pypa/gh-action-pypi-publish@release/v1.10
|
||||
|
||||
- name: 🚀 Publish to Test-PyPi
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
uses: pypa/gh-action-pypi-publish@release/v1.10
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
user: ${{ secrets.PYPI_TEST_USERNAME }}
|
||||
password: ${{ secrets.PYPI_TEST_PASSWORD }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Supervision Releases to PyPi
|
||||
name: Publish Supervision Releases to PyPI and TestPyPI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -7,8 +7,10 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-and-publish-pre-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
@@ -27,14 +29,11 @@ jobs:
|
||||
python -m pip install --upgrade build twine
|
||||
python -m build
|
||||
twine check --strict dist/*
|
||||
|
||||
- name: 🚀 Publish to PyPi
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: ${{ secrets.PYPI_USERNAME }}
|
||||
password: ${{ secrets.PYPI_PASSWORD }}
|
||||
uses: pypa/gh-action-pypi-publish@release/v1.10
|
||||
|
||||
- name: 🚀 Publish to Test-PyPi
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
uses: pypa/gh-action-pypi-publish@release/v1.10
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
user: ${{ secrets.PYPI_TEST_USERNAME }}
|
||||
password: ${{ secrets.PYPI_TEST_PASSWORD }}
|
||||
|
||||
@@ -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
|
||||
|
||||
+17
-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,35 +18,38 @@ 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.10'
|
||||
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.8
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
- id: ruff-format
|
||||
types_or: [ python, pyi, jupyter ]
|
||||
|
||||
# - repo: https://github.com/executablebooks/mdformat
|
||||
# rev: 0.7.17
|
||||
# hooks:
|
||||
# - id: mdformat
|
||||
# additional_dependencies:
|
||||
# - "mdformat-mkdocs[recommended]>=2.1.0"
|
||||
# args: ["--number"]
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies:
|
||||
- tomli
|
||||
|
||||
+16
-17
@@ -1,4 +1,3 @@
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
@@ -6,7 +5,7 @@
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
identity and expression, level of experience, education, socioeconomic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
@@ -18,23 +17,23 @@ diverse, inclusive, and healthy community.
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
- Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
- The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
@@ -121,14 +120,14 @@ version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
[Mozilla's code of conduct enforcement ladder][mozilla coc].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/faq][faq]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[faq]: https://www.contributor-covenant.org/faq
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[mozilla coc]: https://github.com/mozilla/diversity
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
|
||||
+129
-34
@@ -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](https://supervision.roboflow.com/latest/code_of_conduct/). 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,50 @@ 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.
|
||||
Common types include:
|
||||
|
||||
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).
|
||||
- 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 +124,82 @@ 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 +207,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 +221,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).
|
||||
|
||||
@@ -8,27 +8,33 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
[notebooks](https://github.com/roboflow/notebooks) | [inference](https://github.com/roboflow/inference) | [autodistill](https://github.com/autodistill/autodistill) | [maestro](https://github.com/roboflow/multimodal-maestro)
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
[](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)
|
||||
[](https://huggingface.co/spaces/Roboflow/Annotators)
|
||||
[](https://discord.gg/GbfgXGJ8Bk)
|
||||
[](https://squidfunk.github.io/mkdocs-material/)
|
||||
|
||||
<div align="center">
|
||||
<a href="https://trendshift.io/repositories/124" target="_blank"><img src="https://trendshift.io/api/badge/repositories/124" alt="roboflow%2Fsupervision | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
## 👋 hello
|
||||
|
||||
**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/roboflow/supervision/issues?q=is%3Aissue+is%3Aopen+label%3Ahacktoberfest)
|
||||
|
||||
## 💻 install
|
||||
|
||||
@@ -53,7 +59,7 @@ import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
|
||||
image = cv2.imread(...)
|
||||
model = YOLO('yolov8s.pt')
|
||||
model = YOLO("yolov8s.pt")
|
||||
result = model(image)[0]
|
||||
detections = sv.Detections.from_ultralytics(result)
|
||||
|
||||
@@ -66,27 +72,27 @@ len(detections)
|
||||
|
||||
- inference
|
||||
|
||||
Running with [Inference](https://github.com/roboflow/inference) requires a [Roboflow API KEY](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key).
|
||||
Running with [Inference](https://github.com/roboflow/inference) requires a [Roboflow API KEY](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key).
|
||||
|
||||
```python
|
||||
import cv2
|
||||
import supervision as sv
|
||||
from inference import get_model
|
||||
```python
|
||||
import cv2
|
||||
import supervision as sv
|
||||
from inference import get_model
|
||||
|
||||
image = cv2.imread(...)
|
||||
model = get_model(model_id="yolov8s-640", api_key=<ROBOFLOW API KEY>)
|
||||
result = model.infer(image)[0]
|
||||
detections = sv.Detections.from_inference(result)
|
||||
image = cv2.imread(...)
|
||||
model = get_model(model_id="yolov8s-640", api_key=<ROBOFLOW API KEY>)
|
||||
result = model.infer(image)[0]
|
||||
detections = sv.Detections.from_inference(result)
|
||||
|
||||
len(detections)
|
||||
# 5
|
||||
```
|
||||
len(detections)
|
||||
# 5
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### 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,33 +101,35 @@ import supervision as sv
|
||||
image = cv2.imread(...)
|
||||
detections = sv.Detections(...)
|
||||
|
||||
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||
annotated_frame = bounding_box_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections
|
||||
)
|
||||
box_annotator = sv.BoxAnnotator()
|
||||
annotated_frame = box_annotator.annotate(
|
||||
scene=image.copy(),
|
||||
detections=detections)
|
||||
```
|
||||
|
||||
https://github.com/roboflow/supervision/assets/26109316/691e219c-0565-4403-9218-ab5644f39bce
|
||||
|
||||
### 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>
|
||||
@@ -129,94 +137,94 @@ len(dataset)
|
||||
|
||||
- load
|
||||
|
||||
```python
|
||||
dataset = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
)
|
||||
```python
|
||||
dataset = sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
)
|
||||
|
||||
dataset = sv.DetectionDataset.from_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
dataset = sv.DetectionDataset.from_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
|
||||
dataset = sv.DetectionDataset.from_coco(
|
||||
images_directory_path=...,
|
||||
annotations_path=...
|
||||
)
|
||||
```
|
||||
dataset = sv.DetectionDataset.from_coco(
|
||||
images_directory_path=...,
|
||||
annotations_path=...
|
||||
)
|
||||
```
|
||||
|
||||
- split
|
||||
|
||||
```python
|
||||
train_dataset, test_dataset = dataset.split(split_ratio=0.7)
|
||||
test_dataset, valid_dataset = test_dataset.split(split_ratio=0.5)
|
||||
```python
|
||||
train_dataset, test_dataset = dataset.split(split_ratio=0.7)
|
||||
test_dataset, valid_dataset = test_dataset.split(split_ratio=0.5)
|
||||
|
||||
len(train_dataset), len(test_dataset), len(valid_dataset)
|
||||
# (700, 150, 150)
|
||||
```
|
||||
len(train_dataset), len(test_dataset), len(valid_dataset)
|
||||
# (700, 150, 150)
|
||||
```
|
||||
|
||||
- merge
|
||||
|
||||
```python
|
||||
ds_1 = sv.DetectionDataset(...)
|
||||
len(ds_1)
|
||||
# 100
|
||||
ds_1.classes
|
||||
# ['dog', 'person']
|
||||
```python
|
||||
ds_1 = sv.DetectionDataset(...)
|
||||
len(ds_1)
|
||||
# 100
|
||||
ds_1.classes
|
||||
# ['dog', 'person']
|
||||
|
||||
ds_2 = sv.DetectionDataset(...)
|
||||
len(ds_2)
|
||||
# 200
|
||||
ds_2.classes
|
||||
# ['cat']
|
||||
ds_2 = sv.DetectionDataset(...)
|
||||
len(ds_2)
|
||||
# 200
|
||||
ds_2.classes
|
||||
# ['cat']
|
||||
|
||||
ds_merged = sv.DetectionDataset.merge([ds_1, ds_2])
|
||||
len(ds_merged)
|
||||
# 300
|
||||
ds_merged.classes
|
||||
# ['cat', 'dog', 'person']
|
||||
```
|
||||
ds_merged = sv.DetectionDataset.merge([ds_1, ds_2])
|
||||
len(ds_merged)
|
||||
# 300
|
||||
ds_merged.classes
|
||||
# ['cat', 'dog', 'person']
|
||||
```
|
||||
|
||||
- save
|
||||
|
||||
```python
|
||||
dataset.as_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
)
|
||||
```python
|
||||
dataset.as_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
)
|
||||
|
||||
dataset.as_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
dataset.as_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
|
||||
dataset.as_coco(
|
||||
images_directory_path=...,
|
||||
annotations_path=...
|
||||
)
|
||||
```
|
||||
dataset.as_coco(
|
||||
images_directory_path=...,
|
||||
annotations_path=...
|
||||
)
|
||||
```
|
||||
|
||||
- convert
|
||||
|
||||
```python
|
||||
sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
).as_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
```
|
||||
```python
|
||||
sv.DetectionDataset.from_yolo(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...,
|
||||
data_yaml_path=...
|
||||
).as_pascal_voc(
|
||||
images_directory_path=...,
|
||||
annotations_directory_path=...
|
||||
)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 🎬 tutorials
|
||||
|
||||
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/>
|
||||
|
||||
@@ -262,7 +270,7 @@ We love your input! Please see our [contributing guide](https://github.com/robof
|
||||
|
||||
<div align="center">
|
||||
|
||||
<div align="center">
|
||||
<div align="center">
|
||||
<a href="https://youtube.com/roboflow">
|
||||
<img
|
||||
src="https://media.roboflow.com/notebooks/template/icons/purple/youtube.png?ik-sdk-version=javascript-1.4.3&updatedAt=1672949634652"
|
||||
@@ -291,7 +299,7 @@ We love your input! Please see our [contributing guide](https://github.com/robof
|
||||
/>
|
||||
</a>
|
||||
<img src="https://raw.githubusercontent.com/ultralytics/assets/main/social/logo-transparent.png" width="3%"/>
|
||||
<a href="https://disuss.roboflow.com">
|
||||
<a href="https://discuss.roboflow.com">
|
||||
<img
|
||||
src="https://media.roboflow.com/notebooks/template/icons/purple/forum.png?ik-sdk-version=javascript-1.4.3&updatedAt=1672949633584"
|
||||
width="3%"
|
||||
|
||||
Vendored
+1478
-1342
File diff suppressed because one or more lines are too long
@@ -13,7 +13,6 @@ To install the Supervision assets utility, you can use `pip`. This utility is av
|
||||
as an extra within the Supervision package.
|
||||
|
||||
!!! example "pip install"
|
||||
|
||||
```bash
|
||||
pip install "supervision[assets]"
|
||||
```
|
||||
|
||||
+440
-23
@@ -1,8 +1,430 @@
|
||||
# CHANGELOG
|
||||
|
||||
### 0.24.0 <small>Oct 4, 2024</small>
|
||||
|
||||
- Added [F1 score](https://supervision.roboflow.com/0.24.0/metrics/f1_score/#supervision.metrics.f1_score.F1Score) as a new metric for detection and segmentation. [#1521](https://github.com/roboflow/supervision/pull/1521)
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
from supervision.metrics import F1Score
|
||||
|
||||
predictions = sv.Detections(...)
|
||||
targets = sv.Detections(...)
|
||||
|
||||
f1_metric = F1Score()
|
||||
f1_result = f1_metric.update(predictions, targets).compute()
|
||||
|
||||
print(f1_result)
|
||||
print(f1_result.f1_50)
|
||||
print(f1_result.small_objects.f1_50)
|
||||
```
|
||||
|
||||
- Added new cookbook: [Small Object Detection with SAHI](https://supervision.roboflow.com/0.24.0/notebooks/small-object-detection-with-sahi/). This cookbook provides a detailed guide on using [`InferenceSlicer`](https://supervision.roboflow.com/0.24.0/detection/tools/inference_slicer/) for small object detection. [#1483](https://github.com/roboflow/supervision/pull/1483)
|
||||
|
||||
- Added an [Embedded Workflow](https://roboflow.com/workflows), which allows you to [preview annotators](https://supervision.roboflow.com/0.24.0/detection/annotators/). [#1533](https://github.com/roboflow/supervision/pull/1533)
|
||||
|
||||
- Enhanced [`LineZoneAnnotator`](https://supervision.roboflow.com/0.24.0/detection/tools/line_zone/#supervision.detection.line_zone.LineZoneAnnotator), allowing the labels to align with the line, even when it's not horizontal. Also, you can now disable text background, and choose to draw labels off-center which minimizes overlaps for multiple [`LineZone`](https://supervision.roboflow.com/0.24.0/detection/tools/line_zone/#supervision.detection.line_zone.LineZone) labels. [#854](https://github.com/roboflow/supervision/pull/854)
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
import cv2
|
||||
|
||||
image = cv2.imread("<SOURCE_IMAGE_PATH>")
|
||||
|
||||
line_zone = sv.LineZone(
|
||||
start=sv.Point(0, 100),
|
||||
end=sv.Point(50, 200)
|
||||
)
|
||||
line_zone_annotator = sv.LineZoneAnnotator(
|
||||
text_orient_to_line=True,
|
||||
display_text_box=False,
|
||||
text_centered=False
|
||||
)
|
||||
|
||||
annotated_frame = line_zone_annotator.annotate(
|
||||
frame=image.copy(), line_counter=line_zone
|
||||
)
|
||||
|
||||
sv.plot_image(frame)
|
||||
```
|
||||
|
||||
- Added per-class counting capabilities to [`LineZone`](https://supervision.roboflow.com/0.24.0/detection/tools/line_zone/#supervision.detection.line_zone.LineZone) and introduced [`LineZoneAnnotatorMulticlass`](https://supervision.roboflow.com/0.24.0/detection/tools/line_zone/#supervision.detection.line_zone.LineZoneAnnotatorMulticlass) for visualizing the counts per class. This feature allows tracking of individual classes crossing a line, enhancing the flexibility of use cases like traffic monitoring or crowd analysis. [#1555](https://github.com/roboflow/supervision/pull/1555)
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
import cv2
|
||||
|
||||
image = cv2.imread("<SOURCE_IMAGE_PATH>")
|
||||
|
||||
line_zone = sv.LineZone(
|
||||
start=sv.Point(0, 100),
|
||||
end=sv.Point(50, 200)
|
||||
)
|
||||
line_zone_annotator = sv.LineZoneAnnotatorMulticlass()
|
||||
|
||||
frame = line_zone_annotator.annotate(
|
||||
frame=frame, line_zones=[line_zone]
|
||||
)
|
||||
|
||||
sv.plot_image(frame)
|
||||
```
|
||||
|
||||
- Added [`from_easyocr`](https://supervision.roboflow.com/0.24.0/detection/core/#supervision.detection.core.Detections.from_easyocr), allowing integration of OCR results into the supervision framework. [EasyOCR](https://github.com/JaidedAI/EasyOCR) is an open-source optical character recognition (OCR) library that can read text from images. [#1515](https://github.com/roboflow/supervision/pull/1515)
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
import easyocr
|
||||
import cv2
|
||||
|
||||
image = cv2.imread("<SOURCE_IMAGE_PATH>")
|
||||
|
||||
reader = easyocr.Reader(["en"])
|
||||
result = reader.readtext("<SOURCE_IMAGE_PATH>", paragraph=True)
|
||||
detections = sv.Detections.from_easyocr(result)
|
||||
|
||||
box_annotator = sv.BoxAnnotator(color_lookup=sv.ColorLookup.INDEX)
|
||||
label_annotator = sv.LabelAnnotator(color_lookup=sv.ColorLookup.INDEX)
|
||||
|
||||
annotated_image = image.copy()
|
||||
annotated_image = box_annotator.annotate(scene=annotated_image, detections=detections)
|
||||
annotated_image = label_annotator.annotate(scene=annotated_image, detections=detections)
|
||||
|
||||
sv.plot_image(annotated_image)
|
||||
```
|
||||
|
||||
- Added [`oriented_box_iou_batch`](https://supervision.roboflow.com/0.24.0/detection/utils/#supervision.detection.utils.oriented_box_iou_batch) function to `detection.utils`. This function computes Intersection over Union (IoU) for oriented or rotated bounding boxes (OBB). [#1502](https://github.com/roboflow/supervision/pull/1502)
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
|
||||
boxes_true = np.array([[[1, 0], [0, 1], [3, 4], [4, 3]]])
|
||||
boxes_detection = np.array([[[1, 1], [2, 0], [4, 2], [3, 3]]])
|
||||
ious = sv.oriented_box_iou_batch(boxes_true, boxes_detection)
|
||||
print("IoU between true and detected boxes:", ious)
|
||||
```
|
||||
|
||||
- Extended [`PolygonZoneAnnotator`](https://supervision.roboflow.com/0.24.0/detection/tools/polygon_zone/#supervision.detection.tools.polygon_zone.PolygonZoneAnnotator) to allow setting opacity when drawing zones, providing enhanced visualization by filling the zone with adjustable transparency. [#1527](https://github.com/roboflow/supervision/pull/1527)
|
||||
|
||||
```python
|
||||
import cv2
|
||||
from ncnn.model_zoo import get_model
|
||||
import supervision as sv
|
||||
|
||||
image = cv2.imread("<SOURCE_IMAGE_PATH>")
|
||||
model = get_model(
|
||||
"yolov8s",
|
||||
target_size=640,
|
||||
prob_threshold=0.5,
|
||||
nms_threshold=0.45,
|
||||
num_threads=4,
|
||||
use_gpu=True,
|
||||
)
|
||||
result = model(image)
|
||||
detections = sv.Detections.from_ncnn(result)
|
||||
```
|
||||
|
||||
!!! failure "Removed"
|
||||
|
||||
The `frame_resolution_wh` parameter in [`PolygonZone`](https://supervision.roboflow.com/0.24.0/detection/tools/polygon_zone/#supervision.detection.tools.polygon_zone.PolygonZone) has been removed.
|
||||
|
||||
!!! failure "Removed"
|
||||
|
||||
Supervision installation methods `"headless"` and `"desktop"` were removed, as they are no longer needed. `pip install supervision[headless]` will install the base library and harmlessly warn of non-existent extras.
|
||||
|
||||
- Supervision now depends on `opencv-python` rather than `opencv-python-headless`. [#1530](https://github.com/roboflow/supervision/pull/1530)
|
||||
|
||||
- Fixed the COCO 101 point Average Precision algorithm to correctly interpolate precision, providing a more precise calculation of average precision without averaging out intermediate values. [#1500](https://github.com/roboflow/supervision/pull/1500)
|
||||
|
||||
- Resolved miscellaneous issues highlighted when building documentation. This mostly includes whitespace adjustments and type inconsistencies. Updated documentation for clarity and fixed formatting issues. Added explicit version for `mkdocstrings-python`. [#1549](https://github.com/roboflow/supervision/pull/1549)
|
||||
|
||||
- Enabled and fixed Ruff rules for code formatting, including changes like avoiding unnecessary iterable allocations and using Optional for default mutable arguments. [#1526](https://github.com/roboflow/supervision/pull/1526)
|
||||
|
||||
### 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/0.23.0/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/0.23.0/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/0.23.0/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/0.23.0/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/0.23.0/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/0.23.0/detection/annotators/#supervision.annotators.core.TriangleAnnotator) and [`DotAnnotator`](https://supervision.roboflow.com/0.23.0/detection/annotators/#supervision.annotators.core.DotAnnotator).
|
||||
|
||||
- Added [#1409](https://github.com/roboflow/supervision/pull/1409): `text_color` option for [`VertexLabelAnnotator`](https://supervision.roboflow.com/0.23.0/keypoint/annotators/#supervision.keypoint.annotators.VertexLabelAnnotator) keypoint annotator.
|
||||
|
||||
- Changed [#1434](https://github.com/roboflow/supervision/pull/1434): [`InferenceSlicer`](https://supervision.roboflow.com/0.23.0/detection/tools/inference_slicer/) now features an `overlap_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/0.23.0/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/0.22.0/datasets/core/#supervision.dataset.core.DetectionDataset) and [`sv.ClassificationDataset`](https://supervision.roboflow.com/0.22.0/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/0.22.0/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/0.22.0/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/0.22.0/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/0.22.0/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/0.22.0/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/0.22.0/detection/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/0.22.0/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/0.22.0/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/0.22.0/detection/annotators/#supervision.annotators.core.BoxAnnotator) and [`LabelAnnotator`](https://supervision.roboflow.com/0.22.0/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/0.22.0/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/0.22.0/detection/utils/#supervision.detection.utils.xywh_to_xyxy) and [`xcycwh_to_xyxy`](https://supervision.roboflow.com/0.22.0/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/0.22.0/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/0.22.0/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/0.22.0/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/0.21.0/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/0.21.0/detection/core/#supervision.detection.core.Detections.from_lmm) allowing to parse Large Multimodal Model (LMM) text result into [`sv.Detections`](https://supervision.roboflow.com/0.21.0/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
|
||||
@@ -12,7 +434,7 @@ detections = sv.Detections.from_lmm(
|
||||
sv.LMM.PALIGEMMA,
|
||||
paligemma_result,
|
||||
resolution_wh=(1000, 1000),
|
||||
classes=['cat', 'dog']
|
||||
classes=["cat", "dog"],
|
||||
)
|
||||
detections.xyxy
|
||||
# array([[250., 250., 750., 750.]])
|
||||
@@ -21,7 +443,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/0.21.0/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
|
||||
@@ -39,15 +461,15 @@ annotated_frame = edge_annotator.annotate(
|
||||
)
|
||||
```
|
||||
|
||||
- Added [#1147](https://github.com/roboflow/supervision/pull/1147): [`sv.KeyPoints.from_inference`](https://supervision.roboflow.com/develop/keypoint/core/#supervision.keypoint.core.KeyPoints.from_inference) allowing to create [`sv.KeyPoints`](https://supervision.roboflow.com/develop/keypoint/core/#supervision.keypoint.core.KeyPoints) from [Inference](https://github.com/roboflow/inference) result.
|
||||
- Added [#1147](https://github.com/roboflow/supervision/pull/1147): [`sv.KeyPoints.from_inference`](https://supervision.roboflow.com/0.21.0/keypoint/core/#supervision.keypoint.core.KeyPoints.from_inference) allowing to create [`sv.KeyPoints`](https://supervision.roboflow.com/0.21.0/keypoint/core/#supervision.keypoint.core.KeyPoints) from [Inference](https://github.com/roboflow/inference) result.
|
||||
|
||||
- Added [#1138](https://github.com/roboflow/supervision/pull/1138): [`sv.KeyPoints.from_yolo_nas`](https://supervision.roboflow.com/develop/keypoint/core/#supervision.keypoint.core.KeyPoints.from_yolo_nas) allowing to create [`sv.KeyPoints`](https://supervision.roboflow.com/develop/keypoint/core/#supervision.keypoint.core.KeyPoints) from [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) result.
|
||||
- Added [#1138](https://github.com/roboflow/supervision/pull/1138): [`sv.KeyPoints.from_yolo_nas`](https://supervision.roboflow.com/0.21.0/keypoint/core/#supervision.keypoint.core.KeyPoints.from_yolo_nas) allowing to create [`sv.KeyPoints`](https://supervision.roboflow.com/0.21.0/keypoint/core/#supervision.keypoint.core.KeyPoints) from [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) result.
|
||||
|
||||
- Added [#1163](https://github.com/roboflow/supervision/pull/1163): [`sv.mask_to_rle`](https://supervision.roboflow.com/develop/datasets/utils/#supervision.dataset.utils.rle_to_mask) and [`sv.rle_to_mask`](https://supervision.roboflow.com/develop/datasets/utils/#supervision.dataset.utils.rle_to_mask) allowing for easy conversion between mask and rle formats.
|
||||
- Added [#1163](https://github.com/roboflow/supervision/pull/1163): [`sv.mask_to_rle`](https://supervision.roboflow.com/0.21.0/datasets/utils/#supervision.dataset.utils.rle_to_mask) and [`sv.rle_to_mask`](https://supervision.roboflow.com/0.21.0/datasets/utils/#supervision.dataset.utils.rle_to_mask) allowing for easy conversion between mask and rle formats.
|
||||
|
||||
- Changed [#1236](https://github.com/roboflow/supervision/pull/1236): [`sv.InferenceSlicer`](https://supervision.roboflow.com/develop/detection/tools/inference_slicer/) allowing to select overlap filtering strategy (`NONE`, `NON_MAX_SUPPRESSION` and `NON_MAX_MERGE`).
|
||||
- Changed [#1236](https://github.com/roboflow/supervision/pull/1236): [`sv.InferenceSlicer`](https://supervision.roboflow.com/0.21.0/detection/tools/inference_slicer/) allowing to select overlap filtering strategy (`NONE`, `NON_MAX_SUPPRESSION` and `NON_MAX_MERGE`).
|
||||
|
||||
- Changed [#1178](https://github.com/roboflow/supervision/pull/1178): [`sv.InferenceSlicer`](https://supervision.roboflow.com/develop/detection/tools/inference_slicer/) adding instance segmentation model support.
|
||||
- Changed [#1178](https://github.com/roboflow/supervision/pull/1178): [`sv.InferenceSlicer`](https://supervision.roboflow.com/0.21.0/detection/tools/inference_slicer/) adding instance segmentation model support.
|
||||
|
||||
```python
|
||||
import cv2
|
||||
@@ -74,9 +496,9 @@ annotated_image = label_annotator.annotate(
|
||||
scene=annotated_image, detections=detections)
|
||||
```
|
||||
|
||||
- Changed [#1228](https://github.com/roboflow/supervision/pull/1228): [`sv.LineZone`](https://supervision.roboflow.com/develop/detection/tools/line_zone/) making it 10-20 times faster, depending on the use case.
|
||||
- Changed [#1228](https://github.com/roboflow/supervision/pull/1228): [`sv.LineZone`](https://supervision.roboflow.com/0.21.0/detection/tools/line_zone/) making it 10-20 times faster, depending on the use case.
|
||||
|
||||
- Changed [#1163](https://github.com/roboflow/supervision/pull/1163): [`sv.DetectionDataset.from_coco`](https://supervision.roboflow.com/develop/datasets/core/#supervision.dataset.core.DetectionDataset.from_coco) and [`sv.DetectionDataset.as_coco`](https://supervision.roboflow.com/develop/datasets/core/#supervision.dataset.core.DetectionDataset.as_coco) adding support for run-length encoding (RLE) mask format.
|
||||
- Changed [#1163](https://github.com/roboflow/supervision/pull/1163): [`sv.DetectionDataset.from_coco`](https://supervision.roboflow.com/0.21.0/datasets/core/#supervision.dataset.core.DetectionDataset.from_coco) and [`sv.DetectionDataset.as_coco`](https://supervision.roboflow.com/0.21.0/datasets/core/#supervision.dataset.core.DetectionDataset.as_coco) adding support for run-length encoding (RLE) mask format.
|
||||
|
||||
### 0.20.0 <small>April 24, 2024</small>
|
||||
|
||||
@@ -164,8 +586,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
|
||||
@@ -205,15 +625,15 @@ annotated_frame = crop_annotator.annotate(
|
||||
)
|
||||
```
|
||||
|
||||
- Changed [#827](https://github.com/roboflow/supervision/pull/827): [`sv.ByteTrack.reset`](/0.19.0/tracking/#supervision.tracking.ByteTrack.reset) allowing users to clear trackers state, enabling the processing of multiple video files in sequence.
|
||||
- Changed [#827](https://github.com/roboflow/supervision/pull/827): [`sv.ByteTrack.reset`](/0.19.0/trackers/#supervision.tracker.ByteTrack.reset) allowing users to clear trackers state, enabling the processing of multiple video files in sequence.
|
||||
|
||||
- Changed [#802](https://github.com/roboflow/supervision/pull/802): [`sv.LineZoneAnnotator`](/0.19.0/detection/tools/line_zone/#supervision.detection.line_zone.LineZone) allowing to hide in/out count using `display_in_count` and `display_out_count` properties.
|
||||
|
||||
- Changed [#787](https://github.com/roboflow/supervision/pull/787): [`sv.ByteTrack`](/0.19.0/tracking/#supervision.tracking.ByteTrack) input arguments and docstrings updated to improve readability and ease of use.
|
||||
- Changed [#787](https://github.com/roboflow/supervision/pull/787): [`sv.ByteTrack`](/0.19.0/trackers/#supervision.tracker.ByteTrack) input arguments and docstrings updated to improve readability and ease of use.
|
||||
|
||||
!!! 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 +705,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 +713,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 +805,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.
|
||||
@@ -639,7 +1056,7 @@ array([
|
||||
|
||||
- Added [#101](https://github.com/roboflow/supervision/pull/101): ability to extract masks from YOLOv8 result using [`sv.Detections.from_yolov8`](/0.8.0/detection/core/#supervision.detection.core.Detections.from_yolov8). Here is an example illustrating how to extract boolean masks from the result of the YOLOv8 model inference.
|
||||
|
||||
- Added [#122](https://github.com/roboflow/supervision/pull/122): ability to crop image using [`sv.crop`](/latest/utils/image/#crop). Here is an example showing how to get a separate crop for each detection in `sv.Detections`.
|
||||
- Added [#122](https://github.com/roboflow/supervision/pull/122): ability to crop image using [`sv.crop`](/0.9.0/utils/image/#crop). Here is an example showing how to get a separate crop for each detection in `sv.Detections`.
|
||||
|
||||
- Added [#120](https://github.com/roboflow/supervision/pull/120): ability to conveniently save multiple images into directory using [`sv.ImageSink`](/0.9.0/utils/image/#imagesink). Here is an example showing how to save every tenth video frame as a separate image.
|
||||
|
||||
@@ -651,11 +1068,11 @@ array([
|
||||
... sink.save_image(image=image)
|
||||
```
|
||||
|
||||
- Fixed [#106](https://github.com/roboflow/supervision/issues/106): inconvenient handling of [`sv.PolygonZone`](/0.8.0/supervision/detection/tools/polygon_zone/#polygonzone) coordinates. Now `sv.PolygonZone` accepts coordinates in the form of `[[x1, y1], [x2, y2], ...]` that can be both integers and floats.
|
||||
- Fixed [#106](https://github.com/roboflow/supervision/issues/106): inconvenient handling of [`sv.PolygonZone`](/0.8.0/detection/tools/polygon_zone/#polygonzone) coordinates. Now `sv.PolygonZone` accepts coordinates in the form of `[[x1, y1], [x2, y2], ...]` that can be both integers and floats.
|
||||
|
||||
### 0.8.0 <small>May 17, 2023</small>
|
||||
|
||||
- Added [#100](https://github.com/roboflow/supervision/pull/100): support for dataset inheritance. The current `Dataset` got renamed to `DetectionDataset`. Now [`DetectionDataset`](/0.8.0/supervision/dataset/core/#detectiondataset) inherits from `BaseDataset`. This change was made to enforce the future consistency of APIs of different types of computer vision datasets.
|
||||
- Added [#100](https://github.com/roboflow/supervision/pull/100): support for dataset inheritance. The current `Dataset` got renamed to `DetectionDataset`. Now [`DetectionDataset`](/0.8.0/dataset/core/#detectiondataset) inherits from `BaseDataset`. This change was made to enforce the future consistency of APIs of different types of computer vision datasets.
|
||||
- Added [#100](https://github.com/roboflow/supervision/pull/100): ability to save datasets in YOLO format using [`DetectionDataset.as_yolo`](/0.8.0/dataset/core/#supervision.dataset.core.DetectionDataset.as_yolo).
|
||||
|
||||
```python
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Datasets
|
||||
|
||||
!!! warning
|
||||
|
||||
Dataset API is still fluid and may change. If you use Dataset API in your project until further notice, freeze the
|
||||
`supervision` version in your `requirements.txt` or `setup.py`.
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Datasets Utils
|
||||
|
||||
+36
-13
@@ -3,17 +3,40 @@ 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.
|
||||
|
||||
- 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.24.0
|
||||
|
||||
- The `frame_resolution_wh ` parameter in [`sv.PolygonZone`](detection/tools/polygon_zone.md/#supervision.detection.tools.polygon_zone.PolygonZone) has been removed.
|
||||
- Supervision installation methods `"headless"` and `"desktop"` were removed, as they are no longer needed. `pip install supervision[headless]` will install the base library and harmlessly warn of non-existent extras.
|
||||
|
||||
### 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_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()` was removed as of `supervision-0.22.0`. Use the constant [`ColorPalette.DEFAULT`](/utils/draw/#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__` 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,13 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Annotators
|
||||
|
||||
=== "BoundingBox"
|
||||
Annotators accept detections and apply box or mask visualizations to the detections. Annotators have many available styles.
|
||||
|
||||
=== "Box"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
@@ -12,8 +15,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 +302,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 +318,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 +413,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 +444,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 +462,39 @@ 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>Try Supervision Annotators on your own image</h2>
|
||||
Visualize annotators on images with COCO classes such as people, vehicles, animals, household items.
|
||||
</div>
|
||||
|
||||
:::supervision.annotators.core.BoundingBoxAnnotator
|
||||
<div style="height: 400px; width: 100%; border-radius: 8px; overflow: hidden;"><iframe src="https://app.roboflow.com/workflows/embed/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ3b3JrZmxvd0lkIjoiNDdtd2xuWW16S25VNWtOYUZjMG8iLCJ3b3Jrc3BhY2VJZCI6ImtyT1RBYm5jRmhvUU1DZExPbGU0IiwidXNlcklkIjoiRVJNUFBZY3FQMmZWWjB1NkRpNXZaYXJDdlZPMiIsImlhdCI6MTcyNjgzOTM2N30.gj2F6SnmmURAScJe4PTC1raUXsAK5mZyrUIGIJ44NhM?hideToolbar=true&hideHeader=true&defaultVisual=true" loading="lazy" title="Roboflow Workflow for Supervision Annotators" style="width: 100%; height: 100%; min-height: 400px; border: none;"></iframe></div>
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.annotators.core.BoxAnnotator">BoxAnnotator</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.annotators.core.BoxAnnotator
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.annotators.core.RoundBoxAnnotator">RoundBoxAnnotator</a></h2>
|
||||
@@ -528,6 +586,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 +616,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>
|
||||
|
||||
@@ -14,3 +14,9 @@ status: new
|
||||
</div>
|
||||
|
||||
:::supervision.detection.line_zone.LineZoneAnnotator
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2>LineZoneAnnotatorMulticlass</h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.line_zone.LineZoneAnnotatorMulticlass
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
<div class="md-typeset">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Detection Utils
|
||||
@@ -16,6 +17,12 @@ comments: true
|
||||
|
||||
:::supervision.detection.utils.mask_iou_batch
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.oriented_box_iou_batch">oriented_box_iou_batch</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.detection.utils.oriented_box_iou_batch
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.detection.utils.polygon_to_mask">polygon_to_mask</a></h2>
|
||||
</div>
|
||||
@@ -76,6 +83,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>
|
||||
|
||||
@@ -6,7 +6,7 @@ comments: true
|
||||
|
||||
Supervision provides a seamless process for annotating predictions generated by various
|
||||
object detection and segmentation models. This guide shows how to perform inference
|
||||
with the [Inference](https://github.com/roboflow/inference),
|
||||
with the [Inference](https://github.com/roboflow/inference),
|
||||
[Ultralytics](https://github.com/ultralytics/ultralytics) or
|
||||
[Transformers](https://github.com/huggingface/transformers) packages. Following this,
|
||||
you'll learn how to import these predictions into Supervision and use them to annotate
|
||||
@@ -20,7 +20,6 @@ First, you'll need to obtain predictions from your object detection or segmentat
|
||||
model.
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```python
|
||||
import cv2
|
||||
from inference import get_model
|
||||
@@ -31,7 +30,6 @@ model.
|
||||
```
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```python
|
||||
import cv2
|
||||
from ultralytics import YOLO
|
||||
@@ -42,7 +40,6 @@ model.
|
||||
```
|
||||
|
||||
=== "Transformers"
|
||||
|
||||
```python
|
||||
import torch
|
||||
from PIL import Image
|
||||
@@ -68,8 +65,7 @@ model.
|
||||
Now that we have predictions from a model, we can load them into Supervision.
|
||||
|
||||
=== "Inference"
|
||||
|
||||
We can do so using the [`sv.Detections.from_inference`](detection/core/#supervision.detection.core.Detections.from_inference) method, which accepts model results from both detection and segmentation models.
|
||||
We can do so using the [`sv.Detections.from_inference`](/latest/detection/core/#supervision.detection.core.Detections.from_inference) method, which accepts model results from both detection and segmentation models.
|
||||
|
||||
```{ .py hl_lines="2 8" }
|
||||
import cv2
|
||||
@@ -83,8 +79,7 @@ Now that we have predictions from a model, we can load them into Supervision.
|
||||
```
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
We can do so using the [`sv.Detections.from_ultralytics`](detection/core/#supervision.detection.core.Detections.from_ultralytics) method, which accepts model results from both detection and segmentation models.
|
||||
We can do so using the [`sv.Detections.from_ultralytics`](/latest/detection/core/#supervision.detection.core.Detections.from_ultralytics) method, which accepts model results from both detection and segmentation models.
|
||||
|
||||
```{ .py hl_lines="2 8" }
|
||||
import cv2
|
||||
@@ -98,8 +93,7 @@ Now that we have predictions from a model, we can load them into Supervision.
|
||||
```
|
||||
|
||||
=== "Transformers"
|
||||
|
||||
We can do so using the [`sv.Detections.from_transformers`](detection/core/#supervision.detection.core.Detections.from_transformers) method, which accepts model results from both detection and segmentation models.
|
||||
We can do so using the [`sv.Detections.from_transformers`](/latest/detection/core/#supervision.detection.core.Detections.from_transformers) method, which accepts model results from both detection and segmentation models.
|
||||
|
||||
```{ .py hl_lines="2 19-21" }
|
||||
import torch
|
||||
@@ -135,10 +129,9 @@ 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/detection/annotators/#supervision.annotators.core.BoxAnnotator) and [`sv.LabelAnnotator`](/latest/detection/annotators/#supervision.annotators.core.LabelAnnotator) classes.
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="10-16" }
|
||||
import cv2
|
||||
import supervision as sv
|
||||
@@ -149,17 +142,16 @@ 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)
|
||||
```
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="10-16" }
|
||||
import cv2
|
||||
import supervision as sv
|
||||
@@ -170,17 +162,16 @@ 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)
|
||||
```
|
||||
|
||||
=== "Transformers"
|
||||
|
||||
```{ .py hl_lines="23-30" }
|
||||
import torch
|
||||
import supervision as sv
|
||||
@@ -204,10 +195,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)
|
||||
@@ -217,12 +208,11 @@ Finally, we can annotate the image with the predictions. Since we are working wi
|
||||
|
||||
## Display Custom Labels
|
||||
|
||||
By default, [`sv.LabelAnnotator`](/latest/annotators/#supervision.annotators.core.LabelAnnotator)
|
||||
By default, [`sv.LabelAnnotator`](/latest/detection/annotators/#supervision.annotators.core.LabelAnnotator)
|
||||
will label each detection with its `class_name` (if possible) or `class_id`. You can
|
||||
override this behavior by passing a list of custom `labels` to the `annotate` method.
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="13-17 22" }
|
||||
import cv2
|
||||
import supervision as sv
|
||||
@@ -233,7 +223,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,14 +232,13 @@ 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)
|
||||
```
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="13-17 22" }
|
||||
import cv2
|
||||
import supervision as sv
|
||||
@@ -260,7 +249,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,14 +258,13 @@ 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)
|
||||
```
|
||||
|
||||
=== "Transformers"
|
||||
|
||||
```{ .py hl_lines="26-30 35" }
|
||||
import torch
|
||||
import supervision as sv
|
||||
@@ -300,7 +288,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 +297,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)
|
||||
@@ -320,13 +308,12 @@ override this behavior by passing a list of custom `labels` to the `annotate` me
|
||||
## Annotate Image with Segmentations
|
||||
|
||||
If you are running the segmentation model
|
||||
[`sv.MaskAnnotator`](/latest/annotators/#supervision.annotators.core.MaskAnnotator)
|
||||
[`sv.MaskAnnotator`](/latest/detection/annotators/#supervision.annotators.core.MaskAnnotator)
|
||||
is a drop-in replacement for
|
||||
[`sv.BoundingBoxAnnotator`](/latest/annotators/#supervision.annotators.core.BoundingBoxAnnotator)
|
||||
[`sv.BoxAnnotator`](/latest/detection/annotators/#supervision.annotators.core.BoxAnnotator)
|
||||
that will allow you to draw masks instead of boxes.
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```python
|
||||
import cv2
|
||||
import supervision as sv
|
||||
@@ -347,7 +334,6 @@ that will allow you to draw masks instead of boxes.
|
||||
```
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```python
|
||||
import cv2
|
||||
import supervision as sv
|
||||
@@ -368,7 +354,6 @@ that will allow you to draw masks instead of boxes.
|
||||
```
|
||||
|
||||
=== "Transformers"
|
||||
|
||||
```python
|
||||
import torch
|
||||
import supervision as sv
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Detect Small Objects
|
||||
@@ -21,7 +20,6 @@ Small object detection in high-resolution images presents challenges due to the
|
||||
size relative to the image resolution.
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```python
|
||||
import cv2
|
||||
import supervision as sv
|
||||
@@ -32,17 +30,16 @@ 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)
|
||||
```
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```python
|
||||
import cv2
|
||||
import supervision as sv
|
||||
@@ -53,17 +50,16 @@ 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)
|
||||
```
|
||||
|
||||
=== "Transformers"
|
||||
|
||||
```python
|
||||
import torch
|
||||
import supervision as sv
|
||||
@@ -85,7 +81,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 +90,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)
|
||||
@@ -109,7 +105,6 @@ identification at the cost of processing speed and increased memory usage. This
|
||||
is less effective for ultra-high-resolution images (4K and above).
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="5" }
|
||||
import cv2
|
||||
import supervision as sv
|
||||
@@ -120,17 +115,16 @@ 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)
|
||||
```
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="7" }
|
||||
import cv2
|
||||
import supervision as sv
|
||||
@@ -141,10 +135,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)
|
||||
@@ -163,7 +157,6 @@ objects within each, and aggregating the results.
|
||||
</video>
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="9-14" }
|
||||
import cv2
|
||||
import numpy as np
|
||||
@@ -180,17 +173,16 @@ 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)
|
||||
```
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="9-14" }
|
||||
import cv2
|
||||
import numpy as np
|
||||
@@ -207,17 +199,16 @@ 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)
|
||||
```
|
||||
|
||||
=== "Transformers"
|
||||
|
||||
```{ .py hl_lines="13-28" }
|
||||
import cv2
|
||||
import torch
|
||||
@@ -248,7 +239,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 +248,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)
|
||||
@@ -270,7 +261,6 @@ objects within each, and aggregating the results.
|
||||
[`InferenceSlicer`](/latest/detection/tools/inference_slicer/#supervision.detection.tools.inference_slicer.InferenceSlicer) can perform segmentation tasks too.
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="6 16 19-20" }
|
||||
import cv2
|
||||
import numpy as np
|
||||
@@ -297,7 +287,6 @@ objects within each, and aggregating the results.
|
||||
```
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="6 16 19-20" }
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
@@ -15,7 +15,6 @@ the filters in their applications.
|
||||
Allows you to select detections that belong only to one selected class.
|
||||
|
||||
=== "After"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
@@ -30,7 +29,6 @@ Allows you to select detections that belong only to one selected class.
|
||||
</div>
|
||||
|
||||
=== "Before"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
@@ -49,7 +47,6 @@ Allows you to select detections that belong only to one selected class.
|
||||
Allows you to select detections that belong only to selected set of classes.
|
||||
|
||||
=== "After"
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
@@ -66,7 +63,6 @@ Allows you to select detections that belong only to selected set of classes.
|
||||
</div>
|
||||
|
||||
=== "Before"
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
@@ -87,7 +83,6 @@ Allows you to select detections that belong only to selected set of classes.
|
||||
Allows you to select detections with specific confidence value, for example higher than selected threshold.
|
||||
|
||||
=== "After"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
@@ -102,7 +97,6 @@ Allows you to select detections with specific confidence value, for example high
|
||||
</div>
|
||||
|
||||
=== "Before"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
@@ -122,7 +116,6 @@ Allows you to select detections based on their size. We define the area as the n
|
||||
detection in the image. In the example below, we have sifted out the detections that are too small.
|
||||
|
||||
=== "After"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
@@ -137,7 +130,6 @@ detection in the image. In the example below, we have sifted out the detections
|
||||
</div>
|
||||
|
||||
=== "Before"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
@@ -159,7 +151,6 @@ but small on a 3840x2160 image. In such cases, we can filter out detections base
|
||||
occupied by them. In the example below, we remove too large detections.
|
||||
|
||||
=== "After"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
@@ -178,7 +169,6 @@ occupied by them. In the example below, we remove too large detections.
|
||||
</div>
|
||||
|
||||
=== "Before"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
@@ -203,7 +193,6 @@ can be criteria for rejecting detection. Implementing such filtering requires a
|
||||
simple and fast.
|
||||
|
||||
=== "After"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
@@ -220,7 +209,6 @@ simple and fast.
|
||||
</div>
|
||||
|
||||
=== "Before"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
@@ -242,7 +230,6 @@ Allows you to use `Detections` in combination with `PolygonZone` to weed out bou
|
||||
zone. In the example below you can see how to filter out all detections located in the lower part of the image.
|
||||
|
||||
=== "After"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
@@ -259,7 +246,6 @@ zone. In the example below you can see how to filter out all detections located
|
||||
</div>
|
||||
|
||||
=== "Before"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
@@ -280,7 +266,6 @@ zone. In the example below you can see how to filter out all detections located
|
||||
`Detections`' greatest strength, however, is that you can build arbitrarily complex logical conditions by simply combining separate conditions using `&` or `|`.
|
||||
|
||||
=== "After"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
@@ -297,7 +282,6 @@ zone. In the example below you can see how to filter out all detections located
|
||||
</div>
|
||||
|
||||
=== "Before"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
|
||||
|
||||
@@ -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'])
|
||||
)
|
||||
```
|
||||
|
||||

|
||||
@@ -19,7 +19,6 @@ model. You can learn more on this topic in our
|
||||
[How to Detect and Annotate](/latest/how_to/detect_and_annotate.md) guide.
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
from inference import get_model
|
||||
@@ -34,7 +33,6 @@ model. You can learn more on this topic in our
|
||||
```
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```python
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
@@ -49,7 +47,6 @@ model. You can learn more on this topic in our
|
||||
```
|
||||
|
||||
=== "Transformers"
|
||||
|
||||
```python
|
||||
import torch
|
||||
import supervision as sv
|
||||
@@ -83,7 +80,6 @@ and then pass the
|
||||
object resulting from the inference to it. Its fields are parsed and saved on disk.
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="7 12" }
|
||||
import supervision as sv
|
||||
from inference import get_model
|
||||
@@ -100,7 +96,6 @@ object resulting from the inference to it. Its fields are parsed and saved on di
|
||||
```
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="7 12" }
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
@@ -117,7 +112,6 @@ object resulting from the inference to it. Its fields are parsed and saved on di
|
||||
```
|
||||
|
||||
=== "Transformers"
|
||||
|
||||
```{ .py hl_lines="9 23" }
|
||||
import torch
|
||||
import supervision as sv
|
||||
@@ -144,11 +138,11 @@ object resulting from the inference to it. Its fields are parsed and saved on di
|
||||
sink.append(detections, {})
|
||||
```
|
||||
|
||||
| x_min | y_min | x_max | y_max | class_id | confidence | tracker_id | class_name |
|
||||
|---------|----------|---------|----------|----------|------------|------------|------------|
|
||||
| 2941.14 | 1269.31 | 3220.77 | 1500.67 | 2 | 0.8517 | | car |
|
||||
| 944.889 | 899.641 | 1235.42 | 1308.80 | 7 | 0.6752 | | truck |
|
||||
| 1439.78 | 1077.79 | 1621.27 | 1231.40 | 2 | 0.6450 | | car |
|
||||
| x_min | y_min | x_max | y_max | class_id | confidence | tracker_id | class_name |
|
||||
| ------- | ------- | ------- | ------- | -------- | ---------- | ---------- | ---------- |
|
||||
| 2941.14 | 1269.31 | 3220.77 | 1500.67 | 2 | 0.8517 | | car |
|
||||
| 944.889 | 899.641 | 1235.42 | 1308.80 | 7 | 0.6752 | | truck |
|
||||
| 1439.78 | 1077.79 | 1621.27 | 1231.40 | 2 | 0.6450 | | car |
|
||||
|
||||
## Custom Fields
|
||||
|
||||
@@ -160,7 +154,6 @@ also allows you to add custom information to each row, which can be passed via t
|
||||
frame index from which the detections originate.
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="8 12" }
|
||||
import supervision as sv
|
||||
from inference import get_model
|
||||
@@ -177,7 +170,6 @@ frame index from which the detections originate.
|
||||
```
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="8 12" }
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
@@ -194,7 +186,6 @@ frame index from which the detections originate.
|
||||
```
|
||||
|
||||
=== "Transformers"
|
||||
|
||||
```{ .py hl_lines="10 23" }
|
||||
import torch
|
||||
import supervision as sv
|
||||
@@ -221,11 +212,11 @@ frame index from which the detections originate.
|
||||
sink.append(detections, {"frame_index": frame_index})
|
||||
```
|
||||
|
||||
| x_min | y_min | x_max | y_max | class_id | confidence | tracker_id | class_name | frame_index |
|
||||
|---------|----------|---------|----------|----------|------------|------------|------------|-------------|
|
||||
| 2941.14 | 1269.31 | 3220.77 | 1500.67 | 2 | 0.8517 | | car | 0 |
|
||||
| 944.889 | 899.641 | 1235.42 | 1308.80 | 7 | 0.6752 | | truck | 0 |
|
||||
| 1439.78 | 1077.79 | 1621.27 | 1231.40 | 2 | 0.6450 | | car | 0 |
|
||||
| x_min | y_min | x_max | y_max | class_id | confidence | tracker_id | class_name | frame_index |
|
||||
| ------- | ------- | ------- | ------- | -------- | ---------- | ---------- | ---------- | ----------- |
|
||||
| 2941.14 | 1269.31 | 3220.77 | 1500.67 | 2 | 0.8517 | | car | 0 |
|
||||
| 944.889 | 899.641 | 1235.42 | 1308.80 | 7 | 0.6752 | | truck | 0 |
|
||||
| 1439.78 | 1077.79 | 1621.27 | 1231.40 | 2 | 0.6450 | | car | 0 |
|
||||
|
||||
## Save Detections as JSON
|
||||
|
||||
@@ -236,7 +227,6 @@ with
|
||||
[`sv.JSONSink`](/latest/detection/tools/save_detections/#supervision.detection.tools.csv_sink.JSONSink).
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="7" }
|
||||
import supervision as sv
|
||||
from inference import get_model
|
||||
@@ -253,7 +243,6 @@ with
|
||||
```
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="7" }
|
||||
import supervision as sv
|
||||
from ultralytics import YOLO
|
||||
@@ -270,7 +259,6 @@ with
|
||||
```
|
||||
|
||||
=== "Transformers"
|
||||
|
||||
```{ .py hl_lines="9" }
|
||||
import torch
|
||||
import supervision as sv
|
||||
|
||||
@@ -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
|
||||
@@ -41,7 +41,6 @@ This `callback` function will be essential in the subsequent steps of the tutori
|
||||
it will be modified to include tracking, labeling, and trace annotations.
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
@@ -63,7 +62,6 @@ it will be modified to include tracking, labeling, and trace annotations.
|
||||
```
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
@@ -97,7 +95,6 @@ functionality, each detected object is assigned a unique tracker ID,
|
||||
enabling the continuous following of the object's motion path across different frames.
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="6 12" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
@@ -121,7 +118,6 @@ enabling the continuous following of the object's motion path across different f
|
||||
```
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="6 12" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
@@ -148,12 +144,11 @@ enabling the continuous following of the object's motion path across different f
|
||||
|
||||
Annotating the video with tracking IDs helps in distinguishing and following each object
|
||||
distinctly. With the
|
||||
[`sv.LabelAnnotator`](/latest/annotators.md/#supervision.annotators.core.LabelAnnotator)
|
||||
[`sv.LabelAnnotator`](/latest/detection/annotators/#supervision.annotators.core.LabelAnnotator)
|
||||
in Supervision, we can overlay the tracker IDs and class labels on the detected objects,
|
||||
offering a clear visual representation of each object's class and unique identifier.
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="8 15-19 23-24" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
@@ -188,7 +183,6 @@ offering a clear visual representation of each object's class and unique identif
|
||||
```
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="8 15-19 23-24" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
@@ -230,12 +224,11 @@ offering a clear visual representation of each object's class and unique identif
|
||||
|
||||
Adding traces to the video involves overlaying the historical paths of the detected
|
||||
objects. This feature, powered by the
|
||||
[`sv.TraceAnnotator`](/latest/annotators/#supervision.annotators.core.TraceAnnotator),
|
||||
[`sv.TraceAnnotator`](/latest/detection/annotators/#supervision.annotators.core.TraceAnnotator),
|
||||
allows for visualizing the trajectories of objects, helping in understanding the
|
||||
movement patterns and interactions between objects in the video.
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
```{ .py hl_lines="9 26-27" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
@@ -273,7 +266,6 @@ movement patterns and interactions between objects in the video.
|
||||
```
|
||||
|
||||
=== "Inference"
|
||||
|
||||
```{ .py hl_lines="9 26-27" }
|
||||
import numpy as np
|
||||
import supervision as sv
|
||||
|
||||
+22
-29
@@ -34,25 +34,18 @@ You can install `supervision` in a
|
||||
[**Python>=3.8**](https://www.python.org/) environment.
|
||||
|
||||
!!! example "pip install (recommended)"
|
||||
|
||||
=== "headless"
|
||||
The headless installation of `supervision` is designed for environments where graphical user interfaces (GUI) are not needed, making it more lightweight and suitable for server-side applications.
|
||||
=== "pip"
|
||||
[](https://badge.fury.io/py/supervision)
|
||||
[](https://pypistats.org/packages/supervision)
|
||||
[](https://github.com/roboflow/supervision/blob/main/LICENSE.md)
|
||||
[](https://badge.fury.io/py/supervision)
|
||||
|
||||
```bash
|
||||
pip install supervision
|
||||
```
|
||||
|
||||
=== "desktop"
|
||||
If you require the full version of `supervision` with GUI support you can install the desktop version. This version includes the GUI components of OpenCV, allowing you to display images and videos on the screen.
|
||||
|
||||
```bash
|
||||
pip install "supervision[desktop]"
|
||||
```
|
||||
|
||||
!!! example "conda/mamba install"
|
||||
|
||||
=== "conda"
|
||||
|
||||
[](https://anaconda.org/conda-forge/supervision) [](https://anaconda.org/conda-forge/supervision) [](https://anaconda.org/conda-forge/supervision) [](https://anaconda.org/conda-forge/supervision)
|
||||
|
||||
```bash
|
||||
@@ -60,7 +53,6 @@ You can install `supervision` in a
|
||||
```
|
||||
|
||||
=== "mamba"
|
||||
|
||||
[](https://anaconda.org/conda-forge/supervision) [](https://anaconda.org/conda-forge/supervision) [](https://anaconda.org/conda-forge/supervision) [](https://anaconda.org/conda-forge/supervision)
|
||||
|
||||
```bash
|
||||
@@ -68,9 +60,7 @@ You can install `supervision` in a
|
||||
```
|
||||
|
||||
!!! example "git clone (for development)"
|
||||
|
||||
=== "virtualenv"
|
||||
|
||||
```bash
|
||||
# clone repository and navigate to root directory
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
@@ -81,15 +71,11 @@ You can install `supervision` in a
|
||||
source venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
|
||||
# headless install
|
||||
# installation
|
||||
pip install -e "."
|
||||
|
||||
# desktop install
|
||||
pip install -e ".[desktop]"
|
||||
```
|
||||
|
||||
=== "poetry"
|
||||
|
||||
```bash
|
||||
# clone repository and navigate to root directory
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
@@ -99,18 +85,15 @@ You can install `supervision` in a
|
||||
poetry env use python3.10
|
||||
poetry shell
|
||||
|
||||
# headless install
|
||||
# installation
|
||||
poetry install
|
||||
|
||||
# desktop install
|
||||
poetry install --extras "desktop"
|
||||
```
|
||||
|
||||
## 🚀 Quickstart
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
|
||||
- __Detect and Annotate__
|
||||
- **Detect and Annotate**
|
||||
|
||||
---
|
||||
|
||||
@@ -118,7 +101,7 @@ You can install `supervision` in a
|
||||
|
||||
[:octicons-arrow-right-24: Tutorial](how_to/detect_and_annotate.md)
|
||||
|
||||
- __Track Objects__
|
||||
- **Track Objects**
|
||||
|
||||
---
|
||||
|
||||
@@ -126,7 +109,7 @@ You can install `supervision` in a
|
||||
|
||||
[:octicons-arrow-right-24: Tutorial](how_to/track_objects.md)
|
||||
|
||||
- __Detect Small Objects__
|
||||
- **Detect Small Objects**
|
||||
|
||||
---
|
||||
|
||||
@@ -134,16 +117,26 @@ You can install `supervision` in a
|
||||
|
||||
[:octicons-arrow-right-24: Tutorial](how_to/detect_small_objects.md)
|
||||
|
||||
- > __Count Objects Crossing Line__
|
||||
- **Count Objects Crossing Line**
|
||||
|
||||
---
|
||||
|
||||
Explore methods to accurately count and analyze objects crossing a predefined line
|
||||
|
||||
- > __Filter Objects in Zone__
|
||||
[:octicons-arrow-right-24: Notebook](https://supervision.roboflow.com/latest/notebooks/count-objects-crossing-the-line/)
|
||||
|
||||
- > **Filter Objects in Zone**
|
||||
|
||||
---
|
||||
|
||||
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
|
||||
---
|
||||
|
||||
# Annotators
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# Keypoint Detection
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
comments: true
|
||||
status: new
|
||||
---
|
||||
|
||||
# F1 Score
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.metrics.f1_score.F1Score">F1Score</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.metrics.f1_score.F1Score
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.metrics.f1_score.F1ScoreResult">F1ScoreResult</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.metrics.f1_score.F1ScoreResult
|
||||
@@ -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
|
||||
+5
-5
@@ -287,10 +287,10 @@
|
||||
],
|
||||
"source": [
|
||||
"# Create a bounding box annotator object.\n",
|
||||
"bounding_box = sv.BoundingBoxAnnotator()\n",
|
||||
"box_annotator = sv.BoxAnnotator()\n",
|
||||
"\n",
|
||||
"# Annotate our frame with detections.\n",
|
||||
"annotated_frame = bounding_box.annotate(scene=frame.copy(), detections=detections)\n",
|
||||
"annotated_frame = box_annotator.annotate(scene=frame.copy(), detections=detections)\n",
|
||||
"\n",
|
||||
"# Display the frame.\n",
|
||||
"sv.plot_image(annotated_frame)"
|
||||
@@ -302,7 +302,7 @@
|
||||
"id": "o8SsyCid6YV3"
|
||||
},
|
||||
"source": [
|
||||
"Notice that we create a `box_annoator` variable by initalizing a [BoundingBoxAnnotator](https://supervision.roboflow.com/latest/annotators/#boundingboxannotator). We can change the color and thickness, but for simplicity we keep the defaults. There are a ton of easy to use [annotators](https://supervision.roboflow.com/latest/annotators/) available in the Supervision package other than a bounding box that are fun to play with."
|
||||
"Notice that we create a `box_annotator` variable by initalizing a [BoxAnnotator](https://supervision.roboflow.com/latest/detection/annotators/#boxannotator). We can change the color and thickness, but for simplicity we keep the defaults. There are a ton of easy to use [annotators](https://supervision.roboflow.com/latest/detection/annotators/) available in the Supervision package other than a bounding box that are fun to play with."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -342,7 +342,7 @@
|
||||
" detections = sv.Detections.from_inference(result)\n",
|
||||
"\n",
|
||||
" # Apply bounding box to detections on a copy of the frame.\n",
|
||||
" annotated_frame = bounding_box.annotate(\n",
|
||||
" annotated_frame = box_annotator.annotate(\n",
|
||||
" scene=frame.copy(),\n",
|
||||
" detections=detections\n",
|
||||
" )\n",
|
||||
@@ -368,7 +368,7 @@
|
||||
"<br>\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"In the code above we've created a`video_info` variable to pass information about the video to our `VideoSink`. The `VideoSink` is a cool little context manager that allows us to `write_frames()` to a video ouput file. We're also optionally using `tqdm` to display a progress bar with a % complete. We only scratched the surface of all of the customizable Annotators and additional features that Supervision and Inference have to offer. Stay tuned for more cookbooks on how to take advantge of them in your computer vision applications. Happy building! \ud83d\ude80"
|
||||
"In the code above we've created a`video_info` variable to pass information about the video to our `VideoSink`. The `VideoSink` is a cool little context manager that allows us to `write_frames()` to a video output file. We're also optionally using `tqdm` to display a progress bar with a % complete. We only scratched the surface of all of the customizable Annotators and additional features that Supervision and Inference have to offer. Stay tuned for more cookbooks on how to take advantage of them in your computer vision applications. Happy building! \ud83d\ude80"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
+223
-223
File diff suppressed because one or more lines are too long
+218
-218
File diff suppressed because one or more lines are too long
Vendored
+7
-7
File diff suppressed because one or more lines are too long
+1
-1
@@ -951,7 +951,7 @@
|
||||
},
|
||||
"source": [
|
||||
"#### Import your data\n",
|
||||
"To load your data back in, upload the saved file to the Colab enviorment and run the code cell."
|
||||
"To load your data back in, upload the saved file to the Colab environment and run the code cell."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
Vendored
+322
-189
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
+1074
File diff suppressed because one or more lines are too long
+347
-347
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
+43
-8
@@ -11,14 +11,49 @@
|
||||
<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>
|
||||
<a href="/develop/notebooks/small-object-detection-with-sahi">
|
||||
<p class="card repo-card" data-name="Small Object Detection with SAHI"
|
||||
data-labels="DETECTIONS,SAHI,SMALL,OBJECT,INFERENCE" data-version="v0.23.0" data-author="ediardo"></p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -28,6 +28,12 @@ comments: true
|
||||
|
||||
:::supervision.draw.utils.draw_polygon
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.draw.utils.draw_filled_polygon">draw_filled_polygon</a></h2>
|
||||
</div>
|
||||
|
||||
:::supervision.draw.utils.draw_filled_polygon
|
||||
|
||||
<div class="md-typeset">
|
||||
<h2><a href="#supervision.draw.utils.draw_text">draw_text</a></h2>
|
||||
</div>
|
||||
|
||||
@@ -16,67 +16,76 @@ https://github.com/roboflow/supervision/assets/26109316/f84db7b5-79e2-4142-a1da-
|
||||
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/count_people_in_zone
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/count_people_in_zone
|
||||
```
|
||||
|
||||
- setup python environment and activate it [optional]
|
||||
- setup python environment and activate it \[optional\]
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
- install required dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
- download `traffic_analysis.pt` and `traffic_analysis.mov` files
|
||||
|
||||
```bash
|
||||
./setup.sh
|
||||
```
|
||||
```bash
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
## 🛠️ script arguments
|
||||
|
||||
- ultralytics
|
||||
|
||||
- `--source_weights_path` (optional): The path to the YOLO model's weights file.
|
||||
Defaults to `"yolov8x.pt"` if not specified.
|
||||
- `--source_weights_path` (optional): The path to the YOLO model's weights file.
|
||||
Defaults to `"yolov8x.pt"` if not specified.
|
||||
|
||||
- `--zone_configuration_path`: Specifies the path to the JSON file containing zone
|
||||
configurations. This file defines the polygonal areas in the video where objects will
|
||||
be counted.
|
||||
- `--source_video_path`: The path to the source video file that will be analyzed.
|
||||
- `--target_video_path` (optional): The path to save the output video with annotations.
|
||||
If not provided, the processed video will be displayed in real-time.
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO model
|
||||
to filter detections. Default is `0.3`.
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold
|
||||
for the model. Default is `0.7`.
|
||||
- `--zone_configuration_path`: Specifies the path to the JSON file containing zone
|
||||
configurations. This file defines the polygonal areas in the video where objects will
|
||||
be counted.
|
||||
|
||||
- `--source_video_path`: The path to the source video file that will be analyzed.
|
||||
|
||||
- `--target_video_path` (optional): The path to save the output video with annotations.
|
||||
If not provided, the processed video will be displayed in real-time.
|
||||
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO model
|
||||
to filter detections. Default is `0.3`.
|
||||
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold
|
||||
for the model. Default is `0.7`.
|
||||
|
||||
- inference
|
||||
|
||||
- `--roboflow_api_key` (optional): The API key for Roboflow services. If not provided
|
||||
directly, the script tries to fetch it from the `ROBOFLOW_API_KEY` environment
|
||||
variable. Follow [this guide](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key)
|
||||
to acquire your `API KEY`.
|
||||
- `--model_id` (optional): Designates the Roboflow model ID to be used. The default
|
||||
value is `"yolov8x-1280"`.
|
||||
- `--roboflow_api_key` (optional): The API key for Roboflow services. If not provided
|
||||
directly, the script tries to fetch it from the `ROBOFLOW_API_KEY` environment
|
||||
variable. Follow [this guide](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key)
|
||||
to acquire your `API KEY`.
|
||||
|
||||
- `--zone_configuration_path`: Specifies the path to the JSON file containing zone
|
||||
configurations. This file defines the polygonal areas in the video where objects will
|
||||
be counted.
|
||||
- `--source_video_path`: The path to the source video file that will be analyzed.
|
||||
- `--target_video_path` (optional): The path to save the output video with annotations.
|
||||
If not provided, the processed video will be displayed in real-time.
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO model
|
||||
to filter detections. Default is `0.3`.
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold
|
||||
for the model. Default is `0.7`.
|
||||
- `--model_id` (optional): Designates the Roboflow model ID to be used. The default
|
||||
value is `"yolov8x-1280"`.
|
||||
|
||||
- `--zone_configuration_path`: Specifies the path to the JSON file containing zone
|
||||
configurations. This file defines the polygonal areas in the video where objects will
|
||||
be counted.
|
||||
|
||||
- `--source_video_path`: The path to the source video file that will be analyzed.
|
||||
|
||||
- `--target_video_path` (optional): The path to save the output video with annotations.
|
||||
If not provided, the processed video will be displayed in real-time.
|
||||
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO model
|
||||
to filter detections. Default is `0.3`.
|
||||
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold
|
||||
for the model. Default is `0.7`.
|
||||
|
||||
## 📌 zone configuration
|
||||
|
||||
@@ -89,35 +98,35 @@ https://github.com/roboflow/supervision/assets/26109316/f84db7b5-79e2-4142-a1da-
|
||||
|
||||
- ultralytics
|
||||
|
||||
```bash
|
||||
python ultralytics_example.py \
|
||||
--zone_configuration_path data/multi-zone-config.json \
|
||||
--source_video_path data/market-square.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
```bash
|
||||
python ultralytics_example.py \
|
||||
--zone_configuration_path data/multi-zone-config.json \
|
||||
--source_video_path data/market-square.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
|
||||
- inference
|
||||
|
||||
```bash
|
||||
python inference_example.py \
|
||||
--roboflow_api_key <ROBOFLOW API KEY> \
|
||||
--zone_configuration_path data/multi-zone-config.json \
|
||||
--source_video_path data/market-square.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
```bash
|
||||
python inference_example.py \
|
||||
--roboflow_api_key <ROBOFLOW API KEY> \
|
||||
--zone_configuration_path data/multi-zone-config.json \
|
||||
--source_video_path data/market-square.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
|
||||
## © license
|
||||
|
||||
This demo integrates two main components, each with its own licensing:
|
||||
|
||||
- ultralytics: The object detection model used in this demo, YOLOv8, is distributed
|
||||
under the [AGPL-3.0 license](https://github.com/ultralytics/ultralytics/blob/main/LICENSE).
|
||||
You can find more details about this license here.
|
||||
under the [AGPL-3.0 license](https://github.com/ultralytics/ultralytics/blob/main/LICENSE).
|
||||
You can find more details about this license here.
|
||||
|
||||
- supervision: The analytics code that powers the zone-based analysis in this demo is
|
||||
based on the Supervision library, which is licensed under the
|
||||
[MIT license](https://github.com/roboflow/supervision/blob/develop/LICENSE.md). This
|
||||
makes the Supervision part of the code fully open source and freely usable in your
|
||||
projects.
|
||||
based on the Supervision library, which is licensed under the
|
||||
[MIT license](https://github.com/roboflow/supervision/blob/develop/LICENSE.md). This
|
||||
makes the Supervision part of the code fully open source and freely usable in your
|
||||
projects.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -6,7 +6,6 @@ This script performs heatmap and tracking analysis using YOLOv8, an object-detec
|
||||
ByteTrack, a simple yet effective online multi-object tracking method. It uses the
|
||||
supervision package for multiple tasks such as drawing heatmap annotations, tracking objects, etc.
|
||||
|
||||
|
||||
## 💻 install
|
||||
|
||||
- clone repository and navigate to example directory
|
||||
@@ -16,7 +15,7 @@ supervision package for multiple tasks such as drawing heatmap annotations, trac
|
||||
cd supervision/examples/heatmap_and_track
|
||||
```
|
||||
|
||||
- setup python environment and activate it [optional]
|
||||
- setup python environment and activate it \[optional\]
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
@@ -32,17 +31,17 @@ supervision package for multiple tasks such as drawing heatmap annotations, trac
|
||||
## 🛠️ script arguments
|
||||
|
||||
- `--source_weights_path`: Required. Specifies the path to the weights file for the
|
||||
YOLO model. This file contains the trained model data necessary for object detection.
|
||||
YOLO model. This file contains the trained model data necessary for object detection.
|
||||
- `--source_video_path` (optional): The path to the source video file that will be
|
||||
analyzed. This is the input video on which crowd analysis will be performed.
|
||||
If not specified default is `people-walking.mp4` from supervision assets
|
||||
analyzed. This is the input video on which crowd analysis will be performed.
|
||||
If not specified default is `people-walking.mp4` from supervision assets
|
||||
- `--target_video_path` (optional): The path to save the output.mp4 video with annotations.
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO model
|
||||
to filter detections. Default is `0.3`. This determines how confident the model should
|
||||
be to recognize an object in the video.
|
||||
to filter detections. Default is `0.3`. This determines how confident the model should
|
||||
be to recognize an object in the video.
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold
|
||||
for the model. Default is 0.7. This value is used to manage object detection accuracy,
|
||||
particularly in distinguishing between different objects.
|
||||
for the model. Default is 0.7. This value is used to manage object detection accuracy,
|
||||
particularly in distinguishing between different objects.
|
||||
- `--heatmap_alpha` (optional): Opacity of the overlay mask, between 0 and 1.
|
||||
- `--radius` (optional): Radius of the heat circle.
|
||||
- `--track_threshold` (optional): Detection confidence threshold for track activation.
|
||||
@@ -53,11 +52,11 @@ particularly in distinguishing between different objects.
|
||||
|
||||
```bash
|
||||
python script.py \
|
||||
--source_weights_path weight.pt \
|
||||
--source_video_path input_video.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5 \
|
||||
--target_video_path output_video.mp4
|
||||
--source_weights_path weight.pt \
|
||||
--source_video_path input_video.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5 \
|
||||
--target_video_path output_video.mp4
|
||||
```
|
||||
|
||||
## © license
|
||||
@@ -65,11 +64,11 @@ python script.py \
|
||||
This demo integrates two main components, each with its own licensing:
|
||||
|
||||
- ultralytics: The object detection model used in this demo, YOLOv8, is distributed
|
||||
under the [AGPL-3.0 license](https://github.com/ultralytics/ultralytics/blob/main/LICENSE).
|
||||
You can find more details about this license here.
|
||||
under the [AGPL-3.0 license](https://github.com/ultralytics/ultralytics/blob/main/LICENSE).
|
||||
You can find more details about this license here.
|
||||
|
||||
- supervision: The analytics code that powers the zone-based analysis in this demo is
|
||||
based on the Supervision library, which is licensed under the
|
||||
[MIT license](https://github.com/roboflow/supervision/blob/develop/LICENSE.md). This
|
||||
makes the Supervision part of the code fully open source and freely usable in your
|
||||
projects.
|
||||
based on the Supervision library, which is licensed under the
|
||||
[MIT license](https://github.com/roboflow/supervision/blob/develop/LICENSE.md). This
|
||||
makes the Supervision part of the code fully open source and freely usable in your
|
||||
projects.
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ supervision package for multiple tasks such as tracking, annotations, etc.
|
||||
|
||||
https://github.com/roboflow/supervision/assets/26109316/d50118c1-2ae4-458d-915a-5d860fd36f71
|
||||
|
||||
> [!IMPORTANT]
|
||||
> \[!IMPORTANT\]
|
||||
> Adjust the [`SOURCE`](https://github.com/roboflow/supervision/blob/e32b05a636dab2ea1f39299e529c4b22b8baa8da/examples/speed_estimation/ultralytics_example.py#L10)
|
||||
> and [`TARGET`](https://github.com/roboflow/supervision/blob/e32b05a636dab2ea1f39299e529c4b22b8baa8da/examples/speed_estimation/ultralytics_example.py#L15)
|
||||
> configuration if you plan to run a speed estimation script on your video file. Those must be adjusted separately for each camera view. You can learn more
|
||||
@@ -21,97 +21,102 @@ https://github.com/roboflow/supervision/assets/26109316/d50118c1-2ae4-458d-915a-
|
||||
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/speed_estimation
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/speed_estimation
|
||||
```
|
||||
|
||||
- setup python environment and activate it [optional]
|
||||
- setup python environment and activate it \[optional\]
|
||||
|
||||
```bash
|
||||
python3.10 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
```bash
|
||||
python3.10 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
- install required dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
- download `vehicles.mp4` file
|
||||
|
||||
```bash
|
||||
python3.10 video_downloader.py
|
||||
```
|
||||
```bash
|
||||
python3.10 video_downloader.py
|
||||
```
|
||||
|
||||
## 🛠️ script arguments
|
||||
|
||||
- `--roboflow_api_key` (optional): The API key for Roboflow services. If not provided
|
||||
directly, the script tries to fetch it from the `ROBOFLOW_API_KEY` environment
|
||||
variable. Follow [this guide](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key)
|
||||
to acquire your `API KEY`.
|
||||
directly, the script tries to fetch it from the `ROBOFLOW_API_KEY` environment
|
||||
variable. Follow [this guide](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key)
|
||||
to acquire your `API KEY`.
|
||||
|
||||
- `--model_id` (optional): Designates the Roboflow model ID to be used. The default
|
||||
value is `"yolov8x-1280"`.
|
||||
value is `"yolov8x-1280"`.
|
||||
|
||||
- `--source_weights_path`: Required. Specifies the path to the YOLO model's weights
|
||||
file, which is essential for the object detection process. This file contains the
|
||||
data that the model uses to identify objects in the video.
|
||||
file, which is essential for the object detection process. This file contains the
|
||||
data that the model uses to identify objects in the video.
|
||||
|
||||
- `--source_video_path`: Required. The path to the source video file that will be
|
||||
analyzed. This is the input video on which traffic flow analysis will be performed.
|
||||
analyzed. This is the input video on which traffic flow analysis will be performed.
|
||||
|
||||
- `--target_video_path`: The path to save the output video with
|
||||
annotations. If not specified, the processed video will be displayed in real-time
|
||||
without being saved.
|
||||
annotations. If not specified, the processed video will be displayed in real-time
|
||||
without being saved.
|
||||
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO
|
||||
model to filter detections. Default is `0.3`. This determines how confident the
|
||||
model should be to recognize an object in the video.
|
||||
model to filter detections. Default is `0.3`. This determines how confident the
|
||||
model should be to recognize an object in the video.
|
||||
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold
|
||||
for the model. Default is 0.7. This value is used to manage object detection
|
||||
accuracy, particularly in distinguishing between different objects.
|
||||
for the model. Default is 0.7. This value is used to manage object detection
|
||||
accuracy, particularly in distinguishing between different objects.
|
||||
|
||||
## ⚙️ run
|
||||
|
||||
- yolo-nas
|
||||
|
||||
```bash
|
||||
```bash
|
||||
python yolo_nas_example.py \
|
||||
--source_video_path data/vehicles.mp4 \
|
||||
--target_video_path data/vehicles-result.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
--source_video_path data/vehicles.mp4 \
|
||||
--target_video_path data/vehicles-result.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
|
||||
- inference
|
||||
|
||||
```bash
|
||||
```bash
|
||||
python inference_example.py \
|
||||
--roboflow_api_key <ROBOFLOW API KEY> \
|
||||
--source_video_path data/vehicles.mp4 \
|
||||
--target_video_path data/vehicles-result.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
--roboflow_api_key <ROBOFLOW API KEY> \
|
||||
--source_video_path data/vehicles.mp4 \
|
||||
--target_video_path data/vehicles-result.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
|
||||
- ultralytics
|
||||
|
||||
```bash
|
||||
```bash
|
||||
python ultralytics_example.py \
|
||||
--source_video_path data/vehicles.mp4 \
|
||||
--target_video_path data/vehicles-result.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
--source_video_path data/vehicles.mp4 \
|
||||
--target_video_path data/vehicles-result.mp4 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5
|
||||
```
|
||||
|
||||
## © license
|
||||
|
||||
This demo integrates two main components, each with its own licensing:
|
||||
|
||||
- ultralytics: The object detection model used in this demo, YOLOv8, is distributed
|
||||
under the [AGPL-3.0 license](https://github.com/ultralytics/ultralytics/blob/main/LICENSE).
|
||||
You can find more details about this license here.
|
||||
under the [AGPL-3.0 license](https://github.com/ultralytics/ultralytics/blob/main/LICENSE).
|
||||
You can find more details about this license here.
|
||||
|
||||
- supervision: The analytics code that powers the zone-based analysis in this demo is
|
||||
based on the Supervision library, which is licensed under the
|
||||
[MIT license](https://github.com/roboflow/supervision/blob/develop/LICENSE.md). This
|
||||
makes the Supervision part of the code fully open source and freely usable in your
|
||||
projects.
|
||||
based on the Supervision library, which is licensed under the
|
||||
[MIT license](https://github.com/roboflow/supervision/blob/develop/LICENSE.md). This
|
||||
makes the Supervision part of the code fully open source and freely usable in your
|
||||
projects.
|
||||
|
||||
@@ -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(
|
||||
|
||||
+114
-111
@@ -15,23 +15,23 @@ https://github.com/roboflow/supervision/assets/26109316/d051cc8a-dd15-41d4-aa36-
|
||||
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/time_in_zone
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/time_in_zone
|
||||
```
|
||||
|
||||
- setup python environment and activate it [optional]
|
||||
- setup python environment and activate it \[optional\]
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
- install required dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 🛠 scripts
|
||||
|
||||
@@ -45,16 +45,16 @@ This script allows you to download a video from YouTube.
|
||||
|
||||
```bash
|
||||
python scripts/download_from_youtube.py \
|
||||
--url "https://www.youtube.com/watch?v=-8zyEwAa50Q" \
|
||||
--output_path "data/checkout" \
|
||||
--file_name "video.mp4"
|
||||
--url "https://www.youtube.com/watch?v=-8zyEwAa50Q" \
|
||||
--output_path "data/checkout" \
|
||||
--file_name "video.mp4"
|
||||
```
|
||||
|
||||
```bash
|
||||
python scripts/download_from_youtube.py \
|
||||
--url "https://www.youtube.com/watch?v=MNn9qKG2UFI" \
|
||||
--output_path "data/traffic" \
|
||||
--file_name "video.mp4"
|
||||
--url "https://www.youtube.com/watch?v=MNn9qKG2UFI" \
|
||||
--output_path "data/traffic" \
|
||||
--file_name "video.mp4"
|
||||
```
|
||||
|
||||
### `stream_from_file`
|
||||
@@ -68,14 +68,14 @@ mock a live video stream for local testing. Video will be streamed in a loop und
|
||||
|
||||
```bash
|
||||
python scripts/stream_from_file.py \
|
||||
--video_directory "data/checkout" \
|
||||
--number_of_streams 1
|
||||
--video_directory "data/checkout" \
|
||||
--number_of_streams 1
|
||||
```
|
||||
|
||||
```bash
|
||||
python scripts/stream_from_file.py \
|
||||
--video_directory "data/traffic" \
|
||||
--number_of_streams 1
|
||||
--video_directory "data/traffic" \
|
||||
--number_of_streams 1
|
||||
```
|
||||
|
||||
### `draw_zones`
|
||||
@@ -86,24 +86,27 @@ window where you can draw polygons on the source image or video file. The polygo
|
||||
be saved as a JSON file.
|
||||
|
||||
- `--source_path`: Path to the source image or video file for drawing polygons.
|
||||
|
||||
- `--zone_configuration_path`: Path where the polygon annotations will be saved as a JSON file.
|
||||
|
||||
|
||||
- `enter` - finish drawing the current polygon.
|
||||
|
||||
- `escape` - cancel drawing the current polygon.
|
||||
|
||||
- `q` - quit the drawing window.
|
||||
|
||||
- `s` - save zone configuration to a JSON file.
|
||||
|
||||
```bash
|
||||
python scripts/draw_zones.py \
|
||||
--source_path "data/checkout/video.mp4" \
|
||||
--zone_configuration_path "data/checkout/config.json"
|
||||
--source_path "data/checkout/video.mp4" \
|
||||
--zone_configuration_path "data/checkout/config.json"
|
||||
```
|
||||
|
||||
```bash
|
||||
python scripts/draw_zones.py \
|
||||
--source_path "data/traffic/video.mp4" \
|
||||
--zone_configuration_path "data/traffic/config.json"
|
||||
--source_path "data/traffic/video.mp4" \
|
||||
--zone_configuration_path "data/traffic/config.json"
|
||||
```
|
||||
|
||||
https://github.com/roboflow/supervision/assets/26109316/9d514c9e-2a61-418b-ae49-6ac1ad6ae5ac
|
||||
@@ -114,33 +117,33 @@ https://github.com/roboflow/supervision/assets/26109316/9d514c9e-2a61-418b-ae49-
|
||||
|
||||
Script to run object detection on a video file using the Roboflow Inference model.
|
||||
|
||||
- `--zone_configuration_path`: Path to the zone configuration JSON file.
|
||||
- `--source_video_path`: Path to the source video file.
|
||||
- `--model_id`: Roboflow model ID.
|
||||
- `--classes`: List of class IDs to track. If empty, all classes are tracked.
|
||||
- `--confidence_threshold`: Confidence level for detections (`0` to `1`). Default is `0.3`.
|
||||
- `--iou_threshold`: IOU threshold for non-max suppression. Default is `0.7`.
|
||||
- `--zone_configuration_path`: Path to the zone configuration JSON file.
|
||||
- `--source_video_path`: Path to the source video file.
|
||||
- `--model_id`: Roboflow model ID.
|
||||
- `--classes`: List of class IDs to track. If empty, all classes are tracked.
|
||||
- `--confidence_threshold`: Confidence level for detections (`0` to `1`). Default is `0.3`.
|
||||
- `--iou_threshold`: IOU threshold for non-max suppression. Default is `0.7`.
|
||||
|
||||
```bash
|
||||
python inference_file_example.py \
|
||||
--zone_configuration_path "data/checkout/config.json" \
|
||||
--source_video_path "data/checkout/video.mp4" \
|
||||
--model_id "yolov8x-640" \
|
||||
--classes 0 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.7
|
||||
--zone_configuration_path "data/checkout/config.json" \
|
||||
--source_video_path "data/checkout/video.mp4" \
|
||||
--model_id "yolov8x-640" \
|
||||
--classes 0 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.7
|
||||
```
|
||||
|
||||
https://github.com/roboflow/supervision/assets/26109316/d051cc8a-dd15-41d4-aa36-d38b86334c39
|
||||
|
||||
```bash
|
||||
python inference_file_example.py \
|
||||
--zone_configuration_path "data/traffic/config.json" \
|
||||
--source_video_path "data/traffic/video.mp4" \
|
||||
--model_id "yolov8x-640" \
|
||||
--classes 2 5 6 7 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.7
|
||||
--zone_configuration_path "data/traffic/config.json" \
|
||||
--source_video_path "data/traffic/video.mp4" \
|
||||
--model_id "yolov8x-640" \
|
||||
--classes 2 5 6 7 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.7
|
||||
```
|
||||
|
||||
https://github.com/roboflow/supervision/assets/26109316/5ec896d7-4b39-4426-8979-11e71666878b
|
||||
@@ -149,31 +152,31 @@ https://github.com/roboflow/supervision/assets/26109316/5ec896d7-4b39-4426-8979-
|
||||
|
||||
Script to run object detection on a video stream using the Roboflow Inference model.
|
||||
|
||||
- `--zone_configuration_path`: Path to the zone configuration JSON file.
|
||||
- `--rtsp_url`: Complete RTSP URL for the video stream.
|
||||
- `--model_id`: Roboflow model ID.
|
||||
- `--classes`: List of class IDs to track. If empty, all classes are tracked.
|
||||
- `--confidence_threshold`: Confidence level for detections (`0` to `1`). Default is `0.3`.
|
||||
- `--iou_threshold`: IOU threshold for non-max suppression. Default is `0.7`.
|
||||
- `--zone_configuration_path`: Path to the zone configuration JSON file.
|
||||
- `--rtsp_url`: Complete RTSP URL for the video stream.
|
||||
- `--model_id`: Roboflow model ID.
|
||||
- `--classes`: List of class IDs to track. If empty, all classes are tracked.
|
||||
- `--confidence_threshold`: Confidence level for detections (`0` to `1`). Default is `0.3`.
|
||||
- `--iou_threshold`: IOU threshold for non-max suppression. Default is `0.7`.
|
||||
|
||||
```bash
|
||||
python inference_stream_example.py \
|
||||
--zone_configuration_path "data/checkout/config.json" \
|
||||
--rtsp_url "rtsp://localhost:8554/live0.stream" \
|
||||
--model_id "yolov8x-640" \
|
||||
--classes 0 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.7
|
||||
--zone_configuration_path "data/checkout/config.json" \
|
||||
--rtsp_url "rtsp://localhost:8554/live0.stream" \
|
||||
--model_id "yolov8x-640" \
|
||||
--classes 0 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.7
|
||||
```
|
||||
|
||||
```bash
|
||||
python inference_stream_example.py \
|
||||
--zone_configuration_path "data/traffic/config.json" \
|
||||
--rtsp_url "rtsp://localhost:8554/live0.stream" \
|
||||
--model_id "yolov8x-640" \
|
||||
--classes 2 5 6 7 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.7
|
||||
--zone_configuration_path "data/traffic/config.json" \
|
||||
--rtsp_url "rtsp://localhost:8554/live0.stream" \
|
||||
--model_id "yolov8x-640" \
|
||||
--classes 2 5 6 7 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.7
|
||||
```
|
||||
|
||||
<details>
|
||||
@@ -183,68 +186,68 @@ python inference_stream_example.py \
|
||||
|
||||
Script to run object detection on a video file using the Ultralytics YOLOv8 model.
|
||||
|
||||
- `--zone_configuration_path`: Path to the zone configuration JSON file.
|
||||
- `--source_video_path`: Path to the source video file.
|
||||
- `--weights`: Path to the model weights file. Default is `'yolov8s.pt'`.
|
||||
- `--device`: Computation device (`'cpu'`, `'mps'` or `'cuda'`). Default is `'cpu'`.
|
||||
- `--classes`: List of class IDs to track. If empty, all classes are tracked.
|
||||
- `--confidence_threshold`: Confidence level for detections (`0` to `1`). Default is `0.3`.
|
||||
- `--iou_threshold`: IOU threshold for non-max suppression. Default is `0.7`.
|
||||
- `--zone_configuration_path`: Path to the zone configuration JSON file.
|
||||
- `--source_video_path`: Path to the source video file.
|
||||
- `--weights`: Path to the model weights file. Default is `'yolov8s.pt'`.
|
||||
- `--device`: Computation device (`'cpu'`, `'mps'` or `'cuda'`). Default is `'cpu'`.
|
||||
- `--classes`: List of class IDs to track. If empty, all classes are tracked.
|
||||
- `--confidence_threshold`: Confidence level for detections (`0` to `1`). Default is `0.3`.
|
||||
- `--iou_threshold`: IOU threshold for non-max suppression. Default is `0.7`.
|
||||
|
||||
```bash
|
||||
python ultralytics_file_example.py \
|
||||
--zone_configuration_path "data/checkout/config.json" \
|
||||
--source_video_path "data/checkout/video.mp4" \
|
||||
--weights "yolov8x.pt" \
|
||||
--device "cpu" \
|
||||
--classes 0 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.7
|
||||
--zone_configuration_path "data/checkout/config.json" \
|
||||
--source_video_path "data/checkout/video.mp4" \
|
||||
--weights "yolov8x.pt" \
|
||||
--device "cpu" \
|
||||
--classes 0 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.7
|
||||
```
|
||||
|
||||
```bash
|
||||
python ultralytics_file_example.py \
|
||||
--zone_configuration_path "data/traffic/config.json" \
|
||||
--source_video_path "data/traffic/video.mp4" \
|
||||
--weights "yolov8x.pt" \
|
||||
--device "cpu" \
|
||||
--classes 2 5 6 7 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.7
|
||||
--zone_configuration_path "data/traffic/config.json" \
|
||||
--source_video_path "data/traffic/video.mp4" \
|
||||
--weights "yolov8x.pt" \
|
||||
--device "cpu" \
|
||||
--classes 2 5 6 7 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.7
|
||||
```
|
||||
|
||||
### `ultralytics_stream_example`
|
||||
|
||||
Script to run object detection on a video stream using the Ultralytics YOLOv8 model.
|
||||
|
||||
- `--zone_configuration_path`: Path to the zone configuration JSON file.
|
||||
- `--rtsp_url`: Complete RTSP URL for the video stream.
|
||||
- `--weights`: Path to the model weights file. Default is `'yolov8s.pt'`.
|
||||
- `--device`: Computation device (`'cpu'`, `'mps'` or `'cuda'`). Default is `'cpu'`.
|
||||
- `--classes`: List of class IDs to track. If empty, all classes are tracked.
|
||||
- `--confidence_threshold`: Confidence level for detections (`0` to `1`). Default is `0.3`.
|
||||
- `--iou_threshold`: IOU threshold for non-max suppression. Default is `0.7`.
|
||||
- `--zone_configuration_path`: Path to the zone configuration JSON file.
|
||||
- `--rtsp_url`: Complete RTSP URL for the video stream.
|
||||
- `--weights`: Path to the model weights file. Default is `'yolov8s.pt'`.
|
||||
- `--device`: Computation device (`'cpu'`, `'mps'` or `'cuda'`). Default is `'cpu'`.
|
||||
- `--classes`: List of class IDs to track. If empty, all classes are tracked.
|
||||
- `--confidence_threshold`: Confidence level for detections (`0` to `1`). Default is `0.3`.
|
||||
- `--iou_threshold`: IOU threshold for non-max suppression. Default is `0.7`.
|
||||
|
||||
```bash
|
||||
python ultralytics_stream_example.py \
|
||||
--zone_configuration_path "data/checkout/config.json" \
|
||||
--rtsp_url "rtsp://localhost:8554/live0.stream" \
|
||||
--weights "yolov8x.pt" \
|
||||
--device "cpu" \
|
||||
--classes 0 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.7
|
||||
--zone_configuration_path "data/checkout/config.json" \
|
||||
--rtsp_url "rtsp://localhost:8554/live0.stream" \
|
||||
--weights "yolov8x.pt" \
|
||||
--device "cpu" \
|
||||
--classes 0 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.7
|
||||
```
|
||||
|
||||
```bash
|
||||
python ultralytics_stream_example.py \
|
||||
--zone_configuration_path "data/traffic/config.json" \
|
||||
--rtsp_url "rtsp://localhost:8554/live0.stream" \
|
||||
--weights "yolov8x.pt" \
|
||||
--device "cpu" \
|
||||
--classes 2 5 6 7 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.7
|
||||
--zone_configuration_path "data/traffic/config.json" \
|
||||
--rtsp_url "rtsp://localhost:8554/live0.stream" \
|
||||
--weights "yolov8x.pt" \
|
||||
--device "cpu" \
|
||||
--classes 2 5 6 7 \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.7
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -254,11 +257,11 @@ python ultralytics_stream_example.py \
|
||||
This demo integrates two main components, each with its own licensing:
|
||||
|
||||
- ultralytics: The object detection model used in this demo, YOLOv8, is distributed
|
||||
under the [AGPL-3.0 license](https://github.com/ultralytics/ultralytics/blob/main/LICENSE).
|
||||
You can find more details about this license here.
|
||||
under the [AGPL-3.0 license](https://github.com/ultralytics/ultralytics/blob/main/LICENSE).
|
||||
You can find more details about this license here.
|
||||
|
||||
- supervision: The analytics code that powers the zone-based analysis in this demo is
|
||||
based on the Supervision library, which is licensed under the
|
||||
[MIT license](https://github.com/roboflow/supervision/blob/develop/LICENSE.md). This
|
||||
makes the Supervision part of the code fully open source and freely usable in your
|
||||
projects.
|
||||
based on the Supervision library, which is licensed under the
|
||||
[MIT license](https://github.com/roboflow/supervision/blob/develop/LICENSE.md). This
|
||||
makes the Supervision part of the code fully open source and freely usable in your
|
||||
projects.
|
||||
|
||||
@@ -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
|
||||
|
||||
+66
-59
@@ -9,93 +9,100 @@ detection and Supervision for tracking and annotation.
|
||||
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/tracking
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/tracking
|
||||
```
|
||||
|
||||
- setup python environment and activate it [optional]
|
||||
- setup python environment and activate it \[optional\]
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
- install required dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 🛠️ script arguments
|
||||
|
||||
- ultralytics
|
||||
|
||||
- `--source_weights_path`: Required. Specifies the path to the YOLO model's weights
|
||||
file, which is essential for the object detection process. This file contains the data
|
||||
that the model uses to identify objects in the video.
|
||||
- `--source_weights_path`: Required. Specifies the path to the YOLO model's weights
|
||||
file, which is essential for the object detection process. This file contains the data
|
||||
that the model uses to identify objects in the video.
|
||||
|
||||
- `--source_video_path`: Required. The path to the source video file to be processed.
|
||||
This is the video on which object detection and annotation will be performed.
|
||||
- `--target_video_path`: Required. The path where the processed video, with annotations
|
||||
added, will be saved. This is your output video file.
|
||||
- `--confidence_threshold` (optional): Sets the confidence level at which the model
|
||||
identifies objects in the video. Default is `0.3`. A higher threshold makes the model
|
||||
more selective, while a lower threshold makes it more inclusive in identifying objects.
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold
|
||||
for the model, defaulting to `0.7`. This parameter helps in differentiating between
|
||||
distinct objects, especially in crowded scenes.
|
||||
- `--source_video_path`: Required. The path to the source video file to be processed.
|
||||
This is the video on which object detection and annotation will be performed.
|
||||
|
||||
- `--target_video_path`: Required. The path where the processed video, with annotations
|
||||
added, will be saved. This is your output video file.
|
||||
|
||||
- `--confidence_threshold` (optional): Sets the confidence level at which the model
|
||||
identifies objects in the video. Default is `0.3`. A higher threshold makes the model
|
||||
more selective, while a lower threshold makes it more inclusive in identifying objects.
|
||||
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold
|
||||
for the model, defaulting to `0.7`. This parameter helps in differentiating between
|
||||
distinct objects, especially in crowded scenes.
|
||||
|
||||
- inference
|
||||
|
||||
- `--roboflow_api_key` (optional): The API key for Roboflow services. If not provided
|
||||
directly, the script tries to fetch it from the `ROBOFLOW_API_KEY` environment
|
||||
variable. Follow [this guide](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key)
|
||||
to acquire your `API KEY`.
|
||||
- `--model_id` (optional): Designates the Roboflow model ID to be used. The default
|
||||
value is `"yolov8x-1280"`.
|
||||
- `--roboflow_api_key` (optional): The API key for Roboflow services. If not provided
|
||||
directly, the script tries to fetch it from the `ROBOFLOW_API_KEY` environment
|
||||
variable. Follow [this guide](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key)
|
||||
to acquire your `API KEY`.
|
||||
|
||||
- `--source_video_path`: Required. The path to the source video file to be processed.
|
||||
This is the video on which object detection and annotation will be performed.
|
||||
- `--target_video_path`: Required. The path where the processed video, with annotations
|
||||
added, will be saved. This is your output video file.
|
||||
- `--confidence_threshold` (optional): Sets the confidence level at which the model
|
||||
identifies objects in the video. Default is `0.3`. A higher threshold makes the model
|
||||
more selective, while a lower threshold makes it more inclusive in identifying objects.
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold
|
||||
for the model, defaulting to `0.7`. This parameter helps in differentiating between
|
||||
distinct objects, especially in crowded scenes.
|
||||
- `--model_id` (optional): Designates the Roboflow model ID to be used. The default
|
||||
value is `"yolov8x-1280"`.
|
||||
|
||||
- `--source_video_path`: Required. The path to the source video file to be processed.
|
||||
This is the video on which object detection and annotation will be performed.
|
||||
|
||||
- `--target_video_path`: Required. The path where the processed video, with annotations
|
||||
added, will be saved. This is your output video file.
|
||||
|
||||
- `--confidence_threshold` (optional): Sets the confidence level at which the model
|
||||
identifies objects in the video. Default is `0.3`. A higher threshold makes the model
|
||||
more selective, while a lower threshold makes it more inclusive in identifying objects.
|
||||
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold
|
||||
for the model, defaulting to `0.7`. This parameter helps in differentiating between
|
||||
distinct objects, especially in crowded scenes.
|
||||
|
||||
## ⚙️ run
|
||||
|
||||
- inference
|
||||
|
||||
```bash
|
||||
python inference_example.py \
|
||||
--roboflow_api_key <ROBOFLOW API KEY> \
|
||||
--source_video_path input.mp4 \
|
||||
--target_video_path tracking_result.mp4
|
||||
```
|
||||
```bash
|
||||
python inference_example.py \
|
||||
--roboflow_api_key <ROBOFLOW API KEY> \
|
||||
--source_video_path input.mp4 \
|
||||
--target_video_path tracking_result.mp4
|
||||
```
|
||||
|
||||
- ultralytics
|
||||
|
||||
```bash
|
||||
python ultralytics_example.py \
|
||||
--source_weights_path yolov8s.pt \
|
||||
--source_video_path input.mp4 \
|
||||
--target_video_path tracking_result.mp4
|
||||
```
|
||||
```bash
|
||||
python ultralytics_example.py \
|
||||
--source_weights_path yolov8s.pt \
|
||||
--source_video_path input.mp4 \
|
||||
--target_video_path tracking_result.mp4
|
||||
```
|
||||
|
||||
## © license
|
||||
|
||||
This demo integrates two main components, each with its own licensing:
|
||||
|
||||
- ultralytics: The object detection model used in this demo, YOLOv8, is distributed
|
||||
under the [AGPL-3.0 license](https://github.com/ultralytics/ultralytics/blob/main/LICENSE).
|
||||
You can find more details about this license here.
|
||||
under the [AGPL-3.0 license](https://github.com/ultralytics/ultralytics/blob/main/LICENSE).
|
||||
You can find more details about this license here.
|
||||
|
||||
- supervision: The analytics code that powers the zone-based analysis in this demo is
|
||||
based on the Supervision library, which is licensed under the
|
||||
[MIT license](https://github.com/roboflow/supervision/blob/develop/LICENSE.md). This
|
||||
makes the Supervision part of the code fully open source and freely usable in your
|
||||
projects.
|
||||
based on the Supervision library, which is licensed under the
|
||||
[MIT license](https://github.com/roboflow/supervision/blob/develop/LICENSE.md). This
|
||||
makes the Supervision part of the code fully open source and freely usable in your
|
||||
projects.
|
||||
|
||||
@@ -12,105 +12,112 @@ https://github.com/roboflow/supervision/assets/26109316/c9436828-9fbf-4c25-ae8c-
|
||||
|
||||
- clone repository and navigate to example directory
|
||||
|
||||
```bash
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/traffic_analysis
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/roboflow/supervision.git
|
||||
cd supervision/examples/traffic_analysis
|
||||
```
|
||||
|
||||
- setup python environment and activate it [optional]
|
||||
- setup python environment and activate it \[optional\]
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
- install required dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
- download `traffic_analysis.pt` and `traffic_analysis.mov` files
|
||||
|
||||
```bash
|
||||
./setup.sh
|
||||
```
|
||||
```bash
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
## 🛠️ script arguments
|
||||
|
||||
- ultralytics
|
||||
|
||||
- `--source_weights_path`: Required. Specifies the path to the YOLO model's weights
|
||||
file, which is essential for the object detection process. This file contains the
|
||||
data that the model uses to identify objects in the video.
|
||||
- `--source_weights_path`: Required. Specifies the path to the YOLO model's weights
|
||||
file, which is essential for the object detection process. This file contains the
|
||||
data that the model uses to identify objects in the video.
|
||||
|
||||
- `--source_video_path`: Required. The path to the source video file that will be
|
||||
analyzed. This is the input video on which traffic flow analysis will be performed.
|
||||
- `--target_video_path` (optional): The path to save the output video with
|
||||
annotations. If not specified, the processed video will be displayed in real-time
|
||||
without being saved.
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO
|
||||
model to filter detections. Default is `0.3`. This determines how confident the
|
||||
model should be to recognize an object in the video.
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold
|
||||
for the model. Default is 0.7. This value is used to manage object detection
|
||||
accuracy, particularly in distinguishing between different objects.
|
||||
- `--source_video_path`: Required. The path to the source video file that will be
|
||||
analyzed. This is the input video on which traffic flow analysis will be performed.
|
||||
|
||||
- `--target_video_path` (optional): The path to save the output video with
|
||||
annotations. If not specified, the processed video will be displayed in real-time
|
||||
without being saved.
|
||||
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO
|
||||
model to filter detections. Default is `0.3`. This determines how confident the
|
||||
model should be to recognize an object in the video.
|
||||
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold
|
||||
for the model. Default is 0.7. This value is used to manage object detection
|
||||
accuracy, particularly in distinguishing between different objects.
|
||||
|
||||
- inference
|
||||
|
||||
- `--roboflow_api_key` (optional): The API key for Roboflow services. If not provided
|
||||
directly, the script tries to fetch it from the `ROBOFLOW_API_KEY` environment
|
||||
variable. Follow [this guide](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key)
|
||||
to acquire your `API KEY`.
|
||||
- `--model_id` (optional): Designates the Roboflow model ID to be used. The default
|
||||
value is `"vehicle-count-in-drone-video/6"`.
|
||||
- `--roboflow_api_key` (optional): The API key for Roboflow services. If not provided
|
||||
directly, the script tries to fetch it from the `ROBOFLOW_API_KEY` environment
|
||||
variable. Follow [this guide](https://docs.roboflow.com/api-reference/authentication#retrieve-an-api-key)
|
||||
to acquire your `API KEY`.
|
||||
|
||||
- `--source_video_path`: Required. The path to the source video file that will be
|
||||
analyzed. This is the input video on which traffic flow analysis will be performed.
|
||||
- `--target_video_path` (optional): The path to save the output video with
|
||||
annotations. If not specified, the processed video will be displayed in real-time
|
||||
without being saved.
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO
|
||||
model to filter detections. Default is `0.3`. This determines how confident the
|
||||
model should be to recognize an object in the video.
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold
|
||||
for the model. Default is 0.7. This value is used to manage object detection
|
||||
accuracy, particularly in distinguishing between different objects.
|
||||
- `--model_id` (optional): Designates the Roboflow model ID to be used. The default
|
||||
value is `"vehicle-count-in-drone-video/6"`.
|
||||
|
||||
- `--source_video_path`: Required. The path to the source video file that will be
|
||||
analyzed. This is the input video on which traffic flow analysis will be performed.
|
||||
|
||||
- `--target_video_path` (optional): The path to save the output video with
|
||||
annotations. If not specified, the processed video will be displayed in real-time
|
||||
without being saved.
|
||||
|
||||
- `--confidence_threshold` (optional): Sets the confidence threshold for the YOLO
|
||||
model to filter detections. Default is `0.3`. This determines how confident the
|
||||
model should be to recognize an object in the video.
|
||||
|
||||
- `--iou_threshold` (optional): Specifies the IOU (Intersection Over Union) threshold
|
||||
for the model. Default is 0.7. This value is used to manage object detection
|
||||
accuracy, particularly in distinguishing between different objects.
|
||||
|
||||
## ⚙️ run
|
||||
|
||||
- ultralytics
|
||||
|
||||
```bash
|
||||
python ultralytics_example.py \
|
||||
--source_weights_path data/traffic_analysis.pt \
|
||||
--source_video_path data/traffic_analysis.mov \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5 \
|
||||
--target_video_path data/traffic_analysis_result.mov
|
||||
```
|
||||
```bash
|
||||
python ultralytics_example.py \
|
||||
--source_weights_path data/traffic_analysis.pt \
|
||||
--source_video_path data/traffic_analysis.mov \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5 \
|
||||
--target_video_path data/traffic_analysis_result.mov
|
||||
```
|
||||
|
||||
- inference
|
||||
|
||||
```bash
|
||||
python inference_example.py \
|
||||
--roboflow_api_key <ROBOFLOW API KEY> \
|
||||
--source_video_path data/traffic_analysis.mov \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5 \
|
||||
--target_video_path data/traffic_analysis_result.mov
|
||||
```
|
||||
```bash
|
||||
python inference_example.py \
|
||||
--roboflow_api_key <ROBOFLOW API KEY> \
|
||||
--source_video_path data/traffic_analysis.mov \
|
||||
--confidence_threshold 0.3 \
|
||||
--iou_threshold 0.5 \
|
||||
--target_video_path data/traffic_analysis_result.mov
|
||||
```
|
||||
|
||||
## © license
|
||||
|
||||
This demo integrates two main components, each with its own licensing:
|
||||
|
||||
- ultralytics: The object detection model used in this demo, YOLOv8, is distributed
|
||||
under the [AGPL-3.0 license](https://github.com/ultralytics/ultralytics/blob/main/LICENSE).
|
||||
You can find more details about this license here.
|
||||
under the [AGPL-3.0 license](https://github.com/ultralytics/ultralytics/blob/main/LICENSE).
|
||||
You can find more details about this license here.
|
||||
|
||||
- supervision: The analytics code that powers the zone-based analysis in this demo is
|
||||
based on the Supervision library, which is licensed under the
|
||||
[MIT license](https://github.com/roboflow/supervision/blob/develop/LICENSE.md). This
|
||||
makes the Supervision part of the code fully open source and freely usable in your
|
||||
projects.
|
||||
based on the Supervision library, which is licensed under the
|
||||
[MIT license](https://github.com/roboflow/supervision/blob/develop/LICENSE.md). This
|
||||
makes the Supervision part of the code fully open source and freely usable in your
|
||||
projects.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import argparse
|
||||
import os
|
||||
from typing import Dict, Iterable, List, Set
|
||||
from typing import Dict, Iterable, List, Optional, Set
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
@@ -77,7 +77,7 @@ class VideoProcessor:
|
||||
roboflow_api_key: str,
|
||||
model_id: str,
|
||||
source_video_path: str,
|
||||
target_video_path: str = None,
|
||||
target_video_path: Optional[str] = None,
|
||||
confidence_threshold: float = 0.3,
|
||||
iou_threshold: float = 0.7,
|
||||
) -> None:
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import argparse
|
||||
from typing import Dict, Iterable, List, Set
|
||||
from typing import Dict, Iterable, List, Optional, Set
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
@@ -74,7 +74,7 @@ class VideoProcessor:
|
||||
self,
|
||||
source_weights_path: str,
|
||||
source_video_path: str,
|
||||
target_video_path: str = None,
|
||||
target_video_path: Optional[str] = None,
|
||||
confidence_threshold: float = 0.3,
|
||||
iou_threshold: float = 0.7,
|
||||
) -> None:
|
||||
@@ -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
|
||||
)
|
||||
|
||||
+56
-48
@@ -33,7 +33,6 @@ extra_css:
|
||||
- stylesheets/extra.css
|
||||
- stylesheets/cookbooks-card.css
|
||||
|
||||
|
||||
nav:
|
||||
- Supervision: index.md
|
||||
- Learn:
|
||||
@@ -42,51 +41,54 @@ 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
|
||||
- F1 Score: metrics/f1_score.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 +105,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 +127,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 +154,6 @@ plugins:
|
||||
- git-revision-date-localized:
|
||||
enable_creation_date: true
|
||||
|
||||
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- pymdownx.details
|
||||
@@ -178,3 +177,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
+1626
-1339
File diff suppressed because it is too large
Load Diff
+107
-47
@@ -1,18 +1,29 @@
|
||||
[tool.poetry]
|
||||
name = "supervision"
|
||||
version = "0.21.0"
|
||||
version = "0.24.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>"]
|
||||
maintainers = [
|
||||
"Piotr Skalski <piotr.skalski92@gmail.com>",
|
||||
"Linas Kondrackis <linas@roboflow.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"]
|
||||
documentation = "https://supervision.roboflow.com/latest/"
|
||||
keywords = [
|
||||
"machine-learning",
|
||||
"deep-learning",
|
||||
"vision",
|
||||
"ML",
|
||||
"DL",
|
||||
"AI",
|
||||
"Roboflow",
|
||||
]
|
||||
|
||||
classifiers=[
|
||||
classifiers = [
|
||||
'Development Status :: 4 - Beta',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: Education',
|
||||
@@ -27,33 +38,38 @@ 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"
|
||||
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"
|
||||
requests = { version = ">=2.26.0,<=2.32.3", optional = true }
|
||||
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 }
|
||||
opencv-python = ">=4.5.5.64"
|
||||
|
||||
[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,23 +79,31 @@ 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 = ">=0.25.2,<0.27.0"
|
||||
mkdocstrings-python = "^1.10.9"
|
||||
mike = "^2.0.0"
|
||||
# For Documentation Development use Python 3.10 or above
|
||||
# Use Latest mkdocs-jupyter min 0.24.6 for Jupyter Notebook Theme support
|
||||
mkdocs-jupyter = "^0.24.3"
|
||||
mkdocs-git-committers-plugin-2 = "^2.2.3"
|
||||
mkdocs-git-committers-plugin-2 = "^2.4.1"
|
||||
mkdocs-git-revision-date-localized-plugin = "^1.2.4"
|
||||
|
||||
[tool.poetry.group.typecheck]
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.typecheck.dependencies]
|
||||
types-pyyaml = "^6.0.12.20240808"
|
||||
types-cffi = "^1.16.0.20240331"
|
||||
types-requests = "^2.32.0.20240712"
|
||||
types-tqdm = "^4.66.0.20240417"
|
||||
|
||||
[tool.isort]
|
||||
line_length = 88
|
||||
profile = "black"
|
||||
|
||||
[tool.bandit]
|
||||
target = ["test", "supervision"]
|
||||
@@ -90,25 +114,6 @@ check = true
|
||||
imports = ["cv2", "supervision"]
|
||||
|
||||
|
||||
[tool.black]
|
||||
target-version = ["py38"]
|
||||
line-length = 88
|
||||
include = '\.pyi?$'
|
||||
exclude = '''
|
||||
/(
|
||||
\.git
|
||||
| \.hg
|
||||
| \.mypy_cache
|
||||
| \.tox
|
||||
| \.venv
|
||||
| _build
|
||||
| buck-out
|
||||
| build
|
||||
| dist
|
||||
| docs
|
||||
)/
|
||||
'''
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py38"
|
||||
|
||||
@@ -145,10 +150,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", "RUF"]
|
||||
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 +213,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"
|
||||
@@ -182,11 +236,17 @@ skip-magic-trailing-comma = false
|
||||
# Like Black, automatically detect the appropriate line ending.
|
||||
line-ending = "auto"
|
||||
|
||||
[tool.codespell]
|
||||
skip = "*.ipynb,poetry.lock"
|
||||
count = true
|
||||
quiet-level = 3
|
||||
ignore-words-list = "STrack,sTrack,strack"
|
||||
|
||||
[tool.setuptools]
|
||||
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.
|
||||
+12
-2
@@ -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,9 +39,12 @@ 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.line_zone import (
|
||||
LineZone,
|
||||
LineZoneAnnotator,
|
||||
LineZoneAnnotatorMulticlass,
|
||||
)
|
||||
from supervision.detection.lmm import LMM
|
||||
from supervision.detection.overlap_filter import (
|
||||
OverlapFilter,
|
||||
@@ -63,15 +69,19 @@ from supervision.detection.utils import (
|
||||
mask_to_xyxy,
|
||||
move_boxes,
|
||||
move_masks,
|
||||
oriented_box_iou_batch,
|
||||
pad_boxes,
|
||||
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 (
|
||||
calculate_optimal_line_thickness,
|
||||
calculate_optimal_text_scale,
|
||||
draw_filled_polygon,
|
||||
draw_filled_rectangle,
|
||||
draw_image,
|
||||
draw_line,
|
||||
|
||||
+505
-108
File diff suppressed because it is too large
Load Diff
@@ -51,14 +51,17 @@ def resolve_color_idx(
|
||||
if detections.class_id is None:
|
||||
raise ValueError(
|
||||
"Could not resolve color by class because "
|
||||
"Detections do not have class_id"
|
||||
"Detections do not have class_id. If using an annotator, "
|
||||
"try setting color_lookup to sv.ColorLookup.INDEX or "
|
||||
"sv.ColorLookup.TRACK."
|
||||
)
|
||||
return detections.class_id[detection_idx]
|
||||
elif color_lookup == ColorLookup.TRACK:
|
||||
if detections.tracker_id is None:
|
||||
raise ValueError(
|
||||
"Could not resolve color by track because "
|
||||
"Detections do not have tracker_id"
|
||||
"Detections do not have tracker_id. Did you call "
|
||||
"tracker.update_with_detections(...) before annotating?"
|
||||
)
|
||||
return detections.tracker_id[detection_idx]
|
||||
|
||||
|
||||
+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
|
||||
+193
-92
@@ -6,13 +6,26 @@ 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.config import (
|
||||
CLASS_NAME_DATA_FIELD,
|
||||
ORIENTED_BOX_COORDINATES,
|
||||
)
|
||||
from supervision.detection.lmm import (
|
||||
LMM,
|
||||
from_florence_2,
|
||||
from_paligemma,
|
||||
validate_lmm_parameters,
|
||||
)
|
||||
from supervision.detection.overlap_filter import (
|
||||
box_non_max_merge,
|
||||
box_non_max_suppression,
|
||||
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 +38,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
|
||||
|
||||
|
||||
@@ -35,7 +48,7 @@ class Detections:
|
||||
The `sv.Detections` class in the Supervision library standardizes results from
|
||||
various object detection and segmentation models into a consistent format. This
|
||||
class simplifies data manipulation and filtering, providing a uniform API for
|
||||
integration with Supervision [trackers](/trackers/), [annotators](/detection/annotators/), and [tools](/detection/tools/line_zone/).
|
||||
integration with Supervision [trackers](/trackers/), [annotators](/latest/detection/annotators/), and [tools](/detection/tools/line_zone/).
|
||||
|
||||
=== "Inference"
|
||||
|
||||
@@ -239,11 +252,7 @@ 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:
|
||||
class_id = ultralytics_results.obb.cls.cpu().numpy().astype(int)
|
||||
@@ -262,6 +271,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(
|
||||
@@ -342,7 +359,7 @@ class Detections:
|
||||
result = model(img)
|
||||
detections = sv.Detections.from_tensorflow(result)
|
||||
```
|
||||
""" # noqa: E501 // docs
|
||||
"""
|
||||
|
||||
boxes = tensorflow_results["detection_boxes"][0].numpy()
|
||||
boxes[:, [0, 2]] *= resolution_wh[0]
|
||||
@@ -417,7 +434,7 @@ class Detections:
|
||||
result = inference_detector(model, image)
|
||||
detections = sv.Detections.from_mmdetection(result)
|
||||
```
|
||||
""" # noqa: E501 // docs
|
||||
"""
|
||||
|
||||
return cls(
|
||||
xyxy=mmdet_results.pred_instances.bboxes.cpu().numpy(),
|
||||
@@ -433,15 +450,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,46 +493,45 @@ class Detections:
|
||||
id2label=model.config.id2label
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
!!! tip
|
||||
if (
|
||||
transformers_results.__class__.__name__ == "Tensor"
|
||||
or "segmentation" in transformers_results
|
||||
):
|
||||
return cls(
|
||||
**process_transformers_v5_segmentation_result(
|
||||
transformers_results, id2label
|
||||
)
|
||||
)
|
||||
|
||||
Class names values can be accessed using `detections["class_name"]`.
|
||||
""" # noqa: E501 // docs
|
||||
if "masks" in transformers_results or "png_string" in transformers_results:
|
||||
return cls(
|
||||
**process_transformers_v4_segmentation_result(
|
||||
transformers_results, id2label
|
||||
)
|
||||
)
|
||||
|
||||
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 "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,
|
||||
**process_transformers_detection_result(transformers_results, id2label)
|
||||
)
|
||||
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Only object detection and semantic segmentation results are supported."
|
||||
raise ValueError(
|
||||
"The provided Transformers results do not contain any valid fields."
|
||||
" Expected fields are 'boxes', 'masks', 'segments_info' or"
|
||||
" 'segmentation'."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_detectron2(cls, detectron2_results) -> Detections:
|
||||
def from_detectron2(cls, detectron2_results: Any) -> Detections:
|
||||
"""
|
||||
Create a Detections object from the
|
||||
[Detectron2](https://github.com/facebookresearch/detectron2) inference result.
|
||||
|
||||
Args:
|
||||
detectron2_results: The output of a
|
||||
detectron2_results (Any): The output of a
|
||||
Detectron2 model containing instances with prediction data.
|
||||
|
||||
Returns:
|
||||
@@ -540,6 +560,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 +598,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 +619,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 +658,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 +791,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: Any
|
||||
) -> Detections:
|
||||
"""
|
||||
Creates a Detections object from the given result string based on the specified
|
||||
Large Multimodal Model (LMM).
|
||||
@@ -819,7 +801,7 @@ class Detections:
|
||||
Args:
|
||||
lmm (Union[LMM, str]): The type of LMM (Large Multimodal Model) to use.
|
||||
result (str): The result string containing the detection data.
|
||||
**kwargs: Additional keyword arguments required by the specified LMM.
|
||||
**kwargs (Any): Additional keyword arguments required by the specified LMM.
|
||||
|
||||
Returns:
|
||||
Detections: A new Detections object.
|
||||
@@ -847,15 +829,134 @@ 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
|
||||
def from_easyocr(cls, easyocr_results: list) -> Detections:
|
||||
"""
|
||||
Create a Detections object from the
|
||||
[EasyOCR](https://github.com/JaidedAI/EasyOCR) result.
|
||||
|
||||
Results are placed in the `data` field with the key `"class_name"`.
|
||||
|
||||
Args:
|
||||
easyocr_results (List): The output Results instance from EasyOCR
|
||||
|
||||
Returns:
|
||||
Detections: A new Detections object.
|
||||
|
||||
Example:
|
||||
```python
|
||||
import supervision as sv
|
||||
import easyocr
|
||||
|
||||
reader = easyocr.Reader(['en'])
|
||||
results = reader.readtext(<SOURCE_IMAGE_PATH>)
|
||||
detections = sv.Detections.from_easyocr(results)
|
||||
detected_text = detections["class_name"]
|
||||
```
|
||||
"""
|
||||
if len(easyocr_results) == 0:
|
||||
return cls.empty()
|
||||
|
||||
bbox = np.array([result[0] for result in easyocr_results])
|
||||
xyxy = np.hstack((np.min(bbox, axis=1), np.max(bbox, axis=1)))
|
||||
confidence = np.array(
|
||||
[
|
||||
result[2] if len(result) > 2 and result[2] else 0
|
||||
for result in easyocr_results
|
||||
]
|
||||
)
|
||||
ocr_text = np.array([result[1] for result in easyocr_results])
|
||||
|
||||
return cls(
|
||||
xyxy=xyxy.astype(np.float32),
|
||||
confidence=confidence.astype(np.float32),
|
||||
data={
|
||||
CLASS_NAME_DATA_FIELD: ocr_text,
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_ncnn(cls, ncnn_results) -> Detections:
|
||||
"""
|
||||
Creates a Detections instance from the
|
||||
[ncnn](https://github.com/Tencent/ncnn) inference result.
|
||||
Supports object detection models.
|
||||
|
||||
Arguments:
|
||||
ncnn_results (dict): The output Results instance from ncnn.
|
||||
|
||||
Returns:
|
||||
Detections: A new Detections object.
|
||||
|
||||
Example:
|
||||
```python
|
||||
import cv2
|
||||
from ncnn.model_zoo import get_model
|
||||
import supervision as sv
|
||||
|
||||
image = cv2.imread(<SOURCE_IMAGE_PATH>)
|
||||
model = get_model(
|
||||
"yolov8s",
|
||||
target_size=640
|
||||
prob_threshold=0.5,
|
||||
nms_threshold=0.45,
|
||||
num_threads=4,
|
||||
use_gpu=True,
|
||||
)
|
||||
result = model(image)
|
||||
detections = sv.Detections.from_ncnn(result)
|
||||
```
|
||||
"""
|
||||
|
||||
xywh, confidences, class_ids = [], [], []
|
||||
|
||||
if len(ncnn_results) == 0:
|
||||
return cls.empty()
|
||||
|
||||
for ncnn_result in ncnn_results:
|
||||
rect = ncnn_result.rect
|
||||
xywh.append(
|
||||
[
|
||||
rect.x.astype(np.float32),
|
||||
rect.y.astype(np.float32),
|
||||
rect.w.astype(np.float32),
|
||||
rect.h.astype(np.float32),
|
||||
]
|
||||
)
|
||||
|
||||
confidences.append(ncnn_result.prob)
|
||||
class_ids.append(ncnn_result.label)
|
||||
|
||||
return cls(
|
||||
xyxy=xywh_to_xyxy(np.array(xywh, dtype=np.float32)),
|
||||
confidence=np.array(confidences, dtype=np.float32),
|
||||
class_id=np.array(class_ids, dtype=int),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> Detections:
|
||||
"""
|
||||
@@ -1171,10 +1272,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 +1327,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.
|
||||
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import math
|
||||
import warnings
|
||||
from typing import Dict, Iterable, Optional, Tuple
|
||||
from collections import Counter
|
||||
from functools import lru_cache
|
||||
from typing import Any, Dict, Iterable, List, Literal, Optional, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from supervision.config import CLASS_NAME_DATA_FIELD
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.utils import cross_product
|
||||
from supervision.draw.color import Color
|
||||
from supervision.draw.utils import draw_text
|
||||
from supervision.geometry.core import Point, Position, Vector
|
||||
from supervision.draw.utils import draw_rectangle, draw_text
|
||||
from supervision.geometry.core import Point, Position, Rect, Vector
|
||||
from supervision.utils.image import overlay_image
|
||||
from supervision.utils.internal import SupervisionWarnings
|
||||
|
||||
TEXT_MARGIN = 10
|
||||
|
||||
|
||||
class LineZone:
|
||||
"""
|
||||
@@ -55,7 +62,7 @@ class LineZone:
|
||||
line_zone.in_count, line_zone.out_count
|
||||
# 7, 2
|
||||
```
|
||||
""" # noqa: E501 // docs
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -81,11 +88,44 @@ class LineZone:
|
||||
self.vector = Vector(start=start, end=end)
|
||||
self.limits = self.calculate_region_of_interest_limits(vector=self.vector)
|
||||
self.tracker_state: Dict[str, bool] = {}
|
||||
self.in_count: int = 0
|
||||
self.out_count: int = 0
|
||||
self._in_count_per_class: Counter = Counter()
|
||||
self._out_count_per_class: Counter = Counter()
|
||||
self.triggering_anchors = triggering_anchors
|
||||
if not list(self.triggering_anchors):
|
||||
raise ValueError("Triggering anchors cannot be empty.")
|
||||
self.class_id_to_name: Dict[int, str] = {}
|
||||
|
||||
@property
|
||||
def in_count(self) -> int:
|
||||
"""
|
||||
Number of objects that have crossed the line from
|
||||
outside to inside.
|
||||
"""
|
||||
return sum(self._in_count_per_class.values())
|
||||
|
||||
@property
|
||||
def out_count(self) -> int:
|
||||
"""
|
||||
Number of objects that have crossed the line from
|
||||
inside to outside.
|
||||
"""
|
||||
return sum(self._out_count_per_class.values())
|
||||
|
||||
@property
|
||||
def in_count_per_class(self) -> Dict[int, int]:
|
||||
"""
|
||||
Number of objects of each class that have crossed
|
||||
the line from outside to inside.
|
||||
"""
|
||||
return dict(self._in_count_per_class)
|
||||
|
||||
@property
|
||||
def out_count_per_class(self) -> Dict[int, int]:
|
||||
"""
|
||||
Number of objects of each class that have crossed the line
|
||||
from inside to outside.
|
||||
"""
|
||||
return dict(self._out_count_per_class)
|
||||
|
||||
@staticmethod
|
||||
def calculate_region_of_interest_limits(vector: Vector) -> Tuple[Vector, Vector]:
|
||||
@@ -170,7 +210,22 @@ class LineZone:
|
||||
has_any_left_trigger = np.any(triggers, axis=0)
|
||||
has_any_right_trigger = np.any(~triggers, axis=0)
|
||||
is_uniformly_triggered = ~(has_any_left_trigger & has_any_right_trigger)
|
||||
for i, tracker_id in enumerate(detections.tracker_id):
|
||||
|
||||
class_ids = (
|
||||
list(detections.class_id)
|
||||
if detections.class_id is not None
|
||||
else [None] * len(detections)
|
||||
)
|
||||
tracker_ids = list(detections.tracker_id)
|
||||
|
||||
if CLASS_NAME_DATA_FIELD in detections.data:
|
||||
class_names = detections.data[CLASS_NAME_DATA_FIELD]
|
||||
for class_id, class_name in zip(class_ids, class_names):
|
||||
if class_id is None:
|
||||
class_name = "No class"
|
||||
self.class_id_to_name[class_id] = class_name
|
||||
|
||||
for i, (class_ids, tracker_id) in enumerate(zip(class_ids, tracker_ids)):
|
||||
if not in_limits[i]:
|
||||
continue
|
||||
|
||||
@@ -187,10 +242,10 @@ class LineZone:
|
||||
|
||||
self.tracker_state[tracker_id] = tracker_state
|
||||
if tracker_state:
|
||||
self.in_count += 1
|
||||
self._in_count_per_class[class_ids] += 1
|
||||
crossed_in[i] = True
|
||||
else:
|
||||
self.out_count += 1
|
||||
self._out_count_per_class[class_ids] += 1
|
||||
crossed_out[i] = True
|
||||
|
||||
return crossed_in, crossed_out
|
||||
@@ -199,9 +254,9 @@ class LineZone:
|
||||
class LineZoneAnnotator:
|
||||
def __init__(
|
||||
self,
|
||||
thickness: float = 2,
|
||||
thickness: int = 2,
|
||||
color: Color = Color.WHITE,
|
||||
text_thickness: float = 2,
|
||||
text_thickness: int = 2,
|
||||
text_color: Color = Color.BLACK,
|
||||
text_scale: float = 0.5,
|
||||
text_offset: float = 1.5,
|
||||
@@ -210,86 +265,67 @@ class LineZoneAnnotator:
|
||||
custom_out_text: Optional[str] = None,
|
||||
display_in_count: bool = True,
|
||||
display_out_count: bool = True,
|
||||
display_text_box: bool = True,
|
||||
text_orient_to_line: bool = False,
|
||||
text_centered: bool = True,
|
||||
):
|
||||
"""
|
||||
Initialize the LineCounterAnnotator object with default values.
|
||||
A class for drawing the `LineZone` and its detected object count
|
||||
on an image.
|
||||
|
||||
Attributes:
|
||||
thickness (float): The thickness of the line that will be drawn.
|
||||
color (Color): The color of the line that will be drawn.
|
||||
text_thickness (float): The thickness of the text that will be drawn.
|
||||
text_color (Color): The color of the text that will be drawn.
|
||||
text_scale (float): The scale of the text that will be drawn.
|
||||
text_offset (float): The offset of the text that will be drawn.
|
||||
text_padding (int): The padding of the text that will be drawn.
|
||||
display_in_count (bool): Whether to display the in count or not.
|
||||
display_out_count (bool): Whether to display the out count or not.
|
||||
thickness (int): Line thickness.
|
||||
color (Color): Line color.
|
||||
text_thickness (int): Text thickness.
|
||||
text_color (Color): Text color.
|
||||
text_scale (float): Text scale.
|
||||
text_offset (float): How far the text will be from the line.
|
||||
text_padding (int): The empty space in the text box, surrounding the text.
|
||||
custom_in_text (Optional[str]): Write something else instead of "in".
|
||||
custom_out_text (Optional[str]): Write something else instead of "out".
|
||||
display_in_count (bool): Pass `False` to hide the "in" count.
|
||||
display_out_count (bool): Pass `False` to hide the "out" count.
|
||||
display_text_box (bool): Pass `False` to hide the text background box.
|
||||
text_orient_to_line (bool): ⭐ Match text orientation to the line.
|
||||
Recommended to set to `True`.
|
||||
text_centered (bool): Pass `False` to disable text centering. Useful
|
||||
when the label overlaps something important.
|
||||
|
||||
"""
|
||||
self.thickness: float = thickness
|
||||
self.thickness: int = thickness
|
||||
self.color: Color = color
|
||||
self.text_thickness: float = text_thickness
|
||||
self.text_thickness: int = text_thickness
|
||||
self.text_color: Color = text_color
|
||||
self.text_scale: float = text_scale
|
||||
self.text_offset: float = text_offset
|
||||
self.text_padding: int = text_padding
|
||||
self.custom_in_text: str = custom_in_text
|
||||
self.custom_out_text: str = custom_out_text
|
||||
self.in_text: str = custom_in_text if custom_in_text else "in"
|
||||
self.out_text: str = custom_out_text if custom_out_text else "out"
|
||||
self.display_in_count: bool = display_in_count
|
||||
self.display_out_count: bool = display_out_count
|
||||
|
||||
def _annotate_count(
|
||||
self,
|
||||
frame: np.ndarray,
|
||||
center_text_anchor: Point,
|
||||
text: str,
|
||||
is_in_count: bool,
|
||||
) -> None:
|
||||
"""This method is drawing the text on the frame.
|
||||
|
||||
Args:
|
||||
frame (np.ndarray): The image on which the text will be drawn.
|
||||
center_text_anchor: The center point that the text will be drawn.
|
||||
text (str): The text that will be drawn.
|
||||
is_in_count (bool): Whether to display the in count or out count.
|
||||
"""
|
||||
_, text_height = cv2.getTextSize(
|
||||
text, cv2.FONT_HERSHEY_SIMPLEX, self.text_scale, self.text_thickness
|
||||
)[0]
|
||||
|
||||
if is_in_count:
|
||||
center_text_anchor.y -= int(self.text_offset * text_height)
|
||||
else:
|
||||
center_text_anchor.y += int(self.text_offset * text_height)
|
||||
|
||||
draw_text(
|
||||
scene=frame,
|
||||
text=text,
|
||||
text_anchor=center_text_anchor,
|
||||
text_color=self.text_color,
|
||||
text_scale=self.text_scale,
|
||||
text_thickness=self.text_thickness,
|
||||
text_padding=self.text_padding,
|
||||
background_color=self.color,
|
||||
)
|
||||
self.display_text_box: bool = display_text_box
|
||||
self.text_orient_to_line: bool = text_orient_to_line
|
||||
self.text_centered: bool = text_centered
|
||||
|
||||
def annotate(self, frame: np.ndarray, line_counter: LineZone) -> np.ndarray:
|
||||
"""
|
||||
Draws the line on the frame using the line_counter provided.
|
||||
Draws the line on the frame using the line zone provided.
|
||||
|
||||
Attributes:
|
||||
frame (np.ndarray): The image on which the line will be drawn.
|
||||
line_counter (LineCounter): The line counter
|
||||
line_counter (LineZone): The line zone
|
||||
that will be used to draw the line.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The image with the line drawn on it.
|
||||
(np.ndarray): The image with the line drawn on it.
|
||||
|
||||
"""
|
||||
line_start = line_counter.vector.start.as_xy_int_tuple()
|
||||
line_end = line_counter.vector.end.as_xy_int_tuple()
|
||||
cv2.line(
|
||||
frame,
|
||||
line_counter.vector.start.as_xy_int_tuple(),
|
||||
line_counter.vector.end.as_xy_int_tuple(),
|
||||
line_start,
|
||||
line_end,
|
||||
self.color.as_bgr(),
|
||||
self.thickness,
|
||||
lineType=cv2.LINE_AA,
|
||||
@@ -297,7 +333,7 @@ class LineZoneAnnotator:
|
||||
)
|
||||
cv2.circle(
|
||||
frame,
|
||||
line_counter.vector.start.as_xy_int_tuple(),
|
||||
line_start,
|
||||
radius=5,
|
||||
color=self.text_color.as_bgr(),
|
||||
thickness=-1,
|
||||
@@ -305,40 +341,456 @@ class LineZoneAnnotator:
|
||||
)
|
||||
cv2.circle(
|
||||
frame,
|
||||
line_counter.vector.end.as_xy_int_tuple(),
|
||||
line_end,
|
||||
radius=5,
|
||||
color=self.text_color.as_bgr(),
|
||||
thickness=-1,
|
||||
lineType=cv2.LINE_AA,
|
||||
)
|
||||
|
||||
text_anchor = Vector(
|
||||
start=line_counter.vector.start, end=line_counter.vector.end
|
||||
in_text = f"{self.in_text}: {line_counter.in_count}"
|
||||
out_text = f"{self.out_text}: {line_counter.out_count}"
|
||||
line_angle_degrees = self._get_line_angle(line_counter)
|
||||
|
||||
for text, is_shown, is_in_count in [
|
||||
(in_text, self.display_in_count, True),
|
||||
(out_text, self.display_out_count, False),
|
||||
]:
|
||||
if not is_shown:
|
||||
continue
|
||||
|
||||
if line_angle_degrees == 0 or not self.text_orient_to_line:
|
||||
self._draw_basic_label(
|
||||
frame=frame,
|
||||
line_center=line_counter.vector.center,
|
||||
text=text,
|
||||
is_in_count=is_in_count,
|
||||
)
|
||||
else:
|
||||
self._draw_oriented_label(
|
||||
frame=frame,
|
||||
line_zone=line_counter,
|
||||
text=text,
|
||||
is_in_count=is_in_count,
|
||||
)
|
||||
|
||||
return frame
|
||||
|
||||
def _get_line_angle(self, line_zone: LineZone) -> float:
|
||||
"""
|
||||
Calculate the line counter angle (in degrees).
|
||||
|
||||
Args:
|
||||
line_zone (LineZone): The line zone object.
|
||||
|
||||
Returns:
|
||||
(float): Line counter angle, in degrees.
|
||||
"""
|
||||
start_point = line_zone.vector.start.as_xy_int_tuple()
|
||||
end_point = line_zone.vector.end.as_xy_int_tuple()
|
||||
|
||||
delta_x = end_point[0] - start_point[0]
|
||||
delta_y = end_point[1] - start_point[1]
|
||||
|
||||
if delta_x == 0:
|
||||
line_angle = 90.0
|
||||
line_angle += 180 if delta_y < 0 else 0
|
||||
else:
|
||||
line_angle = math.degrees(math.atan(delta_y / delta_x))
|
||||
line_angle += 180 if delta_x < 0 else 0
|
||||
|
||||
return line_angle
|
||||
|
||||
def _calculate_anchor_in_frame(
|
||||
self,
|
||||
line_zone: LineZone,
|
||||
text_width: int,
|
||||
text_height: int,
|
||||
is_in_count: bool,
|
||||
label_dimension: int,
|
||||
) -> Tuple[int, int]:
|
||||
"""
|
||||
Calculate insertion anchor in frame to position the center of the count image.
|
||||
|
||||
Args:
|
||||
line_zone (LineZone): The line counter object used for counting.
|
||||
text_width (int): Text width.
|
||||
text_height (int): Text height.
|
||||
is_in_count (bool): Whether the count should be placed over or below line.
|
||||
label_dimension (int): Size of the label image. Assumes the
|
||||
label is rectangular.
|
||||
|
||||
Returns:
|
||||
(Tuple[int, int]): xy, point in an image where the label will be placed.
|
||||
"""
|
||||
line_angle = self._get_line_angle(line_zone)
|
||||
|
||||
if self.text_centered:
|
||||
mid_point = Vector(
|
||||
start=line_zone.vector.start, end=line_zone.vector.end
|
||||
).center.as_xy_int_tuple()
|
||||
anchor = list(mid_point)
|
||||
else:
|
||||
end_point = line_zone.vector.end.as_xy_int_tuple()
|
||||
anchor = list(end_point)
|
||||
|
||||
move_along_x = int(
|
||||
math.cos(math.radians(line_angle))
|
||||
* (text_width / 2 + self.text_padding)
|
||||
)
|
||||
move_along_y = int(
|
||||
math.sin(math.radians(line_angle))
|
||||
* (text_width / 2 + self.text_padding)
|
||||
)
|
||||
|
||||
anchor[0] -= move_along_x
|
||||
anchor[1] -= move_along_y
|
||||
|
||||
move_perpendicular_x = int(
|
||||
math.sin(math.radians(line_angle)) * (self.text_offset * text_height)
|
||||
)
|
||||
move_perpendicular_y = int(
|
||||
math.cos(math.radians(line_angle)) * (self.text_offset * text_height)
|
||||
)
|
||||
|
||||
if self.display_in_count:
|
||||
in_text = (
|
||||
f"{self.custom_in_text}: {line_counter.in_count}"
|
||||
if self.custom_in_text is not None
|
||||
else f"in: {line_counter.in_count}"
|
||||
)
|
||||
self._annotate_count(
|
||||
frame=frame,
|
||||
center_text_anchor=text_anchor.center,
|
||||
text=in_text,
|
||||
is_in_count=True,
|
||||
if is_in_count:
|
||||
anchor[0] += move_perpendicular_x
|
||||
anchor[1] -= move_perpendicular_y
|
||||
else:
|
||||
anchor[0] -= move_perpendicular_x
|
||||
anchor[1] += move_perpendicular_y
|
||||
|
||||
x1 = max(anchor[0] - label_dimension // 2, 0)
|
||||
y1 = max(anchor[1] - label_dimension // 2, 0)
|
||||
|
||||
return x1, y1
|
||||
|
||||
def _draw_basic_label(
|
||||
self,
|
||||
frame: np.ndarray,
|
||||
line_center: Point,
|
||||
text: str,
|
||||
is_in_count: bool,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Draw the count label on the frame. For example: "out: 7".
|
||||
The label contains horizontal text and is not rotated.
|
||||
|
||||
Args:
|
||||
frame (np.ndarray): The entire scene, on which the label will be placed.
|
||||
line_center (Point): The center of the line zone.
|
||||
text (str): The text that will be drawn.
|
||||
is_in_count (bool): Whether to display the in count (above line)
|
||||
or out count (below line).
|
||||
|
||||
Returns:
|
||||
(np.ndarray): The scene with the label drawn on it.
|
||||
"""
|
||||
_, text_height = cv2.getTextSize(
|
||||
text, cv2.FONT_HERSHEY_SIMPLEX, self.text_scale, self.text_thickness
|
||||
)[0]
|
||||
|
||||
if is_in_count:
|
||||
line_center.y -= int(self.text_offset * text_height)
|
||||
else:
|
||||
line_center.y += int(self.text_offset * text_height)
|
||||
|
||||
draw_text(
|
||||
scene=frame,
|
||||
text=text,
|
||||
text_anchor=line_center,
|
||||
text_color=self.text_color,
|
||||
text_scale=self.text_scale,
|
||||
text_thickness=self.text_thickness,
|
||||
text_padding=self.text_padding,
|
||||
background_color=self.color if self.display_text_box else None,
|
||||
)
|
||||
|
||||
return frame
|
||||
|
||||
def _draw_oriented_label(
|
||||
self,
|
||||
frame: np.ndarray,
|
||||
line_zone: LineZone,
|
||||
text: str,
|
||||
is_in_count: bool,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Draw the count label on the frame. For example: "out: 7".
|
||||
The label is oriented to match the line angle.
|
||||
|
||||
Args:
|
||||
frame (np.ndarray): The entire scene, on which the label will be placed.
|
||||
line_zone (LineZone): The line zone responsible for counting
|
||||
objects crossing it.
|
||||
text (str): The text that will be drawn.
|
||||
is_in_count (bool): Whether to display the in count (above line)
|
||||
or out count (below line).
|
||||
|
||||
Returns:
|
||||
(np.ndarray): The scene with the label drawn on it.
|
||||
"""
|
||||
|
||||
line_angle_degrees = self._get_line_angle(line_zone)
|
||||
label_image = self._make_label_image(
|
||||
text,
|
||||
text_scale=self.text_scale,
|
||||
text_thickness=self.text_thickness,
|
||||
text_padding=self.text_padding,
|
||||
text_color=self.text_color,
|
||||
text_box_show=self.display_text_box,
|
||||
text_box_color=self.color,
|
||||
line_angle_degrees=line_angle_degrees,
|
||||
)
|
||||
assert label_image.shape[0] == label_image.shape[1]
|
||||
|
||||
text_width, text_height = cv2.getTextSize(
|
||||
text, cv2.FONT_HERSHEY_SIMPLEX, self.text_scale, self.text_thickness
|
||||
)[0]
|
||||
|
||||
label_anchor = self._calculate_anchor_in_frame(
|
||||
line_zone=line_zone,
|
||||
text_width=text_width,
|
||||
text_height=text_height,
|
||||
is_in_count=is_in_count,
|
||||
label_dimension=label_image.shape[0],
|
||||
)
|
||||
|
||||
frame = overlay_image(frame, label_image, label_anchor)
|
||||
|
||||
return frame
|
||||
|
||||
@staticmethod
|
||||
@lru_cache(maxsize=32)
|
||||
def _make_label_image(
|
||||
text: str,
|
||||
*,
|
||||
text_scale: float,
|
||||
text_thickness: int,
|
||||
text_padding: int,
|
||||
text_color: Color,
|
||||
text_box_show: bool,
|
||||
text_box_color: Color,
|
||||
line_angle_degrees: float,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Create the small text box displaying line zone count. E.g. "out: 7".
|
||||
|
||||
Args:
|
||||
text (str): The text to display.
|
||||
text_scale (float): The scale of the text.
|
||||
text_thickness (int): The thickness of the text.
|
||||
text_padding (int): The padding around the text.
|
||||
text_color (Color): The color of the text.
|
||||
text_box_show (bool): Whether to display the text box.
|
||||
text_box_color (Color): The color of the text box.
|
||||
line_angle_degrees (float): The angle of the line in degrees.
|
||||
|
||||
Returns:
|
||||
(np.ndarray): The label of shape (H, W, 4), in BGRA format.
|
||||
"""
|
||||
text_width, text_height = cv2.getTextSize(
|
||||
text, cv2.FONT_HERSHEY_SIMPLEX, text_scale, text_thickness
|
||||
)[0]
|
||||
|
||||
annotation_dim = int((max(text_width, text_height) + text_padding * 2) * 1.5)
|
||||
annotation_shape = (annotation_dim, annotation_dim)
|
||||
annotation_center = Point(annotation_dim // 2, annotation_dim // 2)
|
||||
|
||||
annotation = np.zeros((*annotation_shape, 3), dtype=np.uint8)
|
||||
annotation_alpha = np.zeros((*annotation_shape, 1), dtype=np.uint8)
|
||||
|
||||
text_args: Dict[str, Any] = dict(
|
||||
text=text,
|
||||
text_anchor=annotation_center,
|
||||
text_scale=text_scale,
|
||||
text_thickness=text_thickness,
|
||||
text_padding=text_padding,
|
||||
)
|
||||
draw_text(
|
||||
scene=annotation,
|
||||
text_color=text_color,
|
||||
background_color=text_box_color if text_box_show else None,
|
||||
**text_args,
|
||||
)
|
||||
draw_text(
|
||||
scene=annotation_alpha,
|
||||
text_color=Color.WHITE,
|
||||
background_color=Color.WHITE if text_box_show else None,
|
||||
**text_args,
|
||||
)
|
||||
annotation = np.dstack((annotation, annotation_alpha))
|
||||
|
||||
# Make sure text is displayed upright
|
||||
if 90 < line_angle_degrees % 360 < 270:
|
||||
annotation = cv2.flip(annotation, flipCode=-1).astype(np.uint8)
|
||||
|
||||
rotation_angle = -line_angle_degrees
|
||||
rotation_matrix = cv2.getRotationMatrix2D(
|
||||
annotation_center.as_xy_float_tuple(), rotation_angle, scale=1
|
||||
)
|
||||
annotation = cv2.warpAffine(annotation, rotation_matrix, annotation_shape)
|
||||
|
||||
return annotation
|
||||
|
||||
|
||||
class LineZoneAnnotatorMulticlass:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
table_position: Literal[
|
||||
Position.TOP_LEFT,
|
||||
Position.TOP_RIGHT,
|
||||
Position.BOTTOM_LEFT,
|
||||
Position.BOTTOM_RIGHT,
|
||||
] = Position.TOP_RIGHT,
|
||||
table_color: Color = Color.WHITE,
|
||||
table_margin: int = 10,
|
||||
table_padding: int = 10,
|
||||
table_max_width: int = 400,
|
||||
text_color: Color = Color.BLACK,
|
||||
text_scale: float = 0.75,
|
||||
text_thickness: int = 1,
|
||||
force_draw_class_ids: bool = False,
|
||||
):
|
||||
"""
|
||||
Draw a table showing how many items of each class crossed each line.
|
||||
|
||||
Args:
|
||||
table_position (Position): The position of the table.
|
||||
table_color (Color): The color of the table.
|
||||
table_margin (int): The margin of the table from the image border.
|
||||
table_padding (int): The padding of the table.
|
||||
table_max_width (int): The maximum width of the table.
|
||||
text_color (Color): The color of the text.
|
||||
text_scale (float): The scale of the text.
|
||||
text_thickness (int): The thickness of the text.
|
||||
force_draw_class_ids (bool): Instead of writing the class names,
|
||||
on the table, write the class IDs. E.g. instead of `person: 6`,
|
||||
write `0: 6`.
|
||||
"""
|
||||
if table_position not in {
|
||||
Position.TOP_LEFT,
|
||||
Position.TOP_RIGHT,
|
||||
Position.BOTTOM_LEFT,
|
||||
Position.BOTTOM_RIGHT,
|
||||
}:
|
||||
raise ValueError(
|
||||
"Invalid table position. Supported values are:"
|
||||
" TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT."
|
||||
)
|
||||
|
||||
if self.display_out_count:
|
||||
out_text = (
|
||||
f"{self.custom_out_text}: {line_counter.out_count}"
|
||||
if self.custom_out_text is not None
|
||||
else f"out: {line_counter.out_count}"
|
||||
)
|
||||
self._annotate_count(
|
||||
frame=frame,
|
||||
center_text_anchor=text_anchor.center,
|
||||
text=out_text,
|
||||
is_in_count=False,
|
||||
self.table_position = table_position
|
||||
self.table_color = table_color
|
||||
self.table_margin = table_margin
|
||||
self.table_padding = table_padding
|
||||
self.table_max_width = table_max_width
|
||||
self.text_color = text_color
|
||||
self.text_scale = text_scale
|
||||
self.text_thickness = text_thickness
|
||||
self.force_draw_class_ids = force_draw_class_ids
|
||||
|
||||
def annotate(
|
||||
self,
|
||||
frame: np.ndarray,
|
||||
line_zones: List[LineZone],
|
||||
line_zone_labels: Optional[List[str]] = None,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Draws a table with the number of objects of each class that crossed each line.
|
||||
|
||||
Attributes:
|
||||
frame (np.ndarray): The image on which the table will be drawn.
|
||||
line_zones (List[LineZone]): The line zones to be annotated.
|
||||
line_zone_labels (Optional[List[str]]): The labels, one for each
|
||||
line zone. If not provided, the default labels will be used.
|
||||
|
||||
Returns:
|
||||
(np.ndarray): The image with the table drawn on it.
|
||||
|
||||
"""
|
||||
if line_zone_labels is None:
|
||||
line_zone_labels = [f"Line {i + 1}:" for i in range(len(line_zones))]
|
||||
if len(line_zones) != len(line_zone_labels):
|
||||
raise ValueError("The number of line zones and their labels must match.")
|
||||
|
||||
text_lines = ["Line Crossings:"]
|
||||
for line_zone, line_zone_label in zip(line_zones, line_zone_labels):
|
||||
text_lines.append(line_zone_label)
|
||||
class_id_to_name = line_zone.class_id_to_name
|
||||
|
||||
for direction, count_per_class in [
|
||||
("In", line_zone.in_count_per_class),
|
||||
("Out", line_zone.out_count_per_class),
|
||||
]:
|
||||
if not count_per_class:
|
||||
continue
|
||||
|
||||
text_lines.append(f" {direction}:")
|
||||
for class_id, count in count_per_class.items():
|
||||
class_name = (
|
||||
class_id_to_name.get(class_id, str(class_id))
|
||||
if not self.force_draw_class_ids
|
||||
else str(class_id)
|
||||
)
|
||||
text_lines.append(f" {class_name}: {count}")
|
||||
|
||||
table_width, table_height = 0, 0
|
||||
for line in text_lines:
|
||||
text_width, text_height = cv2.getTextSize(
|
||||
line, cv2.FONT_HERSHEY_SIMPLEX, self.text_scale, self.text_thickness
|
||||
)[0]
|
||||
text_height += TEXT_MARGIN
|
||||
table_width = max(table_width, text_width)
|
||||
table_height += text_height
|
||||
|
||||
table_width += 2 * self.table_padding
|
||||
table_height += 2 * self.table_padding
|
||||
table_max_height = frame.shape[0] - 2 * self.table_margin
|
||||
table_height = min(table_height, table_max_height)
|
||||
table_width = min(table_width, self.table_max_width)
|
||||
|
||||
position_map = {
|
||||
Position.TOP_LEFT: (self.table_margin, self.table_margin),
|
||||
Position.TOP_RIGHT: (
|
||||
frame.shape[1] - table_width - self.table_margin,
|
||||
self.table_margin,
|
||||
),
|
||||
Position.BOTTOM_LEFT: (
|
||||
self.table_margin,
|
||||
frame.shape[0] - table_height - self.table_margin,
|
||||
),
|
||||
Position.BOTTOM_RIGHT: (
|
||||
frame.shape[1] - table_width - self.table_margin,
|
||||
frame.shape[0] - table_height - self.table_margin,
|
||||
),
|
||||
}
|
||||
table_x1, table_y1 = position_map[self.table_position]
|
||||
|
||||
table_rect = Rect(
|
||||
x=table_x1, y=table_y1, width=table_width, height=table_height
|
||||
)
|
||||
frame = draw_rectangle(
|
||||
scene=frame, rect=table_rect, color=self.table_color, thickness=-1
|
||||
)
|
||||
|
||||
for i, line in enumerate(text_lines):
|
||||
_, text_height = cv2.getTextSize(
|
||||
line, cv2.FONT_HERSHEY_SIMPLEX, self.text_scale, self.text_thickness
|
||||
)[0]
|
||||
text_height += TEXT_MARGIN
|
||||
anchor_x = table_x1 + self.table_padding
|
||||
anchor_y = table_y1 + self.table_padding + (i + 1) * text_height
|
||||
|
||||
cv2.putText(
|
||||
img=frame,
|
||||
text=line,
|
||||
org=(anchor_x, anchor_y),
|
||||
fontFace=cv2.FONT_HERSHEY_SIMPLEX,
|
||||
fontScale=self.text_scale,
|
||||
color=self.text_color.as_bgr(),
|
||||
thickness=self.text_thickness,
|
||||
lineType=cv2.LINE_AA,
|
||||
)
|
||||
|
||||
return frame
|
||||
|
||||
@@ -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 = next(iter(result.keys()))
|
||||
if task not in SUPPORTED_TASKS_FLORENCE_2:
|
||||
raise ValueError(
|
||||
f"{task} not supported. Supported tasks are: {SUPPORTED_TASKS_FLORENCE_2}"
|
||||
)
|
||||
result = result[task]
|
||||
|
||||
if task in ["<OD>", "<CAPTION_TO_PHRASE_GROUNDING>", "<DENSE_REGION_CAPTION>"]:
|
||||
xyxy = np.array(result["bboxes"], dtype=np.float32)
|
||||
labels = np.array(result["labels"])
|
||||
return xyxy, labels, None, None
|
||||
|
||||
if task == "<REGION_PROPOSAL>":
|
||||
xyxy = np.array(result["bboxes"], dtype=np.float32)
|
||||
# provides labels, but they are ["", "", "", ...]
|
||||
return xyxy, None, None, None
|
||||
|
||||
if task == "<OCR_WITH_REGION>":
|
||||
xyxyxyxy = np.array(result["quad_boxes"], dtype=np.float32)
|
||||
xyxyxyxy = xyxyxyxy.reshape(-1, 4, 2)
|
||||
xyxy = np.array([polygon_to_xyxy(polygon) for polygon in xyxyxyxy])
|
||||
labels = np.array(result["labels"])
|
||||
return xyxy, labels, None, xyxyxyxy
|
||||
|
||||
if task in ["<REFERRING_EXPRESSION_SEGMENTATION>", "<REGION_TO_SEGMENTATION>"]:
|
||||
xyxy_list = []
|
||||
masks_list = []
|
||||
for polygons_of_same_class in result["polygons"]:
|
||||
for polygon in polygons_of_same_class:
|
||||
polygon = np.reshape(polygon, (-1, 2)).astype(np.int32)
|
||||
mask = polygon_to_mask(polygon, resolution_wh).astype(bool)
|
||||
masks_list.append(mask)
|
||||
xyxy = polygon_to_xyxy(polygon)
|
||||
xyxy_list.append(xyxy)
|
||||
# per-class labels also provided, but they are ["", "", "", ...]
|
||||
# when we figure out how to set class names, we can do
|
||||
# zip(result["labels"], result["polygons"])
|
||||
xyxy = np.array(xyxy_list, dtype=np.float32)
|
||||
masks = np.array(masks_list)
|
||||
return xyxy, None, masks, None
|
||||
|
||||
if task == "<OPEN_VOCABULARY_DETECTION>":
|
||||
xyxy = np.array(result["bboxes"], dtype=np.float32)
|
||||
labels = np.array(result["bboxes_labels"])
|
||||
# Also has "polygons" and "polygons_labels", but they don't seem to be used
|
||||
return xyxy, labels, None, None
|
||||
|
||||
if task in ["<REGION_TO_CATEGORY>", "<REGION_TO_DESCRIPTION>"]:
|
||||
assert isinstance(
|
||||
result, str
|
||||
), f"Expected string as <REGION_TO_CATEGORY> result, got {type(result)}"
|
||||
|
||||
if result == "No object detected.":
|
||||
return np.empty((0, 4), dtype=np.float32), np.array([]), None, None
|
||||
|
||||
pattern = re.compile(r"<loc_(\d+)><loc_(\d+)><loc_(\d+)><loc_(\d+)>")
|
||||
match = pattern.search(result)
|
||||
assert (
|
||||
match is not None
|
||||
), f"Expected string to end in location tags, but got {result}"
|
||||
|
||||
w, h = resolution_wh
|
||||
xyxy = np.array([match.groups()], dtype=np.float32)
|
||||
xyxy *= np.array([w, h, w, h]) / 1000
|
||||
result_string = result[: match.start()]
|
||||
labels = np.array([result_string])
|
||||
return xyxy, labels, None, None
|
||||
|
||||
assert False, f"Unimplemented task: {task}"
|
||||
|
||||
@@ -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:
|
||||
@@ -64,7 +66,7 @@ def mask_non_max_suppression(
|
||||
|
||||
Raises:
|
||||
AssertionError: If `iou_threshold` is not within the closed
|
||||
range from `0` to `1`.
|
||||
range from `0` to `1`.
|
||||
"""
|
||||
assert 0 <= iou_threshold <= 1, (
|
||||
"Value of `iou_threshold` must be in the closed range from 0 to 1, "
|
||||
@@ -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:
|
||||
@@ -181,7 +183,7 @@ def group_overlapping_boxes(
|
||||
ious = ious.flatten()
|
||||
|
||||
above_threshold = ious >= iou_threshold
|
||||
merge_group = [idx] + np.flip(order[above_threshold]).tolist()
|
||||
merge_group = [idx, *np.flip(order[above_threshold]).tolist()]
|
||||
merge_groups.append(merge_group)
|
||||
order = order[~above_threshold]
|
||||
return merge_groups
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -48,7 +48,7 @@ class CSVSink:
|
||||
detections = sv.Detections.from_ultralytics(result)
|
||||
sink.append(detections, custom_data={'<CUSTOM_LABEL>':'<CUSTOM_DATA>'})
|
||||
```
|
||||
""" # noqa: E501 // docs
|
||||
"""
|
||||
|
||||
def __init__(self, file_name: str = "output.csv") -> None:
|
||||
"""
|
||||
@@ -104,7 +104,7 @@ class CSVSink:
|
||||
|
||||
@staticmethod
|
||||
def parse_detection_data(
|
||||
detections: Detections, custom_data: Dict[str, Any] = None
|
||||
detections: Detections, custom_data: Optional[Dict[str, Any]] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
parsed_rows = []
|
||||
for i in range(len(detections.xyxy)):
|
||||
@@ -137,7 +137,7 @@ class CSVSink:
|
||||
return parsed_rows
|
||||
|
||||
def append(
|
||||
self, detections: Detections, custom_data: Dict[str, Any] = None
|
||||
self, detections: Detections, custom_data: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Append detection data to the CSV file.
|
||||
|
||||
@@ -4,11 +4,15 @@ 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,
|
||||
warn_deprecated,
|
||||
)
|
||||
|
||||
|
||||
def move_detections(
|
||||
@@ -28,6 +32,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 +57,18 @@ 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]]): [⚠️ Deprecated: please set
|
||||
to `None` and use `overlap_wh`] 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. Takes precedence over
|
||||
`overlap_ratio_wh`.
|
||||
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.
|
||||
@@ -72,19 +87,26 @@ class InferenceSlicer:
|
||||
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`. Please manually set it to "
|
||||
"`None` and 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 +151,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 +162,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 +200,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 +210,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 +233,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 +257,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}"
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ class JSONSink:
|
||||
detections = sv.Detections.from_ultralytics(result)
|
||||
sink.append(detections, custom_data={'<CUSTOM_LABEL>':'<CUSTOM_DATA>'})
|
||||
```
|
||||
""" # noqa: E501 // docs
|
||||
"""
|
||||
|
||||
def __init__(self, file_name: str = "output.json") -> None:
|
||||
"""
|
||||
@@ -92,7 +92,7 @@ class JSONSink:
|
||||
|
||||
@staticmethod
|
||||
def parse_detection_data(
|
||||
detections: Detections, custom_data: Dict[str, Any] = None
|
||||
detections: Detections, custom_data: Optional[Dict[str, Any]] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
parsed_rows = []
|
||||
for i in range(len(detections.xyxy)):
|
||||
@@ -126,7 +126,7 @@ class JSONSink:
|
||||
return parsed_rows
|
||||
|
||||
def append(
|
||||
self, detections: Detections, custom_data: Dict[str, Any] = None
|
||||
self, detections: Detections, custom_data: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Append detection data to the JSON file.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import warnings
|
||||
from dataclasses import replace
|
||||
from typing import Iterable, Optional, Tuple
|
||||
from typing import Iterable, Optional
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
@@ -9,10 +8,9 @@ import numpy.typing as npt
|
||||
from supervision import Detections
|
||||
from supervision.detection.utils import clip_boxes, polygon_to_mask
|
||||
from supervision.draw.color import Color
|
||||
from supervision.draw.utils import draw_polygon, draw_text
|
||||
from supervision.draw.utils import draw_filled_polygon, 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
|
||||
|
||||
|
||||
class PolygonZone:
|
||||
@@ -30,28 +28,11 @@ 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],
|
||||
frame_resolution_wh: Optional[Tuple[int, int]] = None,
|
||||
triggering_anchors: Iterable[Position] = (Position.BOTTOM_CENTER,),
|
||||
):
|
||||
if frame_resolution_wh is not None:
|
||||
warnings.warn(
|
||||
"The `frame_resolution_wh` parameter is no longer required and will be "
|
||||
"dropped in version supervision-0.24.0. The mask resolution is now "
|
||||
"calculated automatically based on the polygon coordinates.",
|
||||
category=SupervisionWarnings,
|
||||
)
|
||||
|
||||
self.polygon = polygon.astype(int)
|
||||
self.triggering_anchors = triggering_anchors
|
||||
if not list(self.triggering_anchors):
|
||||
@@ -117,6 +98,7 @@ class PolygonZoneAnnotator:
|
||||
default is cv2.FONT_HERSHEY_SIMPLEX
|
||||
center (Tuple[int, int]): The center of the polygon for text placement
|
||||
display_in_zone_count (bool): Show the label of the zone or not. Default is True
|
||||
opacity: The opacity of zone filling when drawn on the scene. Default is 0
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -129,6 +111,7 @@ class PolygonZoneAnnotator:
|
||||
text_thickness: int = 1,
|
||||
text_padding: int = 10,
|
||||
display_in_zone_count: bool = True,
|
||||
opacity: float = 0,
|
||||
):
|
||||
self.zone = zone
|
||||
self.color = color
|
||||
@@ -140,6 +123,7 @@ class PolygonZoneAnnotator:
|
||||
self.font = cv2.FONT_HERSHEY_SIMPLEX
|
||||
self.center = get_polygon_center(polygon=zone.polygon)
|
||||
self.display_in_zone_count = display_in_zone_count
|
||||
self.opacity = opacity
|
||||
|
||||
def annotate(self, scene: np.ndarray, label: Optional[str] = None) -> np.ndarray:
|
||||
"""
|
||||
@@ -147,18 +131,32 @@ 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:
|
||||
np.ndarray: The image with the polygon zone and count of detected objects
|
||||
"""
|
||||
annotated_frame = draw_polygon(
|
||||
scene=scene,
|
||||
polygon=self.zone.polygon,
|
||||
color=self.color,
|
||||
thickness=self.thickness,
|
||||
)
|
||||
if self.opacity == 0:
|
||||
annotated_frame = draw_polygon(
|
||||
scene=scene,
|
||||
polygon=self.zone.polygon,
|
||||
color=self.color,
|
||||
thickness=self.thickness,
|
||||
)
|
||||
else:
|
||||
annotated_frame = draw_filled_polygon(
|
||||
scene=scene.copy(),
|
||||
polygon=self.zone.polygon,
|
||||
color=self.color,
|
||||
opacity=self.opacity,
|
||||
)
|
||||
annotated_frame = draw_polygon(
|
||||
scene=annotated_frame,
|
||||
polygon=self.zone.polygon,
|
||||
color=self.color,
|
||||
thickness=self.thickness,
|
||||
)
|
||||
|
||||
if self.display_in_zone_count:
|
||||
annotated_frame = draw_text(
|
||||
|
||||
@@ -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,10 +50,10 @@ 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
|
||||
"""
|
||||
|
||||
def __init__(self, length: int = 5) -> None:
|
||||
"""
|
||||
|
||||
@@ -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`.
|
||||
@@ -140,6 +140,45 @@ def mask_iou_batch(
|
||||
return np.vstack(ious)
|
||||
|
||||
|
||||
def oriented_box_iou_batch(
|
||||
boxes_true: np.ndarray, boxes_detection: np.ndarray
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Compute Intersection over Union (IoU) of two sets of oriented bounding boxes -
|
||||
`boxes_true` and `boxes_detection`. Both sets of boxes are expected to be in
|
||||
`((x1, y1), (x2, y2), (x3, y3), (x4, y4))` format.
|
||||
|
||||
Args:
|
||||
boxes_true (np.ndarray): a `np.ndarray` representing ground-truth boxes.
|
||||
`shape = (N, 4, 2)` where `N` is number of true objects.
|
||||
boxes_detection (np.ndarray): a `np.ndarray` representing detection boxes.
|
||||
`shape = (M, 4, 2)` where `M` is number of detected objects.
|
||||
|
||||
Returns:
|
||||
np.ndarray: Pairwise IoU of boxes from `boxes_true` and `boxes_detection`.
|
||||
`shape = (N, M)` where `N` is number of true objects and
|
||||
`M` is number of detected objects.
|
||||
"""
|
||||
|
||||
boxes_true = boxes_true.reshape(-1, 4, 2)
|
||||
boxes_detection = boxes_detection.reshape(-1, 4, 2)
|
||||
|
||||
max_height = max(boxes_true[:, :, 0].max(), boxes_detection[:, :, 0].max()) + 1
|
||||
# adding 1 because we are 0-indexed
|
||||
max_width = max(boxes_true[:, :, 1].max(), boxes_detection[:, :, 1].max()) + 1
|
||||
|
||||
mask_true = np.zeros((boxes_true.shape[0], max_height, max_width))
|
||||
for i, box_true in enumerate(boxes_true):
|
||||
mask_true[i] = polygon_to_mask(box_true, (max_width, max_height))
|
||||
|
||||
mask_detection = np.zeros((boxes_detection.shape[0], max_height, max_width))
|
||||
for i, box_detection in enumerate(boxes_detection):
|
||||
mask_detection[i] = polygon_to_mask(box_detection, (max_width, max_height))
|
||||
|
||||
ious = mask_iou_batch(mask_true, mask_detection)
|
||||
return ious
|
||||
|
||||
|
||||
def clip_boxes(xyxy: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray:
|
||||
"""
|
||||
Clips bounding boxes coordinates to fit within the frame resolution.
|
||||
@@ -147,7 +186,7 @@ def clip_boxes(xyxy: np.ndarray, resolution_wh: Tuple[int, int]) -> np.ndarray:
|
||||
Args:
|
||||
xyxy (np.ndarray): A numpy array of shape `(N, 4)` where each
|
||||
row corresponds to a bounding box in
|
||||
the format `(x_min, y_min, x_max, y_max)`.
|
||||
the format `(x_min, y_min, x_max, y_max)`.
|
||||
resolution_wh (Tuple[int, int]): A tuple of the form `(width, height)`
|
||||
representing the resolution of the frame.
|
||||
|
||||
@@ -228,10 +267,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 +635,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],
|
||||
|
||||
+28
-71
@@ -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,45 +255,16 @@ 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")
|
||||
def __hash__(self):
|
||||
return hash((self.r, self.g, self.b))
|
||||
|
||||
@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")
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
isinstance(other, Color)
|
||||
and self.r == other.r
|
||||
and self.g == other.g
|
||||
and self.b == other.b
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -346,33 +317,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 +363,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:
|
||||
"""
|
||||
@@ -449,6 +397,15 @@ class ColorPalette:
|
||||
idx = idx % len(self.colors)
|
||||
return self.colors[idx]
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""
|
||||
Returns the number of colors in the palette.
|
||||
|
||||
Returns:
|
||||
int: The number of colors.
|
||||
"""
|
||||
return len(self.colors)
|
||||
|
||||
|
||||
def unify_to_bgr(color: Union[Tuple[int, int, int], Color]) -> Tuple[int, int, int]:
|
||||
"""
|
||||
|
||||
+60
-17
@@ -59,7 +59,9 @@ def draw_rectangle(
|
||||
return scene
|
||||
|
||||
|
||||
def draw_filled_rectangle(scene: np.ndarray, rect: Rect, color: Color) -> np.ndarray:
|
||||
def draw_filled_rectangle(
|
||||
scene: np.ndarray, rect: Rect, color: Color, opacity: float = 1
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Draws a filled rectangle on an image.
|
||||
|
||||
@@ -67,17 +69,32 @@ def draw_filled_rectangle(scene: np.ndarray, rect: Rect, color: Color) -> np.nda
|
||||
scene (np.ndarray): The scene on which the rectangle will be drawn
|
||||
rect (Rect): The rectangle to be drawn
|
||||
color (Color): The color of the rectangle
|
||||
opacity (float): The opacity of rectangle when drawn on the scene.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The scene with the rectangle drawn on it
|
||||
"""
|
||||
cv2.rectangle(
|
||||
scene,
|
||||
rect.top_left.as_xy_int_tuple(),
|
||||
rect.bottom_right.as_xy_int_tuple(),
|
||||
color.as_bgr(),
|
||||
-1,
|
||||
)
|
||||
if opacity == 1:
|
||||
cv2.rectangle(
|
||||
scene,
|
||||
rect.top_left.as_xy_int_tuple(),
|
||||
rect.bottom_right.as_xy_int_tuple(),
|
||||
color.as_bgr(),
|
||||
-1,
|
||||
)
|
||||
else:
|
||||
scene_with_annotations = scene.copy()
|
||||
cv2.rectangle(
|
||||
scene_with_annotations,
|
||||
rect.top_left.as_xy_int_tuple(),
|
||||
rect.bottom_right.as_xy_int_tuple(),
|
||||
color.as_bgr(),
|
||||
-1,
|
||||
)
|
||||
cv2.addWeighted(
|
||||
scene_with_annotations, opacity, scene, 1 - opacity, gamma=0, dst=scene
|
||||
)
|
||||
|
||||
return scene
|
||||
|
||||
|
||||
@@ -142,7 +159,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.
|
||||
@@ -153,6 +170,32 @@ def draw_polygon(
|
||||
return scene
|
||||
|
||||
|
||||
def draw_filled_polygon(
|
||||
scene: np.ndarray, polygon: np.ndarray, color: Color, opacity: float = 1
|
||||
) -> np.ndarray:
|
||||
"""Draw a filled polygon on a scene.
|
||||
|
||||
Parameters:
|
||||
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.
|
||||
opacity (float): The opacity of polygon when drawn on the scene.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The scene with the polygon drawn on it.
|
||||
"""
|
||||
if opacity == 1:
|
||||
cv2.fillPoly(scene, [polygon], color=color.as_bgr())
|
||||
else:
|
||||
scene_with_annotations = scene.copy()
|
||||
cv2.fillPoly(scene_with_annotations, [polygon], color=color.as_bgr())
|
||||
cv2.addWeighted(
|
||||
scene_with_annotations, opacity, scene, 1 - opacity, gamma=0, dst=scene
|
||||
)
|
||||
|
||||
return scene
|
||||
|
||||
|
||||
def draw_text(
|
||||
scene: np.ndarray,
|
||||
text: str,
|
||||
@@ -172,14 +215,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 +338,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](/latest/keypoint/annotators).
|
||||
|
||||
=== "Ultralytics"
|
||||
|
||||
Use [`sv.KeyPoints.from_ultralytics`](/keypoint/core/#supervision.keypoint.core.KeyPoints.from_ultralytics)
|
||||
method, which accepts model results.
|
||||
Use [`sv.KeyPoints.from_ultralytics`](/latest/keypoint/core/#supervision.keypoint.core.KeyPoints.from_ultralytics)
|
||||
method, which accepts [YOLOv8](https://github.com/ultralytics/ultralytics)
|
||||
pose result.
|
||||
|
||||
```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`](/latest/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`](/latest/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,11 +423,11 @@ 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)
|
||||
```
|
||||
"""
|
||||
if len(yolo_nas_results.prediction.poses) == 0:
|
||||
@@ -278,38 +458,96 @@ class KeyPoints:
|
||||
data=data,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_detectron2(cls, detectron2_results: Any) -> KeyPoints:
|
||||
"""
|
||||
Create a `sv.KeyPoints` object from the
|
||||
[Detectron2](https://github.com/facebookresearch/detectron2) inference result.
|
||||
|
||||
Args:
|
||||
detectron2_results (Any): 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,16 @@
|
||||
from supervision.metrics.core import (
|
||||
CLASS_ID_NONE,
|
||||
AveragingMethod,
|
||||
Metric,
|
||||
MetricTarget,
|
||||
)
|
||||
from supervision.metrics.f1_score import F1Score, F1ScoreResult
|
||||
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,73 @@
|
||||
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"
|
||||
|
||||
|
||||
class AveragingMethod(Enum):
|
||||
"""
|
||||
Defines different ways of averaging the metric results.
|
||||
|
||||
Suppose, before returning the final result, a metric is computed for each class.
|
||||
How do you combine those to get the final number?
|
||||
|
||||
* MACRO: Calculate the metric for each class and average the results. The simplest
|
||||
averaging method, but it does not take class imbalance into account.
|
||||
* MICRO: Calculate the metric globally by counting the total true positives, false
|
||||
positives, and false negatives. Micro averaging is useful when you want to give
|
||||
more importance to classes with more samples. It's also more appropriate if you
|
||||
have an imbalance in the number of instances per class.
|
||||
* WEIGHTED: Calculate the metric for each class and average the results, weighted by
|
||||
the number of true instances of each class. Use weighted averaging if you want
|
||||
to take class imbalance into account.
|
||||
"""
|
||||
|
||||
MACRO = "macro"
|
||||
MICRO = "micro"
|
||||
WEIGHTED = "weighted"
|
||||
@@ -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,
|
||||
@@ -441,8 +440,8 @@ class ConfusionMatrix:
|
||||
class_names = classes if classes is not None else self.classes
|
||||
use_labels_for_ticks = class_names is not None and (0 < len(class_names) < 99)
|
||||
if use_labels_for_ticks:
|
||||
x_tick_labels = class_names + ["FN"]
|
||||
y_tick_labels = class_names + ["FP"]
|
||||
x_tick_labels = [*class_names, "FN"]
|
||||
y_tick_labels = [*class_names, "FP"]
|
||||
num_ticks = len(x_tick_labels)
|
||||
else:
|
||||
x_tick_labels = None
|
||||
@@ -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,545 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
from matplotlib import pyplot as plt
|
||||
|
||||
from supervision.config import ORIENTED_BOX_COORDINATES
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.utils import box_iou_batch, mask_iou_batch
|
||||
from supervision.draw.color import LEGACY_COLOR_PALETTE
|
||||
from supervision.metrics.core import AveragingMethod, 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 F1Score(Metric):
|
||||
def __init__(
|
||||
self,
|
||||
metric_target: MetricTarget = MetricTarget.BOXES,
|
||||
averaging_method: AveragingMethod = AveragingMethod.WEIGHTED,
|
||||
):
|
||||
self._metric_target = metric_target
|
||||
if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
|
||||
raise NotImplementedError(
|
||||
"F1 score is not implemented for oriented bounding boxes."
|
||||
)
|
||||
|
||||
self._metric_target = metric_target
|
||||
self.averaging_method = averaging_method
|
||||
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]],
|
||||
) -> F1Score:
|
||||
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) -> F1ScoreResult:
|
||||
result = self._compute(self._predictions_list, self._targets_list)
|
||||
|
||||
small_predictions, small_targets = self._filter_predictions_and_targets_by_size(
|
||||
self._predictions_list, self._targets_list, ObjectSizeCategory.SMALL
|
||||
)
|
||||
result.small_objects = self._compute(small_predictions, small_targets)
|
||||
|
||||
medium_predictions, medium_targets = (
|
||||
self._filter_predictions_and_targets_by_size(
|
||||
self._predictions_list, self._targets_list, ObjectSizeCategory.MEDIUM
|
||||
)
|
||||
)
|
||||
result.medium_objects = self._compute(medium_predictions, medium_targets)
|
||||
|
||||
large_predictions, large_targets = self._filter_predictions_and_targets_by_size(
|
||||
self._predictions_list, self._targets_list, ObjectSizeCategory.LARGE
|
||||
)
|
||||
result.large_objects = self._compute(large_predictions, large_targets)
|
||||
|
||||
return result
|
||||
|
||||
def _compute(
|
||||
self, predictions_list: List[Detections], targets_list: List[Detections]
|
||||
) -> F1ScoreResult:
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
if not stats:
|
||||
return F1ScoreResult(
|
||||
metric_target=self._metric_target,
|
||||
averaging_method=self.averaging_method,
|
||||
f1_scores=np.zeros(iou_thresholds.shape[0]),
|
||||
f1_per_class=np.zeros((0, iou_thresholds.shape[0])),
|
||||
iou_thresholds=iou_thresholds,
|
||||
matched_classes=np.array([], dtype=int),
|
||||
small_objects=None,
|
||||
medium_objects=None,
|
||||
large_objects=None,
|
||||
)
|
||||
|
||||
concatenated_stats = [np.concatenate(items, 0) for items in zip(*stats)]
|
||||
f1_scores, f1_per_class, unique_classes = self._compute_f1_for_classes(
|
||||
*concatenated_stats
|
||||
)
|
||||
|
||||
return F1ScoreResult(
|
||||
metric_target=self._metric_target,
|
||||
averaging_method=self.averaging_method,
|
||||
f1_scores=f1_scores,
|
||||
f1_per_class=f1_per_class,
|
||||
iou_thresholds=iou_thresholds,
|
||||
matched_classes=unique_classes,
|
||||
small_objects=None,
|
||||
medium_objects=None,
|
||||
large_objects=None,
|
||||
)
|
||||
|
||||
def _compute_f1_for_classes(
|
||||
self,
|
||||
matches: np.ndarray,
|
||||
prediction_confidence: np.ndarray,
|
||||
prediction_class_ids: np.ndarray,
|
||||
true_class_ids: np.ndarray,
|
||||
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
sorted_indices = np.argsort(-prediction_confidence)
|
||||
matches = matches[sorted_indices]
|
||||
prediction_class_ids = prediction_class_ids[sorted_indices]
|
||||
unique_classes, class_counts = np.unique(true_class_ids, return_counts=True)
|
||||
|
||||
# Shape: PxTh,P,C,C -> CxThx3
|
||||
confusion_matrix = self._compute_confusion_matrix(
|
||||
matches, prediction_class_ids, unique_classes, class_counts
|
||||
)
|
||||
|
||||
# Shape: CxThx3 -> CxTh
|
||||
f1_per_class = self._compute_f1(confusion_matrix)
|
||||
|
||||
# Shape: CxTh -> Th
|
||||
if self.averaging_method == AveragingMethod.MACRO:
|
||||
f1_scores = np.mean(f1_per_class, axis=0)
|
||||
elif self.averaging_method == AveragingMethod.MICRO:
|
||||
confusion_matrix_merged = confusion_matrix.sum(0)
|
||||
f1_scores = self._compute_f1(confusion_matrix_merged)
|
||||
elif self.averaging_method == AveragingMethod.WEIGHTED:
|
||||
class_counts = class_counts.astype(np.float32)
|
||||
f1_scores = np.average(f1_per_class, axis=0, weights=class_counts)
|
||||
|
||||
return f1_scores, f1_per_class, unique_classes
|
||||
|
||||
@staticmethod
|
||||
def _match_detection_batch(
|
||||
predictions_classes: np.ndarray,
|
||||
target_classes: np.ndarray,
|
||||
iou: np.ndarray,
|
||||
iou_thresholds: np.ndarray,
|
||||
) -> np.ndarray:
|
||||
num_predictions, num_iou_levels = (
|
||||
predictions_classes.shape[0],
|
||||
iou_thresholds.shape[0],
|
||||
)
|
||||
correct = np.zeros((num_predictions, num_iou_levels), dtype=bool)
|
||||
correct_class = target_classes[:, None] == predictions_classes
|
||||
|
||||
for i, iou_level in enumerate(iou_thresholds):
|
||||
matched_indices = np.where((iou >= iou_level) & correct_class)
|
||||
|
||||
if matched_indices[0].shape[0]:
|
||||
combined_indices = np.stack(matched_indices, axis=1)
|
||||
iou_values = iou[matched_indices][:, None]
|
||||
matches = np.hstack([combined_indices, iou_values])
|
||||
|
||||
if matched_indices[0].shape[0] > 1:
|
||||
matches = matches[matches[:, 2].argsort()[::-1]]
|
||||
matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
|
||||
matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
|
||||
|
||||
correct[matches[:, 1].astype(int), i] = True
|
||||
|
||||
return correct
|
||||
|
||||
@staticmethod
|
||||
def _compute_confusion_matrix(
|
||||
sorted_matches: np.ndarray,
|
||||
sorted_prediction_class_ids: np.ndarray,
|
||||
unique_classes: np.ndarray,
|
||||
class_counts: np.ndarray,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Compute the confusion matrix for each class and IoU threshold.
|
||||
|
||||
Assumes the matches and prediction_class_ids are sorted by confidence
|
||||
in descending order.
|
||||
|
||||
Arguments:
|
||||
sorted_matches: np.ndarray, bool, shape (P, Th), that is True
|
||||
if the prediction is a true positive at the given IoU threshold.
|
||||
sorted_prediction_class_ids: np.ndarray, int, shape (P,), containing
|
||||
the class id for each prediction.
|
||||
unique_classes: np.ndarray, int, shape (C,), containing the unique
|
||||
class ids.
|
||||
class_counts: np.ndarray, int, shape (C,), containing the number
|
||||
of true instances for each class.
|
||||
|
||||
Returns:
|
||||
np.ndarray, shape (C, Th, 3), containing the true positives, false
|
||||
positives, and false negatives for each class and IoU threshold.
|
||||
"""
|
||||
|
||||
num_thresholds = sorted_matches.shape[1]
|
||||
num_classes = unique_classes.shape[0]
|
||||
|
||||
confusion_matrix = np.zeros((num_classes, num_thresholds, 3))
|
||||
for class_idx, class_id in enumerate(unique_classes):
|
||||
is_class = sorted_prediction_class_ids == class_id
|
||||
num_true = class_counts[class_idx]
|
||||
num_predictions = is_class.sum()
|
||||
|
||||
if num_predictions == 0:
|
||||
true_positives = np.zeros(num_thresholds)
|
||||
false_positives = np.zeros(num_thresholds)
|
||||
false_negatives = np.full(num_thresholds, num_true)
|
||||
elif num_true == 0:
|
||||
true_positives = np.zeros(num_thresholds)
|
||||
false_positives = np.full(num_thresholds, num_predictions)
|
||||
false_negatives = np.zeros(num_thresholds)
|
||||
else:
|
||||
true_positives = sorted_matches[is_class].sum(0)
|
||||
false_positives = (1 - sorted_matches[is_class]).sum(0)
|
||||
false_negatives = num_true - true_positives
|
||||
confusion_matrix[class_idx] = np.stack(
|
||||
[true_positives, false_positives, false_negatives], axis=1
|
||||
)
|
||||
|
||||
return confusion_matrix
|
||||
|
||||
@staticmethod
|
||||
def _compute_f1(confusion_matrix: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Broadcastable function, computing the F1 score from the confusion matrix.
|
||||
|
||||
Arguments:
|
||||
confusion_matrix: np.ndarray, shape (N, ..., 3), where the last dimension
|
||||
contains the true positives, false positives, and false negatives.
|
||||
|
||||
Returns:
|
||||
np.ndarray, shape (N, ...), containing the F1 score for each element.
|
||||
"""
|
||||
if not confusion_matrix.shape[-1] == 3:
|
||||
raise ValueError(
|
||||
f"Confusion matrix must have shape (..., 3), got "
|
||||
f"{confusion_matrix.shape}"
|
||||
)
|
||||
true_positives = confusion_matrix[..., 0]
|
||||
false_positives = confusion_matrix[..., 1]
|
||||
false_negatives = confusion_matrix[..., 2]
|
||||
|
||||
# Alternate formula, avoids multiple zero division checks
|
||||
denominator = 2 * true_positives + false_positives + false_negatives
|
||||
f1_score = np.where(denominator == 0, 0, 2 * true_positives / denominator)
|
||||
|
||||
return f1_score
|
||||
|
||||
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 np.empty((0, 0, 0), dtype=bool)
|
||||
)
|
||||
if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
|
||||
if obb := detections.data.get(ORIENTED_BOX_COORDINATES):
|
||||
return np.ndarray(obb, dtype=np.float32)
|
||||
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
|
||||
|
||||
def _filter_predictions_and_targets_by_size(
|
||||
self,
|
||||
predictions_list: List[Detections],
|
||||
targets_list: List[Detections],
|
||||
size_category: ObjectSizeCategory,
|
||||
) -> Tuple[List[Detections], List[Detections]]:
|
||||
"""
|
||||
Filter predictions and targets by object size category.
|
||||
"""
|
||||
new_predictions_list = []
|
||||
new_targets_list = []
|
||||
for predictions, targets in zip(predictions_list, targets_list):
|
||||
new_predictions_list.append(
|
||||
self._filter_detections_by_size(predictions, size_category)
|
||||
)
|
||||
new_targets_list.append(
|
||||
self._filter_detections_by_size(targets, size_category)
|
||||
)
|
||||
return new_predictions_list, new_targets_list
|
||||
|
||||
|
||||
@dataclass
|
||||
class F1ScoreResult:
|
||||
"""
|
||||
The results of the F1 score metric calculation.
|
||||
|
||||
Defaults to `0` if no detections or targets were provided.
|
||||
Provides a custom `__str__` method for pretty printing.
|
||||
|
||||
Attributes:
|
||||
metric_target (MetricTarget): the type of data used for the metric -
|
||||
boxes, masks or oriented bounding boxes.
|
||||
averaging_method (AveragingMethod): the averaging method used to compute the
|
||||
F1 scores. Determines how the F1 scores are aggregated across classes.
|
||||
f1_50 (float): the F1 score at IoU threshold of `0.5`.
|
||||
f1_75 (float): the F1 score at IoU threshold of `0.75`.
|
||||
f1_scores (np.ndarray): the F1 scores at each IoU threshold.
|
||||
Shape: `(num_iou_thresholds,)`
|
||||
f1_per_class (np.ndarray): the F1 scores per class and IoU threshold.
|
||||
Shape: `(num_target_classes, num_iou_thresholds)`
|
||||
iou_thresholds (np.ndarray): the IoU thresholds used in the calculations.
|
||||
matched_classes (np.ndarray): the class IDs of all matched classes.
|
||||
Corresponds to the rows of `f1_per_class`.
|
||||
small_objects (Optional[F1ScoreResult]): the F1 metric results
|
||||
for small objects.
|
||||
medium_objects (Optional[F1ScoreResult]): the F1 metric results
|
||||
for medium objects.
|
||||
large_objects (Optional[F1ScoreResult]): the F1 metric results
|
||||
for large objects.
|
||||
"""
|
||||
|
||||
metric_target: MetricTarget
|
||||
averaging_method: AveragingMethod
|
||||
|
||||
@property
|
||||
def f1_50(self) -> float:
|
||||
return self.f1_scores[0]
|
||||
|
||||
@property
|
||||
def f1_75(self) -> float:
|
||||
return self.f1_scores[5]
|
||||
|
||||
f1_scores: np.ndarray
|
||||
f1_per_class: np.ndarray
|
||||
iou_thresholds: np.ndarray
|
||||
matched_classes: np.ndarray
|
||||
|
||||
small_objects: Optional[F1ScoreResult]
|
||||
medium_objects: Optional[F1ScoreResult]
|
||||
large_objects: Optional[F1ScoreResult]
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Format as a pretty string.
|
||||
|
||||
Example:
|
||||
```python
|
||||
print(f1_result)
|
||||
```
|
||||
"""
|
||||
out_str = (
|
||||
f"{self.__class__.__name__}:\n"
|
||||
f"Metric target: {self.metric_target}\n"
|
||||
f"Averaging method: {self.averaging_method}\n"
|
||||
f"F1 @ 50: {self.f1_50:.4f}\n"
|
||||
f"F1 @ 75: {self.f1_75:.4f}\n"
|
||||
f"F1 @ thresh: {self.f1_scores}\n"
|
||||
f"IoU thresh: {self.iou_thresholds}\n"
|
||||
f"F1 per class:\n"
|
||||
)
|
||||
if self.f1_per_class.size == 0:
|
||||
out_str += " No results\n"
|
||||
for class_id, f1_of_class in zip(self.matched_classes, self.f1_per_class):
|
||||
out_str += f" {class_id}: {f1_of_class}\n"
|
||||
|
||||
indent = " "
|
||||
if self.small_objects is not None:
|
||||
indented = indent + str(self.small_objects).replace("\n", f"\n{indent}")
|
||||
out_str += f"\nSmall objects:\n{indented}"
|
||||
if self.medium_objects is not None:
|
||||
indented = indent + str(self.medium_objects).replace("\n", f"\n{indent}")
|
||||
out_str += f"\nMedium objects:\n{indented}"
|
||||
if self.large_objects is not None:
|
||||
indented = indent + str(self.large_objects).replace("\n", f"\n{indent}")
|
||||
out_str += f"\nLarge objects:\n{indented}"
|
||||
|
||||
return out_str
|
||||
|
||||
def to_pandas(self) -> "pd.DataFrame":
|
||||
"""
|
||||
Convert the result to a pandas DataFrame.
|
||||
|
||||
Returns:
|
||||
(pd.DataFrame): The result as a DataFrame.
|
||||
"""
|
||||
ensure_pandas_installed()
|
||||
import pandas as pd
|
||||
|
||||
pandas_data = {
|
||||
"F1@50": self.f1_50,
|
||||
"F1@75": self.f1_75,
|
||||
}
|
||||
|
||||
if self.small_objects is not None:
|
||||
small_objects_df = self.small_objects.to_pandas()
|
||||
for key, value in small_objects_df.items():
|
||||
pandas_data[f"small_objects_{key}"] = value
|
||||
if self.medium_objects is not None:
|
||||
medium_objects_df = self.medium_objects.to_pandas()
|
||||
for key, value in medium_objects_df.items():
|
||||
pandas_data[f"medium_objects_{key}"] = value
|
||||
if self.large_objects is not None:
|
||||
large_objects_df = self.large_objects.to_pandas()
|
||||
for key, value in large_objects_df.items():
|
||||
pandas_data[f"large_objects_{key}"] = value
|
||||
|
||||
return pd.DataFrame(pandas_data, index=[0])
|
||||
|
||||
def plot(self):
|
||||
"""
|
||||
Plot the F1 results.
|
||||
"""
|
||||
|
||||
labels = ["F1@50", "F1@75"]
|
||||
values = [self.f1_50, self.f1_75]
|
||||
colors = [LEGACY_COLOR_PALETTE[0]] * 2
|
||||
|
||||
if self.small_objects is not None:
|
||||
small_objects = self.small_objects
|
||||
labels += ["Small: F1@50", "Small: F1@75"]
|
||||
values += [small_objects.f1_50, small_objects.f1_75]
|
||||
colors += [LEGACY_COLOR_PALETTE[3]] * 2
|
||||
|
||||
if self.medium_objects is not None:
|
||||
medium_objects = self.medium_objects
|
||||
labels += ["Medium: F1@50", "Medium: F1@75"]
|
||||
values += [medium_objects.f1_50, medium_objects.f1_75]
|
||||
colors += [LEGACY_COLOR_PALETTE[2]] * 2
|
||||
|
||||
if self.large_objects is not None:
|
||||
large_objects = self.large_objects
|
||||
labels += ["Large: F1@50", "Large: F1@75"]
|
||||
values += [large_objects.f1_50, large_objects.f1_75]
|
||||
colors += [LEGACY_COLOR_PALETTE[4]] * 2
|
||||
|
||||
plt.rcParams["font.family"] = "monospace"
|
||||
|
||||
_, ax = plt.subplots(figsize=(10, 6))
|
||||
ax.set_ylim(0, 1)
|
||||
ax.set_ylabel("Value", fontweight="bold")
|
||||
title = (
|
||||
f"F1 Score, by Object Size"
|
||||
f"\n(target: {self.metric_target.value},"
|
||||
f" averaging: {self.averaging_method.value})"
|
||||
)
|
||||
ax.set_title(title, fontweight="bold")
|
||||
|
||||
x_positions = range(len(labels))
|
||||
bars = ax.bar(x_positions, values, color=colors, align="center")
|
||||
|
||||
ax.set_xticks(x_positions)
|
||||
ax.set_xticklabels(labels, rotation=45, ha="right")
|
||||
|
||||
for bar in bars:
|
||||
y_value = bar.get_height()
|
||||
ax.text(
|
||||
bar.get_x() + bar.get_width() / 2,
|
||||
y_value + 0.02,
|
||||
f"{y_value:.2f}",
|
||||
ha="center",
|
||||
va="bottom",
|
||||
)
|
||||
|
||||
plt.rcParams["font.family"] = "sans-serif"
|
||||
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
@@ -0,0 +1,562 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
from matplotlib import pyplot as plt
|
||||
|
||||
from supervision.config import ORIENTED_BOX_COORDINATES
|
||||
from supervision.detection.core import Detections
|
||||
from supervision.detection.utils import box_iou_batch, mask_iou_batch
|
||||
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 thresholds.
|
||||
|
||||
Returns:
|
||||
(MeanAveragePrecisionResult): 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, unique_classes = self._average_precisions_per_class(
|
||||
*concatenated_stats
|
||||
)
|
||||
mAP_scores = np.mean(average_precisions, axis=0)
|
||||
else:
|
||||
mAP_scores = np.zeros((10,), dtype=np.float32)
|
||||
unique_classes = np.empty((0,), dtype=int)
|
||||
average_precisions = np.empty((0, len(iou_thresholds)), dtype=np.float32)
|
||||
|
||||
return MeanAveragePrecisionResult(
|
||||
metric_target=self._metric_target,
|
||||
mAP_scores=mAP_scores,
|
||||
iou_thresholds=iou_thresholds,
|
||||
matched_classes=unique_classes,
|
||||
ap_per_class=average_precisions,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _compute_average_precision(recall: np.ndarray, precision: np.ndarray) -> float:
|
||||
"""
|
||||
Compute the average precision using 101-point interpolation (COCO), given
|
||||
the recall and precision curves.
|
||||
|
||||
Args:
|
||||
recall (np.ndarray): The recall curve.
|
||||
precision (np.ndarray): The precision curve.
|
||||
|
||||
Returns:
|
||||
(float): Average precision.
|
||||
"""
|
||||
if len(recall) == 0 and len(precision) == 0:
|
||||
return 0.0
|
||||
|
||||
recall_levels = np.linspace(0, 1, 101)
|
||||
precision_levels = np.zeros_like(recall_levels)
|
||||
for r, p in zip(recall[::-1], precision[::-1]):
|
||||
precision_levels[recall_levels <= r] = p
|
||||
|
||||
average_precision = (1 / 100 * precision_levels).sum()
|
||||
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,
|
||||
) -> Tuple[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:
|
||||
(Tuple[np.ndarray, np.ndarray]): Average precision for different
|
||||
IoU levels, and an array of class IDs that were matched.
|
||||
"""
|
||||
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)
|
||||
false_negatives = total_true - true_positives
|
||||
|
||||
recall = true_positives / (true_positives + false_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, unique_classes
|
||||
|
||||
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:
|
||||
"""
|
||||
The result of the Mean Average Precision calculation.
|
||||
|
||||
Defaults to `0` when no detections or targets are present.
|
||||
|
||||
Attributes:
|
||||
metric_target (MetricTarget): the type of data used for the metric -
|
||||
boxes, masks or oriented bounding boxes.
|
||||
mAP_map50_95 (float): the mAP score at IoU thresholds from `0.5` to `0.95`.
|
||||
mAP_map50 (float): the mAP score at IoU threshold of `0.5`.
|
||||
mAP_map75 (float): the mAP score at IoU threshold of `0.75`.
|
||||
mAP_scores (np.ndarray): the mAP scores at each IoU threshold.
|
||||
Shape: `(num_iou_thresholds,)`
|
||||
ap_per_class (np.ndarray): the average precision scores per
|
||||
class and IoU threshold. Shape: `(num_target_classes, num_iou_thresholds)`
|
||||
iou_thresholds (np.ndarray): the IoU thresholds used in the calculations.
|
||||
matched_classes (np.ndarray): the class IDs of all matched classes.
|
||||
Corresponds to the rows of `ap_per_class`.
|
||||
small_objects (Optional[MeanAveragePrecisionResult]): the mAP results
|
||||
for small objects.
|
||||
medium_objects (Optional[MeanAveragePrecisionResult]): the mAP results
|
||||
for medium objects.
|
||||
large_objects (Optional[MeanAveragePrecisionResult]): the mAP results
|
||||
for large objects.
|
||||
"""
|
||||
|
||||
metric_target: MetricTarget
|
||||
|
||||
@property
|
||||
def map50_95(self) -> float:
|
||||
return self.mAP_scores.mean()
|
||||
|
||||
@property
|
||||
def map50(self) -> float:
|
||||
return self.mAP_scores[0]
|
||||
|
||||
@property
|
||||
def map75(self) -> float:
|
||||
return self.mAP_scores[5]
|
||||
|
||||
mAP_scores: np.ndarray
|
||||
ap_per_class: np.ndarray
|
||||
iou_thresholds: np.ndarray
|
||||
matched_classes: np.ndarray
|
||||
small_objects: Optional[MeanAveragePrecisionResult] = None
|
||||
medium_objects: Optional[MeanAveragePrecisionResult] = None
|
||||
large_objects: Optional[MeanAveragePrecisionResult] = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Format as a pretty string.
|
||||
|
||||
Example:
|
||||
```python
|
||||
print(map_result)
|
||||
```
|
||||
"""
|
||||
|
||||
out_str = (
|
||||
f"{self.__class__.__name__}:\n"
|
||||
f"Metric target: {self.metric_target}\n"
|
||||
f"mAP @ 50:95: {self.map50_95:.4f}\n"
|
||||
f"mAP @ 50: {self.map50:.4f}\n"
|
||||
f"mAP @ 75: {self.map75:.4f}\n"
|
||||
f"mAP scores: {self.mAP_scores}\n"
|
||||
f"IoU thresh: {self.iou_thresholds}\n"
|
||||
f"AP per class:\n"
|
||||
)
|
||||
if self.ap_per_class.size == 0:
|
||||
out_str += " No results\n"
|
||||
for class_id, ap_of_class in zip(self.matched_classes, self.ap_per_class):
|
||||
out_str += f" {class_id}: {ap_of_class}\n"
|
||||
|
||||
indent = " "
|
||||
if self.small_objects is not None:
|
||||
indented = indent + str(self.small_objects).replace("\n", f"\n{indent}")
|
||||
out_str += f"\nSmall objects:\n{indented}"
|
||||
if self.medium_objects is not None:
|
||||
indented = indent + str(self.medium_objects).replace("\n", f"\n{indent}")
|
||||
out_str += f"\nMedium objects:\n{indented}"
|
||||
if self.large_objects is not None:
|
||||
indented = indent + str(self.large_objects).replace("\n", f"\n{indent}")
|
||||
out_str += f"\nLarge objects:\n{indented}"
|
||||
|
||||
return out_str
|
||||
|
||||
def to_pandas(self) -> "pd.DataFrame":
|
||||
"""
|
||||
Convert the result to a pandas DataFrame.
|
||||
|
||||
Returns:
|
||||
(pd.DataFrame): The result as a DataFrame.
|
||||
"""
|
||||
ensure_pandas_installed()
|
||||
import pandas as pd
|
||||
|
||||
pandas_data = {
|
||||
"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: mAP@50:95", "Small: mAP@50", "Small: 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: mAP@50:95", "Medium: mAP@50", "Medium: 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: mAP@50:95", "Large: mAP@50", "Large: 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,158 @@
|
||||
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, 4, 2).
|
||||
|
||||
Returns:
|
||||
(np.ndarray) The size category of each bounding box, matching
|
||||
the enum values of ObjectSizeCategory. Shaped (N,).
|
||||
"""
|
||||
if len(xyxyxyxy.shape) != 3 or xyxyxyxy.shape[1] != 4 or xyxyxyxy.shape[2] != 2:
|
||||
raise ValueError("Oriented bounding boxes must be shaped (N, 4, 2)")
|
||||
|
||||
# Shoelace formula
|
||||
x = xyxyxyxy[:, :, 0]
|
||||
y = xyxyxyxy[:, :, 1]
|
||||
x1, x2, x3, x4 = x.T
|
||||
y1, y2, y3, y4 = y.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,9 @@
|
||||
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`"
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user