LnRbW2MC_N}%e;wJh{64+HZOJI-Dl5jrvg3fxohz=(%F51T{~T=mY6o3?^|Us>
zE&LYSO2ob|Z&H+0)q=N7ap$C<2)cCXOh00i|Gx_!=6;FO>EsW7m5$KuY)5I>m&?xu
z{5fN9%(7)$k*KK0^XjXwqJ;Q({J^&!-FNSu^s8HLHfK55>doxt;7yuZ1RHcWHIb&p
zeCC-&qW&Hz=Mz?pjn#)^!lca
z2F0c3P;_*3NEWSCv=jBA11F?}1awVor8ia`q3Dt|LMyXCZfO&Jxe5%CF}6r0{e116rJTmRnzayVO0
zfQml*E|q57NL{;jrI;9NmmUHzWhD-w95Fu^V`yu4x0Nf`2(N7`qMUXFTohR(@*>bq
z#ni*gQDf67Y9TR8gv9n+_a3Lavv<(j
z?AdIn|7>BsHMBE;`q1|tJ5BQjeM`%JaYPV!#73!+>YI+y)!%!LqMW`*uf;YOQ`6Zm
zxw2FIKr2PG9}F1Q$pcPLphaTG&1x&J*K+Wo){_Jqh`#gA8V?OH5>QP$TKgR-sG?=R
z-b0TFTFx8zExq^XDXBz1
z#wR+tm>zg)5%ukVN_&0$?eDa7?mpW4ne_)wx|;NgEor5T_A>E3oaoKDG_1iVWBp+H
zHhp^B(CWzys!Sl_*(v+!@k_s@4R0Nxp%?Ggr!@Uf(7jJR$pBcwNR^x=!{S}?lSAAJ
zwg*9k^|8@=q`?nn&_Gr6>-jJ00uM5B+-}K+boDvBe_Y8e3KDHE(9=v%NYpUiG}C!_x#?Evs2d%L(Q
zSgfF%ilu`UQWmz8>FY*k6-}7>b!$-IJ4=|U?a3EP)L`Fq&gD}#vjz4F18fa{uNkW%
zy10W)I{`c>2w71q9>xXZ(*}5XE6Bl=SX8fk4noK!<94udZyiJ2s{%qZ6kUrcp(iLJ
z
z4@%7QyNuENdDOT)^w;cTR9l?N#xUAvuAm%|vbpx@Ues%t`Mr9|k!6Y~X9p_h2Y$Ta
zFH}-0T+xza$GBT=`34NQ7#BKGn4|aXLErtsU>f_wXl~^&U{(R|6SklpXa0*8@SJ?<
z)YFLZ-?R<})6PDxDOs!exl?DYr%_Y>&7fOvih^RX`=eP_TRndXdoTiXRFkm=#sEf7
zSxd9$zN=E{Zq^Xvdw@yH>32N;9zL#skH6OV;hNxg1`;h+3zRz67Y=JdS>ox6tuk-V
zVC#Bj*n^J*DqIvnP2cL(Z(G`WL0N179YX$k(K_y@FzWODSzonk=FXE<;QLtjJAQHR
z3cm1~O#%=LUJn#jgHJDEkA?jbvo!L+b27zK%M{RBy5-)#w*elfnqU#$?_t1eIqkSoF(d~WR^L^;uxVd26@E#CqDiF?BQZzOf;
z?ey&9l+(Yc6?op7x~YO%bS$4DH{ZLQ`VHOc24D-hj#XA30s#Ew5elW?`^8cTt@q{R
zUS2(Q&D}-+b?Zto;8pFW<~9aqT2x)Fa=dPUf5nVLQeERfu8Zf>S+YtGSokDuh%(t52;KGzChz^j9z8Zh)*c5kOBBl{$y
zZv}MQL(6#N(q}OZeU%((d2NALFq?7CwnYQ=jSkwiWhnFe;>oCb=!z*SunJlN`XrhV=DOQi
zMORGS=F!+{%Gu(Fa(YWrN-d|LqH^qDFRH0d@+>y|ziHPXYUp(jC8oN{uB_gJr_fFm
zI@eU3rnnQ&QfApfy7Q@KFUv+WXy`ukTAcZKj*Kzdz-y||d=GMde_v8fagG!=4#su>
zYzC|b!77yRSs}b9&6rU#RW4w6d2HR!au<|@27K%S^4!47pb!a4piqe+7o)3ZlxIJ6
z&2|N1yz_Zn>Q3keTLQL1{WX&9XZJFMlu+fs=P5ls-Ini#wX3YGq?rBlD5mxc>SJO2
zjss6Uh7552fCs_&Bj>mG%kTO`Jw^Owl+7U`YDN8QMMZ_|TQGX-*!rIo2mqr(1-w8H
z6j6c&a=>$0y+&s>O`El$HPE73IHFe1N{OxA#uouVVr8>im&K^zJ2&w8jdf?O4H{f9
z&lK=lY5Pt_ZtJA;rid0lJ9U4+!70hUE#8vh24G%L!RMx$ubTxP(hEI%ns2KPp9lkA
zAeE4Havuia@!X-?Y0}i4F5o5f)QrppTmJo@;d~nod@`%MjzLiYeE9rt_H?!Eb3xVB
z=@|uhpqxS>#HnA?#bdwX(NkCPx>G`_xT4yD?0rO0{TRs>7FGe!EbV+<*2h!$HCi=D
zhIloBCtmRDiqC;+j+yc)O`i2JFBS7FAa!n!EgI2ZGZ=QD*-%3?wtP`l@zU}
zcbCtguRgwl5zts>6{1OG3m4WwAbw!~{<*Y#`Kx@3+wXq&q?w}w6hk!J_Gl+V`9$`#
zQ1*S$F0a12B%s41K{XK3Y^-w%K@Q$sTuLcjcJ)W}i-%sK@l(H~^iIBm8$Iw*pM~Z;
z`U*KK3W5s0XBXwsi9N%qxu_FOnA3yS{jr29imauc-@T(Nt@@u6v~O!E6;+kegz;@F
zTB1j;VN9SGLsh)CrAy0m`N9pZh;mfM7$4TqvgZMEp8MR?^&xOIocVuar+!ZH$rW_;
zP%mm~h-2_`2!Rk~X-OF|o~W*@AnJMDb=T6NLxt>dSoZDRva+%r=gt+cmtqbkzz08X
zqKvgu+KdMxVs(*99;%Ut#9`&eP5FXPtme(51Zt%3M#|=|CX%!6M9_eD;9xr4aqCa1
zi{nB{a&)2p+4LPorS@76@S=<9zXxuoqOxOj+W9p-`Sb^L$uQrB%P|x88wYN-OmDSp
z$m^kCno)OHMJt0;HC0LU#mD36-z%q5Sy6Xp31ftS2Rp=1;>
zn{GY;pPSo{Tfxsi-#{fL!T~De0rxWNCrq0hlHfxk{Vq}9Ef?riY$ULD`gM@4{#^f|
zJ8Ak2@6zC*`=vhL6DDWYuKAm}LG}0kv!OdZHUFoS6*Z8}T&CO4u}9Fs42u#8^J-@?
z6+6GBU*GpWT|M=b3V^OaSTL=>sO;G>fLX#RwDXHGY^>tgL%+3$6&4Tz3nm
znezxmb=^pRdTA}CrZiD|k`iVC-(5bP2WK#DdI@>}(28831OTzyJ<~4mm6AYuVcoh9
zecA$A4Ls1M=;R{0_o=6RB9~gxyQrKy+H4bYmTh$9p+YGCc;^E8>q|ePPLBR;`J_p2
zO$tPyxN4ne>5Ov^&AQ4{CYcH8cm@k9=oS~GZZw#TRntqhv$Y2u?8x*>!COn{P
zNsi7GRo$OoPkWV@Ggb@0Br;C#4{gq>w)$8Wo2_Kr4^q^^RluT*N`hE7V5w!ncTDt=t
zsQbn*`U@$qKmaJ&UNGNX0cI%C0-(#u%F4#Zty@LET{#9!rff%aa2#HK%f{{TwDVvy
zjU9E4;^W-9uch86WX?Bn?sQuz
zr(e%4pdWs(By1PXf6e{@QwEnZs@IEodWj*mdfp?6c8I0UL*(rGIlaANUD%G_`VE=%
z#8YD^zIq57!`_tW=p+R^Z<+gcE)AQW-p0zIb-V<{6uNBon59+u60HNg*JWs?QKSgR
zLS29TPdNCzEg-7x0X#ExjNJOMB^={O;J!<9LlS+mA(hTn)zY|8#R(_q~sJOV9fCy!dsK%@BZI@n{9w*`z=0dIaFR1^8V{QTLx
zo)d>(!q?Li1XsTOmlc}DWdjj9k7X7mj#8+!G?CtXFNJBbv(%$oL)a{&>Z%0V{>3Hi
zIupYRd{3U)(y8s*Vq*EXP1f;lN9=1bCczl{a$>IN_Jt
zy@b$X2*v|-o>zcjS0IRNqFCDM
z$Q>%*K|^CX4Nas~AEi-ZN+n%9q$(^HQ`hV=TJ!e_TuDSxbQnRenOnXa=l+vYQo=)7
zz5lQ7^w2}!Wi)dLqnSCBTPJ?QXzm?P_V;{CA`$_|CL3%tzN#YpNIW
z+Ry0Ay3G!{@WN70IwDk3TFS0x$A#<)6GE0?_l2I5x7HYO#Rke7bui?>yX~G!XwTM5
zsIy~`+rLW*ilW;8Srv`EHj|PBi?BgMVF@8ft4RoI`7#*zRx7GR_-qAOyBA-4p3_X(
zT?=T^8k_?_9BwZ#jfeF+JBHAiL&NCLZ}bcKVjnebH#IqHH5&$D;!%l@AVA)9Yr2@>
zbMSfiT&;!0JKM19!TAZHmdBp@4xK$ToX=OP$G-~>L#Q#?y&D_F(RE!C3%m*@%3TPW
zBMLlh6*99q7JgpAnqZ~?Tow>%h!jUR>g2e9P9E$Z@&$%Y&9NzGd4pPS(EZ@y?)3QH
zi})Ti91zlLSdt~*htI+1;dAl%_#ST%qEZOgbJ4IcsBWq5&F2a8Dg*dqGOrS(ZCY2TSpm31k<96`$NMc5m2XsI9
zZ8x8gH%{uYLV6Ev^<(h)_#Op>_>;QEXHNu_QA}*%d{o1A}XjYHgh(FixDo8
zv4Ashhr>k8tJyBoNGsHog)Zmj;50Or7c!$$Sg2*l(1SE&T0A}U)u1+@^8~>9##!=R
zTA!nU@PBs=ra@EVXymwEVF5k_%A!5JT0$pSuV&7LKrrT^hwc~l2mn5z2&dVTgg`f~
zK4r!%{GuIkykSwO^WqXx1~GD6@Br6ll!RPW=d8Y_gMPK9xyhr-Z)X{NTY(TtPbg
zw1xgxPZZU%0__nlXOS!AXyK6Dj-guS-|XRMC-{>FJsqm;vVQ*Fj}F%8)*N5AJuJqfs3_IFu{eAxf8Mg$aNtf0MP4Y|&mLOC%0uq0
zvV2jF@Te?Fs{M*;YFuY1=z=FFK0wN;A2C%9PDyXt92VnorYPObOdMgeX5cQ#u3^-G
z?%c6Q7<@vO4+W;-q&3WVeP}m#=s;FLpMym3k~gy1qZ@s)A&pL+HW$E2Y$>Fg$|Aqi
zGf>*8_kM+PR+XQk499-c<3RS%nz~ldv
z1kw%I>jYUmbUOiB+Yd;o;ksOvd5mfn0L#99c^~j^XiZ;*=5xKf-)M|
zzmi4_tE9pG>x6bxMAzT^TD0my$c*;0vsu$T<+G
z_mnVF9DJ+T<2jL6-*(%bJeg<|&G}?;F;Un-&z12jJ}uv?1dDIMQv+!8h9Q*c$l-0>
zIrtond?RXOaTV=ikAg2M=`U|oQ};`VMow#_J{hMeJGqp4q*u_OZoUNujm`urYwAv&
z{rG%H2_PYE)^9fQ`OemNrG}Pxii`GbMzxPoz?0?abh;{oj#c!atslhE$*n}Oj$~f6
zfJe`$RGtXOm^L};nNmALk37t_T$&`sb&V(ZcBnEm^A
z77iG2@p=+yhKmor;fA04ZHNm^5!^}^3HUM1T3jU8MjY1e@u!ASRCPbfj_SjEub}J+
z3q?6PxrKqSiCIA$b9K>-c)I-8$=sj9d3bwEOKIbQGO8-5qRt6ts9S0ob<1p~^7t$1
zqV8QNIyzd@>UsP&X8p{t2!E6n-xnLppp%+St9HzxuC#%Q%3`Rfx)YtP>q4oisce4J
zC_Ou!h7QW0%O|JP-VYmT?UUuaCN;(p=k6FC!&Mr_Adx!p1o@6HQ)tHQPTUGk8ZW*M
zzTMG>vK&!i2tC)D#z8E@{FMCRgd>T#2%77zQW8zeNbv9ze1SgzhNIQ<4!I!Wi9~x!
zN~5r~%W(9iG)GVO?&(CY{bca>oczByhm&T%)RRVBo5uYqJZfue*>_cOASw?;7|*eV
z-=|L>>fE_A2Z299y1)$h{8>GKFCig;x_9r+u4gQrJlU5V2~CunltjZA@BqH#!FjMH
zn!;7STz*y)d@xAp^&)xL>mvM*gO8zbDYJfaWB`Z`zFOsG;pgiq&?C=ohXV&QC~ug4
zi^8qj)9Lq54X2u-LDVCv7pIv@k#E~Plxrbg`*cs5a8pr@r<+Kac#I(#WGL0DT!iZ;xxL<
zxhOrhaiua4Vyw{E=$kLQ&^5Dr(!Be2GG9Rn_{Ci~hbaSi3*JkxOe<=J7V}+05F-JJQ&@;{7In
z;SYTG_O2eQ!Zo=YR9M7Hki$Ethu8U|n;zOZ%v{jLC{l{=#wJmK&HEg9qwB)G@(K`*
zD4+|1@f79A;y_HKOscHzLJQ`1rT4~tLyz3IPtyuk^8|ZbnCBd83;1~M{NhrYIeRRRn5w0?C@m~%
zIz{F2$DgFxvwv=8&EpiKRmu-O#f9t-^BMiTf+P;Vgn;i}dF5tWxbRU*%IF+6RV{j&
z`O7bIn6=*HW$pTLYnMSe)Wy+*2M%I6==@VctB#@4w8G3k{N&&3m_K%szU{T1nxbl`
zCjOY=`;zK=Q%p+|Eq-|f4H`6vMvWRtJ$hQdn?t#8y(F|!%#LPJd}A~B1(WISdopO|
zHQ&%JHy-m?L3Evt<_(0y+w$$)`!(J7%wp++6NS0L>Zx5JA91(3MoS4(hrD;Gxr7Fk
z{)`fuI)~lhDedvl;VdqD2W4fK(3zqPsxG>K(j7`9Gu2&V=r9a~{N`T+0zQ69Et?sg
zS@Opvw0ZLu%RUnToH1)U&6qjE@-Y?ZK<9L~s;G08Q5Bt{GUriB%Pyp@*_Hg%*{0$V
zoM~X}!;yMI@tyAbF70bE-PsK^bCR1~>VwY;07CQjuKXVLE1MZ+!|3E^`k~Qo6%|o~
zvx+B*lc}qRVv2OeGHN+z%F;0kbNjFF;NY`MMvWdxi~sNfr3QFzqPHlmL5zKUCqFwF
zX9MDRGRy&Tq-gb?MlQAy9g;(1{L877|Iq%-ziNRUUgX*
z!LNm%&f3H@G4fMUe4k&uP9#%<)-k}r;+YME=mBre4f(WZkNvG2kVaVh$;XxgP^Rm+
z>n!0$ZV6k*KOu}7O1Tk}u!r_B&{f3+w8LK)cw}_9ZUC;7^#RqA($I$nGAf!;H#m%R
znv)+ZR}fxq;`mIjxY>!T|UYjyz)joJOwJXQl)SYdtl
zv!$;urGjk*A+h{ZrcSYZi~zpibf~jN&Ln+J`#LSP3UrXW8R+g-pUa>n!l5O1equPe
zPF*oA2*v<(a9{)E(?JvXd2<6i*IQ3^eY=ZUqn4mQ@V#~j#wjTyx?RjGz=m~**n|bq
zdC<$@3@6Wvb1xI(KBX4DK8354Gnv2FtMYq16%p>D@O?#5XdNeImE`sfT|2`=N9<{m
z3+wmnl>OA+1jgW4>PMWJNeTeBKAjbFCRQ@+jsWs6S_vehmRh%?X%IlXT|F)JyZ@6?{Lih>VZ(b4*Uo}H{TQs4B#4b
zL|)Xpome{zwAO%cI`}VM(O|Z1qM_&IRqxr-(##*u7AIGWXn7H3u;s|#=ej=2Rp9N2
za(F&381hNY)3htAwbK7k`bxTxDdBKRJLw6c^Dr1YyDSoUx#<$8s9@Ykdk|PXJxwRM
zT|`O{b;J&c#)dlfsHf(pCKm{so2jX(fg0-TsJXdG%Adn~@xG?U2L4`rUNhfWQO~~z
z-#28)5Zmr)a0L3(umvB27ck!e
zl;;FR%KRi%?OdprGRx<$eV`&P>t~$9!|U{b=S@k+Tsqped-B(}vOPq&QBHO+`UBym
z6Ljz~Cv&~%YmrOY)cs+<3p~`~FBov*xP2X+r(HEo(1&wTN*A!SAFZzu`QEES=39rW
zG^dsFb$TrAUovl7A9V1!WvS>3!SxZ)*pu@IO6})tU*I!$3aOB!rKi(Rue(k-h+^~*
z5qw|o-o0(Rr%fC6dx3V1x+v`CKDkRA6<_c^ZSL`~tYt3LtJ@3)_@oO2PdYc#$MLsQ
z@1K=Y_Jt`SANaVWXd3$CY`Wqe{h-xx8^H&;=l=TyUugt9r^VlAyT2BTw{MQS1a
z1Mpr3Ji969;y=8k%NC|w_A|=)PA?jKRcGpylSs{tEmU!`KB&sJ@3;&aa%~!&>GKi2
z^~D0Js61=tqY9YSn;Xcr1YFhX-uZyuFF9u3-@C^ky7?yysCV~4RJ5y_j&CXF&!tCd
zZ2?^F6`9m~WSXYt{q=$gy4r93x({uQdP06Qa~1>O2K}iL$@lrOfERt|JG6G)C$=x}
z5hKUzwpe&=VcRxA4NU1C`m~hIdlj4i3R^5(_Jzr8-qWcUoBO?ow$N*DKSCv?rWuXj
ztwKQqpBsR%e%Sk$VfHg-Oum(-PyUtX=jt;J41jeUgaBg|`x(EJ8SoVMpaXPW*)z1B
zUyZ%|H?-~Bb((*BV&S8vk4Y1iit=*0h5_)~Z@$s6XAX%;b!r0domDHjk7=hMzc=@`
zyEOmShbh|mcf4u%8JO?l-Bon1u$Ikvqm&}X9A`3%n%Osj&pYN{QM3Tto9{iXJI4}&
zCP+&I{SeFcb65dDZ!4%L0ASS_{hpgCU1Cl96C1X!p;zD5KAs*GE^Ai5ZM)R~0B@R;
z?_&X-CZ@?7faAuGr`MM)wHEjK5
zG2_?S0z6B>=LR6CCpO>a1@bv+D;O++X1xE2t9AdrV#RATM&PUpE9B`XpQ2Y@euc`*
z%LAHR_;~jQDZm0hZ{j5V)X4+4{D}tTjtuIYT0k)sDJ0h(JL
zm5q6y7|4Ka+qIth_qmwTQ-fEGo0OPJM^Eje(`O1b|GshKX3Ec>9h7T-DiDJR)cTG8pxU}B%4HyojfoG+q-CULG5Yzg=HGbB
z_lioIFkyU9t_4w0fajK9-9iHf4&cATftnTN6`HTT^1D~k9e3VAix$5?vu4fmX@+aJ
zfNC!Fb3zX&=ine%Jpk?hK3q&M|Ls9K8@RPYVC8xSx&$qWi4t5E^x&UMw
zH~K%u8M(+_y?WA+!Gm~&cFdSjw&f=mFJ3|~u!j+Afz`8Pt^l?--+kJOV2UE9H^zoF
z_`E=fK+NkDz&^p9sYY7cSVtV(Uy}ULOpfduR
zsK6qQ9zBWyaU_jl&w*|XhJOdpiPXH_e&e$={K83=&J6-`s~N?_vqiF=Tq%UYU}7O8
zecPdj^0hFoH58Z2}(x@ZhuB3fc)me~EV^>Zg?x+~{rx
zZ3;@q(u?lz?_m4OHm5!aX
zlo70QTf4$gk5kz6>2_Pe=P1DH7`s3S3i{+LzqBfh-C+gQNizbhP6FtVHn1>|0x58V
zZhg3nRoI&YpCdQcPe5Hd?uM2fB6#J`E7s68ncW1f>Ek^|z^4Phw(;Moo*R
zc@OJvDpeNrh~7P@dzi##oH~Y1EAHM$~ZT?V>pMsKa`*uBlwlPB7E-?fMS0
zKpZ$CiV`!cXwsF_bbFbg%4Egz*Juy}Ai&URo_1age*<7-1waO%wFDM5?dPs^fKh!W
zc5sXtI-L&fFzyG6;0Y|8jkK~ufUezxFPI?o2I3&*^B7Ungd~^sGg{av?E(P(?M47l
zys4t7q~Cn22Vi>QuiuJd_0Tv9R)x7gV(@Zp+f-|%x?%dAl2oa_;_Z)-~%
zAC3dgUP|c=y0A_eocG`hR$xZYDk@;aGk#XAABvCxK!1xImhZ(E7irdl+5tZJqL2UX
zNx#=LdGeLCwmY<*V{I8uzk(IW^t*PKPjXzO^!#vCkDiPvLQtwY8h|1X$JjZ
z`jv+CP7y>wZ@+!64-G}svy}OL2+D{`;)F@e=ld@nR8h<0k3Z?R-Gf3m(W&i=4V&qX
zJMRkiw5@Ve%k|X3#X;wRxRdpB?Dh+_EZQfZe8`_+ZO~Rvnh2#v|ZsAGA~fqNT;$0N!%1oF0Gz3mEDh?dnv=gAe+z%rOx>!H2EtXla57_<{q(
zE8Nck3#`@h<*&B!$&AAOc8NN=YLTB80bB%pmQ&M-ZXa=CAi)~=R0|o!^A-Sa*|HTp
z1rQ7nLJ1(E>Ydn!5_nM37xnVt0Ju*hfQx`H7+}nEgYXLXb4H*5R6zL7%GI=V=?Ysy
z{MWdnr|M+BN#H&2_Tkn?Rzw7Rp#)<-xw^1PS)V`t@p;|IB?2A*MW8JO5k#3Xc@lrt
z`Yu>pB;I9#$3S;?WacB_3l{(aeEG7$j)i{eI=wBT(o9$Rw=SreRZI2i%jxekf5&`H
z6TuZUBAOJTml5!Vn_q~25>io8SR({I=FFKLv}=VWeel8Gnz5*s9ETi{2>9#*;9)XR
z&RB6+$9_`(^00UeKxfCwz1m4}-Ebf|XuU0^AVo6grVun73h
zBLFIvaMrBpx`7f=dI_Iy<;vB8rZ6JFi-7O^0-!e#M~@!KpHov)b*bs5Oj4^cuy+UAs~f1#Zwq_T5Fm7ZDvGDm&<54Fhdw
zv7TCYmKR1wHv+zhhyyXh4L|@f-2jZRX4}aXKq-hsEyG*>zW@UO?YNNQS+Etu00000
LNkvXXu0mjfSZx`E
literal 0
HcmV?d00001
diff --git a/docs/changelog.md b/docs/changelog.md
new file mode 100644
index 0000000..f4515da
--- /dev/null
+++ b/docs/changelog.md
@@ -0,0 +1,1801 @@
+---
+description: "Full version history of the supervision Python library โ release notes, breaking changes, new features, and deprecations for every version."
+date_modified: 2026-07-08
+---
+
+# Changelog
+
+### Unreleased upcoming
+
+!!! failure "Python 3.9 Support Terminated"
+
+ With the upcoming `supervision-0.30.0` release, we are terminating official support for Python 3.9, which reached end-of-life in October 2025. The minimum supported Python version is now **3.10**.
+
+ Users on Python 3.9 should upgrade their environment before updating supervision.
+
+### Breaking Changes
+- `sv.JSONSink` now emits native JSON types for numeric and boolean data fields instead of stringified values. Fields previously serialized as `"True"`/`"False"`, `"1"`/`"0.85"`, or `"400.0"` are now `true`/`false`, `1`/`0.85`, `400.0`. Downstream consumers that compare field values as strings (e.g. `row["score"] == "1"`) or use strict string-typed schema validators must be updated. `sv.CSVSink` remains textual, but its custom-data slicing now matches `sv.JSONSink`: NumPy arrays, lists, and tuples are sliced per row only when their length matches the detection count; mismatched-length values are broadcast unchanged ([#2400](https://github.com/roboflow/supervision/pull/2400)).
+- `sv.mask_non_max_merge` now computes exact mask overlap at the original mask resolution and ignores the deprecated `mask_dimension` parameter. Code that relied on downscaled mask overlap should recalibrate thresholds; passing `mask_dimension` positionally now emits a deprecation warning, and the parameter is scheduled for removal in `0.33.0` ([#2400](https://github.com/roboflow/supervision/pull/2400)).
+
+### Fixed
+- `sv.hex_to_rgba` now rejects multiple leading `#` characters instead of silently normalizing them, matching `sv.is_valid_hex` and the documented single optional prefix.
+- `sv.box_iou_batch` now upcasts box corners to `float64` before computing areas and intersections, returning `float32`. This fixes integer-dtype overflow (e.g. `int32` coordinates around `50_000` could previously wrap to a negative area and produce an incorrect `0.0` IoU) and gives full `float64` precision to callers that pass `float64`/`int64` coordinates directly. It does not recover precision already lost when coordinates are stored as `float32` before this function is called (e.g. `Detections.xyxy`, which is `float32` throughout the library) โ such callers must upcast their own arrays to `float64`/`int64` before calling `box_iou_batch` to benefit from this fix. Results for small-coordinate inputs are unchanged.
+- Legacy COCO prediction loading in `sv.EvaluationDataset.load_predictions` now raises `ValueError` for image ids absent from the ground-truth COCO set instead of relying on a bare `assert`, so the check is no longer silently skipped under `python -O`.
+- `sv.HeatMapAnnotator` now exposes a `reset()` method to clear accumulated heat, so a single annotator instance can be reused across independent streams without carrying over heat from a previous stream.
+- `sv.TraceAnnotator` and `sv.DetectionsSmoother` now expose a `reset()` method for interface consistency with `sv.HeatMapAnnotator.reset()`, clearing their accumulated per-track history so a single instance can be reused across independent streams.
+- Fixed [#2416](https://github.com/roboflow/supervision/pull/2416): `sv.process_video` no longer risks hanging during shutdown; the sentinel enqueue is best-effort and worker joins are bounded.
+- Fixed [#2416](https://github.com/roboflow/supervision/pull/2416): COCO and CreateML dataset loaders now canonicalize resolved image paths and reject duplicate aliases for the same file.
+- Fixed [#2416](https://github.com/roboflow/supervision/pull/2416): `DetectionDataset.as_pascal_voc()` now preflights image and annotation basename collisions before writing, so exports fail fast instead of producing partial output.
+- `import supervision` no longer surfaces the deprecated `ByteTrack` warning; the top-level tracker alias now resolves lazily when accessed explicitly.
+- Fixed dataset export edge cases: `DetectionDataset.split()` and `DetectionDataset.merge()` now preserve in-memory image payloads without re-emitting the deprecation warning, and COCO/CreateML exports now reject duplicate image basenames instead of silently collapsing distinct paths into the same output key.
+- Fixed: `sv.Color(...)` now validates direct RGBA channel values and raises `ValueError` when any channel falls outside the 0-255 byte range.
+- Fixed: `approximate_mask_with_polygons` now defaults to no polygon simplification, matching the public dataset export methods.
+- Fixed: `ImageSink.save_image()` now raises `OSError` when `cv2.imwrite()` fails, and deprecation-warning control accepts the correct `SUPERVISION_DEPRECATION_WARNING` environment variable while still honoring the legacy misspelled alias.
+- `sv.Classifications.from_timm` now softmaxes model logits before exposing confidence scores, matching `sv.Classifications.from_clip` and keeping timm confidences on a normalized probability scale. Thresholds calibrated against raw logits may need retuning.
+- `sv.download_assets` now verifies MD5 hashes after fresh downloads and retries once when the downloaded payload is corrupted instead of accepting a bad file.
+- Fixed metrics scoring edge cases: legacy `sv.MeanAveragePrecision` now uses COCO 101-point AP averaging, `sv.ConfusionMatrix` rejects invalid class ids instead of wrapping them through `int16`/negative indexing, `sv.MeanAveragePrecision` preserves user-provided target `ignore` flags, and `sv.MeanAverageRecallResult.recall_per_class` now exposes per-class recall for each max-detection cutoff.
+- Fixed [#2408](https://github.com/roboflow/supervision/pull/2408): `sv.Precision`, `sv.Recall`, `sv.F1Score`, and `sv.MeanAverageRecall` now score size buckets by filtering targets only while leaving predictions eligible to match bucket targets. This preserves bucket matches that would otherwise be stolen by out-of-bucket filtering and keeps mAR top-K ranking intact.
+- `sv.ByteTrack` no longer mutates input `Detections` while assigning tracker IDs. It now keeps detections at the activation-threshold boundary eligible for matching, avoids impossible new-track thresholds above score `1.0`, ignores invalid zero-area/non-finite tensor boxes before Kalman updates, and does not emit unconfirmed `-1` IDs from first-frame tensor updates.
+- Fixed [#2402](https://github.com/roboflow/supervision/pull/2402): `sv.KeyPoints.as_detections` now accepts NumPy arrays, tuples, and generators in `selected_keypoint_indices` without ambiguous truth-value errors; empty index iterables select all keypoints. Valid zero-area skeletons are preserved, while all-zero and non-finite-only skeletons are filtered out.
+- Changed: delayed `sv.ByteTrack`, `supervision.keypoint`, `normalized_xyxy` for `sv.denormalize_boxes`, and `supervision.dataset.utils` RLE compatibility removals from `supervision-0.30.0` to `supervision-0.31.0` so the deprecated APIs keep a full transition window.
+- Fixed [#2407](https://github.com/roboflow/supervision/pull/2407): `sv.ColorPalette.by_idx()` now raises a clear `ValueError` when called on an empty palette instead of leaking a `ZeroDivisionError`. Non-empty palettes keep the existing index-wrapping behavior.
+- Fixed [#2393](https://github.com/roboflow/supervision/pull/2393): `sv.CropAnnotator.annotate` no longer raises `cv2.error` when detections extend outside the scene; out-of-bounds boxes are clipped to scene bounds and zero-area results are skipped silently.
+- Fixed [#2393](https://github.com/roboflow/supervision/pull/2393): `sv.HeatMapAnnotator.annotate` no longer blanks the hottest region when the per-pixel hit count exceeds 255; the heat mask is now derived from the float32 accumulator directly, avoiding uint8 wrap-around.
+- Fixed [#2393](https://github.com/roboflow/supervision/pull/2393): `sv.get_video_frames_generator` now releases the underlying `cv2.VideoCapture` via `try/finally`, so the decoder is freed when a consumer breaks out of iteration early rather than waiting for garbage collection.
+- Fixed [#2382](https://github.com/roboflow/supervision/pull/2382): `sv.Detections.get_anchors_coordinates` now uses oriented bounding box corners (`data["xyxyxyxy"]`) when OBB data is present, instead of falling back to the axis-aligned envelope. Anchors on rotated detections now lie on the oriented body rather than drifting to the envelope. Non-OBB detections and `Position.CENTER_OF_MASS` (which requires a mask) are unaffected.
+- Fixed [#2396](https://github.com/roboflow/supervision/pull/2396): `sv.BackgroundOverlayAnnotator.annotate` no longer leaves detection regions tinted when bounding boxes have negative coordinates (extend outside the left or top scene boundary); boxes are now clipped to scene bounds before the detection region is restored.
+- Fixed: dataset IO/export edge cases now avoid mutating caller-owned `Detections` during `DetectionDataset` construction, reject non-integer and out-of-range class ids with a clear `ValueError`, load COCO annotations that omit optional `iscrowd`/`area` fields, expose `DetectionDataset.from_coco(use_iscrowd=...)` without changing the existing positional `show_progress` argument, export mask pixel area to COCO when no stored area is present, ignore folder-structure root clutter and non-image files inside class folders, and accept PIL-readable YOLO images such as RGBA or palette PNGs.
+
+### Added
+- `KeyPoints.merge` โ combine a list of `KeyPoints` objects into one, mirroring `Detections.merge`. Empty inputs are ignored; all non-empty inputs must share the same number of keypoints per skeleton. Completes the merge-then-suppress workflow introduced by `KeyPoints.with_nms` ([#2412](https://github.com/roboflow/supervision/pull/2412))
+- `BaseAnnotator.requires_mask` โ class-level `bool` flag on all annotators; `True` for `MaskAnnotator`, `PolygonAnnotator`, and `HaloAnnotator`; `False` for all others. Integrations can inspect this before materializing expensive mask payloads ([#2370](https://github.com/roboflow/supervision/pull/2370))
+- `CompactMask.from_coco_rle` โ efficient COCO RLE ingestion into crop-scoped compact mask format without materializing dense `(N, H, W)` arrays ([#2367](https://github.com/roboflow/supervision/pull/2367))
+- `Detections.from_inference(compact_masks=True)` โ opt-in compact mask representation for Roboflow/Inference segmentation results; masks are cropped to detector bounding boxes ([#2367](https://github.com/roboflow/supervision/pull/2367))
+- `CompactMask.image_shape` โ new public property returning `(H, W)` of the full image the mask is scoped to ([#2383](https://github.com/roboflow/supervision/pull/2383))
+- `sv.mask_to_roi` โ explicit exclusive mask-bound helper for NumPy slicing and crop extraction. `sv.mask_to_xyxy` stays inclusive for compatibility with CompactMask and current box-based adapters, so the coordinate-convention migration path is now explicit instead of implicit.
+
+### Changed
+- Performance [#2383](https://github.com/roboflow/supervision/pull/2383): `sv.Detections.merge()` on mixed dense `ndarray` + `CompactMask` inputs now returns a `CompactMask` instead of a dense `ndarray`. Previously (0.29.0/0.29.1) the mixed path fell back to `np.vstack`, allocating a full `(N, H, W)` array; the new path converts dense inputs to `CompactMask` without materialising the full stack (~2 500ร less peak memory, ~13ร faster on 1080p / 40 detections). **Behavior change**: code that checks `isinstance(merged.mask, np.ndarray)` or calls bare ndarray methods (`.astype`, `.reshape`, `.ravel`) on a mixed-merge result will need to be updated. The all-dense path is unchanged and still returns `ndarray`.
+- `DetectionDataset` and `ClassificationDataset` equality now compare the ordered `classes` lists directly instead of treating class labels as an unordered set. This keeps equality aligned with `class_id` indexing semantics, where class position is part of the dataset contract.
+
+### 0.29.1 Jun 23, 2026
+
+- Fixed [#2353](https://github.com/roboflow/supervision/pull/2353): `sv.Detections.from_inference` no longer raises `TypeError` when the Inference package returns a mixed batch where only some predictions carry a `tracker_id`. `detections.tracker_id` is `None` for the full result in that case; fully-tracked and fully-untracked batches are unchanged.
+
+- Added [#2275](https://github.com/roboflow/supervision/pull/2275): `show_progress: bool = False` parameter to all `sv.DetectionDataset` load and save methods โ `from_coco`, `from_yolo`, `from_pascal_voc`, `as_coco`, `as_yolo`, `as_pascal_voc`, and `save_dataset_images`. When `True`, a `tqdm.auto` progress bar is shown (works in terminal and Jupyter). Defaults to `False` for full backward compatibility; no new dependencies.
+
+- Added [#2027](https://github.com/roboflow/supervision/issues/2027): [`sv.InferenceSlicer`](https://supervision.roboflow.com/latest/detection/tools/inference_slicer/#supervision.detection.tools.inference_slicer.InferenceSlicer) now accepts an open rasterio-style dataset in addition to in-memory images. Each tile is read lazily via a windowed read instead of loading the whole image, enabling tiled inference on multi-GB aerial/drone GeoTIFFs without running out of memory. Detection is duck-typed, so `rasterio` stays an optional dependency installable via `pip install "supervision[geotiff]"` and the core library imports no rasterio symbols. A geographic (non-projected) CRS raises `ValueError`.
+
+- Added [#2338](https://github.com/roboflow/supervision/pull/2338): [`sv.KeyPoints.with_nms`](https://supervision.roboflow.com/latest/keypoint/core/#supervision.key_points.core.KeyPoints.with_nms) โ non-maximum suppression for keypoint detections. Derives axis-aligned bounding boxes from valid (non-zero and visible) keypoints and applies `box_non_max_suppression`. Requires `detection_confidence`; supports class-aware and class-agnostic modes via `threshold`, `class_agnostic`, and `overlap_metric`.
+
+- Fixed [#2342](https://github.com/roboflow/supervision/pull/2342): `sv.Detections.from_vlm` with `sv.VLM.GOOGLE_GEMINI_2_0`, `sv.VLM.GOOGLE_GEMINI_2_5`, and `sv.VLM.QWEN_2_5_VL` no longer raises when the model returns valid JSON of the wrong shape (non-list top-level or non-dict elements). A non-string or malformed `"mask"` value in Gemini 2.5 output no longer triggers `AttributeError`; invalid base64 or non-PNG mask data falls back to an empty mask, keeping `xyxy`, `confidence`, and `masks` arrays aligned.
+
+- Fixed [#2341](https://github.com/roboflow/supervision/pull/2341): `sv.DetectionDataset.as_pascal_voc` no longer mutates the source `Detections.xyxy` by the 1-index offset on every call. Previously, repeated exports accumulated a `+1` shift in the caller's bounding boxes.
+
+- Fixed [#2334](https://github.com/roboflow/supervision/pull/2334): `sv.JSONSink` now serializes NumPy scalars (e.g. `np.int64` frame indices) in `custom_data` as JSON numbers instead of raising `TypeError` at close time. File handle is now guaranteed to close even when serialization fails.
+
+- Fixed [#2333](https://github.com/roboflow/supervision/pull/2333): [`sv.DetectionsSmoother`](https://supervision.roboflow.com/latest/detection/tools/smoother/#supervision.detection.tools.smoother.DetectionsSmoother) no longer raises when smoothing detections without `confidence`. Confidence is now averaged over the frames that carry it; when tracks in the same frame disagree on confidence presence, `confidence` is set to `None` for all smoothed detections.
+
+- Fixed [#2332](https://github.com/roboflow/supervision/pull/2332): `sv.approximate_polygon` now returns a polygon within the requested point-count budget (at most `floor(N * (1 - percentage))` points, minimum 3). The function now also validates that `epsilon_step > 0`.
+
+- Fixed [#2331](https://github.com/roboflow/supervision/pull/2331): `sv.Precision` and `sv.F1Score` now count predictions on background images (empty target set) as false positives, and count predictions of classes absent from ground truth as false positives under `MICRO` and `MACRO` averaging. Previously both edge cases were silently ignored, inflating scores. `WEIGHTED` averaging is unchanged โ absent classes retain weight 0, consistent with scikit-learn. Users relying on previous scores should re-evaluate after upgrading; no API change is required.
+
+- Added [#2299](https://github.com/roboflow/supervision/pull/2299): [`DetectionDataset.from_labelme`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.from_labelme) and [`DetectionDataset.as_labelme`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.as_labelme) for loading and exporting [LabelMe](https://github.com/wkentaro/labelme) per-image JSON annotations, following the existing COCO/YOLO/VOC convention. `rectangle` shapes load as boxes and `polygon` shapes as masks; unsupported shape types are skipped with a warning. The mask round-trip is a polygon approximation, not bit-exact.
+
+- Fixed [#2322](https://github.com/roboflow/supervision/pull/2322): COCO export now preserves all polygon parts for multi-component masks. Previously, only the first polygon was written when a non-crowd mask had disjoint segments; all parts are now included.
+
+- Performance [#2339](https://github.com/roboflow/supervision/pull/2339): `sv.HaloAnnotator` now uses the same CompactMask painting path as `sv.MaskAnnotator` via a shared `_paint_masks_by_area` helper. On a 1080p frame with 30 CompactMask detections, `HaloAnnotator` runs approximately 4ร faster; annotated output is unchanged.
+
+- Added [#2284](https://github.com/roboflow/supervision/pull/2284): [`DetectionDataset.from_createml`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.from_createml) and [`DetectionDataset.as_createml`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.as_createml) add load and export support for the CreateML object-detection JSON format, alongside the existing COCO, YOLO, and Pascal VOC formats.
+
+- Performance [#2330](https://github.com/roboflow/supervision/pull/2330): `sv.mask_to_xyxy` and `sv.KeyPoints.as_detections` are now vectorized. `mask_to_xyxy` uses batched occupancy-profile reductions instead of per-mask pixel scans; `KeyPoints.as_detections` computes all bounding boxes in a single batch operation. Both produce bit-identical results.
+
+- Performance [#2323](https://github.com/roboflow/supervision/pull/2323): Mask IoU computation now uses matrix multiplication on flattened masks instead of an explicit `(N, M, H, W)` intersection tensor, reducing peak memory for large mask sets. For masks larger than 4096ร4096 pixels, computation automatically promotes to float64 to preserve exact pixel counts. Results are numerically identical.
+
+### 0.29.0.post0 Jun 17, 2026
+
+- Fixed [#2335](https://github.com/roboflow/supervision/pull/2335): `sv.KeyPoints(confidence=...)` now works again. The `0.29.0` refactor accidentally dropped the deprecated `confidence` constructor kwarg; it is now accepted and mapped to `keypoint_confidence` with a deprecation warning.
+
+### 0.29.0 Jun 15, 2026
+
+- Added [#2314](https://github.com/roboflow/supervision/pull/2314): new cookbook **Oriented Bounding Boxes** showing how an oriented box differs from an axis-aligned one on a marina of boats: DOTA-pretrained detection, the effect on [`with_nms`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.with_nms) and [`Detections.area`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.area), and YOLO OBB dataset export.
+
+- Fixed [#2306](https://github.com/roboflow/supervision/pull/2306): [`sv.Detections.area`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.area) now returns the rotated body's area for detections carrying `data["xyxyxyxy"]` (oriented box corners) instead of the area of the derived axis-aligned bounding box, which overestimates by up to ~2x at 45ยฐ rotation. Affects annotator z-ordering inside [`MaskAnnotator`](https://supervision.roboflow.com/0.29.0/detection/annotators/#supervision.annotators.core.MaskAnnotator) and [`HaloAnnotator`](https://supervision.roboflow.com/0.29.0/detection/annotators/#supervision.annotators.core.HaloAnnotator), and any user code that filters or sorts OBB detections by area. The mask path and the non-OBB AABB fallback are unchanged.
+
+- Added [#2277](https://github.com/roboflow/supervision/pull/2277), [#2286](https://github.com/roboflow/supervision/pull/2286): [`sv.VertexEllipseAreaAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.VertexEllipseAreaAnnotator), [`sv.VertexEllipseOutlineAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.VertexEllipseOutlineAnnotator), and [`sv.VertexEllipseHaloAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.VertexEllipseHaloAnnotator) for visualizing keypoint uncertainty as covariance ellipses. Requires models that output keypoint uncertainty (e.g. RF-DETR keypoint models).
+
+- Added [#2303](https://github.com/roboflow/supervision/pull/2303): [`sv.oriented_box_non_max_suppression`](https://supervision.roboflow.com/0.29.0/detection/utils/iou_and_nms/#supervision.detection.utils.iou_and_nms.oriented_box_non_max_suppression) and [`sv.oriented_box_non_max_merge`](https://supervision.roboflow.com/0.29.0/detection/utils/iou_and_nms/#supervision.detection.utils.iou_and_nms.oriented_box_non_max_merge) for performing NMS and NMM directly on oriented bounding boxes using oriented-box IoU instead of axis-aligned IoU.
+
+- Added [#2247](https://github.com/roboflow/supervision/pull/2247): [`sv.ConfusionMatrix`](https://supervision.roboflow.com/0.29.0/detection/metrics/#supervision.metrics.detection.ConfusionMatrix) now supports `MetricTarget.ORIENTED_BOUNDING_BOXES`, computing IoU via `oriented_box_iou_batch` on `xyxyxyxy` corners. Previously, OBB inputs silently fell back to axis-aligned bounding-box IoU, producing incorrect match scores for rotated detections.
+
+- Added [#2252](https://github.com/roboflow/supervision/pull/2252): [`sv.process_video`](https://supervision.roboflow.com/0.29.0/utils/video/#supervision.utils.video.process_video) gains a `preserve_audio` parameter. When enabled, the audio stream from the source video is muxed into the output using ffmpeg.
+
+- Added [#2302](https://github.com/roboflow/supervision/pull/2302), [#2289](https://github.com/roboflow/supervision/pull/2289): [`sv.DetectionDataset.as_yolo`](https://supervision.roboflow.com/0.29.0/datasets/core/#supervision.dataset.core.DetectionDataset.as_yolo) gains an `is_obb` parameter for exporting oriented bounding box annotations in the YOLO OBB format (9-token lines with 4 corner coordinates).
+
+- Added [#2312](https://github.com/roboflow/supervision/pull/2312): [`sv.xyxyxyxy_to_xyxy`](https://supervision.roboflow.com/0.29.0/detection/utils/boxes/#supervision.detection.utils.boxes.xyxyxyxy_to_xyxy) โ vectorised utility that converts oriented bounding box corners `(N, 4, 2)` to axis-aligned bounding boxes `(N, 4)`.
+
+- Changed [#2286](https://github.com/roboflow/supervision/pull/2286): [`sv.KeyPoints`](https://supervision.roboflow.com/0.29.0/keypoint/core/#supervision.key_points.core.KeyPoints) now separates keypoint-level and detection-level confidence into distinct fields: `keypoint_confidence` (shape `(n, m)`) and `detection_confidence` (shape `(n,)`). A new `visible` mask (shape `(n, m)`) controls per-keypoint visibility. The legacy `KeyPoints.confidence` property still works but is deprecated.
+
+- Changed [#2286](https://github.com/roboflow/supervision/pull/2286): [`sv.EdgeAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.EdgeAnnotator) and [`sv.VertexAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.VertexAnnotator) now respect the `visible` mask. Invisible keypoints and their edges are skipped during rendering.
+
+- Changed [#2286](https://github.com/roboflow/supervision/pull/2286): [`sv.EdgeAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.EdgeAnnotator) and [`sv.VertexLabelAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.VertexLabelAnnotator) now support per-class skeleton definitions, enabling correct rendering when multiple skeleton topologies (e.g. person + animal) coexist in one frame.
+
+- Changed [#2303](https://github.com/roboflow/supervision/pull/2303): [`sv.Detections.with_nms`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.with_nms) and [`sv.Detections.with_nmm`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.with_nmm) now use oriented-box IoU when `data["xyxyxyxy"]` coordinates are present, instead of axis-aligned box IoU. Callers relying on the previous axis-aligned behaviour should remove `data["xyxyxyxy"]` before calling, or recalibrate any IoU thresholds.
+
+- Changed [#2312](https://github.com/roboflow/supervision/pull/2312): [`sv.Detections.with_nmm`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.with_nmm) now computes the merged oriented bounding box as the tightest rectangle at the winner's orientation enclosing all corners from every detection in a merge group.
+
+- Changed [#2325](https://github.com/roboflow/supervision/pull/2325): [`sv.VertexEllipseAreaAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.VertexEllipseAreaAnnotator), [`sv.VertexEllipseOutlineAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.VertexEllipseOutlineAnnotator), and [`sv.VertexEllipseHaloAnnotator`](https://supervision.roboflow.com/0.29.0/keypoint/annotators/#supervision.key_points.annotators.VertexEllipseHaloAnnotator) now draw sigma levels level-by-level (outermost first) across all points, ensuring correct visual layering when ellipses overlap.
+
+- Changed [#2256](https://github.com/roboflow/supervision/pull/2256): [`sv.InferenceSlicer`](https://supervision.roboflow.com/0.29.0/detection/tools/inference_slicer/#supervision.detection.tools.inference_slicer.InferenceSlicer) now detects OBB outputs from callbacks and automatically falls back to sequential processing to avoid thread-safety issues when `thread_workers > 1`.
+
+- Changed [#2324](https://github.com/roboflow/supervision/pull/2324): Project-wide deprecation policy unified to a minimum 3-minor-release window. All current deprecations (including `KeyPoints.confidence` and `validate_*` helpers) are scheduled for removal in `0.32.0`.
+
+- Fixed [#2252](https://github.com/roboflow/supervision/pull/2252): [`sv.process_video`](https://supervision.roboflow.com/0.29.0/utils/video/#supervision.utils.video.process_video) audio muxing path now correctly creates temp files on the same filesystem, decodes ffmpeg errors, and avoids muxing incomplete output.
+
+- Fixed [#2282](https://github.com/roboflow/supervision/pull/2282), [#2317](https://github.com/roboflow/supervision/pull/2317): [`sv.oriented_box_iou_batch`](https://supervision.roboflow.com/0.29.0/detection/utils/iou_and_nms/#supervision.detection.utils.iou_and_nms.oriented_box_iou_batch) now computes exact IoU via convex polygon intersection (`cv2.intersectConvexConvex`) and uses an axis-aligned bounding box envelope gate to skip pairs that cannot overlap, improving both accuracy and performance. Previously, rasterization on a discrete grid was used, which assumed square dimensions and introduced quantisation noise.
+
+- Fixed [#2239](https://github.com/roboflow/supervision/pull/2239): [`sv.Detections.from_vlm`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.from_vlm) no longer returns `None` for `class_id` on empty VLM parses; now returns an empty int ndarray.
+
+- Fixed [#2270](https://github.com/roboflow/supervision/pull/2270): [`sv.Detections.from_inference`](https://supervision.roboflow.com/0.29.0/detection/core/#supervision.detection.core.Detections.from_inference) now preserves `class_name` as a string-dtype array when predictions are empty.
+
+- Fixed [#2269](https://github.com/roboflow/supervision/pull/2269): [`sv.HeatMapAnnotator`](https://supervision.roboflow.com/0.29.0/detection/annotators/#supervision.annotators.core.HeatMapAnnotator) no longer crashes with a divide-by-zero when called with empty detections.
+
+- Fixed [#2276](https://github.com/roboflow/supervision/pull/2276): COCO export now emits 1-indexed `category_id` values as required by the COCO specification.
+
+- Fixed [#2267](https://github.com/roboflow/supervision/pull/2267): COCO annotation and image IDs are now sequential across train/val/test splits via `starting_image_id` and `starting_annotation_id` parameters.
+
+- Fixed [#2289](https://github.com/roboflow/supervision/pull/2289): [`sv.DetectionDataset.as_yolo`](https://supervision.roboflow.com/0.29.0/datasets/core/#supervision.dataset.core.DetectionDataset.as_yolo) no longer loses OBB rotation when exporting oriented bounding boxes.
+
+- Fixed [#2296](https://github.com/roboflow/supervision/pull/2296): YOLO dataset loading now sorts class names by numeric keys when `data.yaml` uses integer class IDs.
+
+- Fixed [#2297](https://github.com/roboflow/supervision/pull/2297): Letterbox utility now supports grayscale images.
+
+- Fixed [#2298](https://github.com/roboflow/supervision/pull/2298): File extension filters now normalize casing (e.g. `.JPG` matches `.jpg`).
+
+- Fixed [#2321](https://github.com/roboflow/supervision/pull/2321): [`sv.DetectionDataset.as_coco()`](https://supervision.roboflow.com/0.29.0/datasets/core/#supervision.dataset.core.DetectionDataset.as_coco) now round-trips polygon and RLE segmentation data. Segmentations loaded from COCO annotations are preserved in `detections.data["coco_raw_segmentation"]` and written back on export, preventing data loss in train/val/test split workflows.
+
+- Deprecated: `KeyPoints.confidence` (use `KeyPoints.keypoint_confidence`), `merge_inner_detection_object_pair`, `merge_inner_detections_objects`, `merge_inner_detections_objects_without_iou`, `validate_detections_fields`, `validate_vlm_parameters`, `validate_fields_both_defined_or_none`, `validate_xyxy`, `validate_mask`, `validate_class_id`, `validate_confidence`, `validate_tracker_id`, `validate_data`, `validate_xy`, `validate_key_point_confidence`, `validate_key_points_fields`, `validate_resolution`, `validate_custom_values`, `validate_input_tensors`, and `validate_labels` are deprecated in `0.29.0` and will be removed in `0.32.0`.
+
+### 0.28.0 Apr 30, 2026
+
+- Added [#2159](https://github.com/roboflow/supervision/pull/2159): [`sv.CompactMask`](https://supervision.roboflow.com/latest/detection/compact_mask/#supervision.detection.compact_mask.CompactMask) for memory-efficient mask storage. Masks are stored as crop-region bounding boxes plus RLE-encoded data instead of full-resolution bitmaps, reducing memory by up to 240ร for sparse masks. Integrates transparently with `sv.Detections.mask` โ filtering, merging, and `area` all work without materialising the full array.
+
+- Added [#2227](https://github.com/roboflow/supervision/pull/2227): [`sv.CompactMask.resize(new_image_shape)`](https://supervision.roboflow.com/latest/detection/compact_mask/#supervision.detection.compact_mask.CompactMask.resize) rescales all stored crops to match a new image resolution, enabling use across frames or after image resizing pipelines.
+
+- Added [#2178](https://github.com/roboflow/supervision/pull/2178): [`sv.Detections.from_inference`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_inference) now supports compressed COCO RLE masks. Inference responses with `rle` or `rle_mask` fields containing a compressed counts string (as produced by `pycocotools`) are decoded directly into binary masks, avoiding a lossy polygon round-trip.
+
+- Added [#2004](https://github.com/roboflow/supervision/pull/2004): [`sv.Color.from_hex`](https://supervision.roboflow.com/latest/utils/draw/#supervision.draw.color.Color.from_hex) now accepts 8-digit hexadecimal RGBA codes (e.g. `#ff00ff80`). [`Color.as_hex()`](https://supervision.roboflow.com/latest/utils/draw/#supervision.draw.color.Color.as_hex) serialises back, including alpha when not fully opaque. New utility functions `sv.hex_to_rgba`, `sv.rgba_to_hex`, and `sv.is_valid_hex` are exported at the top level.
+
+- Added [#709](https://github.com/roboflow/supervision/pull/709): [`sv.BlurAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.BlurAnnotator) and [`sv.PixelateAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.PixelateAnnotator) now support dynamic sizing. When `kernel_size=None` or `pixel_size=None` (the new default), the size is computed per detection as a fraction of the shorter bounding-box dimension, producing consistent visual results across objects of different sizes.
+
+- Added [#2186](https://github.com/roboflow/supervision/pull/2186): [`sv.InferenceSlicer`](https://supervision.roboflow.com/latest/detection/tools/inference_slicer/#supervision.detection.tools.inference_slicer.InferenceSlicer) now emits a warning when detections returned by the callback fall outside the tile boundaries, helping catch coordinate-system bugs in custom callbacks.
+
+- Added [#2103](https://github.com/roboflow/supervision/pull/2103), [#2152](https://github.com/roboflow/supervision/pull/2152): New [`sv.Detections.from_sam3()`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_sam3) classmethod parses SAM3 PCS (text-prompted) and PVS (visual-prompted video segmentation) response formats into a standard `sv.Detections`, both from the local `inference` package and from Roboflow-hosted server responses.
+
+- Added [#2154](https://github.com/roboflow/supervision/pull/2154): The library now uses Python's `logging` module instead of `print` for diagnostic output. Messages are emitted under the `supervision` logger so applications can capture, filter, or silence them through standard `logging` configuration.
+
+- Added [#932](https://github.com/roboflow/supervision/pull/932): [`sv.ImageAssets`](https://supervision.roboflow.com/latest/assets/) for downloading sample images alongside existing video assets, useful for examples and tutorials.
+
+- Changed [#2169](https://github.com/roboflow/supervision/pull/2169): [`sv.MeanAveragePrecisionResult`](https://supervision.roboflow.com/latest/metrics/mean_average_precision/) and related metric arrays (`mAP_scores`, `ap_per_class`, `iou_thresholds`, precision/recall) are now `float32` instead of `float64`. Reduces memory and speeds up computation; numerical results may differ in the last few digits.
+
+- Changed [#2178](https://github.com/roboflow/supervision/pull/2178): [`sv.rle_to_mask`](https://supervision.roboflow.com/latest/detection/utils/converters/#supervision.detection.utils.converters.rle_to_mask) and [`sv.mask_to_rle`](https://supervision.roboflow.com/latest/detection/utils/converters/#supervision.detection.utils.converters.mask_to_rle) moved to `supervision.detection.utils.converters`. The old import path `supervision.dataset.utils` continues to work but is deprecated.
+
+- Fixed [#2178](https://github.com/roboflow/supervision/pull/2178): [`sv.rle_to_mask`](https://supervision.roboflow.com/latest/detection/utils/converters/#supervision.detection.utils.converters.rle_to_mask) now returns `NDArray[bool]` as declared in its signature. Previously the implementation returned `uint8` despite the `bool` annotation; code that relied on the undocumented `uint8` output (e.g. `mask * 255` producing `uint8`) should wrap the result with `.astype(np.uint8)`.
+
+- Fixed [#2210](https://github.com/roboflow/supervision/pull/2210): [`sv.VideoInfo.fps`](https://supervision.roboflow.com/latest/utils/video/#supervision.utils.video.VideoInfo) now returns a `float` instead of a truncated `int`. Previously, frame rates like 23.976, 29.97, and 59.94 were silently truncated, causing frame-timing drift that accumulates over long videos. The type of `VideoInfo.fps` has changed from `int` to `float`; callers that pass `fps` to APIs requiring an integer (such as `deque(maxlen=...)` or `TraceAnnotator(trace_length=...)`) should wrap the value with `int()`.
+
+- Fixed [#2209](https://github.com/roboflow/supervision/pull/2209): [`sv.Detections.is_empty()`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.is_empty) now returns `True` for detections filtered down to zero rows, even when `tracker_id` is an empty array. Previously this case incorrectly returned `False`.
+
+- Fixed [#2199](https://github.com/roboflow/supervision/pull/2199): [`sv.CSVSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.csv_sink.CSVSink) now correctly slices numpy array values in `custom_data` per row. Previously the full array was written for every detection.
+
+- Fixed [#2216](https://github.com/roboflow/supervision/pull/2216): [`sv.CSVSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.csv_sink.CSVSink) and [`sv.JSONSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.json_sink.JSONSink) now slice plain Python `list` and `tuple` values in `custom_data` per detection row. Lists and tuples matching the detection count are indexed per row, consistent with `np.ndarray` behavior.
+
+- Fixed [#2217](https://github.com/roboflow/supervision/pull/2217): [`sv.TraceAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.TraceAnnotator) no longer crashes in `smooth` mode when a tracker remains stationary. Duplicate consecutive points caused `splprep` to fail; the annotator now deduplicates anchor points and falls back to a raw polyline when fewer than 4 unique points are available.
+
+- Fixed [#2218](https://github.com/roboflow/supervision/pull/2218): [`load_coco_annotations`](https://supervision.roboflow.com/latest/datasets/core/) now rejects COCO annotations whose `file_name` escapes the images directory via `../` traversal or absolute paths, preventing path-traversal attacks from malicious annotation files.
+
+- Fixed [#2187](https://github.com/roboflow/supervision/pull/2187): Extreme memory usage when loading OBB (oriented bounding box) datasets, caused by allocating full-image masks for each rotated box, has been resolved.
+
+- Fixed [#2188](https://github.com/roboflow/supervision/pull/2188): [`sv.KeyPoints`](https://supervision.roboflow.com/latest/keypoint/core/#supervision.key_points.core.KeyPoints) boolean mask indexing now works correctly when all instances have the same keypoint count (uniform-count selection).
+
+- Fixed [#2185](https://github.com/roboflow/supervision/pull/2185): [`sv.DetectionDataset.as_coco()`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.as_coco) now preserves `area` and `iscrowd` fields instead of silently dropping them in the round-trip.
+
+- Fixed [#1746](https://github.com/roboflow/supervision/pull/1746): Precision loss when converting annotations with `force_mask=True` in dataset format converters.
+
+- Fixed [#1991](https://github.com/roboflow/supervision/pull/1991): [`sv.PolygonZone`](https://supervision.roboflow.com/latest/detection/tools/polygon_zone/) no longer double-counts the same object when multiple zones overlap. Detection bounding boxes were incorrectly clipped to each zone's ROI before anchor computation, causing the same detection to appear at a different anchor point in each zone; anchor is now computed from the original bounding box so containment is independent per zone.
+
+- Fixed [#1868](https://github.com/roboflow/supervision/pull/1868): [`sv.LineZone`](https://supervision.roboflow.com/latest/detection/tools/line_zone/) no longer mis-attributes crossings when a tracker reuses the same `tracker_id` across different classes. Class-aware bookkeeping prevents a new object from inheriting another class's prior crossing state.
+
+- Fixed [#2022](https://github.com/roboflow/supervision/pull/2022): [`sv.process_video`](https://supervision.roboflow.com/latest/utils/video/#supervision.utils.video.process_video) now raises immediately when the user callback throws, instead of silently swallowing the exception and hanging until the writer is flushed.
+
+- Fixed [#2156](https://github.com/roboflow/supervision/pull/2156): [`sv.DetectionDataset`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset) now populates `data["class_name"]` on every loaded annotation, matching what model connectors produce. Downstream code can rely on `class_name` being present whether detections come from a dataset or a model.
+
+- Fixed [#1364](https://github.com/roboflow/supervision/pull/1364): [`sv.ByteTrack`](https://supervision.roboflow.com/latest/trackers/#supervision.tracker.byte_tracker.core.ByteTrack) now preserves externally assigned `tracker_id` values instead of overwriting them with internal ids on the first update.
+
+- Fixed [#1853](https://github.com/roboflow/supervision/pull/1853): [`sv.ConfusionMatrix`](https://supervision.roboflow.com/latest/detection/metrics/#supervision.metrics.detection.ConfusionMatrix) `evaluate_detection_batch` now matches predictions to ground truth correctly when multiple detections fall on the same target. Previously, double-counting inflated false-positive and false-negative counts.
+
+- Fixed [#2136](https://github.com/roboflow/supervision/pull/2136): [`sv.MeanAverageRecall`](https://supervision.roboflow.com/latest/metrics/mean_average_recall/) now computes mAR@K using the top-K detections per image, matching the COCO definition. Previous values were inflated relative to `pycocotools`.
+
+- Fixed [#1086](https://github.com/roboflow/supervision/pull/1086), [#265](https://github.com/roboflow/supervision/pull/265): COCO export and `force_masks` behaviour are now consistent across dataset formats. Empty polygons no longer raise during `as_coco`, and `force_masks=True` produces masks regardless of source format.
+
+- Deprecated [#2215](https://github.com/roboflow/supervision/pull/2215): [`sv.ByteTrack`](https://supervision.roboflow.com/latest/trackers/#supervision.tracker.byte_tracker.core.ByteTrack) is deprecated in favour of `ByteTrackTracker` from the external [`trackers`](https://pypi.org/project/trackers/) package (`pip install trackers`). The update method is renamed from `update_with_detections()` to `update()`. Removal is now planned for `supervision-0.31.0`.
+
+- Deprecated [#2214](https://github.com/roboflow/supervision/pull/2214): `supervision.keypoint` module is deprecated; use `supervision.key_points` instead. `create_tiles` in `supervision.utils.image`, `ensure_cv2_image_for_processing` in `supervision.utils.conversion`, and keypoint validation utilities in `supervision.validators` are deprecated. The `LMM` enum (use `VLM`) and `from_lmm` method (use `from_vlm`) were deprecated in 0.26.0; this release migrates their deprecation mechanism to `pydeprecate`.
+
+- Deprecated: `normalized_xyxy` argument in [`sv.denormalize_boxes`](https://supervision.roboflow.com/latest/detection/utils/boxes/#supervision.detection.utils.boxes.denormalize_boxes) renamed to `xyxy`. Passing `normalized_xyxy=` now emits a `FutureWarning`; support will be removed in `supervision-0.31.0`.
+
+### 0.27.0 Nov 16, 2025
+
+- Added [#2008](https://github.com/roboflow/supervision/pull/2008): [`sv.filter_segments_by_distance`](https://supervision.roboflow.com/0.27.0/detection/utils/masks/#supervision.detection.utils.masks.filter_segments_by_distance) to keep the largest connected component and nearby components within an absolute or relative distance threshold. Useful for cleaning segmentation predictions from models such as SAM, SAM2, YOLO segmentation, and RF-DETR segmentation.
+
+- Added [#2006](https://github.com/roboflow/supervision/pull/2006): [`sv.xyxy_to_mask`](https://supervision.roboflow.com/0.27.0/detection/utils/converters/#supervision.detection.utils.converters.xyxy_to_mask) to convert bounding boxes into 2D boolean masks, where each mask corresponds to a single box.
+
+- Added [#1943](https://github.com/roboflow/supervision/pull/1943): [`sv.tint_image`](https://supervision.roboflow.com/0.27.0/utils/image/#supervision.utils.image.tint_image) to apply a solid color overlay to an image at a given opacity. Works with both NumPy and PIL inputs.
+
+- Added [#1943](https://github.com/roboflow/supervision/pull/1943): [`sv.grayscale_image`](https://supervision.roboflow.com/0.27.0/utils/image/#supervision.utils.image.tint_image) to convert an image to 3 channel grayscale for compatibility with color based drawing utilities.
+
+- Added [#2014](https://github.com/roboflow/supervision/pull/2014): [`sv.get_image_resolution_wh`](https://supervision.roboflow.com/0.27.0/utils/image/#supervision.utils.image.get_image_resolution_wh) as a unified way to read image width and height from NumPy and PIL inputs.
+
+- Added [#1912](https://github.com/roboflow/supervision/pull/1912): [`sv.edit_distance`](https://supervision.roboflow.com/0.27.0/detection/utils/vlms/#supervision.detection.utils.vlms.edit_distance) for Levenshtein distance between two strings. Supports insert, delete, and substitute operations.
+
+- Added [#1912](https://github.com/roboflow/supervision/pull/1912): [`sv.fuzzy_match_index`](https://supervision.roboflow.com/0.27.0/detection/utils/vlms/#supervision.detection.utils.vlms.fuzzy_match_index) to find the first close match in a list using edit distance.
+
+- Changed [#2015](https://github.com/roboflow/supervision/pull/2015): [`sv.Detections.from_vlm`](https://supervision.roboflow.com/0.27.0/detection/core/#supervision.detection.core.Detections.from_vlm) and legacy `from_lmm` now support Qwen3 VL via `vlm=sv.VLM.QWEN_3_VL`.
+
+- Changed [#1884](https://github.com/roboflow/supervision/pull/1884): [`sv.Detections.from_vlm`](https://supervision.roboflow.com/0.27.0/detection/core/#supervision.detection.core.Detections.from_vlm) and legacy `from_lmm` now support DeepSeek VL 2 via `vlm=sv.VLM.DEEPSEEK_VL_2`.
+
+- Changed [#2015](https://github.com/roboflow/supervision/pull/2015): [`sv.Detections.from_vlm`](https://supervision.roboflow.com/0.27.0/detection/core/#supervision.detection.core.Detections.from_vlm) now parses Qwen 2.5 VL outputs more robustly and handles incomplete or truncated JSON responses.
+
+- Changed [#2014](https://github.com/roboflow/supervision/pull/2014): [`sv.InferenceSlicer`](https://supervision.roboflow.com/0.27.0/detection/tools/inference_slicer/#supervision.detection.tools.inference_slicer.InferenceSlicer) now uses a new offset generation logic that removes redundant tiles and aligns borders cleanly. This reduces the number of processed tiles and shortens inference time without hurting detection quality.
+
+- Changed [#2016](https://github.com/roboflow/supervision/pull/2016): [`sv.Detections`](https://supervision.roboflow.com/0.27.0/detection/core/#supervision.detection.core.Detections) now includes a `box_aspect_ratio` property for vectorized aspect ratio computation, useful for filtering detections based on box shape.
+
+- Changed [#2001](https://github.com/roboflow/supervision/pull/2001): Significantly improved the performance of [`sv.box_iou_batch`](https://supervision.roboflow.com/0.27.0/detection/utils/iou_and_nms/#supervision.detection.utils.iou_and_nms.box_iou_batch). On internal benchmarks, processing runs approximately 2x to 5x faster.
+
+- Changed [#1997](https://github.com/roboflow/supervision/pull/1997): [`sv.process_video`](https://supervision.roboflow.com/0.27.0/utils/video/#supervision.utils.video.process_video) now uses a threaded reader, processor, and writer pipeline. This removes I/O stalls and improves throughput while keeping the callback single threaded and safe for stateful models.
+
+- Changed: [`sv.denormalize_boxes`](https://supervision.roboflow.com/0.27.0/detection/utils/boxes/#supervision.detection.utils.boxes.denormalize_boxes) now supports batch conversion of bounding boxes. The function accepts arrays of shape `(N, 4)` and returns a batch of absolute pixel coordinates.
+
+- Changed [#1917](https://github.com/roboflow/supervision/pull/1917): [`sv.LabelAnnotator`](https://supervision.roboflow.com/0.27.0/detection/annotators/#supervision.annotators.core.LabelAnnotator) and [`sv.RichLabelAnnotator`](https://supervision.roboflow.com/0.27.0/detection/annotators/#supervision.annotators.core.RichLabelAnnotator) now accept `text_offset=(x, y)` to shift the label relative to `text_position`. Works with smart label position and line wrapping.
+
+!!! failure "Removed"
+ Removed the deprecated `overlap_ratio_wh` argument from `sv.InferenceSlicer`. Use the pixel based `overlap_wh` argument to control slice overlap.
+
+!!! info "Tip"
+ Convert your old ratio based overlap to pixel based overlap by multiplying each ratio by the slice dimensions.
+
+ ```python
+ # before
+
+ slice_wh = (640, 640)
+ overlap_ratio_wh = (0.25, 0.25)
+
+ slicer = sv.InferenceSlicer(
+ callback=callback,
+ slice_wh=slice_wh,
+ overlap_ratio_wh=overlap_ratio_wh,
+ overlap_filter=sv.OverlapFilter.NON_MAX_SUPPRESSION,
+ )
+
+ # after
+
+ overlap_wh = (
+ int(overlap_ratio_wh[0] * slice_wh[0]),
+ int(overlap_ratio_wh[1] * slice_wh[1]),
+ )
+
+ slicer = sv.InferenceSlicer(
+ callback=callback,
+ slice_wh=slice_wh,
+ overlap_wh=overlap_wh,
+ overlap_filter=sv.OverlapFilter.NON_MAX_SUPPRESSION,
+ )
+ ```
+
+### 0.26.1 Jul 22, 2025
+
+- Fixed [1894](https://github.com/roboflow/supervision/pull/1894): Error in [`sv.MeanAveragePrecision`](https://supervision.roboflow.com/0.26.1/metrics/mean_average_precision/#supervision.metrics.mean_average_precision.MeanAveragePrecision) where the area used for size-specific evaluation (small / medium / large) was always zero unless explicitly provided in `sv.Detections.data`.
+
+- Fixed [1895](https://github.com/roboflow/supervision/pull/1895): `ID=0` bug in [`sv.MeanAveragePrecision`](https://supervision.roboflow.com/0.26.1/metrics/mean_average_precision/#supervision.metrics.mean_average_precision.MeanAveragePrecision) where objects were getting `0.0` mAP despite perfect IoU matches due to a bug in annotation ID assignment.
+
+- Fixed [1898](https://github.com/roboflow/supervision/pull/1898): Issue where [`sv.MeanAveragePrecision`](https://supervision.roboflow.com/0.26.1/metrics/mean_average_precision/#supervision.metrics.mean_average_precision.MeanAveragePrecision) could return negative values when certain object size categories have no data.
+
+- Fixed [1901](https://github.com/roboflow/supervision/pull/1901): `match_metric` support for [`sv.Detections.with_nms`](https://supervision.roboflow.com/0.26.1/metrics/mean_average_precision/#supervision.detection.core.Detections.with_nms).
+
+- Fixed [1906](https://github.com/roboflow/supervision/pull/1906): `border_thickness` parameter usage for [`sv.PercentageBarAnnotator`](https://supervision.roboflow.com/0.26.1/metrics/mean_average_precision/#supervision.annotators.core.PercentageBarAnnotator).
+
+### 0.26.0 Jul 16, 2025
+
+!!! failure "Removed"
+ `supervision-0.26.0` drops `python3.8` support and upgrade all codes to `python3.9` syntax style.
+
+!!! info "Tip"
+ Supervisionโs documentation theme now has a fresh look that is consistent with the documentations of all Roboflow open-source projects. ([#1858](https://github.com/roboflow/supervision/pull/1858))
+
+- Added [#1774](https://github.com/roboflow/supervision/pull/1774): Support for the IOS (Intersection over Smallest) overlap metric that measures how much of the smaller object is covered by the larger one in [`sv.Detections.with_nms`](https://supervision.roboflow.com/0.26.0/detection/core/#supervision.detection.core.Detections.with_nms), [`sv.Detections.with_nmm`](https://supervision.roboflow.com/0.26.0/detection/core/#supervision.detection.core.Detections.with_nmm), [`sv.box_iou_batch`](https://supervision.roboflow.com/0.26.0/detection/utils/iou_and_nms/#supervision.detection.utils.iou_and_nms.box_iou_batch), and [`sv.mask_iou_batch`](https://supervision.roboflow.com/0.26.0/detection/utils/iou_and_nms/#supervision.detection.utils.iou_and_nms.mask_iou_batch).
+
+ ```python
+ import numpy as np
+ import supervision as sv
+
+ boxes_true = np.array([
+ [100, 100, 200, 200],
+ [300, 300, 400, 400]
+ ])
+ boxes_detection = np.array([
+ [150, 150, 250, 250],
+ [320, 320, 420, 420]
+ ])
+
+ sv.box_iou_batch(
+ boxes_true=boxes_true,
+ boxes_detection=boxes_detection,
+ overlap_metric=sv.OverlapMetric.IOU
+ )
+
+ # array([[0.14285714, 0. ],
+ # [0. , 0.47058824]])
+
+ sv.box_iou_batch(
+ boxes_true=boxes_true,
+ boxes_detection=boxes_detection,
+ overlap_metric=sv.OverlapMetric.IOS
+ )
+
+ # array([[0.25, 0. ],
+ # [0. , 0.64]])
+ ```
+
+- Added [#1874](https://github.com/roboflow/supervision/pull/1874): [`sv.box_iou`](https://supervision.roboflow.com/0.26.0/detection/utils/iou_and_nms/#supervision.detection.utils.iou_and_nms.box_iou) that efficiently computes the Intersection over Union (IoU) between two individual bounding boxes.
+
+- Added [#1816](https://github.com/roboflow/supervision/pull/1816): Support for frame limitations and progress bar in [`sv.process_video`](https://supervision.roboflow.com/0.26.0/utils/video/#supervision.utils.video.process_video).
+
+- Added [#1788](https://github.com/roboflow/supervision/pull/1788): Support for creating [`sv.KeyPoints`](https://supervision.roboflow.com/0.26.0/keypoint/core/#supervision.keypoint.core.KeyPoints) objects from [ViTPose](https://huggingface.co/docs/transformers/en/model_doc/vitpose) and [ViTPose++](https://huggingface.co/docs/transformers/en/model_doc/vitpose#vitpose-models) inference results via [`sv.KeyPoints.from_transformers`](https://supervision.roboflow.com/0.26.0/keypoint/core/#supervision.keypoint.core.KeyPoints.from_transformers).
+
+- Added [#1823](https://github.com/roboflow/supervision/pull/1823): [`sv.xyxy_to_xcycarh`](https://supervision.roboflow.com/0.26.0/detection/utils/converters/#supervision.detection.utils.converters.xyxy_to_xcycarh) function to convert bounding box coordinates from `(x_min, y_min, x_max, y_max)` into measurement space to format `(center x, center y, aspect ratio, height)`, where the aspect ratio is `width / height`.
+
+- Added [#1788](https://github.com/roboflow/supervision/pull/1788): [`sv.xyxy_to_xywh`](https://supervision.roboflow.com/0.26.0/detection/utils/converters/#supervision.detection.utils.converters.xyxy_to_xywh) function to convert bounding box coordinates from `(x_min, y_min, x_max, y_max)` format to `(x, y, width, height)` format.
+
+- Changed [#1820](https://github.com/roboflow/supervision/pull/1820): [`sv.LabelAnnotator`](https://supervision.roboflow.com/0.26.0/detection/annotators/#supervision.annotators.core.LabelAnnotator) now supports the `smart_position` parameter to automatically keep labels within frame boundaries, and the `max_line_length` parameter to control text wrapping for long or multi-line labels.
+
+- Changed [#1825](https://github.com/roboflow/supervision/pull/1825): [`sv.LabelAnnotator`](https://supervision.roboflow.com/0.26.0/detection/annotators/#supervision.annotators.core.LabelAnnotator) now supports non-string labels.
+
+- Changed [#1792](https://github.com/roboflow/supervision/pull/1792): [`sv.Detections.from_vlm`](https://supervision.roboflow.com/0.26.0/detection/core/#supervision.detection.core.Detections.from_vlm) now supports parsing bounding boxes and segmentation masks from responses generated by [Google Gemini models](https://ai.google.dev/gemini-api/docs/vision).
+
+ ```python
+ import supervision as sv
+
+ gemini_response_text = """```json
+ [
+ {"box_2d": [543, 40, 728, 200], "label": "cat", "id": 1},
+ {"box_2d": [653, 352, 820, 522], "label": "dog", "id": 2}
+ ]
+ ```"""
+
+ detections = sv.Detections.from_vlm(
+ sv.VLM.GOOGLE_GEMINI_2_5,
+ gemini_response_text,
+ resolution_wh=(1000, 1000),
+ classes=['cat', 'dog'],
+ )
+
+ detections.xyxy
+ # array([[543., 40., 728., 200.], [653., 352., 820., 522.]])
+
+ detections.data
+ # {'class_name': array(['cat', 'dog'], dtype='Nov 12, 2024
+
+- No removals or deprecations in this release!
+
+- Essential update to the [`LineZone`](https://supervision.roboflow.com/0.25.0/detection/tools/line_zone/): when computing line crossings, detections that jitter might be counted twice (or more). This can now be solved with the `minimum_crossing_threshold` argument. If you set it to `2` or more, extra frames will be used to confirm the crossing, improving the accuracy significantly. ([#1540](https://github.com/roboflow/supervision/pull/1540))
+
+- It is now possible to track objects detected as [`KeyPoints`](https://supervision.roboflow.com/0.25.0/keypoint/core/#supervision.keypoint.core.KeyPoints). See the complete step-by-step guide in the [Object Tracking Guide](https://supervision.roboflow.com/latest/how_to/track_objects/#keypoints). ([#1658](https://github.com/roboflow/supervision/pull/1658))
+
+```python
+import numpy as np
+import supervision as sv
+from ultralytics import YOLO
+
+model = YOLO("yolov8m-pose.pt")
+tracker = sv.ByteTrack()
+trace_annotator = sv.TraceAnnotator()
+
+def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model(frame)[0]
+ key_points = sv.KeyPoints.from_ultralytics(results)
+
+ detections = key_points.as_detections()
+ detections = tracker.update_with_detections(detections)
+
+ annotated_image = trace_annotator.annotate(frame.copy(), detections)
+ return annotated_image
+
+sv.process_video(
+ source_path="input_video.mp4",
+ target_path="output_video.mp4",
+ callback=callback
+)
+```
+
+- Added `is_empty` method to [`KeyPoints`](https://supervision.roboflow.com/0.25.0/keypoint/core/#supervision.keypoint.core.KeyPoints) to check if there are any keypoints in the object. ([#1658](https://github.com/roboflow/supervision/pull/1658))
+
+- Added `as_detections` method to [`KeyPoints`](https://supervision.roboflow.com/0.25.0/keypoint/core/#supervision.keypoint.core.KeyPoints) that converts `KeyPoints` to `Detections`. ([#1658](https://github.com/roboflow/supervision/pull/1658))
+
+- Added a new video to the `supervision.assets` download catalog. ([#1657](https://github.com/roboflow/supervision/pull/1657))
+
+```python
+from supervision.assets import download_assets, VideoAssets
+
+path_to_video = download_assets(VideoAssets.SKIING)
+```
+
+- Supervision can now be used with [`Python 3.13`](https://docs.python.org/3/whatsnew/3.13.html). The most renowned update is the ability to run Python [without Global Interpreter Lock (GIL)](https://docs.python.org/3/whatsnew/3.13.html#whatsnew313-free-threaded-cpython). We expect support for this among our dependencies to be inconsistent, but if you do attempt it - let us know the results! ([#1595](https://github.com/roboflow/supervision/pull/1595))
+
+- Added [`Mean Average Recall`](https://supervision.roboflow.com/latest/metrics/mean_average_recall/) mAR metric, which returns a recall score, averaged over IoU thresholds, detected object classes, and limits imposed on maximum considered detections. ([#1661](https://github.com/roboflow/supervision/pull/1661))
+
+```python
+import supervision as sv
+from supervision.metrics import MeanAverageRecall
+
+predictions = sv.Detections(...)
+targets = sv.Detections(...)
+
+map_metric = MeanAverageRecall()
+map_result = map_metric.update(predictions, targets).compute()
+
+map_result.plot()
+```
+
+- Added [`Precision`](https://supervision.roboflow.com/latest/metrics/precision/) and [`Recall`](https://supervision.roboflow.com/latest/metrics/recall/) metrics, providing a baseline for comparing model outputs to ground truth or another model ([#1609](https://github.com/roboflow/supervision/pull/1609))
+
+```python
+import supervision as sv
+from supervision.metrics import Recall
+
+predictions = sv.Detections(...)
+targets = sv.Detections(...)
+
+recall_metric = Recall()
+recall_result = recall_metric.update(predictions, targets).compute()
+
+recall_result.plot()
+```
+
+- All Metrics now support Oriented Bounding Boxes (OBB) ([#1593](https://github.com/roboflow/supervision/pull/1593))
+
+```python
+import supervision as sv
+from supervision.metrics import F1_Score
+
+predictions = sv.Detections(...)
+targets = sv.Detections(...)
+
+f1_metric = MeanAverageRecall(metric_target=sv.MetricTarget.ORIENTED_BOUNDING_BOXES)
+f1_result = f1_metric.update(predictions, targets).compute()
+```
+
+- Introducing Smart Labels! When `smart_position` is set for [`LabelAnnotator`](https://supervision.roboflow.com/0.25.0/detection/annotators/#supervision.annotators.core.LabelAnnotator), [`RichLabelAnnotator`](https://supervision.roboflow.com/0.25.0/detection/annotators/#supervision.annotators.core.RichLabelAnnotator) or [`VertexLabelAnnotator`](https://supervision.roboflow.com/0.25.0/detection/annotators/#supervision.annotators.core.RichLabelAnnotator), the labels will move around to avoid overlapping others. ([#1625](https://github.com/roboflow/supervision/pull/1625))
+
+```python
+import supervision as sv
+from ultralytics import YOLO
+
+image = cv2.imread("image.jpg")
+
+label_annotator = sv.LabelAnnotator(smart_position=True)
+
+model = YOLO("yolo11m.pt")
+results = model(image)[0]
+detections = sv.Detections.from_ultralytics(results)
+
+annotated_frame = label_annotator.annotate(first_frame.copy(), detections)
+sv.plot_image(annotated_frame)
+```
+
+- Added the `metadata` variable to [`Detections`](https://supervision.roboflow.com/0.25.0/detection/core/#supervision.detection.core.Detections). It allows you to store custom data per-image, rather than per-detected-object as was possible with `data` variable. For example, `metadata` could be used to store the source video path, camera model or camera parameters. ([#1589](https://github.com/roboflow/supervision/pull/1589))
+
+```python
+import supervision as sv
+from ultralytics import YOLO
+
+model = YOLO("yolov8m")
+
+result = model("image.png")[0]
+detections = sv.Detections.from_ultralytics(result)
+
+# Items in `data` must match length of detections
+object_ids = [num for num in range(len(detections))]
+detections.data["object_number"] = object_ids
+
+# Items in `metadata` can be of any length.
+detections.metadata["camera_model"] = "Luxonis OAK-D"
+```
+
+- Added a `py.typed` type hints metafile. It should provide a stronger signal to type annotators and IDEs that type support is available. ([#1586](https://github.com/roboflow/supervision/pull/1586))
+
+- `ByteTrack` no longer requires `detections` to have a `class_id` ([#1637](https://github.com/roboflow/supervision/pull/1637))
+- `draw_line`, `draw_rectangle`, `draw_filled_rectangle`, `draw_polygon`, `draw_filled_polygon` and `PolygonZoneAnnotator` now comes with a default color ([#1591](https://github.com/roboflow/supervision/pull/1591))
+- Dataset classes are treated as case-sensitive when merging multiple datasets. ([#1643](https://github.com/roboflow/supervision/pull/1643))
+- Expanded [metrics documentation](https://supervision.roboflow.com/0.25.0/metrics/f1_score/) with example plots and printed results ([#1660](https://github.com/roboflow/supervision/pull/1660))
+- Added usage example for polygon zone ([#1608](https://github.com/roboflow/supervision/pull/1608))
+- Small improvements to error handling in polygons: ([#1602](https://github.com/roboflow/supervision/pull/1602))
+
+- Updated [`ByteTrack`](https://supervision.roboflow.com/0.25.0/trackers/#supervision.tracker.byte_tracker.core.ByteTrack), removing shared variables. Previously, multiple instances of `ByteTrack` would share some date, requiring liberal use of `tracker.reset()`. ([#1603](https://github.com/roboflow/supervision/pull/1603)), ([#1528](https://github.com/roboflow/supervision/pull/1528))
+- Fixed a bug where `class_agnostic` setting in `MeanAveragePrecision` would not work. ([#1577](https://github.com/roboflow/supervision/pull/1577)) hacktoberfest
+- Removed welcome workflow from our CI system. ([#1596](https://github.com/roboflow/supervision/pull/1596))
+
+- Large refactor of `ByteTrack`: STrack moved to separate class, removed superfluous `BaseTrack` class, removed unused variables ([#1603](https://github.com/roboflow/supervision/pull/1603))
+- Large refactor of `RichLabelAnnotator`, matching its contents with `LabelAnnotator`. ([#1625](https://github.com/roboflow/supervision/pull/1625))
+
+### 0.24.0 Oct 4, 2024
+
+- 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("")
+
+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("")
+
+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("")
+
+reader = easyocr.Reader(["en"])
+result = reader.readtext("", 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("")
+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 Aug 28, 2024
+
+- 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 =
+icon_dog =
+icon_cat =
+
+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 =
+
+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()
+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.
+
+- Fixed [#1448](https://github.com/roboflow/supervision/pull/1448): Various annotator type issues have been resolved, supporting expanded error handling.
+
+- Fixed [#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=,
+ start=60,
+ iterative_seek=True
+):
+ ...
+```
+
+- Fixed [#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 Jul 12, 2024
+
+- 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().project()
+dataset = 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()
+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()
+
+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=)
+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 Jun 5, 2024
+
+- 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/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
+
+paligemma_result = " cat"
+detections = sv.Detections.from_lmm(
+ sv.LMM.PALIGEMMA,
+ paligemma_result,
+ resolution_wh=(1000, 1000),
+ classes=["cat", "dog"],
+)
+detections.xyxy
+# array([[250., 250., 750., 750.]])
+
+detections.class_id
+# array([0])
+```
+
+- 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
+
+image = ...
+key_points = sv.KeyPoints(...)
+
+edge_annotator = sv.EdgeAnnotator(
+ color=sv.Color.GREEN,
+ thickness=5
+)
+annotated_frame = edge_annotator.annotate(
+ scene=image.copy(),
+ key_points=key_points
+)
+```
+
+- 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/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/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/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/0.21.0/detection/tools/inference_slicer/) adding instance segmentation model support.
+
+```python
+import cv2
+import numpy as np
+import supervision as sv
+from inference import get_model
+
+model = get_model(model_id="yolov8x-seg-640")
+image = cv2.imread()
+
+def callback(image_slice: np.ndarray) -> sv.Detections:
+ results = model.infer(image_slice)[0]
+ return sv.Detections.from_inference(results)
+
+slicer = sv.InferenceSlicer(callback = callback)
+detections = slicer(image)
+
+mask_annotator = sv.MaskAnnotator()
+label_annotator = sv.LabelAnnotator()
+
+annotated_image = mask_annotator.annotate(
+ scene=image, detections=detections)
+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/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/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 April 24, 2024
+
+- Added [#1128](https://github.com/roboflow/supervision/pull/1128): [`sv.KeyPoints`](https://supervision.roboflow.com/0.20.0/keypoint/core/#supervision.keypoint.core.KeyPoints) to provide initial support for pose estimation and broader keypoint detection models.
+
+- Added [#1128](https://github.com/roboflow/supervision/pull/1128): [`sv.EdgeAnnotator`](https://supervision.roboflow.com/0.20.0/keypoint/annotators/#supervision.keypoint.annotators.EdgeAnnotator) and [`sv.VertexAnnotator`](https://supervision.roboflow.com/0.20.0/keypoint/annotators/#supervision.keypoint.annotators.VertexAnnotator) to enable rendering of results from keypoint detection models.
+
+```python
+import cv2
+import supervision as sv
+from ultralytics import YOLO
+
+image = cv2.imread()
+model = YOLO('yolov8l-pose')
+
+result = model(image, verbose=False)[0]
+keypoints = sv.KeyPoints.from_ultralytics(result)
+
+edge_annotators = sv.EdgeAnnotator(color=sv.Color.GREEN, thickness=5)
+annotated_image = edge_annotators.annotate(image.copy(), keypoints)
+```
+
+- Changed [#1037](https://github.com/roboflow/supervision/pull/1037): [`sv.LabelAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.LabelAnnotator) by adding an additional `corner_radius` argument that allows for rounding the corners of the bounding box.
+
+- Changed [#1109](https://github.com/roboflow/supervision/pull/1109): [`sv.PolygonZone`](https://supervision.roboflow.com/0.20.0/detection/tools/polygon_zone/#supervision.detection.tools.polygon_zone.PolygonZone) such that the `frame_resolution_wh` argument is no longer required to initialize `sv.PolygonZone`.
+
+!!! failure "Deprecated"
+
+ The `frame_resolution_wh` parameter in `sv.PolygonZone` is deprecated and will be removed in `supervision-0.24.0`.
+
+- Changed [#1084](https://github.com/roboflow/supervision/pull/1084): [`sv.get_polygon_center`](https://supervision.roboflow.com/0.20.0/utils/geometry/#supervision.geometry.core.utils.get_polygon_center) to calculate a more accurate polygon centroid.
+
+- Changed [#1069](https://github.com/roboflow/supervision/pull/1069): [`sv.Detections.from_transformers`](https://supervision.roboflow.com/0.20.0/detection/core/#supervision.detection.core.Detections.from_transformers) by adding support for Transformers segmentation models and extract class names values.
+
+```python
+import torch
+import supervision as sv
+from PIL import Image
+from transformers import DetrImageProcessor, DetrForSegmentation
+
+processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50-panoptic")
+model = DetrForSegmentation.from_pretrained("facebook/detr-resnet-50-panoptic")
+
+image = Image.open()
+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_segmentation(
+ outputs=outputs, target_sizes=target_size)[0]
+detections = sv.Detections.from_transformers(results, id2label=model.config.id2label)
+
+mask_annotator = sv.MaskAnnotator()
+label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER)
+
+annotated_image = mask_annotator.annotate(
+ scene=image, detections=detections)
+annotated_image = label_annotator.annotate(
+ scene=annotated_image, detections=detections)
+```
+
+- Fixed [#787](https://github.com/roboflow/supervision/pull/787): [`sv.ByteTrack.update_with_detections`](https://supervision.roboflow.com/0.20.0/trackers/#supervision.tracker.byte_tracker.core.ByteTrack.update_with_detections) which was removing segmentation masks while tracking. Now, `ByteTrack` can be used alongside segmentation models.
+
+### 0.19.0 March 15, 2024
+
+- Added [#818](https://github.com/roboflow/supervision/pull/818): [`sv.CSVSink`](https://supervision.roboflow.com/0.19.0/detection/tools/save_detections/#supervision.detection.tools.csv_sink.CSVSink) allowing for the straightforward saving of image, video, or stream inference results in a `.csv` file.
+
+```python
+import supervision as sv
+from ultralytics import YOLO
+
+model = YOLO()
+csv_sink = sv.CSVSink()
+frames_generator = sv.get_video_frames_generator()
+
+with csv_sink:
+ for frame in frames_generator:
+ result = model(frame)[0]
+ detections = sv.Detections.from_ultralytics(result)
+ csv_sink.append(detections, custom_data={:})
+```
+
+- Added [#819](https://github.com/roboflow/supervision/pull/819): [`sv.JSONSink`](https://supervision.roboflow.com/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
+import supervision as sv
+from ultralytics import YOLO
+
+model = YOLO()
+json_sink = sv.JSONSink()
+frames_generator = sv.get_video_frames_generator()
+
+with json_sink:
+ for frame in frames_generator:
+ result = model(frame)[0]
+ detections = sv.Detections.from_ultralytics(result)
+ json_sink.append(detections, custom_data={:})
+```
+
+- Added [#847](https://github.com/roboflow/supervision/pull/847): [`sv.mask_iou_batch`](https://supervision.roboflow.com/0.19.0/detection/utils/#supervision.detection.utils.mask_iou_batch) allowing to compute Intersection over Union (IoU) of two sets of masks.
+
+- Added [#847](https://github.com/roboflow/supervision/pull/847): [`sv.mask_non_max_suppression`](https://supervision.roboflow.com/0.19.0/detection/utils/#supervision.detection.utils.mask_non_max_suppression) allowing to perform Non-Maximum Suppression (NMS) on segmentation predictions.
+
+- Added [#888](https://github.com/roboflow/supervision/pull/888): [`sv.CropAnnotator`](https://supervision.roboflow.com/0.19.0/annotators/#supervision.annotators.core.CropAnnotator) allowing users to annotate the scene with scaled-up crops of detections.
+
+```python
+import cv2
+import supervision as sv
+from inference import get_model
+
+image = cv2.imread()
+model = get_model(model_id="yolov8n-640")
+
+result = model.infer(image)[0]
+detections = sv.Detections.from_inference(result)
+
+crop_annotator = sv.CropAnnotator()
+annotated_frame = crop_annotator.annotate(
+ scene=image.copy(),
+ detections=detections
+)
+```
+
+- Changed [#827](https://github.com/roboflow/supervision/pull/827): [`sv.ByteTrack.reset`](https://supervision.roboflow.com/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`](https://supervision.roboflow.com/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`](https://supervision.roboflow.com/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.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`](https://supervision.roboflow.com/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.
+
+!!! failure "Deprecated"
+
+ The `triggering_position ` parameter in `sv.PolygonZone` is deprecated and will be removed in `supervision-0.23.0`. Use `triggering_anchors` instead.
+
+- Changed [#875](https://github.com/roboflow/supervision/pull/875): annotators adding support for Pillow images. All supervision Annotators can now accept an image as either a numpy array or a Pillow Image. They automatically detect its type, draw annotations, and return the output in the same format as the input.
+
+- Fixed [#944](https://github.com/roboflow/supervision/pull/944): [`sv.DetectionsSmoother`](https://supervision.roboflow.com/0.19.0/detection/tools/smoother/#supervision.detection.tools.smoother.DetectionsSmoother) removing `tracking_id` from `sv.Detections`.
+
+### 0.18.0 January 25, 2024
+
+- Added [#720](https://github.com/roboflow/supervision/pull/720): [`sv.PercentageBarAnnotator`](https://supervision.roboflow.com/0.18.0/annotators/#percentagebarannotator) allowing to annotate images and videos with percentage values representing confidence or other custom property.
+
+```python
+>>> import supervision as sv
+
+>>> image = ...
+>>> detections = sv.Detections(...)
+
+>>> percentage_bar_annotator = sv.PercentageBarAnnotator()
+>>> annotated_frame = percentage_bar_annotator.annotate(
+... scene=image.copy(),
+... detections=detections
+... )
+```
+
+- Added [#702](https://github.com/roboflow/supervision/pull/702): [`sv.RoundBoxAnnotator`](https://supervision.roboflow.com/0.18.0/annotators/#roundboxannotator) allowing to annotate images and videos with rounded corners bounding boxes.
+
+- Added [#770](https://github.com/roboflow/supervision/pull/770): [`sv.OrientedBoxAnnotator`](https://supervision.roboflow.com/0.18.0/annotators/#orientedboxannotator) allowing to annotate images and videos with OBB (Oriented Bounding Boxes).
+
+```python
+import cv2
+import supervision as sv
+from ultralytics import YOLO
+
+image = cv2.imread()
+model = YOLO("yolov8n-obb.pt")
+
+result = model(image)[0]
+detections = sv.Detections.from_ultralytics(result)
+
+oriented_box_annotator = sv.OrientedBoxAnnotator()
+annotated_frame = oriented_box_annotator.annotate(
+ scene=image.copy(),
+ detections=detections
+)
+```
+
+- Added [#696](https://github.com/roboflow/supervision/pull/696): [`sv.DetectionsSmoother`](https://supervision.roboflow.com/0.18.0/detection/tools/smoother/#detection-smoother) allowing for smoothing detections over multiple frames in video tracking.
+
+- Added [#769](https://github.com/roboflow/supervision/pull/769): [`sv.ColorPalette.from_matplotlib`](https://supervision.roboflow.com/0.18.0/draw/color/#supervision.draw.color.ColorPalette.from_matplotlib) allowing users to create a `sv.ColorPalette` instance from a Matplotlib color palette.
+
+```python
+>>> import supervision as sv
+
+>>> sv.ColorPalette.from_matplotlib('viridis', 5)
+ColorPalette(colors=[Color(r=68, g=1, b=84), Color(r=59, g=82, b=139), ...])
+```
+
+- Changed [#770](https://github.com/roboflow/supervision/pull/770): [`sv.Detections.from_ultralytics`](https://supervision.roboflow.com/0.18.0/detection/core/#supervision.detection.core.Detections.from_ultralytics) adding support for OBB (Oriented Bounding Boxes).
+
+- Changed [#735](https://github.com/roboflow/supervision/pull/735): [`sv.LineZone`](https://supervision.roboflow.com/0.18.0/detection/tools/line_zone/#linezone) to now accept a list of specific box anchors that must cross the line for a detection to be counted. This update marks a significant improvement from the previous requirement, where all four box corners were necessary. Users can now specify a single anchor, such as `sv.Position.BOTTOM_CENTER`, or any other combination of anchors defined as `List[sv.Position]`.
+
+- Changed [#756](https://github.com/roboflow/supervision/pull/756): [`sv.Color`](https://supervision.roboflow.com/0.18.0/draw/color/#color)'s and [`sv.ColorPalette`](https://supervision.roboflow.com/0.18.0/draw/color/#colorpalette)'s method of accessing predefined colors, transitioning from a function-based approach (`sv.Color.red()`) to a more intuitive and conventional property-based method (`sv.Color.RED`).
+
+!!! failure "Deprecated"
+
+ `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`](https://supervision.roboflow.com/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`](https://supervision.roboflow.com/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).
+
+!!! failure "Deprecated"
+
+ `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`](https://supervision.roboflow.com/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 December 06, 2023
+
+- Added [#633](https://github.com/roboflow/supervision/pull/633): [`sv.PixelateAnnotator`](https://supervision.roboflow.com/0.17.0/annotators/#supervision.annotators.core.PixelateAnnotator) allowing to pixelate objects on images and videos.
+
+- Added [#652](https://github.com/roboflow/supervision/pull/652): [`sv.TriangleAnnotator`](https://supervision.roboflow.com/0.17.0/annotators/#supervision.annotators.core.TriangleAnnotator) allowing to annotate images and videos with triangle markers.
+
+- Added [#602](https://github.com/roboflow/supervision/pull/602): [`sv.PolygonAnnotator`](https://supervision.roboflow.com/0.17.0/annotators/#supervision.annotators.core.PolygonAnnotator) allowing to annotate images and videos with segmentation mask outline.
+
+```python
+>>> import supervision as sv
+
+>>> image = ...
+>>> detections = sv.Detections(...)
+
+>>> polygon_annotator = sv.PolygonAnnotator()
+>>> annotated_frame = polygon_annotator.annotate(
+... scene=image.copy(),
+... detections=detections
+... )
+```
+
+- Added [#476](https://github.com/roboflow/supervision/pull/476): [`sv.assets`](https://supervision.roboflow.com/0.18.0/assets/) allowing download of video files that you can use in your demos.
+
+```python
+>>> from supervision.assets import download_assets, VideoAssets
+>>> download_assets(VideoAssets.VEHICLES)
+"vehicles.mp4"
+```
+
+- Added [#605](https://github.com/roboflow/supervision/pull/605): [`Position.CENTER_OF_MASS`](https://supervision.roboflow.com/0.17.0/geometry/core/#position) allowing to place labels in center of mass of segmentation masks.
+
+- Added [#651](https://github.com/roboflow/supervision/pull/651): [`sv.scale_boxes`](https://supervision.roboflow.com/0.17.0/detection/utils/#supervision.detection.utils.scale_boxes) allowing to scale [`sv.Detections.xyxy`](https://supervision.roboflow.com/0.17.0/detection/core/#supervision.detection.core.Detections) values.
+
+- Added [#637](https://github.com/roboflow/supervision/pull/637): [`sv.calculate_dynamic_text_scale`](https://supervision.roboflow.com/0.17.0/draw/utils/#supervision.draw.utils.calculate_dynamic_text_scale) and [`sv.calculate_dynamic_line_thickness`](https://supervision.roboflow.com/0.17.0/draw/utils/#supervision.draw.utils.calculate_dynamic_line_thickness) allowing text scale and line thickness to match image resolution.
+
+- Added [#620](https://github.com/roboflow/supervision/pull/620): [`sv.Color.as_hex`](https://supervision.roboflow.com/0.17.0/draw/color/#supervision.draw.color.Color.as_hex) allowing to extract color value in HEX format.
+
+- Added [#572](https://github.com/roboflow/supervision/pull/572): [`sv.Classifications.from_timm`](https://supervision.roboflow.com/0.17.0/classification/core/#supervision.classification.core.Classifications.from_timm) allowing to load classification result from [timm](https://huggingface.co/docs/hub/timm) models.
+
+- Added [#478](https://github.com/roboflow/supervision/pull/478): [`sv.Classifications.from_clip`](https://supervision.roboflow.com/0.17.0/classification/core/#supervision.classification.core.Classifications.from_clip) allowing to load classification result from [clip](https://github.com/openai/clip) model.
+
+- Added [#571](https://github.com/roboflow/supervision/pull/571): [`sv.Detections.from_azure_analyze_image`](https://supervision.roboflow.com/0.17.0/detection/core/#supervision.detection.core.Detections.from_azure_analyze_image) allowing to load detection results from [Azure Image Analysis](https://learn.microsoft.com/en-us/azure/ai-services/computer-vision/concept-object-detection-40).
+
+- Changed [#646](https://github.com/roboflow/supervision/pull/646): `sv.BoxMaskAnnotator` renaming it to [`sv.ColorAnnotator`](https://supervision.roboflow.com/0.17.0/annotators/#supervision.annotators.core.ColorAnnotator).
+
+- Changed [#606](https://github.com/roboflow/supervision/pull/606): [`sv.MaskAnnotator`](https://supervision.roboflow.com/0.17.0/annotators/#supervision.annotators.core.MaskAnnotator) to make it **5x faster**.
+
+- Fixed [#584](https://github.com/roboflow/supervision/pull/584): [`sv.DetectionDataset.from_yolo`](https://supervision.roboflow.com/0.17.0/datasets/#supervision.dataset.core.DetectionDataset.from_yolo) to ignore empty lines in annotation files.
+
+- Fixed [#555](https://github.com/roboflow/supervision/pull/555): [`sv.BlurAnnotator`](https://supervision.roboflow.com/0.17.0/annotators/#supervision.annotators.core.BlurAnnotator) to trim negative coordinates before bluring detections.
+
+- Fixed [#511](https://github.com/roboflow/supervision/pull/511): [`sv.TraceAnnotator`](https://supervision.roboflow.com/0.17.0/annotators/#supervision.annotators.core.TraceAnnotator) to respect trace position.
+
+### 0.16.0 October 19, 2023
+
+- Added [#422](https://github.com/roboflow/supervision/pull/422): [`sv.BoxMaskAnnotator`](https://supervision.roboflow.com/0.16.0/annotators/#supervision.annotators.core.BoxMaskAnnotator) allowing to annotate images and videos with mox masks.
+
+- Added [#433](https://github.com/roboflow/supervision/pull/433): [`sv.HaloAnnotator`](https://supervision.roboflow.com/0.16.0/annotators/#supervision.annotators.core.HaloAnnotator) allowing to annotate images and videos with halo effect.
+
+```python
+>>> import supervision as sv
+
+>>> image = ...
+>>> detections = sv.Detections(...)
+
+>>> halo_annotator = sv.HaloAnnotator()
+>>> annotated_frame = halo_annotator.annotate(
+... scene=image.copy(),
+... detections=detections
+... )
+```
+
+- Added [#466](https://github.com/roboflow/supervision/pull/466): [`sv.HeatMapAnnotator`](https://supervision.roboflow.com/0.16.0/annotators/#supervision.annotators.core.HeatMapAnnotator) allowing to annotate videos with heat maps.
+
+- Added [#492](https://github.com/roboflow/supervision/pull/492): [`sv.DotAnnotator`](https://supervision.roboflow.com/0.16.0/annotators/#supervision.annotators.core.DotAnnotator) allowing to annotate images and videos with dots.
+
+- Added [#449](https://github.com/roboflow/supervision/pull/449): [`sv.draw_image`](https://supervision.roboflow.com/0.16.0/draw/utils/#supervision.draw.utils.draw_image) allowing to draw an image onto a given scene with specified opacity and dimensions.
+
+- Added [#280](https://github.com/roboflow/supervision/pull/280): [`sv.FPSMonitor`](https://supervision.roboflow.com/0.16.0/utils/video/#supervision.utils.video.FPSMonitor) for monitoring frames per second (FPS) to benchmark latency.
+
+- Added [#454](https://github.com/roboflow/supervision/pull/454): ๐ค Hugging Face Annotators [space](https://huggingface.co/spaces/Roboflow/Annotators).
+
+- Changed [#482](https://github.com/roboflow/supervision/pull/482): [`sv.LineZone.trigger`](https://supervision.roboflow.com/0.16.0/detection/tools/line_zone/#supervision.detection.line_counter.LineZone.trigger) now return `Tuple[np.ndarray, np.ndarray]`. The first array indicates which detections have crossed the line from outside to inside. The second array indicates which detections have crossed the line from inside to outside.
+
+- Changed [#465](https://github.com/roboflow/supervision/pull/465): Annotator argument name from `color_map: str` to `color_lookup: ColorLookup` enum to increase type safety.
+
+- Changed [#426](https://github.com/roboflow/supervision/pull/426): [`sv.MaskAnnotator`](https://supervision.roboflow.com/0.16.0/annotators/#supervision.annotators.core.MaskAnnotator) allowing 2x faster annotation.
+
+- 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`](https://supervision.roboflow.com/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`](https://supervision.roboflow.com/0.16.0/detection/core/#supervision.detection.core.Detections.from_ultralytics) and [`sv.Classifications.from_ultralytics`](https://supervision.roboflow.com/0.16.0/classification/core/#supervision.classification.core.Classifications.from_ultralytics).
+
+### 0.15.0 October 5, 2023
+
+- Added [#170](https://github.com/roboflow/supervision/pull/170): [`sv.BoundingBoxAnnotator`](https://supervision.roboflow.com/0.15.0/annotators/#supervision.annotators.core.BoundingBoxAnnotator) allowing to annotate images and videos with bounding boxes.
+
+- Added [#170](https://github.com/roboflow/supervision/pull/170): [`sv.BoxCornerAnnotator `](https://supervision.roboflow.com/0.15.0/annotators/#supervision.annotators.core.BoxCornerAnnotator) allowing to annotate images and videos with just bounding box corners.
+
+- Added [#170](https://github.com/roboflow/supervision/pull/170): [`sv.MaskAnnotator`](https://supervision.roboflow.com/0.15.0/annotators/#supervision.annotators.core.MaskAnnotator) allowing to annotate images and videos with segmentation masks.
+
+- Added [#170](https://github.com/roboflow/supervision/pull/170): [`sv.EllipseAnnotator`](https://supervision.roboflow.com/0.15.0/annotators/#supervision.annotators.core.EllipseAnnotator) allowing to annotate images and videos with ellipses (sports game style).
+
+- Added [#386](https://github.com/roboflow/supervision/pull/386): [`sv.CircleAnnotator`](https://supervision.roboflow.com/0.15.0/annotators/#supervision.annotators.core.CircleAnnotator) allowing to annotate images and videos with circles.
+
+- Added [#354](https://github.com/roboflow/supervision/pull/354): [`sv.TraceAnnotator`](https://supervision.roboflow.com/0.15.0/annotators/#supervision.annotators.core.TraceAnnotator) allowing to draw path of moving objects on videos.
+
+- Added [#405](https://github.com/roboflow/supervision/pull/405): [`sv.BlurAnnotator`](https://supervision.roboflow.com/0.15.0/annotators/#supervision.annotators.core.BlurAnnotator) allowing to blur objects on images and videos.
+
+```python
+>>> import supervision as sv
+
+>>> image = ...
+>>> detections = sv.Detections(...)
+
+>>> bounding_box_annotator = sv.BoundingBoxAnnotator()
+>>> annotated_frame = bounding_box_annotator.annotate(
+... scene=image.copy(),
+... detections=detections
+... )
+```
+
+- Added [#354](https://github.com/roboflow/supervision/pull/354): Supervision usage [example](https://github.com/roboflow/supervision/tree/develop/examples/traffic_analysis). You can now learn how to perform traffic flow analysis with Supervision.
+
+- Changed [#399](https://github.com/roboflow/supervision/pull/399): [`sv.Detections.from_roboflow`](https://supervision.roboflow.com/0.15.0/detection/core/#supervision.detection.core.Detections.from_roboflow) now does not require `class_list` to be specified. The `class_id` value can be extracted directly from the [inference](https://github.com/roboflow/inference) response.
+
+- Changed [#381](https://github.com/roboflow/supervision/pull/381): [`sv.VideoSink`](https://supervision.roboflow.com/0.15.0/utils/video/#videosink) now allows to customize the output codec.
+
+- Changed [#361](https://github.com/roboflow/supervision/pull/361): [`sv.InferenceSlicer`](https://supervision.roboflow.com/0.15.0/detection/tools/inference_slicer/#supervision.detection.tools.inference_slicer.InferenceSlicer) can now operate in multithreading mode.
+
+- Fixed [#348](https://github.com/roboflow/supervision/pull/348): [`sv.Detections.from_deepsparse`](https://supervision.roboflow.com/0.15.0/detection/core/#supervision.detection.core.Detections.from_deepsparse) to allow processing empty [deepsparse](https://github.com/neuralmagic/deepsparse) result object.
+
+### 0.14.0 August 31, 2023
+
+- Added [#282](https://github.com/roboflow/supervision/pull/282): support for SAHI inference technique with [`sv.InferenceSlicer`](https://supervision.roboflow.com/0.14.0/detection/tools/inference_slicer).
+
+```python
+>>> import cv2
+>>> import supervision as sv
+>>> from ultralytics import YOLO
+
+>>> image = cv2.imread(SOURCE_IMAGE_PATH)
+>>> model = YOLO(...)
+
+>>> def callback(image_slice: np.ndarray) -> sv.Detections:
+... result = model(image_slice)[0]
+... return sv.Detections.from_ultralytics(result)
+
+>>> slicer = sv.InferenceSlicer(callback = callback)
+
+>>> detections = slicer(image)
+```
+
+- Added [#297](https://github.com/roboflow/supervision/pull/297): [`Detections.from_deepsparse`](https://supervision.roboflow.com/0.14.0/detection/core/#supervision.detection.core.Detections.from_deepsparse) to enable seamless integration with [DeepSparse](https://github.com/neuralmagic/deepsparse) framework.
+
+- Added [#281](https://github.com/roboflow/supervision/pull/281): [`sv.Classifications.from_ultralytics`](https://supervision.roboflow.com/0.14.0/classification/core/#supervision.classification.core.Classifications.from_ultralytics) to enable seamless integration with [Ultralytics](https://github.com/ultralytics/ultralytics) framework. This will enable you to use supervision with all [models](https://docs.ultralytics.com/models/) that Ultralytics supports.
+
+!!! failure "Deprecated"
+
+ [sv.Detections.from_yolov8](https://supervision.roboflow.com/0.14.0/detection/core/#supervision.detection.core.Detections.from_yolov8) and [sv.Classifications.from_yolov8](https://supervision.roboflow.com/0.14.0/classification/core/#supervision.classification.core.Classifications.from_yolov8) are now deprecated and will be removed with `supervision-0.16.0` release.
+
+- Added [#341](https://github.com/roboflow/supervision/pull/341): First supervision usage example script showing how to detect and track objects on video using YOLOv8 + Supervision.
+
+- Changed [#296](https://github.com/roboflow/supervision/pull/296): [`sv.ClassificationDataset`](https://supervision.roboflow.com/0.14.0/dataset/core/#supervision.dataset.core.ClassificationDataset) and [`sv.DetectionDataset`](https://supervision.roboflow.com/0.14.0/dataset/core/#supervision.dataset.core.DetectionDataset) now use image path (not image name) as dataset keys.
+
+- Fixed [#300](https://github.com/roboflow/supervision/pull/300): [`Detections.from_roboflow`](https://supervision.roboflow.com/0.14.0/detection/core/#supervision.detection.core.Detections.from_roboflow) to filter out polygons with less than 3 points.
+
+### 0.13.0 August 8, 2023
+
+- Added [#236](https://github.com/roboflow/supervision/pull/236): support for mean average precision (mAP) for object detection models with [`sv.MeanAveragePrecision`](https://supervision.roboflow.com/0.13.0/metrics/detection/#meanaverageprecision).
+
+```python
+>>> import supervision as sv
+>>> from ultralytics import YOLO
+
+>>> dataset = sv.DetectionDataset.from_yolo(...)
+
+>>> model = YOLO(...)
+>>> def callback(image: np.ndarray) -> sv.Detections:
+... result = model(image)[0]
+... return sv.Detections.from_yolov8(result)
+
+>>> mean_average_precision = sv.MeanAveragePrecision.benchmark(
+... dataset = dataset,
+... callback = callback
+... )
+
+>>> mean_average_precision.map50_95
+0.433
+```
+
+- Added [#256](https://github.com/roboflow/supervision/pull/256): support for ByteTrack for object tracking with [`sv.ByteTrack`](https://supervision.roboflow.com/0.13.0/tracker/core/#bytetrack).
+
+- Added [#222](https://github.com/roboflow/supervision/pull/222): [`sv.Detections.from_ultralytics`](https://supervision.roboflow.com/0.13.0/detection/core/#supervision.detection.core.Detections.from_ultralytics) to enable seamless integration with [Ultralytics](https://github.com/ultralytics/ultralytics) framework. This will enable you to use `supervision` with all [models](https://docs.ultralytics.com/models/) that Ultralytics supports.
+
+!!! failure "Deprecated"
+
+ [`sv.Detections.from_yolov8`](https://supervision.roboflow.com/0.13.0/detection/core/#supervision.detection.core.Detections.from_yolov8) is now deprecated and will be removed with `supervision-0.15.0` release.
+
+- Added [#191](https://github.com/roboflow/supervision/pull/191): [`sv.Detections.from_paddledet`](https://supervision.roboflow.com/0.13.0/detection/core/#supervision.detection.core.Detections.from_paddledet) to enable seamless integration with [PaddleDetection](https://github.com/PaddlePaddle/PaddleDetection) framework.
+
+- Added [#245](https://github.com/roboflow/supervision/pull/245): support for loading PASCAL VOC segmentation datasets with [`sv.DetectionDataset.`](https://supervision.roboflow.com/0.13.0/dataset/core/#supervision.dataset.core.DetectionDataset.from_pascal_voc).
+
+### 0.12.0 July 24, 2023
+
+!!! failure "Python 3.7. Support Terminated"
+
+ With the `supervision-0.12.0` release, we are terminating official support for Python 3.7.
+
+- Added [#177](https://github.com/roboflow/supervision/pull/177): initial support for object detection model benchmarking with [`sv.ConfusionMatrix`](https://supervision.roboflow.com/0.12.0/metrics/detection/#confusionmatrix).
+
+```python
+>>> import supervision as sv
+>>> from ultralytics import YOLO
+
+>>> dataset = sv.DetectionDataset.from_yolo(...)
+
+>>> model = YOLO(...)
+>>> def callback(image: np.ndarray) -> sv.Detections:
+... result = model(image)[0]
+... return sv.Detections.from_yolov8(result)
+
+>>> confusion_matrix = sv.ConfusionMatrix.benchmark(
+... dataset = dataset,
+... callback = callback
+... )
+
+>>> confusion_matrix.matrix
+array([
+ [0., 0., 0., 0.],
+ [0., 1., 0., 1.],
+ [0., 1., 1., 0.],
+ [1., 1., 0., 0.]
+])
+```
+
+- Added [#173](https://github.com/roboflow/supervision/pull/173): [`Detections.from_mmdetection`](https://supervision.roboflow.com/0.12.0/detection/core/#supervision.detection.core.Detections.from_mmdetection) to enable seamless integration with [MMDetection](https://github.com/open-mmlab/mmdetection) framework.
+
+- Added [#130](https://github.com/roboflow/supervision/issues/130): ability to [install](https://supervision.roboflow.com/) package in `headless` or `desktop` mode.
+
+- Changed [#180](https://github.com/roboflow/supervision/pull/180): packing method from `setup.py` to `pyproject.toml`.
+
+- Fixed [#188](https://github.com/roboflow/supervision/issues/188): [`sv.DetectionDataset.from_cooc`](https://supervision.roboflow.com/0.12.0/dataset/core/#supervision.dataset.core.DetectionDataset.from_coco) can't be loaded when there are images without annotations.
+
+- Fixed [#226](https://github.com/roboflow/supervision/issues/226): [`sv.DetectionDataset.from_yolo`](https://supervision.roboflow.com/0.12.0/dataset/core/#supervision.dataset.core.DetectionDataset.from_yolo) can't load background instances.
+
+### 0.11.1 June 29, 2023
+
+- Fixed [#165](https://github.com/roboflow/supervision/pull/165): [`as_folder_structure`](https://supervision.roboflow.com/0.11.1/dataset/core/#supervision.dataset.core.ClassificationDataset.as_folder_structure) fails to save [`sv.ClassificationDataset`](https://supervision.roboflow.com/0.11.1/dataset/core/#classificationdataset) when it is result of inference.
+
+### 0.11.0 June 28, 2023
+
+- Added [#150](https://github.com/roboflow/supervision/pull/150): ability to load and save [`sv.DetectionDataset`](https://supervision.roboflow.com/0.11.0/dataset/core/#detectiondataset) in COCO format using [`as_coco`](https://supervision.roboflow.com/0.11.0/dataset/core/#supervision.dataset.core.DetectionDataset.as_coco) and [`from_coco`](https://supervision.roboflow.com/0.11.0/dataset/core/#supervision.dataset.core.DetectionDataset.from_coco) methods.
+
+```python
+>>> import supervision as sv
+
+>>> ds = sv.DetectionDataset.from_coco(
+... images_directory_path='...',
+... annotations_path='...'
+... )
+
+>>> ds.as_coco(
+... images_directory_path='...',
+... annotations_path='...'
+... )
+```
+
+- Added [#158](https://github.com/roboflow/supervision/pull/158): ability to merge multiple [`sv.DetectionDataset`](https://supervision.roboflow.com/0.11.0/dataset/core/#detectiondataset) together using [`merge`](https://supervision.roboflow.com/0.11.0/dataset/core/#supervision.dataset.core.DetectionDataset.merge) method.
+
+```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']
+```
+
+- Added [#162](https://github.com/roboflow/supervision/pull/162): additional `start` and `end` arguments to [`sv.get_video_frames_generator`](https://supervision.roboflow.com/0.11.0/utils/video/#get_video_frames_generator) allowing to generate frames only for a selected part of the video.
+
+- Fixed [#157](https://github.com/roboflow/supervision/pull/157): incorrect loading of YOLO dataset class names from `data.yaml`.
+
+### 0.10.0 June 14, 2023
+
+- Added [#125](https://github.com/roboflow/supervision/pull/125): ability to load and save [`sv.ClassificationDataset`](https://supervision.roboflow.com/0.10.0/dataset/core/#classificationdataset) in a folder structure format.
+
+```python
+>>> import supervision as sv
+
+>>> cs = sv.ClassificationDataset.from_folder_structure(
+... root_directory_path='...'
+... )
+
+>>> cs.as_folder_structure(
+... root_directory_path='...'
+... )
+```
+
+- Added [#125](https://github.com/roboflow/supervision/pull/125): support for [`sv.ClassificationDataset.split`](https://supervision.roboflow.com/0.10.0/dataset/core/#supervision.dataset.core.ClassificationDataset.split) allowing to divide `sv.ClassificationDataset` into two parts.
+
+- Added [#110](https://github.com/roboflow/supervision/pull/110): ability to extract masks from Roboflow API results using [`sv.Detections.from_roboflow`](https://supervision.roboflow.com/0.10.0/detection/core/#supervision.detection.core.Detections.from_roboflow).
+
+- Added [commit hash](https://github.com/roboflow/supervision/commit/d000292eb2f2342544e0947b65528082e60fb8d6): Supervision Quickstart [notebook](https://colab.research.google.com/github/roboflow/supervision/blob/main/demo.ipynb) where you can learn more about Detection, Dataset and Video APIs.
+
+- Changed [#135](https://github.com/roboflow/supervision/pull/135): `sv.get_video_frames_generator` documentation to better describe actual behavior.
+
+### 0.9.0 June 7, 2023
+
+- Added [#118](https://github.com/roboflow/supervision/pull/118): ability to select [`sv.Detections`](https://supervision.roboflow.com/0.9.0/detection/core/#supervision.detection.core.Detections.__getitem__) by index, list of indexes or slice. Here is an example illustrating the new selection methods.
+
+```python
+>>> import supervision as sv
+
+>>> detections = sv.Detections(...)
+>>> len(detections[0])
+1
+>>> len(detections[[0, 1]])
+2
+>>> len(detections[0:2])
+2
+```
+
+- Added [#101](https://github.com/roboflow/supervision/pull/101): ability to extract masks from YOLOv8 result using [`sv.Detections.from_yolov8`](https://supervision.roboflow.com/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`](https://supervision.roboflow.com/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`](https://supervision.roboflow.com/0.9.0/utils/image/#imagesink). Here is an example showing how to save every tenth video frame as a separate image.
+
+```python
+>>> import supervision as sv
+
+>>> with sv.ImageSink(target_dir_path='target/directory/path') as sink:
+... for image in sv.get_video_frames_generator(source_path='source_video.mp4', stride=10):
+... sink.save_image(image=image)
+```
+
+- Fixed [#106](https://github.com/roboflow/supervision/issues/106): inconvenient handling of [`sv.PolygonZone`](https://supervision.roboflow.com/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 May 17, 2023
+
+- Added [#100](https://github.com/roboflow/supervision/pull/100): support for dataset inheritance. The current `Dataset` got renamed to `DetectionDataset`. Now [`DetectionDataset`](https://supervision.roboflow.com/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`](https://supervision.roboflow.com/0.8.0/dataset/core/#supervision.dataset.core.DetectionDataset.as_yolo).
+
+```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("yolov5")
+
+>>> ds = 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.classes
+['dog', 'person']
+```
+
+- Added [#103](https://github.com/roboflow/supervision/pull/103): support for [`DetectionDataset.split`](https://supervision.roboflow.com/0.8.0/dataset/core/#supervision.dataset.core.DetectionDataset.split) allowing to divide `DetectionDataset` into two parts.
+
+```python
+>>> import supervision as sv
+
+>>> ds = sv.DetectionDataset(...)
+>>> train_ds, test_ds = ds.split(split_ratio=0.7, random_state=42, shuffle=True)
+
+>>> len(train_ds), len(test_ds)
+(700, 300)
+```
+
+- Changed [#100](https://github.com/roboflow/supervision/pull/100): default value of `approximation_percentage` parameter from `0.75` to `0.0` in `DetectionDataset.as_yolo` and `DetectionDataset.as_pascal_voc`.
+
+### 0.7.0 May 11, 2023
+
+- Added [#91](https://github.com/roboflow/supervision/pull/91): `Detections.from_yolo_nas` to enable seamless integration with [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) model.
+- Added [#86](https://github.com/roboflow/supervision/pull/86): ability to load datasets in YOLO format using `Dataset.from_yolo`.
+- Added [#84](https://github.com/roboflow/supervision/pull/84): `Detections.merge` to merge multiple `Detections` objects together.
+- Fixed [#81](https://github.com/roboflow/supervision/pull/81): `LineZoneAnnotator.annotate` does not return annotated frame.
+- Changed [#44](https://github.com/roboflow/supervision/pull/44): `LineZoneAnnotator.annotate` to allow for custom text for the in and out tags.
+
+### 0.6.0 April 19, 2023
+
+- Added [#71](https://github.com/roboflow/supervision/pull/71): initial `Dataset` support and ability to save `Detections` in Pascal VOC XML format.
+- Added [#71](https://github.com/roboflow/supervision/pull/71): new `mask_to_polygons`, `filter_polygons_by_area`, `polygon_to_xyxy` and `approximate_polygon` utilities.
+- Added [#72](https://github.com/roboflow/supervision/pull/72): ability to load Pascal VOC XML **object detections** dataset as `Dataset`.
+- Changed [#70](https://github.com/roboflow/supervision/pull/70): order of `Detections` attributes to make it consistent with order of objects in `__iter__` tuple.
+- Changed [#71](https://github.com/roboflow/supervision/pull/71): `generate_2d_mask` to `polygon_to_mask`.
+
+### 0.5.2 April 13, 2023
+
+- Fixed [#63](https://github.com/roboflow/supervision/pull/63): `LineZone.trigger` function expects 4 values instead of 5.
+
+### 0.5.1 April 12, 2023
+
+- Fixed `Detections.__getitem__` method did not return mask for selected item.
+- Fixed `Detections.area` crashed for mask detections.
+
+### 0.5.0 April 10, 2023
+
+- Added [#58](https://github.com/roboflow/supervision/pull/58): `Detections.mask` to enable segmentation support.
+- Added [#58](https://github.com/roboflow/supervision/pull/58): `MaskAnnotator` to allow easy `Detections.mask` annotation.
+- Added [#58](https://github.com/roboflow/supervision/pull/58): `Detections.from_sam` to enable native Segment Anything Model (SAM) support.
+- Changed [#58](https://github.com/roboflow/supervision/pull/58): `Detections.area` behaviour to work not only with boxes but also with masks.
+
+### 0.4.0 April 5, 2023
+
+- Added [#46](https://github.com/roboflow/supervision/discussions/48): `Detections.empty` to allow easy creation of empty `Detections` objects.
+- Added [#56](https://github.com/roboflow/supervision/pull/56): `Detections.from_roboflow` to allow easy creation of `Detections` objects from Roboflow API inference results.
+- Added [#56](https://github.com/roboflow/supervision/pull/56): `plot_images_grid` to allow easy plotting of multiple images on single plot.
+- Added [#56](https://github.com/roboflow/supervision/pull/56): initial support for Pascal VOC XML format with `detections_to_voc_xml` method.
+- Changed [#56](https://github.com/roboflow/supervision/pull/56): `show_frame_in_notebook` refactored and renamed to `plot_image`.
+
+### 0.3.2 March 23, 2023
+
+- Changed [#50](https://github.com/roboflow/supervision/issues/50): Allow `Detections.class_id` to be `None`.
+
+### 0.3.1 March 6, 2023
+
+- Fixed [#41](https://github.com/roboflow/supervision/issues/41): `PolygonZone` throws an exception when the object touches the bottom edge of the image.
+- Fixed [#42](https://github.com/roboflow/supervision/issues/42): `Detections.wth_nms` method throws an exception when `Detections` is empty.
+- Changed [#36](https://github.com/roboflow/supervision/pull/36): `Detections.wth_nms` support class agnostic and non-class agnostic case.
+
+### 0.3.0 March 6, 2023
+
+- Changed: Allow `Detections.confidence` to be `None`.
+- Added: `Detections.from_transformers` and `Detections.from_detectron2` to enable seamless integration with Transformers and Detectron2 models.
+- Added: `Detections.area` to dynamically calculate bounding box area.
+- Added: `Detections.wth_nms` to filter out double detections with NMS. Initial - only class agnostic - implementation.
+
+### 0.2.0 February 2, 2023
+
+- Added: Advanced `Detections` filtering with pandas-like API.
+- Added: `Detections.from_yolov5` and `Detections.from_yolov8` to enable seamless integration with YOLOv5 and YOLOv8 models.
+
+### 0.1.0 January 19, 2023
+
+Say hello to Supervision ๐
diff --git a/docs/classification/core.md b/docs/classification/core.md
new file mode 100644
index 0000000..30298bc
--- /dev/null
+++ b/docs/classification/core.md
@@ -0,0 +1,7 @@
+---
+comments: true
+---
+
+# Classifications
+
+:::supervision.classification.core.Classifications
diff --git a/docs/code_of_conduct.md b/docs/code_of_conduct.md
new file mode 100644
index 0000000..3030b9b
--- /dev/null
+++ b/docs/code_of_conduct.md
@@ -0,0 +1 @@
+--8<-- ".github/CODE_OF_CONDUCT.md"
diff --git a/docs/contact.md b/docs/contact.md
new file mode 100644
index 0000000..6b6e520
--- /dev/null
+++ b/docs/contact.md
@@ -0,0 +1,29 @@
+---
+comments: true
+description: Contact and support channels for Supervision documentation, bugs, feature requests, security reports, and community help.
+---
+
+# Contact
+
+Use the channel that matches the kind of request you have.
+
+## Bugs and Feature Requests
+
+Open a GitHub issue for reproducible bugs, API requests, documentation fixes, and feature proposals:
+
+[github.com/roboflow/supervision/issues](https://github.com/roboflow/supervision/issues)
+
+## Community Support
+
+Join the Roboflow Discord for community questions, examples, and implementation discussion:
+
+[discord.gg/GbfgXGJ8Bk](https://discord.gg/GbfgXGJ8Bk)
+
+## Package and Source
+
+- PyPI package: [pypi.org/project/supervision](https://pypi.org/project/supervision/)
+- GitHub repository: [github.com/roboflow/supervision](https://github.com/roboflow/supervision)
+
+## Security Reports
+
+For security-sensitive reports, avoid posting public exploit details in an issue. Use GitHub's private vulnerability reporting flow for the repository when available, or contact Roboflow through the security and support channels listed on [roboflow.com](https://roboflow.com/).
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 0000000..0c7bc9c
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1 @@
+--8<-- ".github/CONTRIBUTING.md"
diff --git a/docs/cookbooks.md b/docs/cookbooks.md
new file mode 100644
index 0000000..5d0d13a
--- /dev/null
+++ b/docs/cookbooks.md
@@ -0,0 +1,8 @@
+---
+template: cookbooks.html
+comments: true
+description: Collection of practical computer vision cookbooks โ object tracking, zero-shot detection, SAHI small object detection, occupancy analytics, and more.
+hide:
+ - navigation
+ - toc
+---
diff --git a/docs/datasets/core.md b/docs/datasets/core.md
new file mode 100644
index 0000000..0fdcd05
--- /dev/null
+++ b/docs/datasets/core.md
@@ -0,0 +1,22 @@
+---
+comments: true
+description: API reference for supervision's DetectionDataset and ClassificationDataset โ load, merge, split, and convert datasets in YOLO, COCO, VOC, CreateML, and LabelMe formats.
+---
+
+# 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`.
+
+
+
DetectionDataset
+
+
+:::supervision.dataset.core.DetectionDataset
+
+
+
ClassificationDataset
+
+
+:::supervision.dataset.core.ClassificationDataset
diff --git a/docs/deprecated.md b/docs/deprecated.md
new file mode 100644
index 0000000..256e7dc
--- /dev/null
+++ b/docs/deprecated.md
@@ -0,0 +1,54 @@
+---
+comments: true
+status: deprecated
+---
+
+# Deprecated
+
+These features are phased out due to better alternatives or potential issues in future versions. Deprecated functionalities are typically supported for multiple subsequent releases, providing time for users to transition to updated methods.
+
+- [`sv.ByteTrack`](https://supervision.roboflow.com/latest/trackers/#supervision.tracker.byte_tracker.core.ByteTrack) is deprecated in `supervision-0.28.0` in favour of `ByteTrackTracker` from the external [`trackers`](https://pypi.org/project/trackers/) package (`pip install trackers`). The update method is renamed from `update_with_detections()` to `update()`. Removal is planned for `supervision-0.31.0`.
+- `supervision.keypoint` module is deprecated in `supervision-0.27.0`; use `supervision.key_points` instead. It will be removed in `supervision-0.31.0`.
+- `create_tiles` in `supervision.utils.image` is deprecated in `supervision-0.27.0`. It will be removed in `supervision-0.31.0`.
+- `ensure_cv2_image_for_processing` in `supervision.utils.conversion` is deprecated in `supervision-0.27.0`. It will be removed in `supervision-0.31.0`.
+- Keypoint validation utilities in `supervision.validators` are deprecated in `supervision-0.27.0`. They will be removed in `supervision-0.31.0`.
+- `normalized_xyxy` argument in [`sv.denormalize_boxes`](https://supervision.roboflow.com/latest/detection/utils/boxes/#supervision.detection.utils.boxes.denormalize_boxes) is deprecated in `supervision-0.27.0` and renamed to `xyxy`. Passing `normalized_xyxy=` emits a `FutureWarning`; support will be removed in `supervision-0.31.0`.
+- `supervision.dataset.utils` import path for [`sv.rle_to_mask`](https://supervision.roboflow.com/latest/detection/utils/converters/#supervision.detection.utils.converters.rle_to_mask) and [`sv.mask_to_rle`](https://supervision.roboflow.com/latest/detection/utils/converters/#supervision.detection.utils.converters.mask_to_rle) is deprecated in `supervision-0.28.0`. These functions moved to `supervision.detection.utils.converters` and will be removed from `supervision.dataset.utils` in `supervision-0.31.0`.
+- `sv.LMM` enum is deprecated in `supervision-0.27.0` and will be removed in `supervision-0.31.0`. Use `sv.VLM` instead.
+- [`sv.Detections.from_lmm`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_lmm) classmethod is deprecated in `supervision-0.26.0` and will be removed in `supervision-0.31.0`. Use [`sv.Detections.from_vlm`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_vlm) instead.
+- `KeyPoints.confidence` is deprecated in `supervision-0.29.0`. Use `KeyPoints.keypoint_confidence` instead. It will be removed in `supervision-0.32.0`.
+- Public `validate_*` helper functions are deprecated in `supervision-0.29.0` and will be removed in `supervision-0.32.0`. Supervision internals now use private `_validate_*` helpers.
+
+# Removed
+
+### 0.27.0
+
+- `overlap_ratio_wh` parameter in [`sv.InferenceSlicer`](https://supervision.roboflow.com/latest/detection/tools/inference_slicer/) has been removed. Use the pixel-based `overlap_wh` parameter instead.
+- `overlap_filter_strategy` parameter in [`sv.InferenceSlicer`](https://supervision.roboflow.com/latest/detection/tools/inference_slicer/) has been removed. Use `overlap_strategy` instead.
+
+### 0.26.0
+
+- The `sv.DetectionDataset.images` property has been 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. Also, constructing `sv.DetectionDataset` with parameter `images` as `Dict[str, np.ndarray]` is deprecated and has been removed in `supervision-0.26.0`. Please pass a list of paths `List[str]` instead.
+- The name `sv.BoundingBoxAnnotator` is deprecated and has been removed in `supervision-0.26.0`. It has been renamed to [`sv.BoxAnnotator`](https://supervision.roboflow.com/0.22.0/detection/annotators/#supervision.annotators.core.BoxAnnotator).
+
+### 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
+
+- `sv.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 `sv.Color.white()` was removed as of `supervision-0.22.0`. Use the constant `sv.Color.WHITE` instead.
+- The method `sv.Color.black()` was removed as of `supervision-0.22.0`. Use the constant `sv.Color.BLACK` instead.
+- The method `sv.Color.red()` was removed as of `supervision-0.22.0`. Use the constant `sv.Color.RED` instead.
+- The method `sv.Color.green()` was removed as of `supervision-0.22.0`. Use the constant `sv.Color.GREEN` instead.
+- The method `sv.Color.blue()` was removed as of `supervision-0.22.0`. Use the constant `sv.Color.BLUE` instead.
+- The method `sv.ColorPalette.default()` was removed as of `supervision-0.22.0`. Use the constant [`ColorPalette.DEFAULT`](utils/draw.md/#supervision.draw.color.ColorPalette.DEFAULT) instead.
+- `sv.BoxAnnotator` was removed as of `supervision-0.22.0`, however `sv.BoundingBoxAnnotator` was immediately renamed to `sv.BoxAnnotator`. Use [`BoxAnnotator`](detection/annotators.md/#supervision.annotators.core.BoxAnnotator) and [`LabelAnnotator`](detection/annotators.md/#supervision.annotators.core.LabelAnnotator) instead of the old `sv.BoxAnnotator`.
+- The method `sv.FPSMonitor.__call__` was removed as of `supervision-0.22.0`. Use the attribute [`sv.FPSMonitor.fps`](utils/video.md/#supervision.utils.video.FPSMonitor.fps) instead.
diff --git a/docs/detection/annotators.md b/docs/detection/annotators.md
new file mode 100644
index 0000000..91a6e27
--- /dev/null
+++ b/docs/detection/annotators.md
@@ -0,0 +1,685 @@
+---
+comments: true
+description: API reference for supervision's annotator classes โ draw bounding boxes, masks, labels, tracks, and heatmaps on images with one method call.
+---
+
+# Annotators
+
+Annotators accept detections and apply box or mask visualizations to the detections. Annotators have many available styles.
+
+=== "Outlines"
+
+ === "Box"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ box_annotator = sv.BoxAnnotator()
+ annotated_frame = box_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+ === "RoundBox"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ round_box_annotator = sv.RoundBoxAnnotator()
+ annotated_frame = round_box_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+ === "BoxCorner"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ corner_annotator = sv.BoxCornerAnnotator()
+ annotated_frame = corner_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+ === "Circle"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ circle_annotator = sv.CircleAnnotator()
+ annotated_frame = circle_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+ === "Ellipse"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ ellipse_annotator = sv.EllipseAnnotator()
+ annotated_frame = ellipse_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+ === "Polygon"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ polygon_annotator = sv.PolygonAnnotator()
+ annotated_frame = polygon_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+=== "Shading"
+
+ === "Color"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ color_annotator = sv.ColorAnnotator()
+ annotated_frame = color_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+ === "Halo"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ halo_annotator = sv.HaloAnnotator()
+ annotated_frame = halo_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+ === "Mask"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ mask_annotator = sv.MaskAnnotator()
+ annotated_frame = mask_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ )
+ ```
+
+ !!! note
+
+ `MaskAnnotator` expects `detections.mask` to contain instance segmentation masks aligned to the image passed to `annotate`. For dense masks, provide a boolean array of shape `(N, H, W)` where `(H, W)` matches the image height and width (it also accepts `sv.CompactMask`). If your model returns framework-specific results, convert them to `sv.Detections` first, for example with `sv.Detections.from_ultralytics(...)` or `sv.Detections.from_inference(...)`.
+
+
+
+ { align=center width="800" }
+
+
+
+=== "Markers"
+
+ === "Dot"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ dot_annotator = sv.DotAnnotator()
+ annotated_frame = dot_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+ === "Triangle"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ triangle_annotator = sv.TriangleAnnotator()
+ annotated_frame = triangle_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+=== "Labels"
+
+ === "Label"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ labels = [
+ f"{class_name} {confidence:.2f}"
+ for class_name, confidence in zip(
+ detections["class_name"],
+ detections.confidence,
+ )
+ ]
+
+ label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER)
+ annotated_frame = label_annotator.annotate(
+ scene=image.copy(), detections=detections, labels=labels
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+ === "RichLabel"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ labels = [
+ f"{class_name} {confidence:.2f}"
+ for class_name, confidence in zip(
+ detections["class_name"],
+ detections.confidence,
+ )
+ ]
+
+ rich_label_annotator = sv.RichLabelAnnotator(
+ font_path="TTF_FONT_PATH",
+ text_position=sv.Position.CENTER,
+ )
+ annotated_frame = rich_label_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ labels=labels,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+=== "Transformative"
+
+ === "Blur"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ blur_annotator = sv.BlurAnnotator()
+ annotated_frame = blur_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+ === "Pixelate"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ pixelate_annotator = sv.PixelateAnnotator()
+ annotated_frame = pixelate_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+
+
+=== "Tracking & Aggregation"
+
+ === "Trace"
+
+ ```python
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8x.pt")
+
+ trace_annotator = sv.TraceAnnotator()
+
+ video_info = sv.VideoInfo.from_video_path(video_path="...")
+ frames_generator = sv.get_video_frames_generator(source_path="...")
+ tracker = sv.ByteTrack()
+
+ with sv.VideoSink(target_path="...", video_info=video_info) as sink:
+ for frame in frames_generator:
+ result = model(frame)[0]
+ detections = sv.Detections.from_ultralytics(result)
+ detections = tracker.update_with_detections(detections)
+ annotated_frame = trace_annotator.annotate(
+ scene=frame.copy(),
+ detections=detections,
+ )
+ sink.write_frame(frame=annotated_frame)
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+ === "HeatMap"
+
+ ```python
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8x.pt")
+
+ heat_map_annotator = sv.HeatMapAnnotator()
+
+ video_info = sv.VideoInfo.from_video_path(video_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:
+ result = model(frame)[0]
+ detections = sv.Detections.from_ultralytics(result)
+ annotated_frame = heat_map_annotator.annotate(
+ scene=frame.copy(),
+ detections=detections,
+ )
+ sink.write_frame(frame=annotated_frame)
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+=== "Others"
+
+ === "PercentageBar"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ percentage_bar_annotator = sv.PercentageBarAnnotator()
+ annotated_frame = percentage_bar_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+ === "Icon"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ icon_paths = ["" for _ in detections]
+
+ icon_annotator = sv.IconAnnotator()
+ annotated_frame = icon_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ icon_path=icon_paths,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+ === "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,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+ === "Comparison"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections_1 = sv.Detections(...)
+ detections_2 = sv.Detections(...)
+
+ comparison_annotator = sv.ComparisonAnnotator()
+ annotated_frame = comparison_annotator.annotate(
+ scene=image.copy(),
+ detections_1=detections_1,
+ detections_2=detections_2,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+
+
Try Supervision Annotators on your own image
+ Visualize annotators on images with COCO classes such as people, vehicles, animals, household items.
+
+
+
+
+
+
+:::supervision.annotators.core.BoxAnnotator
+
+
+
+:::supervision.annotators.core.RoundBoxAnnotator
+
+
+
+:::supervision.annotators.core.BoxCornerAnnotator
+
+
+
+:::supervision.annotators.core.OrientedBoxAnnotator
+
+
+
+:::supervision.annotators.core.ColorAnnotator
+
+
+
+:::supervision.annotators.core.CircleAnnotator
+
+
+
+:::supervision.annotators.core.DotAnnotator
+
+
+
+:::supervision.annotators.core.TriangleAnnotator
+
+
+
+:::supervision.annotators.core.EllipseAnnotator
+
+
+
+:::supervision.annotators.core.HaloAnnotator
+
+
+
+:::supervision.annotators.core.PercentageBarAnnotator
+
+
+
+:::supervision.annotators.core.HeatMapAnnotator
+
+
+
+:::supervision.annotators.core.MaskAnnotator
+
+
+
+:::supervision.annotators.core.PolygonAnnotator
+
+
+
+:::supervision.annotators.core.LabelAnnotator
+
+
+
+:::supervision.annotators.core.RichLabelAnnotator
+
+
+
+:::supervision.annotators.core.IconAnnotator
+
+
+
+:::supervision.annotators.core.BlurAnnotator
+
+
+
+:::supervision.annotators.core.PixelateAnnotator
+
+
+
+:::supervision.annotators.core.TraceAnnotator
+
+
+
+:::supervision.annotators.core.CropAnnotator
+
+
+
+:::supervision.annotators.core.BackgroundOverlayAnnotator
+
+
+
+:::supervision.annotators.core.ComparisonAnnotator
+
+
+
+:::supervision.annotators.utils.ColorLookup
diff --git a/docs/detection/compact_mask.md b/docs/detection/compact_mask.md
new file mode 100644
index 0000000..6d8ad44
--- /dev/null
+++ b/docs/detection/compact_mask.md
@@ -0,0 +1,7 @@
+---
+comments: true
+---
+
+# CompactMask
+
+:::supervision.detection.compact_mask.CompactMask
diff --git a/docs/detection/core.md b/docs/detection/core.md
new file mode 100644
index 0000000..0b40cd3
--- /dev/null
+++ b/docs/detection/core.md
@@ -0,0 +1,8 @@
+---
+comments: true
+description: API reference for supervision's Detections class โ the core data structure for bounding boxes, masks, confidence scores, and tracker IDs.
+---
+
+# Detections
+
+:::supervision.detection.core.Detections
diff --git a/docs/detection/metrics.md b/docs/detection/metrics.md
new file mode 100644
index 0000000..cd719e0
--- /dev/null
+++ b/docs/detection/metrics.md
@@ -0,0 +1,25 @@
+---
+comments: true
+---
+
+# 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.
+
+Install the metrics extra before using this page's APIs:
+
+```bash
+pip install "supervision[metrics]"
+```
+
+
+
+:::supervision.metrics.detection.ConfusionMatrix
+
+
+
+:::supervision.metrics.detection.MeanAveragePrecision
diff --git a/docs/detection/tools/inference_slicer.md b/docs/detection/tools/inference_slicer.md
new file mode 100644
index 0000000..5e16495
--- /dev/null
+++ b/docs/detection/tools/inference_slicer.md
@@ -0,0 +1,54 @@
+---
+comments: true
+---
+
+# InferenceSlicer
+
+## GeoTIFF Datasets
+
+Install the optional GeoTIFF dependencies before running this example:
+
+```bash
+pip install "supervision[geotiff]"
+wget -O RGB.byte.tif https://raw.githubusercontent.com/rasterio/rasterio/main/tests/data/RGB.byte.tif
+```
+
+`InferenceSlicer` can read an open `rasterio` dataset window-by-window. This keeps large GeoTIFFs out of memory while passing each tile to the callback as an `(H, W, C)` NumPy array.
+
+```python
+import numpy as np
+import rasterio
+import supervision as sv
+
+
+def callback(tile: np.ndarray) -> sv.Detections:
+ h, w = tile.shape[:2]
+ return sv.Detections(
+ xyxy=np.array([[w * 0.25, h * 0.25, w * 0.75, h * 0.75]], dtype=float),
+ confidence=np.array([0.9]),
+ class_id=np.array([0]),
+ )
+
+
+slicer = sv.InferenceSlicer(
+ callback=callback,
+ slice_wh=(256, 256),
+ overlap_wh=(64, 64),
+ overlap_filter=sv.OverlapFilter.NONE,
+)
+
+with rasterio.open("RGB.byte.tif") as dataset:
+ detections = slicer(dataset)
+
+print(len(detections))
+```
+
+GeoTIFF inputs must use a projected coordinate reference system. Reproject geographic rasters before passing them to `InferenceSlicer`.
+
+
+
+:::supervision.detection.tools.inference_slicer.WindowedRasterDataset
+
+:::supervision.detection.tools.inference_slicer.InferenceSlicer
diff --git a/docs/detection/tools/line_zone.md b/docs/detection/tools/line_zone.md
new file mode 100644
index 0000000..8bca3cf
--- /dev/null
+++ b/docs/detection/tools/line_zone.md
@@ -0,0 +1,21 @@
+---
+comments: true
+---
+
+
+
LineZone
+
+
+:::supervision.detection.line_zone.LineZone
+
+
+
LineZoneAnnotator
+
+
+:::supervision.detection.line_zone.LineZoneAnnotator
+
+
+
LineZoneAnnotatorMulticlass
+
+
+:::supervision.detection.line_zone.LineZoneAnnotatorMulticlass
diff --git a/docs/detection/tools/polygon_zone.md b/docs/detection/tools/polygon_zone.md
new file mode 100644
index 0000000..cbe76c2
--- /dev/null
+++ b/docs/detection/tools/polygon_zone.md
@@ -0,0 +1,15 @@
+---
+comments: true
+---
+
+
+
PolygonZone
+
+
+:::supervision.detection.tools.polygon_zone.PolygonZone
+
+
+
PolygonZoneAnnotator
+
+
+:::supervision.detection.tools.polygon_zone.PolygonZoneAnnotator
diff --git a/docs/detection/tools/save_detections.md b/docs/detection/tools/save_detections.md
new file mode 100644
index 0000000..a24cee5
--- /dev/null
+++ b/docs/detection/tools/save_detections.md
@@ -0,0 +1,17 @@
+---
+comments: true
+---
+
+# Save Detections
+
+
+
CSV Sink
+
+
+:::supervision.detection.tools.csv_sink.CSVSink
+
+
+
JSON Sink
+
+
+:::supervision.detection.tools.json_sink.JSONSink
diff --git a/docs/detection/tools/smoother.md b/docs/detection/tools/smoother.md
new file mode 100644
index 0000000..8076be1
--- /dev/null
+++ b/docs/detection/tools/smoother.md
@@ -0,0 +1,7 @@
+---
+comments: true
+---
+
+# Detection Smoother
+
+:::supervision.detection.tools.smoother.DetectionsSmoother
diff --git a/docs/detection/utils/boxes.md b/docs/detection/utils/boxes.md
new file mode 100644
index 0000000..306dc20
--- /dev/null
+++ b/docs/detection/utils/boxes.md
@@ -0,0 +1,41 @@
+---
+comments: true
+---
+
+# Boxes Utils
+
+
+
+:::supervision.detection.utils.boxes.move_boxes
+
+
+
+:::supervision.detection.utils.boxes.scale_boxes
+
+
+
+:::supervision.detection.utils.boxes.clip_boxes
+
+
+
+:::supervision.detection.utils.boxes.pad_boxes
+
+
+
+:::supervision.detection.utils.boxes.denormalize_boxes
+
+
+
+:::supervision.detection.utils.boxes.xyxyxyxy_to_xyxy
diff --git a/docs/detection/utils/converters.md b/docs/detection/utils/converters.md
new file mode 100644
index 0000000..5bab82f
--- /dev/null
+++ b/docs/detection/utils/converters.md
@@ -0,0 +1,84 @@
+---
+comments: true
+status: new
+---
+
+# Converters Utils
+
+
+
+:::supervision.detection.utils.converters.xyxy_to_xywh
+
+
+
+:::supervision.detection.utils.converters.xywh_to_xyxy
+
+
+
+:::supervision.detection.utils.converters.xyxy_to_xcycarh
+
+
+
+:::supervision.detection.utils.converters.xcycwh_to_xyxy
+
+
+
+:::supervision.detection.utils.converters.xyxy_to_polygons
+
+
+
+:::supervision.detection.utils.converters.mask_to_xyxy
+
+
+
+:::supervision.detection.utils.converters.mask_to_polygons
+
+
+
+:::supervision.detection.utils.converters.polygon_to_mask
+
+
+
+:::supervision.detection.utils.converters.polygon_to_xyxy
+
+
+
+:::supervision.detection.utils.converters.xyxy_to_mask
+
+
+
+:::supervision.detection.utils.converters.rle_to_mask
+
+
+
+:::supervision.detection.utils.converters.mask_to_rle
+
+
+
+:::supervision.detection.utils.converters.is_compressed_rle
diff --git a/docs/detection/utils/iou_and_nms.md b/docs/detection/utils/iou_and_nms.md
new file mode 100644
index 0000000..93e98e8
--- /dev/null
+++ b/docs/detection/utils/iou_and_nms.md
@@ -0,0 +1,83 @@
+---
+comments: true
+---
+
+# IoU and NMS Utils
+
+
+
+:::supervision.detection.utils.iou_and_nms.OverlapFilter
+
+
+
+:::supervision.detection.utils.iou_and_nms.OverlapMetric
+
+
+
+:::supervision.detection.utils.iou_and_nms.box_iou
+
+
+
+:::supervision.detection.utils.iou_and_nms.box_iou_batch
+
+
+
+:::supervision.detection.utils.iou_and_nms.box_iou_batch_with_jaccard
+
+
+
+:::supervision.detection.utils.iou_and_nms.mask_iou_batch
+
+
+
+:::supervision.detection.utils.iou_and_nms.oriented_box_iou_batch
+
+
+
+:::supervision.detection.utils.iou_and_nms.box_non_max_suppression
+
+
+
+:::supervision.detection.utils.iou_and_nms.mask_non_max_suppression
+
+
+
+:::supervision.detection.utils.iou_and_nms.box_non_max_merge
+
+
+
+:::supervision.detection.utils.iou_and_nms.mask_non_max_merge
+
+
+
+:::supervision.detection.utils.iou_and_nms.oriented_box_non_max_suppression
+
+
+
+:::supervision.detection.utils.iou_and_nms.oriented_box_non_max_merge
diff --git a/docs/detection/utils/masks.md b/docs/detection/utils/masks.md
new file mode 100644
index 0000000..bf99c75
--- /dev/null
+++ b/docs/detection/utils/masks.md
@@ -0,0 +1,42 @@
+---
+comments: true
+status: new
+---
+
+# Masks Utils
+
+
+
+:::supervision.detection.utils.masks.mask_to_roi
+
+
+
+:::supervision.detection.utils.masks.move_masks
+
+
+
+:::supervision.detection.utils.masks.contains_holes
+
+
+
+:::supervision.detection.utils.masks.contains_multiple_segments
+
+
+
+:::supervision.detection.utils.masks.filter_segments_by_distance
+
+
+
+:::supervision.detection.utils.masks.calculate_masks_centroids
diff --git a/docs/detection/utils/polygons.md b/docs/detection/utils/polygons.md
new file mode 100644
index 0000000..8a7cf1e
--- /dev/null
+++ b/docs/detection/utils/polygons.md
@@ -0,0 +1,17 @@
+---
+comments: true
+---
+
+# Polygons Utils
+
+
+
+:::supervision.detection.utils.polygons.filter_polygons_by_area
+
+
+
+:::supervision.detection.utils.polygons.approximate_polygon
diff --git a/docs/detection/utils/vlms.md b/docs/detection/utils/vlms.md
new file mode 100644
index 0000000..d62bc60
--- /dev/null
+++ b/docs/detection/utils/vlms.md
@@ -0,0 +1,36 @@
+---
+comments: true
+status: new
+---
+
+# VLM Utils
+
+
+
+:::supervision.detection.vlm.VLM
+
+
+
+:::supervision.detection.vlm.LMM
+
+
+
+:::supervision.detection.vlm.validate_vlm_parameters
+
+
+
+:::supervision.detection.utils.vlms.edit_distance
+
+
+
+:::supervision.detection.utils.vlms.fuzzy_match_index
diff --git a/docs/faq.md b/docs/faq.md
new file mode 100644
index 0000000..7c17d7b
--- /dev/null
+++ b/docs/faq.md
@@ -0,0 +1,58 @@
+---
+comments: true
+description: Frequently asked questions about installing Supervision, supported computer vision models, datasets, tracking, metrics, and licensing.
+---
+
+# Frequently Asked Questions
+
+## What is Supervision?
+
+Supervision is an open-source Python library by Roboflow for computer vision workflows. It provides a unified `Detections` class with converters for supported object detection, segmentation, and VLM outputs.
+
+## How do I install Supervision?
+
+Install the base package with:
+
+```bash
+pip install supervision
+```
+
+Use the `metrics` extra when you need optional metric dependencies:
+
+```bash
+pip install "supervision[metrics]"
+```
+
+Sample asset utilities are part of the base package under `supervision.assets`.
+
+## Which object detection models work with Supervision?
+
+Supervision is model agnostic. `sv.Detections` includes converters for Ultralytics YOLO, Roboflow Inference, Hugging Face Transformers outputs, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers including Florence-2, PaliGemma, Qwen VL, Gemini, DeepSeek VL 2, and Moondream. Keypoint outputs have separate `sv.KeyPoints` converters, including MediaPipe.
+
+## What can I do with Supervision?
+
+You can annotate images and video, filter detections, track objects, count objects in zones or across lines, load and convert datasets, evaluate models with detection metrics, and export predictions for downstream analysis.
+
+## How do I track objects across video frames?
+
+Assign persistent tracker IDs before visualization. The built-in `sv.ByteTrack` wrapper accepts `Detections` through `update_with_detections()`, but it is deprecated in favor of `ByteTrackTracker` from the external `trackers` package. After tracking, combine the output with annotators such as `sv.TraceAnnotator`, `sv.BoxAnnotator`, and `sv.LabelAnnotator`.
+
+## What dataset formats does Supervision support?
+
+For detection datasets, Supervision supports YOLO, COCO JSON, Pascal VOC, CreateML, and LabelMe. Use `DetectionDataset.from_yolo()`, `DetectionDataset.from_coco()`, `DetectionDataset.from_pascal_voc()`, `DetectionDataset.from_createml()`, or `DetectionDataset.from_labelme()` to load datasets, and the matching `as_*` methods to export them.
+
+## How do I count objects in a zone?
+
+Use `sv.PolygonZone` for arbitrary polygon regions and `sv.LineZone` for line-crossing counts. Line crossing requires `detections.tracker_id`, so run a tracker before calling the line zone trigger.
+
+## How do I benchmark a model?
+
+Install `supervision[metrics]`, then use `supervision.metrics.mean_average_precision.MeanAveragePrecision` for mAP and `sv.ConfusionMatrix` for confusion matrices. Accumulate predictions and ground-truth `Detections`, then call `compute()` to calculate metrics.
+
+## Is Supervision free to use?
+
+Yes. Supervision is free and open source under the MIT license.
+
+## Where is the source code?
+
+The source code is available at [github.com/roboflow/supervision](https://github.com/roboflow/supervision).
diff --git a/docs/how_to/benchmark_a_model.md b/docs/how_to/benchmark_a_model.md
new file mode 100644
index 0000000..a3dce68
--- /dev/null
+++ b/docs/how_to/benchmark_a_model.md
@@ -0,0 +1,478 @@
+---
+comments: true
+description: Benchmark object detection models with supervision โ compute mAP, confusion matrix, and per-class metrics to compare model performance.
+authors:
+ - name: Piotr Skalski
+ role: Computer Vision Engineer, Roboflow
+ github: https://github.com/SkalskiP
+date_modified: 2026-04-22
+---
+
+
+
+# Benchmark a Model
+
+Have you ever trained multiple detection models and wondered which one performs best on your specific use case? Or maybe you've downloaded a pre-trained model and want to verify its performance on your dataset? Model benchmarking is essential for making informed decisions about which model to deploy in production.
+
+This guide will show an easy way to benchmark your results using `supervision`. It will go over:
+
+1. [Loading a dataset](#loading-a-dataset)
+2. [Loading a model](#loading-a-model)
+3. [Benchmarking Basics](#benchmarking-basics)
+4. [Running a Model](#running-a-model)
+5. [Remapping Classes](#remapping-classes)
+6. [Visual Benchmarking](#visual-benchmarking)
+7. [Benchmarking Metrics](#benchmarking-metrics)
+8. [Mean Average Precision (mAP)](#mean-average-precision-map)
+9. [F1 Score](#f1-score)
+10. [Bonus: Model Leaderboard](#model-leaderboard)
+
+This guide will use an instance segmentation model, but it applies to object detection, instance segmentation, and oriented bounding box models (OBB) too.
+
+A condensed version of this guide is available as a [Colab Notebook](https://colab.research.google.com/drive/1HoOY9pZoVwGiRMmLHtir0qT6Uj45w6Ps?usp=sharing).
+
+## Loading a Dataset
+
+Suppose you start with a dataset. Perhaps you found it on [Universe](https://universe.roboflow.com/); perhaps you [labeled your own](https://roboflow.com/how-to-label/yolo11). In either case, this guide assumes you know of a labelled dataset at hand.
+
+We'll use the following libraries:
+
+- `roboflow` to manage the dataset and deploy models
+- `inference` to run the models
+- `supervision` to evaluate the model results
+
+```bash
+pip install roboflow inference "supervision[metrics]"
+```
+
+Here's how you can download a dataset:
+
+```python
+from roboflow import Roboflow
+
+rf = Roboflow(api_key="")
+project = rf.workspace("").project("")
+dataset = project.version("").download("")
+```
+
+If your dataset is from Universe, go to `Dataset` > `Download Dataset` > select the format (e.g. `YOLOv11`) > `Show download code`.
+
+If labeling your own data, go to the [dashboard](https://app.roboflow.com/) and check this [guide](https://docs.roboflow.com/api-reference/workspace-and-project-ids) to find your workspace and project IDs.
+
+In this guide, we shall use a small [Corgi v2](https://universe.roboflow.com/model-examples/segmented-animals-basic) dataset. It is well-labeled and comes with a test set.
+
+```python
+from roboflow import Roboflow
+
+rf = Roboflow(api_key="")
+project = rf.workspace("fbamse1-gm2os").project("corgi-v2")
+dataset = project.version(4).download("yolov11")
+```
+
+This will create a folder called `Corgi-v2-4` with the dataset in the current working directory, with `train`, `test`, and `valid` folders and a `data.yaml` file.
+
+## Loading a Model
+
+Let's load a model.
+
+Select and instantiate the detection or segmentation model you want to benchmark. Supervision works with Roboflow Inference for both local and cloud-deployed models, as well as Ultralytics YOLO checkpoints. Choose the tab below that matches your preferred framework, then pass images to the loaded model during the evaluation loop.
+
+=== "Inference, Local"
+
+ Roboflow supports a range of state-of-the-art [pre-trained models](https://inference.roboflow.com/quickstart/aliases/) for object detection, instance segmentation, and pose tracking. You don't even need an API key!
+
+ Let's load such a model with inference [`inference`](https://inference.roboflow.com/).
+
+ ```python
+ from inference import get_model
+
+ model = get_model(model_id="yolov11s-seg-640")
+ ```
+
+=== "Inference, Deployed"
+
+ You can train and deploy a model without leaving the Roboflow platform. See this [guide](https://docs.roboflow.com/train/train/train-from-scratch) for more details.
+
+ To load a model, you can use inference:
+
+ ```python
+ from inference import get_model
+
+ model_id = "/"
+ model = get_model(model_id=model_id)
+ ```
+
+=== "Ultralytics"
+
+ Similarly to Inference, Ultralytics allows you to run a variety of models.
+
+ ```bash
+ pip install "ultralytics<=8.3.40"
+ ```
+
+ ```python
+ from ultralytics import YOLO
+
+ model = YOLO("yolo11s-seg.pt")
+ ```
+
+## Benchmarking Basics
+
+Evaluating your model requires careful selection of the dataset. Which images should you use?Let's go over the different scenarios.
+
+- **Unrelated Dataset**: If you have a dataset that was not used to train the model, this is the best choice.
+- **Training Set**: This is the set of images used to train the model. This is fine if the model was not trained on this dataset. Otherwise, **never** use it for benchmarking - the results will seem unrealistically good.
+- **Validation Set**: This is the set of images used to validate the model during training. Every Nth training epoch, the model is evaluated on the validation set. Often the training is stopped once the validation loss stops improving. Therefore, even while the images aren't used to train the model, it still indirectly influences the training outcome.
+- **Test Set**: This is the set of images kept aside for model testing. It is exactly the set you should use for benchmarking. If the dataset was split correctly, none of these images would be shown to the model during training.
+
+Therefore, an unrelated dataset or the `test` set is the best choice for benchmarking. Several other problems may arise:
+
+- **Extra Classes**: An unrelated dataset may contain additional classes which you may need to [filter out](https://supervision.roboflow.com/how_to/filter_detections/#by-set-of-classes) before computing metrics.
+- **Class Mismatch**: In an unrelated dataset, the class names or IDs may be different to what your model produces, you'll need to remap them, which is [shown in this guide](#running-a-model).
+- **Data Contamination**: The `test` set may not be split correctly, with images from the test set also present in `training` or `validation` set and used during training. In this case, the results will be overly optimistic. This also applies when **very similar** images are used for training and testing - e.g. those taken in the same environment, same lighting conditions, similar angle, etc.
+- **Missing Test Set**: Some datasets do not come with a test set. In this case, you should collect and [label](https://roboflow.com/annotate) your own data. Alternatively, a validation set could be used, but the results could be overly optimistic. Make sure to test in the real world as soon as possible.
+
+## Running a Model
+
+At this stage, you should have:
+
+- A dataset of labeled images to evaluate the model.
+- A model prepared for benchmarking.
+
+With these ready, we can now run the model and obtain predictions. We'll use `supervision` to create a dataset iterator, and then run the model on each image.
+
+=== "Inference"
+
+ ```python
+ import supervision as sv
+
+ test_set = sv.DetectionDataset.from_yolo(
+ images_directory_path=f"{dataset.location}/test/images",
+ annotations_directory_path=f"{dataset.location}/test/labels",
+ data_yaml_path=f"{dataset.location}/data.yaml",
+ )
+
+ image_paths = []
+ predictions_list = []
+ targets_list = []
+
+ for image_path, image, label in test_set:
+ result = model.infer(image)[0]
+ predictions = sv.Detections.from_inference(result)
+
+ image_paths.append(image_path)
+ predictions_list.append(predictions)
+ targets_list.append(label)
+ ```
+
+=== "Ultralytics"
+
+ ```python
+ import supervision as sv
+
+ test_set = sv.DetectionDataset.from_yolo(
+ images_directory_path=f"{dataset.location}/test/images",
+ annotations_directory_path=f"{dataset.location}/test/labels",
+ data_yaml_path=f"{dataset.location}/data.yaml",
+ )
+
+ image_paths = []
+ predictions_list = []
+ targets_list = []
+
+ for image_path, image, label in test_set:
+ result = model(image)[0]
+ predictions = sv.Detections.from_ultralytics(result)
+
+ image_paths.append(image_path)
+ predictions_list.append(predictions)
+ targets_list.append(label)
+ ```
+
+## Remapping classes
+
+Did you notice an issue in the above logic? Since we're using an unrelated dataset, the class names and IDs may be different from what the model was trained on.
+
+We need to remap them to match the dataset classes. Here's how to do it:
+
+```python
+def remap_classes(
+ detections: sv.Detections,
+ class_ids_from_to: dict[int, int],
+ class_names_from_to: dict[str, str],
+) -> None:
+ new_class_ids = [
+ class_ids_from_to.get(class_id, class_id) for class_id in detections.class_id
+ ]
+ detections.class_id = np.array(new_class_ids)
+
+ new_class_names = [
+ class_names_from_to.get(name, name) for name in detections["class_name"]
+ ]
+ predictions["class_name"] = np.array(new_class_names)
+```
+
+Let's also remove the predictions that are not in the dataset classes.
+
+=== "Inference"
+
+ Dataset class names and IDs can be found in the `data.yaml` file, or by printing `dataset.classes`.
+
+ ```python
+ import supervision as sv
+
+ test_set = sv.DetectionDataset.from_yolo(
+ images_directory_path=f"{dataset.location}/test/images",
+ annotations_directory_path=f"{dataset.location}/test/labels",
+ data_yaml_path=f"{dataset.location}/data.yaml",
+ )
+
+ image_paths = []
+ predictions_list = []
+ targets_list = []
+
+ for image_path, image, label in test_set:
+ result = model.infer(image)[0]
+ predictions = sv.Detections.from_inference(result)
+
+ remap_classes(
+ detections=predictions,
+ class_ids_from_to={16: 0},
+ class_names_from_to={"dog": "Corgi"},
+ )
+ predictions = predictions[np.isin(predictions["class_name"], test_set.classes),]
+
+ image_paths.append(image_path)
+ predictions_list.append(predictions)
+ targets_list.append(label)
+ ```
+
+=== "Ultralytics"
+
+ Dataset class names and IDs can be found in the `data.yaml` file, or by printing `dataset.classes`.
+
+ Each model will have a different class mapping, so make sure to check the model's documentation. In this case, the model was trained on the COCO dataset, with a class configuration found [here](https://github.com/ultralytics/ultralytics/blob/main/ultralytics/cfg/datasets/coco8.yaml).
+
+ ```python
+ import supervision as sv
+
+ test_set = sv.DetectionDataset.from_yolo(
+ images_directory_path=f"{dataset.location}/test/images",
+ annotations_directory_path=f"{dataset.location}/test/labels",
+ data_yaml_path=f"{dataset.location}/data.yaml",
+ )
+
+ image_paths = []
+ predictions_list = []
+ targets_list = []
+
+ for image_path, image, label in test_set:
+ result = model(image)[0]
+ predictions = sv.Detections.from_ultralytics(result)
+
+ remap_classes(
+ detections=predictions,
+ class_ids_from_to={16: 0},
+ class_names_from_to={"dog": "Corgi"},
+ )
+ predictions = predictions[np.isin(predictions["class_name"], test_set.classes),]
+
+ image_paths.append(image_path)
+ predictions_list.append(predictions)
+ targets_list.append(label)
+ ```
+
+## Visualizing Predictions
+
+The first step in evaluating your modelโs performance is to visualize its predictions. This gives an intuitive sense of how well your model is detecting objects and where it might be failing.
+
+```python
+import supervision as sv
+
+N = 9
+GRID_SIZE = (3, 3)
+
+target_annotator = sv.PolygonAnnotator(color=sv.Color.from_hex("#8315f9"), thickness=8)
+prediction_annotator = sv.PolygonAnnotator(
+ color=sv.Color.from_hex("#00cfc6"), thickness=6
+)
+
+
+annotated_images = []
+for image_path, predictions, targets in zip(
+ image_paths[:N], predictions_list[:N], targets_list[:N]
+):
+ annotated_image = cv2.imread(image_path)
+ annotated_image = target_annotator.annotate(
+ scene=annotated_image, detections=targets
+ )
+ annotated_image = prediction_annotator.annotate(
+ scene=annotated_image, detections=prediction
+ )
+ annotated_images.append(annotated_image)
+
+sv.plot_images_grid(images=annotated_images, grid_size=GRID_SIZE)
+```
+
+Here, predictions in purple are targets (ground truth), and predictions in teal are model predictions.
+
+
+
+!!! tip
+
+ Use `sv.BoxAnnotator` for object detection and `sv.OrientedBoxAnnotator` for OBB.
+
+ See [annotator documentation](https://supervision.roboflow.com/latest/detection/annotators/) for even more options.
+
+## Visual Benchmarking
+
+To inspect where a model succeeds and fails, pass `save_directory_path` to `sv.ConfusionMatrix.benchmark(...)`. For every dataset image it writes a 2x2 result grid โ `Ground Truth`, `True Positives`, `False Positives`, and `False Negatives` panels โ directly into that directory, reusing the original image filenames. This makes it easy to skim through per-image outcomes alongside the aggregate confusion matrix.
+
+```python
+import supervision as sv
+
+confusion_matrix = sv.ConfusionMatrix.benchmark(
+ dataset=test_set,
+ callback=callback,
+ save_directory_path="./results",
+)
+```
+
+## Benchmarking Metrics
+
+With multiple models, fine details matter. Visual inspection may not be enough. `supervision` provides a collection of metrics that help obtain precise numerical results of model performance.
+
+### Mean Average Precision (mAP)
+
+We'll start with [MeanAveragePrecision (mAP)](https://supervision.roboflow.com/latest/metrics/mean_average_precision/#supervision.metrics.mean_average_precision.MeanAveragePrecision), which is the most commonly used metric for object detection. It measures the average precision across all classes and IoU thresholds.
+
+For a thorough explanation, check out our [blog](https://blog.roboflow.com/mean-average-precision/) and [Youtube video](https://www.youtube.com/watch?v=oqXDdxF_Wuw).
+
+Here, the most popular value is `mAP 50:95`. It represents the average precision across all classes and IoU thresholds (`0.5` to `0.95`), whereas other values such as `mAP 50` or `mAP 75` only consider a single IoU threshold (`0.5` and `0.75` respectively).
+
+Let's compute the mAP:
+
+```python
+from supervision.metrics import MeanAveragePrecision, MetricTarget
+
+map_metric = MeanAveragePrecision(metric_target=MetricTarget.MASKS)
+map_result = map_metric.update(predictions_list, targets_list).compute()
+```
+
+Try printing the result to see it at a glance:
+
+```python
+print(map_result)
+```
+
+```
+MeanAveragePrecisionResult:
+Metric target: MetricTarget.MASKS
+Class agnostic: False
+mAP @ 50:95: 0.2409
+mAP @ 50: 0.3591
+mAP @ 75: 0.2915
+mAP scores: [0.35909 0.3468 0.34556 ...]
+IoU thresh: [0.5 0.55 0.6 ...]
+AP per class:
+ 0: [0.35909 0.3468 0.34556 ...]
+...
+Small objects: ...
+Medium objects: ...
+Large objects: ...
+```
+
+You can also plot the results:
+
+```python
+map_result.plot()
+```
+
+
+
+The metric also breaks down the results by detected object area. Small, medium and large are simply those with area less than 32ยฒ, between 32ยฒ and 96ยฒ, and greater than 96ยฒ pixels respectively.
+
+### F1 Score
+
+The [F1 Score](https://supervision.roboflow.com/latest/metrics/f1_score/) is another useful metric, especially when dealing with an imbalance between false positives and false negatives. Itโs the harmonic mean of **precision** (how many predictions are correct) and **recall** (how many actual instances were detected).
+
+Here's how you can compute the F1 score:
+
+```python
+from supervision.metrics import F1Score, MetricTarget
+
+f1_metric = F1Score(metric_target=MetricTarget.MASKS)
+f1_result = f1_metric.update(predictions_list, targets_list).compute()
+```
+
+As with mAP, you can also print the result:
+
+```python
+print(f1_result)
+```
+
+```
+F1ScoreResult:
+Metric target: MetricTarget.MASKS
+Averaging method: AveragingMethod.WEIGHTED
+F1 @ 50: 0.5341
+F1 @ 75: 0.4636
+F1 @ thresh: [0.53406 0.5278 0.52153 ...]
+IoU thresh: [0.5 0.55 0.6 ...]
+F1 per class:
+ 0: [0.53406 0.5278 0.52153 ...]
+...
+Small objects: ...
+Medium objects: ...
+Large objects: ...
+```
+
+Similarly, you can plot the results:
+
+```python
+f1_result.plot()
+```
+
+
+
+As with mAP, the metric also breaks down the results by detected object area. Small, medium and large are simply those with area less than 32ยฒ, between 32ยฒ and 96ยฒ, and greater than 96ยฒ pixels respectively.
+
+## Model Leaderboard
+
+Here to compare the basic models? We've got you covered. Check out our [Model Leaderboard](https://leaderboard.roboflow.com/) to see how different models perform and to get a sense of the state-of-the-art results. It's a great place to understand what the leading models can achieve and to compare your own results.
+
+Even better, the repository is open source! You can see how the models were benchmarked, run the evaluation yourself, and even add your own models to the leaderboard. Check it out on [GitHub](https://github.com/roboflow/model-leaderboard)!
+
+
+
+## Conclusion
+
+In this guide, you've learned how to set up your environment, train or use pre-trained models, visualize predictions, and evaluate model performance with metrics like [mAP](https://supervision.roboflow.com/latest/metrics/mean_average_precision/), [F1 score](https://supervision.roboflow.com/latest/metrics/f1_score/), and got to know our Model Leaderboard.
+
+A condensed version of this guide is also available as a [Colab Notebook](https://colab.research.google.com/drive/1HoOY9pZoVwGiRMmLHtir0qT6Uj45w6Ps?usp=sharing).
+
+For more details, be sure to check out our [documentation](https://supervision.roboflow.com/latest/) and join our community discussions. If you find any issues, please let us know on [GitHub](https://github.com/roboflow/supervision/issues).
+
+Best of luck with your benchmarking!
+
+## Frequently Asked Questions
+
+### How do I benchmark a model with supervision?
+
+Use `supervision.metrics.mean_average_precision.MeanAveragePrecision` โ accumulate prediction and ground-truth `Detections` with `update(...)` and then call `compute()`. For confusion matrices, use `sv.ConfusionMatrix.from_detections(predictions=predictions, targets=targets, classes=classes)`.
+
+### What IoU thresholds does MeanAveragePrecision use?
+
+It computes mAP over IoU thresholds from 0.50 to 0.95 in steps of 0.05 (mAP@50:95), plus mAP@50 and mAP@75 individually.
+
+### Can I benchmark segmentation models?
+
+Yes, if you want to evaluate their bounding boxes. Convert model outputs to `Detections` and pass them to `MeanAveragePrecision.update(...)`; the current mAP path prepares COCO-style bounding boxes from `detections.xyxy`.
+
+### What is a ConfusionMatrix and how do I use it?
+
+`sv.ConfusionMatrix` visualizes true positives, false positives, and false negatives per class. Create one with `sv.ConfusionMatrix.from_detections(predictions=predictions, targets=targets, classes=classes, conf_threshold=0.5, iou_threshold=0.5)`, then call `confusion_matrix.plot()` to render a heatmap. If you want per-image validation visualizations saved to disk, pass `save_directory_path="./results"` to `sv.ConfusionMatrix.benchmark(...)`; it will write 2x2 result grids directly into that directory using the original image filenames, with `Ground Truth`, `True Positives`, `False Positives`, and `False Negatives` panels.
+
+## Author
+
+- [Piotr Skalski](https://github.com/SkalskiP) โ Computer Vision Engineer, Roboflow
diff --git a/docs/how_to/count_in_zone.md b/docs/how_to/count_in_zone.md
new file mode 100644
index 0000000..8159f1f
--- /dev/null
+++ b/docs/how_to/count_in_zone.md
@@ -0,0 +1,150 @@
+---
+comments: true
+description: Count objects entering a polygon zone in images and video using supervision's PolygonZone โ measure throughput and density in any region.
+authors:
+ - name: Piotr Skalski
+ role: Computer Vision Engineer, Roboflow
+ github: https://github.com/SkalskiP
+date_modified: 2026-04-22
+---
+
+With supervision, you can count the number of objects in a zone in an image or video. In this guide, we will show how to count the number of cars in a traffic video.
+
+[View the notebook that accompanies this tutorial](https://github.com/roboflow/notebooks/blob/main/notebooks/how-to-use-polygonzone-annotate-and-supervision.ipynb).
+
+To make it easier for you to follow our tutorial download the video we will use as an example. You can do this using the `supervision.assets` module:
+
+```python
+from supervision.assets import download_assets, VideoAssets
+
+download_assets(VideoAssets.VEHICLES_2)
+```
+
+## Initialize a Model and Load Video
+
+First, we need to initialize a model. Let's use a YOLOv8 model with the default COCO checkpoint. We also need to load a video on which to run inference.
+
+Create a YOLO model instance and download the source video. The model will process each frame during inference. A shared color palette ensures consistent zone coloring throughout the output video.
+
+```python
+import numpy as np
+import supervision as sv
+import cv2
+
+from ultralytics import YOLO
+from supervision.assets import VideoAssets, download_assets
+
+model = YOLO("yolov8s.pt")
+
+VIDEO = download_assets(VideoAssets.VEHICLES_2)
+
+colors = sv.ColorPalette.DEFAULT
+```
+
+## Calculate Coordinates
+
+To count objects in a zone, you need to know the coordinates where you want to draw the zone.
+
+You can calculate coordinates using the [PolygonZone web utility](https://roboflow.github.io/polygonzone/).
+
+To use the PolygonZone website, you will need to upload an image or frame from a video. You can retrieve a frame using this code:
+
+```python
+generator = sv.get_video_frames_generator(VIDEO)
+iterator = iter(generator)
+
+frame = next(iterator)
+
+cv2.imwrite("first_frame.png", frame)
+```
+
+PolygonZone will give you NumPy arrays that you can use with supervision to count objects in zones.
+
+
+
+
+
+Save the coordinates in an array:
+
+```python
+polygons = [
+ np.array([[718, 595], [927, 592], [851, 1062], [42, 1059]]),
+ np.array([[987, 595], [1199, 595], [1893, 1056], [1015, 1062]]),
+]
+```
+
+## Define Zones
+
+With the coordinates of the zones to draw ready, we can set up our zones:
+
+Instantiate a `PolygonZone` for each polygon array, pairing it with a `PolygonZoneAnnotator` for visual overlay and a `BoxAnnotator` for drawing detection boxes. Each zone will later trigger on incoming detections to determine which objects fall inside its boundaries, enabling per-zone counting in the inference callback.
+
+```python
+zones = [sv.PolygonZone(polygon=polygon) for polygon in polygons]
+zone_annotators = [
+ sv.PolygonZoneAnnotator(
+ zone=zone,
+ color=colors.by_idx(index),
+ thickness=4,
+ text_thickness=8,
+ text_scale=4,
+ )
+ for index, zone in enumerate(zones)
+]
+box_annotators = [
+ sv.BoxAnnotator(
+ color=colors.by_idx(index),
+ thickness=4,
+ )
+ for index in range(len(polygons))
+]
+```
+
+## Run Inference
+
+We can run inference on a video using the [sv.process_video](https://supervision.roboflow.com/utils/video/#process_video) function. This function accepts a callback that runs inference on each frame and compiles the results into a video.
+
+Below, we can call our YOLOv8 model, annotate predictions and zones, then save the results to a file called `result.mp4`.
+
+```python
+def process_frame(frame: np.ndarray, i) -> np.ndarray:
+ results = model(frame, imgsz=1280, verbose=False)[0]
+ detections = sv.Detections.from_ultralytics(results)
+
+ for zone, zone_annotator, box_annotator in zip(
+ zones, zone_annotators, box_annotators
+ ):
+ mask = zone.trigger(detections=detections)
+ detections_filtered = detections[mask]
+ frame = box_annotator.annotate(scene=frame, detections=detections_filtered)
+ frame = zone_annotator.annotate(scene=frame)
+
+ return frame
+
+
+sv.process_video(source_path=VIDEO, target_path="result.mp4", callback=process_frame)
+```
+
+Here is an example of inference run on the video:
+
+
+
+
+
+## Frequently Asked Questions
+
+### How do I count objects in a zone with supervision?
+
+Create `sv.PolygonZone` with a polygon defining your region. Call `zone.trigger(detections)` on each frame โ it returns a mask of detections inside the zone.
+
+### Can I count objects crossing a line instead of entering a zone?
+
+Yes. Use `sv.LineZone` โ define a start and end point. `zone.trigger(detections)` returns a tuple of two boolean arrays, `(crossed_in, crossed_out)`, indicating which detections crossed the line in each direction. `LineZone` requires `detections.tracker_id`; run a tracker first so the same object can be matched across frames.
+
+### Can I combine zone counting with tracking?
+
+Yes. You can pass tracker IDs from `sv.ByteTrack` alongside your detections, but `sv.PolygonZone` still evaluates the zone on each frame and reports which objects are currently inside it. If you want to count each object only once when it first enters the zone, maintain a set of seen `tracker_id` values after filtering detections with `zone.trigger(detections)`, or use a dedicated entry/crossing counting tool such as `sv.LineZone` when it better matches your use case.
+
+## Author
+
+- [Piotr Skalski](https://github.com/SkalskiP) โ Computer Vision Engineer, Roboflow
diff --git a/docs/how_to/detect_and_annotate.md b/docs/how_to/detect_and_annotate.md
new file mode 100644
index 0000000..9cbc3ac
--- /dev/null
+++ b/docs/how_to/detect_and_annotate.md
@@ -0,0 +1,460 @@
+---
+comments: true
+description: Learn to load model predictions, create Detections objects, and annotate images with bounding boxes, labels, and masks using supervision.
+authors:
+ - name: Piotr Skalski
+ role: Computer Vision Engineer, Roboflow
+ github: https://github.com/SkalskiP
+ - name: Borda
+ role: Open Source Engineer, Roboflow
+ github: https://github.com/borda
+date_modified: 2026-04-22
+---
+
+# Detect and Annotate
+
+!!! tip "Sample Image"
+
+ Don't have an image? Download the one used in this tutorial:
+
+ ```bash
+ wget https://media.roboflow.com/notebooks/examples/dog.jpeg
+ ```
+
+ ```
+ Then replace `` with `"dog.jpeg"`.
+ ```
+
+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), [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 source image.
+
+
+
+## Run Detection
+
+First, you'll need to obtain predictions from your object detection or segmentation model.
+
+To run inference, initialize your chosen model and pass the source image to its predict or infer method. Supervision supports Roboflow Inference, Ultralytics YOLO, and Hugging Face Transformers -- select the tab matching your framework. The result is a framework-specific object you will convert to a `Detections` instance in the next step.
+
+=== "Inference"
+
+ ```python
+ import cv2
+ from inference import get_model
+
+ model = get_model(model_id="yolov8n-640")
+ image = cv2.imread("dog.jpeg")
+ results = model.infer(image)[0]
+ ```
+
+=== "Ultralytics"
+
+ ```python
+ import cv2
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8n.pt")
+ image = cv2.imread("dog.jpeg")
+ results = model(image)[0]
+ ```
+
+=== "Transformers"
+
+ ```python
+ import torch
+ 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("dog.jpeg")
+ 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]
+ ```
+
+## Load Predictions into Supervision
+
+Now that we have predictions from a model, we can load them into Supervision.
+
+Each supported framework has a dedicated class method on `sv.Detections` that converts raw model output into a unified Supervision object. Call `from_inference`, `from_ultralytics`, or `from_transformers` depending on the package you used for inference. This normalization step ensures all downstream annotators and filters work identically regardless of the source model.
+
+=== "Inference"
+
+ We can do so using the [`sv.Detections.from_inference`](https://supervision.roboflow.com/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
+ import supervision as sv
+ from inference import get_model
+
+ model = get_model(model_id="yolov8n-640")
+ image = cv2.imread("dog.jpeg")
+ results = model.infer(image)[0]
+ detections = sv.Detections.from_inference(results)
+ ```
+
+=== "Ultralytics"
+
+ We can do so using the [`sv.Detections.from_ultralytics`](https://supervision.roboflow.com/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
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8n.pt")
+ image = cv2.imread("dog.jpeg")
+ results = model(image)[0]
+ detections = sv.Detections.from_ultralytics(results)
+ ```
+
+=== "Transformers"
+
+ We can do so using the [`sv.Detections.from_transformers`](https://supervision.roboflow.com/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
+ 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("dog.jpeg")
+ 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)
+ ```
+
+You can load predictions from other computer vision frameworks and libraries using:
+
+- [`from_deepsparse`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_deepsparse) ([Deepsparse](https://github.com/neuralmagic/deepsparse))
+- [`from_detectron2`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_detectron2) ([Detectron2](https://github.com/facebookresearch/detectron2))
+- [`from_mmdetection`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_mmdetection) ([MMDetection](https://github.com/open-mmlab/mmdetection))
+- [`from_sam`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_sam) ([Segment Anything Model](https://github.com/facebookresearch/segment-anything))
+- [`from_yolo_nas`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections.from_yolo_nas) ([YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md))
+
+## 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.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) classes.
+
+To draw bounding boxes and class labels on your image, create a `BoxAnnotator` and a `LabelAnnotator`, then call their `annotate` methods in sequence. Each annotator returns the modified image, so you can chain multiple annotators together. The result is a single NumPy array with all visual overlays rendered and ready for display or saving.
+
+=== "Inference"
+
+ ```{ .py hl_lines="10-16" }
+ import cv2
+ import supervision as sv
+ from inference import get_model
+
+ model = get_model(model_id="yolov8n-640")
+ image = cv2.imread("dog.jpeg")
+ results = model.infer(image)[0]
+ detections = sv.Detections.from_inference(results)
+
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ 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
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8n.pt")
+ image = cv2.imread("dog.jpeg")
+ results = model(image)[0]
+ detections = sv.Detections.from_ultralytics(results)
+
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ 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
+ 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("dog.jpeg")
+ 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)
+
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ annotated_image = box_annotator.annotate(
+ scene=image, detections=detections)
+ annotated_image = label_annotator.annotate(
+ scene=annotated_image, detections=detections)
+ ```
+
+
+
+## Display Custom Labels
+
+By default, [`sv.LabelAnnotator`](https://supervision.roboflow.com/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
+ from inference import get_model
+
+ model = get_model(model_id="yolov8n-640")
+ image = cv2.imread("dog.jpeg")
+ results = model.infer(image)[0]
+ detections = sv.Detections.from_inference(results)
+
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ labels = [
+ f"{class_name} {confidence:.2f}"
+ for class_name, confidence
+ in zip(detections['class_name'], detections.confidence)
+ ]
+
+ 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
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8n.pt")
+ image = cv2.imread("dog.jpeg")
+ results = model(image)[0]
+ detections = sv.Detections.from_ultralytics(results)
+
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ labels = [
+ f"{class_name} {confidence:.2f}"
+ for class_name, confidence
+ in zip(detections['class_name'], detections.confidence)
+ ]
+
+ 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
+ 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("dog.jpeg")
+ 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)
+
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ labels = [
+ f"{class_name} {confidence:.2f}"
+ for class_name, confidence
+ in zip(detections['class_name'], detections.confidence)
+ ]
+
+ annotated_image = box_annotator.annotate(
+ scene=image, detections=detections)
+ annotated_image = label_annotator.annotate(
+ scene=annotated_image, detections=detections, labels=labels)
+ ```
+
+
+
+## Annotate Image with Segmentations
+
+If you are running the segmentation model [`sv.MaskAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.MaskAnnotator) is a drop-in replacement for [`sv.BoxAnnotator`](https://supervision.roboflow.com/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
+ from inference import get_model
+
+ model = get_model(model_id="yolov8n-seg-640")
+ image = cv2.imread("dog.jpeg")
+ results = model.infer(image)[0]
+ detections = sv.Detections.from_inference(results)
+
+ mask_annotator = sv.MaskAnnotator()
+ label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER_OF_MASS)
+
+ annotated_image = mask_annotator.annotate(
+ scene=image,
+ detections=detections,
+ )
+ annotated_image = label_annotator.annotate(
+ scene=annotated_image,
+ detections=detections,
+ )
+ sv.plot_image(annotated_image)
+ ```
+
+=== "Ultralytics"
+
+ ```python
+ import cv2
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8n-seg.pt")
+ image = cv2.imread("dog.jpeg")
+ results = model(image)[0]
+ detections = sv.Detections.from_ultralytics(results)
+
+ mask_annotator = sv.MaskAnnotator()
+ label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER_OF_MASS)
+
+ annotated_image = mask_annotator.annotate(
+ scene=image,
+ detections=detections,
+ )
+ annotated_image = label_annotator.annotate(
+ scene=annotated_image,
+ detections=detections,
+ )
+ ```
+
+=== "Transformers"
+
+ ```python
+ import torch
+ import supervision as sv
+ from PIL import Image
+ from transformers import DetrImageProcessor, DetrForSegmentation
+
+ processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50-panoptic")
+ model = DetrForSegmentation.from_pretrained("facebook/detr-resnet-50-panoptic")
+
+ image = Image.open("dog.jpeg")
+ 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_segmentation(
+ outputs=outputs, target_sizes=target_size
+ )[0]
+ detections = sv.Detections.from_transformers(
+ transformers_results=results, id2label=model.config.id2label
+ )
+
+ mask_annotator = sv.MaskAnnotator()
+ label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER_OF_MASS)
+
+ labels = [
+ f"{class_name} {confidence:.2f}"
+ for class_name, confidence in zip(
+ detections["class_name"],
+ detections.confidence,
+ )
+ ]
+
+ annotated_image = mask_annotator.annotate(scene=image, detections=detections)
+ annotated_image = label_annotator.annotate(
+ scene=annotated_image, detections=detections, labels=labels
+ )
+ ```
+
+
+
+## Frequently Asked Questions
+
+### How do I detect and annotate objects with supervision?
+
+Pass any model's output to `sv.Detections.from_()` to create a unified `Detections` object. Then pass it to `sv.BoxAnnotator` or `sv.MaskAnnotator` to draw predictions on an image.
+
+### Can I annotate both bounding boxes and masks at the same time?
+
+Yes. Chain annotators: first draw boxes with `BoxAnnotator`, then overlay masks with `MaskAnnotator` on the same scene.
+
+### How do I label detections with class names?
+
+Use `sv.LabelAnnotator` and pass custom text with the `labels` parameter. If a connector provides class names, they are stored in `detections["class_name"]` / `detections.data["class_name"]`; when `labels` is omitted, `LabelAnnotator` uses class names first, then class IDs, then detection indices.
+
+### Can I use supervision with Hugging Face models?
+
+Yes. `sv.Detections.from_transformers()` accepts supported Hugging Face object detection and segmentation outputs. Vision-language model outputs are handled through `sv.Detections.from_vlm(...)`, for example with `sv.VLM.FLORENCE_2` or `sv.VLM.PALIGEMMA`.
+
+## Authors
+
+- [Piotr Skalski](https://github.com/SkalskiP) โ Computer Vision Engineer, Roboflow
+- [Borda](https://github.com/borda) โ Open Source Engineer, Roboflow
diff --git a/docs/how_to/detect_small_objects.md b/docs/how_to/detect_small_objects.md
new file mode 100644
index 0000000..237dff3
--- /dev/null
+++ b/docs/how_to/detect_small_objects.md
@@ -0,0 +1,347 @@
+---
+comments: true
+description: Detect small objects in images by applying SAHI inference slicing with supervision's InferenceSlicer โ improve recall for tiny targets.
+authors:
+ - name: Piotr Skalski
+ role: Computer Vision Engineer, Roboflow
+ github: https://github.com/SkalskiP
+date_modified: 2026-04-22
+---
+
+# Detect Small Objects
+
+This guide shows how to detect small objects with the [Inference](https://github.com/roboflow/inference), [Ultralytics](https://github.com/ultralytics/ultralytics) or [Transformers](https://github.com/huggingface/transformers) packages using [`InferenceSlicer`](https://supervision.roboflow.com/latest/detection/tools/inference_slicer/#supervision.detection.tools.inference_slicer.InferenceSlicer).
+
+
+
+
+
+## Baseline Detection
+
+Small object detection in high-resolution images presents challenges due to the objects' size relative to the image resolution.
+
+Running a standard detection model on the full image establishes a baseline for comparison. Load your chosen model, pass the image through it, and convert the results into a `Detections` object. This baseline reveals how many small objects the model misses at native resolution, motivating the sliced inference approach shown later.
+
+=== "Inference"
+
+ ```python
+ import cv2
+ import supervision as sv
+ from inference import get_model
+
+ model = get_model(model_id="yolov8x-640")
+ image = cv2.imread("")
+ results = model.infer(image)[0]
+ detections = sv.Detections.from_inference(results)
+
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ 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
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8x.pt")
+ image = cv2.imread("")
+ results = model(image)[0]
+ detections = sv.Detections.from_ultralytics(results)
+
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ 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
+ from PIL import Image
+ from transformers import DetrImageProcessor, DetrForSegmentation
+
+ processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50")
+ model = DetrForSegmentation.from_pretrained("facebook/detr-resnet-50")
+
+ image = Image.open("")
+ inputs = processor(images=image, return_tensors="pt")
+
+ with torch.no_grad():
+ outputs = model(**inputs)
+
+ width, height = image_slice.size
+ target_size = torch.tensor([[width, height]])
+ results = processor.post_process_object_detection(
+ outputs=outputs, target_sizes=target_size
+ )[0]
+ detections = sv.Detections.from_transformers(results)
+
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ labels = [model.config.id2label[class_id] for class_id in detections.class_id]
+
+ annotated_image = box_annotator.annotate(scene=image, detections=detections)
+ annotated_image = label_annotator.annotate(
+ scene=annotated_image, detections=detections, labels=labels
+ )
+ ```
+
+
+
+## Input Resolution
+
+Modifying the input resolution of images before detection can enhance small object identification at the cost of processing speed and increased memory usage. This method is less effective for ultra-high-resolution images (4K and above).
+
+=== "Inference"
+
+ ```{ .py hl_lines="5" }
+ import cv2
+ import supervision as sv
+ from inference import get_model
+
+ model = get_model(model_id="yolov8x-1280")
+ image = cv2.imread("")
+ results = model.infer(image)[0]
+ detections = sv.Detections.from_inference(results)
+
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ 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
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8x.pt")
+ image = cv2.imread("")
+ results = model(image, imgsz=1280)[0]
+ detections = sv.Detections.from_ultralytics(results)
+
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ annotated_image = box_annotator.annotate(
+ scene=image, detections=detections)
+ annotated_image = label_annotator.annotate(
+ scene=annotated_image, detections=detections)
+ ```
+
+
+
+## Inference Slicer
+
+[`InferenceSlicer`](https://supervision.roboflow.com/latest/detection/tools/inference_slicer/#supervision.detection.tools.inference_slicer.InferenceSlicer) processes high-resolution images by dividing them into smaller segments, detecting objects within each, and aggregating the results.
+
+
+
+
+
+=== "Inference"
+
+ ```{ .py hl_lines="9-14" }
+ import cv2
+ import numpy as np
+ import supervision as sv
+ from inference import get_model
+
+ model = get_model(model_id="yolov8x-640")
+ image = cv2.imread("")
+
+ def callback(image_slice: np.ndarray) -> sv.Detections:
+ results = model.infer(image_slice)[0]
+ return sv.Detections.from_inference(results)
+
+ slicer = sv.InferenceSlicer(callback = callback)
+ detections = slicer(image)
+
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ 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
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8x.pt")
+ image = cv2.imread("")
+
+ def callback(image_slice: np.ndarray) -> sv.Detections:
+ result = model(image_slice)[0]
+ return sv.Detections.from_ultralytics(result)
+
+ slicer = sv.InferenceSlicer(callback = callback)
+ detections = slicer(image)
+
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ 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
+ import numpy as np
+ 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 = cv2.imread("")
+
+ def callback(image_slice: np.ndarray) -> sv.Detections:
+ image_slice = cv2.cvtColor(image_slice, cv2.COLOR_BGR2RGB)
+ image_slice = Image.fromarray(image_slice)
+ inputs = processor(images=image_slice, return_tensors="pt")
+
+ with torch.no_grad():
+ outputs = model(**inputs)
+
+ width, height = image_slice.size
+ target_size = torch.tensor([[width, height]])
+ results = processor.post_process_object_detection(
+ outputs=outputs, target_sizes=target_size)[0]
+ return sv.Detections.from_transformers(results)
+
+ slicer = sv.InferenceSlicer(callback = callback)
+ detections = slicer(image)
+
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ labels = [
+ model.config.id2label[class_id]
+ for class_id
+ in detections.class_id
+ ]
+
+ annotated_image = box_annotator.annotate(
+ scene=image, detections=detections)
+ annotated_image = label_annotator.annotate(
+ scene=annotated_image, detections=detections, labels=labels)
+ ```
+
+
+
+## Small Object Segmentation
+
+[`InferenceSlicer`](https://supervision.roboflow.com/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
+ import supervision as sv
+ from inference import get_model
+
+ model = get_model(model_id="yolov8x-seg-640")
+ image = cv2.imread("")
+
+ def callback(image_slice: np.ndarray) -> sv.Detections:
+ results = model.infer(image_slice)[0]
+ return sv.Detections.from_inference(results)
+
+ slicer = sv.InferenceSlicer(callback = callback)
+ detections = slicer(image)
+
+ mask_annotator = sv.MaskAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ annotated_image = mask_annotator.annotate(
+ scene=image, detections=detections)
+ annotated_image = label_annotator.annotate(
+ scene=annotated_image, detections=detections)
+ ```
+
+=== "Ultralytics"
+
+ ```{ .py hl_lines="6 16 19-20" }
+ import cv2
+ import numpy as np
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8x-seg.pt")
+ image = cv2.imread("")
+
+ def callback(image_slice: np.ndarray) -> sv.Detections:
+ result = model(image_slice)[0]
+ return sv.Detections.from_ultralytics(result)
+
+ slicer = sv.InferenceSlicer(callback = callback)
+ detections = slicer(image)
+
+ mask_annotator = sv.MaskAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ annotated_image = mask_annotator.annotate(
+ scene=image, detections=detections)
+ annotated_image = label_annotator.annotate(
+ scene=annotated_image, detections=detections)
+ ```
+
+
+
+## Frequently Asked Questions
+
+### How do I detect small objects with supervision?
+
+Use `sv.InferenceSlicer` to split a high-resolution image into overlapping tiles, run detection on each tile, and merge results with non-maximum suppression. This dramatically improves recall for tiny targets.
+
+### What overlap should I use between tiles?
+
+`InferenceSlicer` takes overlap in pixels via `overlap_wh`, not as a percentage. The default is `100` pixels in both directions. Increase `overlap_wh` when objects are close to the tile size or often appear on tile boundaries, and decrease it when speed is more important.
+
+### Can I use InferenceSlicer with any detection model?
+
+Yes. Wrap any model or converter path that can produce `sv.Detections` in a callback, pass that callback to `sv.InferenceSlicer(callback=...)`, and then call the slicer with your image.
+
+## Author
+
+- [Piotr Skalski](https://github.com/SkalskiP) โ Computer Vision Engineer, Roboflow
diff --git a/docs/how_to/filter_detections.md b/docs/how_to/filter_detections.md
new file mode 100644
index 0000000..5eed784
--- /dev/null
+++ b/docs/how_to/filter_detections.md
@@ -0,0 +1,335 @@
+---
+comments: true
+description: Filter and query detection results by class, confidence, or spatial overlap using supervision's Detections API โ clean predictions in one line.
+authors:
+ - name: Piotr Skalski
+ role: Computer Vision Engineer, Roboflow
+ github: https://github.com/SkalskiP
+date_modified: 2026-04-22
+---
+
+# Filter Detections
+
+The advanced filtering capabilities of the `Detections` class offer users a versatile and efficient way to narrow down and refine object detections. This section outlines various filtering methods, including filtering by specific class or a set of classes, confidence, object area, bounding box area, relative area, box dimensions, and designated zones. Each method is demonstrated with concise code examples to provide users with a clear understanding of how to implement the filters in their applications.
+
+### by specific class
+
+Allows you to select detections that belong only to one selected class.
+
+=== "After"
+
+ ```python
+ import supervision as sv
+
+ detections = sv.Detections(...)
+ detections = detections[detections.class_id == 0]
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+=== "Before"
+
+ ```python
+ import supervision as sv
+
+ detections = sv.Detections(...)
+ detections = detections[detections.class_id == 0]
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+### by set of classes
+
+Allows you to select detections that belong only to selected set of classes.
+
+=== "After"
+
+ ```python
+ import numpy as np
+ import supervision as sv
+
+ selected_classes = [0, 2, 3]
+ detections = sv.Detections(...)
+ detections = detections[np.isin(detections.class_id, selected_classes)]
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+=== "Before"
+
+ ```python
+ import numpy as np
+ import supervision as sv
+
+ class_id = [0, 2, 3]
+ detections = sv.Detections(...)
+ detections = detections[np.isin(detections.class_id, class_id)]
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+### by confidence
+
+Allows you to select detections with specific confidence value, for example higher than selected threshold.
+
+=== "After"
+
+ ```python
+ import supervision as sv
+
+ detections = sv.Detections(...)
+ detections = detections[detections.confidence > 0.5]
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+=== "Before"
+
+ ```python
+ import supervision as sv
+
+ detections = sv.Detections(...)
+ detections = detections[detections.confidence > 0.5]
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+### by area
+
+Allows you to select detections based on their size. We define the area as the number of pixels occupied by the detection in the image. In the example below, we have sifted out the detections that are too small.
+
+=== "After"
+
+ ```python
+ import supervision as sv
+
+ detections = sv.Detections(...)
+ detections = detections[detections.area > 1000]
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+=== "Before"
+
+ ```python
+ import supervision as sv
+
+ detections = sv.Detections(...)
+ detections = detections[detections.area > 1000]
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+### by relative area
+
+Allows you to select detections based on their size in relation to the size of whole image. Sometimes the concept of detection size changes depending on the image. Detection occupying 10000 square px can be large on a 1280x720 image but small on a 3840x2160 image. In such cases, we can filter out detections based on the percentage of the image area occupied by them. In the example below, we remove too large detections.
+
+=== "After"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ height, width, channels = image.shape
+ image_area = height * width
+
+ detections = sv.Detections(...)
+ detections = detections[(detections.area / image_area) < 0.8]
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+=== "Before"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ height, width, channels = image.shape
+ image_area = height * width
+
+ detections = sv.Detections(...)
+ detections = detections[(detections.area / image_area) < 0.8]
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+### by box dimensions
+
+Allows you to select detections based on their dimensions. The size of the bounding box, as well as its coordinates, can be criteria for rejecting detection. Implementing such filtering requires a bit of custom code but is relatively simple and fast.
+
+=== "After"
+
+ ```python
+ import supervision as sv
+
+ detections = sv.Detections(...)
+ w = detections.xyxy[:, 2] - detections.xyxy[:, 0]
+ h = detections.xyxy[:, 3] - detections.xyxy[:, 1]
+ detections = detections[(w > 200) & (h > 200)]
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+=== "Before"
+
+ ```python
+ import supervision as sv
+
+ detections = sv.Detections(...)
+ w = detections.xyxy[:, 2] - detections.xyxy[:, 0]
+ h = detections.xyxy[:, 3] - detections.xyxy[:, 1]
+ detections = detections[(w > 200) & (h > 200)]
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+### by `PolygonZone`
+
+Allows you to use `Detections` in combination with `PolygonZone` to weed out bounding boxes that are in and out of the 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
+
+ zone = sv.PolygonZone(...)
+ detections = sv.Detections(...)
+ mask = zone.trigger(detections=detections)
+ detections = detections[mask]
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+=== "Before"
+
+ ```python
+ import supervision as sv
+
+ zone = sv.PolygonZone(...)
+ detections = sv.Detections(...)
+ mask = zone.trigger(detections=detections)
+ detections = detections[mask]
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+### by mixed conditions
+
+`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
+
+ zone = sv.PolygonZone(...)
+ detections = sv.Detections(...)
+ mask = zone.trigger(detections=detections)
+ detections = detections[(detections.confidence > 0.7) & mask]
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+=== "Before"
+
+ ```python
+ import supervision as sv
+
+ zone = sv.PolygonZone(...)
+ detections = sv.Detections(...)
+ mask = zone.trigger(detections=detections)
+ detections = detections[mask]
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+## Frequently Asked Questions
+
+### How do I filter detections by class in supervision?
+
+Use NumPy-style boolean indexing: `detections[detections.class_id == 0]` for class 0. Combine with `&` or `|` for multiple conditions.
+
+### How do I filter by confidence threshold?
+
+`detections[detections.confidence > 0.5]` returns only detections above the threshold. Chain with class filters for precise results.
+
+### How do I filter by bounding box area?
+
+`detections[detections.area > 1000]` filters by pixel area. If masks are present, `detections.area` uses mask area; otherwise it uses bounding box area from `xyxy`. Use `detections.box_area` when you specifically need bounding box area.
+
+### Can I filter by box aspect ratio or dimensions?
+
+Yes. Use `detections.box_aspect_ratio` for aspect ratio filtering. If you need explicit box dimensions, compute them from `detections.xyxy` as `width = detections.xyxy[:, 2] - detections.xyxy[:, 0]` and `height = detections.xyxy[:, 3] - detections.xyxy[:, 1]`.
+
+### How do I remove duplicate detections (NMS) from my results?
+
+Use `detections.with_nms(threshold=0.5)` โ it applies non-maximum suppression on the `xyxy` boxes.
+
+## Author
+
+- [Piotr Skalski](https://github.com/SkalskiP) โ Computer Vision Engineer, Roboflow
diff --git a/docs/how_to/process_datasets.md b/docs/how_to/process_datasets.md
new file mode 100644
index 0000000..a2dffe6
--- /dev/null
+++ b/docs/how_to/process_datasets.md
@@ -0,0 +1,605 @@
+---
+comments: true
+description: Load, split, merge, and convert computer vision datasets between YOLO, COCO, Pascal VOC, CreateML, and LabelMe formats using supervision's DetectionDataset.
+authors:
+ - name: Piotr Skalski
+ role: Computer Vision Engineer, Roboflow
+ github: https://github.com/SkalskiP
+date_modified: 2026-06-25
+---
+
+With Supervision, you can load and manipulate classification, object detection, and segmentation datasets. This tutorial will walk you through how to load, split, merge, visualize, and augment datasets in Supervision.
+
+## Download Dataset
+
+In this tutorial, we will use a dataset from [Roboflow Universe](https://universe.roboflow.com/), a public repository of thousands of computer vision datasets. If you already have your dataset in [COCO](https://roboflow.com/formats/coco-json), [YOLO](https://roboflow.com/formats/yolov8-pytorch-txt), [Pascal VOC](https://roboflow.com/formats/pascal-voc-xml), [CreateML](https://roboflow.com/formats/createml-json), or [LabelMe](https://roboflow.com/formats/labelme-json) format, you can skip this section.
+
+```bash
+pip install roboflow
+```
+
+Next, log into your Roboflow account and download the dataset of your choice. The following snippets show common COCO, YOLO, Pascal VOC, and CreateML exports; LabelMe datasets can also be loaded directly from per-image JSON files in the next section. You can customize the code with your workspace ID, project ID, and version number.
+
+=== "COCO"
+
+ ```python
+ import roboflow
+
+ roboflow.login()
+
+ rf = roboflow.Roboflow()
+ project = rf.workspace("").project("")
+ dataset = project.version("").download("coco")
+ ```
+
+=== "YOLO"
+
+ ```python
+ import roboflow
+
+ roboflow.login()
+
+ rf = roboflow.Roboflow()
+ project = rf.workspace("").project("")
+ dataset = project.version("").download("yolov8")
+ ```
+
+=== "Pascal VOC"
+
+ ```python
+ import roboflow
+
+ roboflow.login()
+
+ rf = roboflow.Roboflow()
+ project = rf.workspace("").project("")
+ dataset = project.version("").download("voc")
+ ```
+
+=== "CreateML"
+
+ ```python
+ import roboflow
+
+ roboflow.login()
+
+ rf = roboflow.Roboflow()
+ project = rf.workspace("").project("")
+ dataset = project.version("").download("createml")
+ ```
+
+## Load Dataset
+
+The Supervision library provides convenient functions to load datasets in various formats. If your dataset is already split into train, test, and valid subsets, you can load each of those as separate [`sv.DetectionDataset`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset) instances.
+
+=== "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
+ ```
+
+=== "CreateML"
+
+ We can do so using the [`sv.DetectionDataset.from_createml`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.from_createml) to load annotations in [CreateML](https://roboflow.com/formats/createml-json) format.
+
+ ```python
+ import supervision as sv
+
+ ds_train = sv.DetectionDataset.from_createml(
+ images_directory_path=f"{dataset.location}/train",
+ annotations_path=f"{dataset.location}/train/_annotations.createml.json",
+ )
+ ds_valid = sv.DetectionDataset.from_createml(
+ images_directory_path=f"{dataset.location}/valid",
+ annotations_path=f"{dataset.location}/valid/_annotations.createml.json",
+ )
+ ds_test = sv.DetectionDataset.from_createml(
+ images_directory_path=f"{dataset.location}/test",
+ annotations_path=f"{dataset.location}/test/_annotations.createml.json",
+ )
+
+ ds_train.classes
+ # ['person', 'bicycle', 'car', ...]
+
+ len(ds_train), len(ds_valid), len(ds_test)
+ # 800, 100, 100
+ ```
+
+=== "LabelMe"
+
+ We can do so using the [`sv.DetectionDataset.from_labelme`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.from_labelme) to load annotations in [LabelMe](https://roboflow.com/formats/labelme-json) format. LabelMe `rectangle` shapes are loaded as bounding boxes and `polygon` shapes are loaded as masks with bounding boxes.
+
+ ```python
+ import supervision as sv
+
+ ds_train = sv.DetectionDataset.from_labelme(
+ images_directory_path="",
+ annotations_directory_path="",
+ )
+ ds_valid = sv.DetectionDataset.from_labelme(
+ images_directory_path="",
+ annotations_directory_path="",
+ )
+ ds_test = sv.DetectionDataset.from_labelme(
+ images_directory_path="",
+ annotations_directory_path="",
+ )
+
+ ds_train.classes
+ # ['person', 'bicycle', 'car', ...]
+
+ len(ds_train), len(ds_valid), len(ds_test)
+ # 800, 100, 100
+ ```
+
+## Split Dataset
+
+If your dataset is not already split into train, test, and valid subsets, you can easily do so using the [`sv.DetectionDataset.split`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.split) method. We can split it as follows, ensuring a random shuffle of the data.
+
+```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
+ ```
+
+=== "CreateML"
+
+ ```{ .py hl_lines="22-28" }
+ import supervision as sv
+
+ ds_train = sv.DetectionDataset.from_createml(
+ images_directory_path=f'{dataset.location}/train',
+ annotations_path=f'{dataset.location}/train/_annotations.createml.json',
+ )
+ ds_valid = sv.DetectionDataset.from_createml(
+ images_directory_path=f'{dataset.location}/valid',
+ annotations_path=f'{dataset.location}/valid/_annotations.createml.json',
+ )
+ ds_test = sv.DetectionDataset.from_createml(
+ images_directory_path=f'{dataset.location}/test',
+ annotations_path=f'{dataset.location}/test/_annotations.createml.json',
+ )
+
+ ds_train.classes
+ # ['person', 'bicycle', 'car', ...]
+
+ len(ds_train), len(ds_valid), len(ds_test)
+ # 800, 100, 100
+
+ ds = sv.DetectionDataset.merge([ds_train, ds_valid, ds_test])
+
+ ds.classes
+ # ['person', 'bicycle', 'car', ...]
+
+ len(ds)
+ # 1000
+ ```
+
+=== "LabelMe"
+
+ ```{ .py hl_lines="22-28" }
+ import supervision as sv
+
+ ds_train = sv.DetectionDataset.from_labelme(
+ images_directory_path="",
+ annotations_directory_path="",
+ )
+ ds_valid = sv.DetectionDataset.from_labelme(
+ images_directory_path="",
+ annotations_directory_path="",
+ )
+ ds_test = sv.DetectionDataset.from_labelme(
+ images_directory_path="",
+ annotations_directory_path="",
+ )
+
+ ds_train.classes
+ # ['person', 'bicycle', 'car', ...]
+
+ len(ds_train), len(ds_valid), len(ds_test)
+ # 800, 100, 100
+
+ ds = sv.DetectionDataset.merge([ds_train, ds_valid, ds_test])
+
+ ds.classes
+ # ['person', 'bicycle', 'car', ...]
+
+ len(ds)
+ # 1000
+ ```
+
+## Iterate over Dataset
+
+There are two ways to loop over a `sv.DetectionDataset`: using a direct [for loop](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.__iter__) called on the `sv.DetectionDataset` instance or loading `sv.DetectionDataset` entries [by index](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.__getitem__).
+
+```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)
+
+sv.plot_images_grid(
+ annotated_images,
+ grid_size=(4, 4),
+)
+```
+
+
+
+## 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="",
+ 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="",
+ annotations_directory_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="",
+ annotations_directory_path="",
+ )
+ ```
+
+=== "CreateML"
+
+ We can do so using the [`sv.DetectionDataset.as_createml`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.as_createml) method to save annotations in [CreateML](https://roboflow.com/formats/createml-json) format.
+
+ ```python
+ import supervision as sv
+
+ ds = sv.DetectionDataset(...)
+
+ ds.as_createml(
+ images_directory_path="",
+ annotations_path="",
+ )
+ ```
+
+=== "LabelMe"
+
+ We can do so using the [`sv.DetectionDataset.as_labelme`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.as_labelme) method to save annotations in [LabelMe](https://roboflow.com/formats/labelme-json) format. Detections with masks are exported as `polygon` shapes; box-only detections are exported as `rectangle` shapes.
+
+ ```python
+ import supervision as sv
+
+ ds = sv.DetectionDataset(...)
+
+ ds.as_labelme(
+ images_directory_path="",
+ 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 albumentations
+```
+
+Albumentations provides a flexible and powerful API for image augmentation. The core of the library is the [`Compose`](https://albumentations.ai/docs/api-reference/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/albumentations/augmentations/geometric/flip/#HorizontalFlip), [`RandomBrightnessContrast`](https://albumentations.ai/docs/api-reference/albumentations/augmentations/pixel/transforms/#RandomBrightnessContrast), or [`Perspective`](https://albumentations.ai/docs/api-reference/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"]),
+)
+```
+
+
+
+## Frequently Asked Questions
+
+### What dataset formats does supervision support?
+
+For detection datasets, supervision supports YOLO, COCO JSON, Pascal VOC, CreateML, and LabelMe. Use `DetectionDataset.from_yolo()`, `from_coco()`, `from_pascal_voc()`, `from_createml()`, or `from_labelme()` to load, and `as_yolo()`, `as_coco()`, `as_pascal_voc()`, `as_createml()`, or `as_labelme()` to save. Classification datasets use `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
+
+### Can I split a dataset into train/val/test sets?
+
+`DetectionDataset.split(split_ratio=0.8)` returns exactly two datasets: train (80%) and test (20%). If you need a validation set, split one of those subsets in a separate step.
+
+### Can I merge two datasets together?
+
+Yes. `DetectionDataset.merge([dataset_a, dataset_b])` combines multiple datasets into one. Useful for combining datasets from different sources.
+
+### What augmentations are available?
+
+Common augmentations such as flip, rotate, translate, scale, crop, color jitter, and Gaussian blur can be applied using an external library like Albumentations, as shown in the augmentation example above. Supervision does not provide an `sv.Augmenter` pipeline.
+
+## Author
+
+- [Piotr Skalski](https://github.com/SkalskiP) โ Computer Vision Engineer, Roboflow
diff --git a/docs/how_to/save_detections.md b/docs/how_to/save_detections.md
new file mode 100644
index 0000000..1266c97
--- /dev/null
+++ b/docs/how_to/save_detections.md
@@ -0,0 +1,305 @@
+---
+comments: true
+description: Save object detection results to CSV or JSON with supervision's CSVSink and JSONSink โ export predictions for analysis and downstream pipelines.
+authors:
+ - name: Piotr Skalski
+ role: Computer Vision Engineer, Roboflow
+ github: https://github.com/SkalskiP
+date_modified: 2026-04-22
+---
+
+# Save Detections
+
+Supervision enables an easy way to save detections in .CSV and .JSON files for offline processing. This guide demonstrates how to perform video inference using the [Inference](https://github.com/roboflow/inference), [Ultralytics](https://github.com/ultralytics/ultralytics) or [Transformers](https://github.com/huggingface/transformers) packages and save their results with [`sv.CSVSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.csv_sink.CSVSink) and [`sv.JSONSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.json_sink.JSONSink).
+
+## Run Detection
+
+First, you'll need to obtain predictions from your object detection or segmentation model. You can learn more on this topic in our [How to Detect and Annotate](https://supervision.roboflow.com/latest/how_to/detect_and_annotate/) guide.
+
+To generate predictions for saving, initialize your model and iterate over video frames using `sv.get_video_frames_generator`. Each frame is passed to the model, and the raw output is converted into a `sv.Detections` object. This detection loop forms the foundation for both CSV and JSON export workflows shown below.
+
+=== "Inference"
+
+ ```python
+ import supervision as sv
+ from inference import get_model
+
+ model = get_model(model_id="yolov8n-640")
+ frames_generator = sv.get_video_frames_generator("")
+
+ for frame in frames_generator:
+ results = model.infer(image)[0]
+ detections = sv.Detections.from_inference(results)
+ ```
+
+=== "Ultralytics"
+
+ ```python
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8n.pt")
+ frames_generator = sv.get_video_frames_generator("")
+
+ for frame in frames_generator:
+ results = model(frame)[0]
+ detections = sv.Detections.from_ultralytics(results)
+ ```
+
+=== "Transformers"
+
+ ```python
+ import torch
+ import supervision as sv
+ from transformers import DetrImageProcessor, DetrForObjectDetection
+
+ processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50")
+ model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50")
+ frames_generator = sv.get_video_frames_generator("")
+
+ for frame in frames_generator:
+ frame = sv.cv2_to_pillow(frame)
+ inputs = processor(images=frame, return_tensors="pt")
+
+ with torch.no_grad():
+ outputs = model(**inputs)
+
+ width, height = frame.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(results)
+ ```
+
+## Save Detections as CSV
+
+To save detections to a `.CSV` file, open our [`sv.CSVSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.csv_sink.CSVSink) and then pass the [`sv.Detections`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections) 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
+
+ model = get_model(model_id="yolov8n-640")
+ frames_generator = sv.get_video_frames_generator("")
+
+ with sv.CSVSink("") as sink:
+ for frame in frames_generator:
+
+ results = model.infer(image)[0]
+ detections = sv.Detections.from_inference(results)
+ sink.append(detections, {})
+ ```
+
+=== "Ultralytics"
+
+ ```{ .py hl_lines="7 12" }
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8n.pt")
+ frames_generator = sv.get_video_frames_generator("")
+
+ with sv.CSVSink("") as sink:
+ for frame in frames_generator:
+
+ results = model(frame)[0]
+ detections = sv.Detections.from_ultralytics(results)
+ sink.append(detections, {})
+ ```
+
+=== "Transformers"
+
+ ```{ .py hl_lines="9 23" }
+ import torch
+ import supervision as sv
+ from transformers import DetrImageProcessor, DetrForObjectDetection
+
+ processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50")
+ model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50")
+ frames_generator = sv.get_video_frames_generator("")
+
+ with sv.CSVSink("") as sink:
+ for frame in frames_generator:
+
+ frame = sv.cv2_to_pillow(frame)
+ inputs = processor(images=frame, return_tensors="pt")
+
+ with torch.no_grad():
+ outputs = model(**inputs)
+
+ width, height = frame.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(results)
+ 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 |
+
+## Custom Fields
+
+Besides regular fields in [`sv.Detections`](https://supervision.roboflow.com/latest/detection/core/#supervision.detection.core.Detections), [`sv.CSVSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.csv_sink.CSVSink) also allows you to add custom information to each row, which can be passed via the `custom_data` dictionary. Let's utilize this feature to save information about the frame index from which the detections originate.
+
+=== "Inference"
+
+ ```{ .py hl_lines="8 12" }
+ import supervision as sv
+ from inference import get_model
+
+ model = get_model(model_id="yolov8n-640")
+ frames_generator = sv.get_video_frames_generator("")
+
+ with sv.CSVSink("") as sink:
+ for frame_index, frame in enumerate(frames_generator):
+
+ results = model.infer(image)[0]
+ detections = sv.Detections.from_inference(results)
+ sink.append(detections, {"frame_index": frame_index})
+ ```
+
+=== "Ultralytics"
+
+ ```{ .py hl_lines="8 12" }
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8n.pt")
+ frames_generator = sv.get_video_frames_generator("")
+
+ with sv.CSVSink("") as sink:
+ for frame_index, frame in enumerate(frames_generator):
+
+ results = model(frame)[0]
+ detections = sv.Detections.from_ultralytics(results)
+ sink.append(detections, {"frame_index": frame_index})
+ ```
+
+=== "Transformers"
+
+ ```{ .py hl_lines="10 23" }
+ import torch
+ import supervision as sv
+ from transformers import DetrImageProcessor, DetrForObjectDetection
+
+ processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50")
+ model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50")
+ frames_generator = sv.get_video_frames_generator("")
+
+ with sv.CSVSink("") as sink:
+ for frame_index, frame in enumerate(frames_generator):
+
+ frame = sv.cv2_to_pillow(frame)
+ inputs = processor(images=frame, return_tensors="pt")
+
+ with torch.no_grad():
+ outputs = model(**inputs)
+
+ width, height = frame.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(results)
+ 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 |
+
+## Save Detections as JSON
+
+If you prefer to save the result in a `.JSON` file instead of a `.CSV` file, all you need to do is replace [`sv.CSVSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.csv_sink.CSVSink) with [`sv.JSONSink`](https://supervision.roboflow.com/latest/detection/tools/save_detections/#supervision.detection.tools.json_sink.JSONSink).
+
+=== "Inference"
+
+ ```{ .py hl_lines="7" }
+ import supervision as sv
+ from inference import get_model
+
+ model = get_model(model_id="yolov8n-640")
+ frames_generator = sv.get_video_frames_generator("")
+
+ with sv.JSONSink("") as sink:
+ for frame_index, frame in enumerate(frames_generator):
+
+ results = model.infer(image)[0]
+ detections = sv.Detections.from_inference(results)
+ sink.append(detections, {"frame_index": frame_index})
+ ```
+
+=== "Ultralytics"
+
+ ```{ .py hl_lines="7" }
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8n.pt")
+ frames_generator = sv.get_video_frames_generator("")
+
+ with sv.JSONSink("") as sink:
+ for frame_index, frame in enumerate(frames_generator):
+
+ results = model(frame)[0]
+ detections = sv.Detections.from_ultralytics(results)
+ sink.append(detections, {"frame_index": frame_index})
+ ```
+
+=== "Transformers"
+
+ ```{ .py hl_lines="9" }
+ import torch
+ import supervision as sv
+ from transformers import DetrImageProcessor, DetrForObjectDetection
+
+ processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50")
+ model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50")
+ frames_generator = sv.get_video_frames_generator("")
+
+ with sv.JSONSink("") as sink:
+ for frame_index, frame in enumerate(frames_generator):
+
+ frame = sv.cv2_to_pillow(frame)
+ inputs = processor(images=frame, return_tensors="pt")
+
+ with torch.no_grad():
+ outputs = model(**inputs)
+
+ width, height = frame.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(results)
+ sink.append(detections, {"frame_index": frame_index})
+ ```
+
+## Frequently Asked Questions
+
+### How do I save detections to CSV with supervision?
+
+Open `sv.CSVSink("output.csv")` as a context manager and call `sink.append(detections)` for each frame. The CSV includes box coordinates, confidence, class ID, tracker ID, and any fields stored in `detections.data`.
+
+### Can I save detections to JSON instead?
+
+Yes. Open `sv.JSONSink("output.json")` as a context manager and call `sink.append(detections)` for each frame. The file is written as a JSON array when the context exits.
+
+### Can I add custom fields to the saved output?
+
+Yes. Pass a dict as the second argument: `sink.append(detections, {"frame_index": 5})` โ the keys become extra columns in the CSV or extra fields in the JSON.
+
+### Can I save only specific classes or confidence levels?
+
+Filter the `Detections` object before saving: `sink.append(detections[detections.confidence > 0.7])`.
+
+## Author
+
+- [Piotr Skalski](https://github.com/SkalskiP) โ Computer Vision Engineer, Roboflow
diff --git a/docs/how_to/track_objects.md b/docs/how_to/track_objects.md
new file mode 100644
index 0000000..20cbefb
--- /dev/null
+++ b/docs/how_to/track_objects.md
@@ -0,0 +1,663 @@
+---
+comments: true
+description: Track objects across video frames with ByteTrack in supervision โ assign persistent IDs and analyze motion from any object detection model.
+authors:
+ - name: Piotr Skalski
+ role: Computer Vision Engineer, Roboflow
+ github: https://github.com/SkalskiP
+ - name: Soumik Mandal
+ role: ML Engineer, Roboflow
+ github: https://github.com/soumik12345
+date_modified: 2026-04-22
+---
+
+# Track Objects
+
+Leverage Supervision's advanced capabilities for enhancing your video analysis by seamlessly [tracking](https://supervision.roboflow.com/latest/trackers/) objects recognized by a multitude of object detection, segmentation and keypoint models. This comprehensive guide will take you through the steps to perform inference using the YOLOv8 model via either the [Inference](https://github.com/roboflow/inference) or [Ultralytics](https://github.com/ultralytics/ultralytics) packages. Following this, you'll discover how to track these objects efficiently and annotate your video content for a deeper analysis.
+
+## Object Detection & Segmentation
+
+To make it easier for you to follow our tutorial download the video we will use as an example. You can do this using the [`supervision.assets`](https://supervision.roboflow.com/latest/assets/) module included in the base package.
+
+This section demonstrates how to detect and segment objects in video frames using YOLOv8 with either the Inference or Ultralytics package. You will download a sample video, define a per-frame callback function that runs model prediction, and process the entire video to produce an annotated output file.
+
+```python
+from supervision.assets import download_assets, VideoAssets
+
+download_assets(VideoAssets.PEOPLE_WALKING)
+```
+
+
+
+
+
+### Run Inference
+
+First, you'll need to obtain predictions from your object detection or segmentation model. In this tutorial, we are using the YOLOv8 model as an example. However, Supervision is versatile and compatible with various models. Check this [link](https://supervision.roboflow.com/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 by obtaining model predictions and then annotating the frame based on these predictions. This `callback` function will be essential in the subsequent steps of the tutorial, as it will be modified to include tracking, labeling, and trace annotations.
+
+!!! tip
+
+ Both object detection and segmentation models are supported. Try it with `yolov8n.pt` or `yolov8n-640-seg`!
+
+=== "Ultralytics"
+
+ ```{ .py }
+ import numpy as np
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8n.pt")
+ box_annotator = sv.BoxAnnotator()
+
+ def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model(frame)[0]
+ detections = sv.Detections.from_ultralytics(results)
+ return box_annotator.annotate(frame.copy(), detections=detections)
+
+ sv.process_video(
+ source_path="people-walking.mp4",
+ target_path="result.mp4",
+ callback=callback
+ )
+ ```
+
+=== "Inference"
+
+ ```{ .py }
+ import numpy as np
+ import supervision as sv
+ from inference.models.utils import get_roboflow_model
+
+ model = get_roboflow_model(model_id="yolov8n-640", api_key="")
+ box_annotator = sv.BoxAnnotator()
+
+ def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model.infer(frame)[0]
+ detections = sv.Detections.from_inference(results)
+ return box_annotator.annotate(frame.copy(), detections=detections)
+
+ sv.process_video(
+ source_path="people-walking.mp4",
+ target_path="result.mp4",
+ callback=callback
+ )
+ ```
+
+
+
+
+
+### Tracking
+
+After running inference and obtaining predictions, the next step is to track the detected objects throughout the video. Utilizing Supervisionโs [`sv.ByteTrack`](https://supervision.roboflow.com/latest/trackers/#supervision.tracker.byte_tracker.core.ByteTrack) functionality, each detected object is assigned a unique tracker ID, enabling the continuous following of the object's motion path across different frames.
+
+!!! warning "Deprecated tracker wrapper"
+
+ `sv.ByteTrack` is deprecated in favor of `ByteTrackTracker` from the external `trackers` package. The external tracker uses `update()` instead of `update_with_detections()`.
+
+=== "Ultralytics"
+
+ ```{ .py hl_lines="6 12" }
+ import numpy as np
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8n.pt")
+ tracker = sv.ByteTrack()
+ box_annotator = sv.BoxAnnotator()
+
+ def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model(frame)[0]
+ detections = sv.Detections.from_ultralytics(results)
+ detections = tracker.update_with_detections(detections)
+ return box_annotator.annotate(frame.copy(), detections=detections)
+
+ sv.process_video(
+ source_path="people-walking.mp4",
+ target_path="result.mp4",
+ callback=callback
+ )
+ ```
+
+=== "Inference"
+
+ ```{ .py hl_lines="6 12" }
+ import numpy as np
+ import supervision as sv
+ from inference.models.utils import get_roboflow_model
+
+ model = get_roboflow_model(model_id="yolov8n-640", api_key="")
+ tracker = sv.ByteTrack()
+ box_annotator = sv.BoxAnnotator()
+
+ def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model.infer(frame)[0]
+ detections = sv.Detections.from_inference(results)
+ detections = tracker.update_with_detections(detections)
+ return box_annotator.annotate(frame.copy(), detections=detections)
+
+ sv.process_video(
+ source_path="people-walking.mp4",
+ target_path="result.mp4",
+ callback=callback
+ )
+ ```
+
+### Annotate Video with Tracking IDs
+
+Annotating the video with tracking IDs helps in distinguishing and following each object distinctly. With the [`sv.LabelAnnotator`](https://supervision.roboflow.com/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
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8n.pt")
+ tracker = sv.ByteTrack()
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model(frame)[0]
+ detections = sv.Detections.from_ultralytics(results)
+ detections = tracker.update_with_detections(detections)
+
+ labels = [
+ f"#{tracker_id} {class_name}"
+ for class_name, tracker_id
+ in zip(detections.data["class_name"], detections.tracker_id)
+ ]
+
+ annotated_frame = box_annotator.annotate(
+ frame.copy(), detections=detections)
+ return label_annotator.annotate(
+ annotated_frame, detections=detections, labels=labels)
+
+ sv.process_video(
+ source_path="people-walking.mp4",
+ target_path="result.mp4",
+ callback=callback
+ )
+ ```
+
+=== "Inference"
+
+ ```{ .py hl_lines="8 15-19 23-24" }
+ import numpy as np
+ import supervision as sv
+ from inference.models.utils import get_roboflow_model
+
+ model = get_roboflow_model(model_id="yolov8n-640", api_key="")
+ tracker = sv.ByteTrack()
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+
+ def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model.infer(frame)[0]
+ detections = sv.Detections.from_inference(results)
+ detections = tracker.update_with_detections(detections)
+
+ labels = [
+ f"#{tracker_id} {class_name}"
+ for class_name, tracker_id
+ in zip(detections.data["class_name"], detections.tracker_id)
+ ]
+
+ annotated_frame = box_annotator.annotate(
+ frame.copy(), detections=detections)
+ return label_annotator.annotate(
+ annotated_frame, detections=detections, labels=labels)
+
+ sv.process_video(
+ source_path="people-walking.mp4",
+ target_path="result.mp4",
+ callback=callback
+ )
+ ```
+
+
+
+
+
+### Annotate Video with Traces
+
+Adding traces to the video involves overlaying the historical paths of the detected objects. This feature, powered by the [`sv.TraceAnnotator`](https://supervision.roboflow.com/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
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8n.pt")
+ tracker = sv.ByteTrack()
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+ trace_annotator = sv.TraceAnnotator()
+
+ def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model(frame)[0]
+ detections = sv.Detections.from_ultralytics(results)
+ detections = tracker.update_with_detections(detections)
+
+ labels = [
+ f"#{tracker_id} {class_name}"
+ for class_name, tracker_id
+ in zip(detections.data["class_name"], detections.tracker_id)
+ ]
+
+ annotated_frame = box_annotator.annotate(
+ frame.copy(), detections=detections)
+ annotated_frame = label_annotator.annotate(
+ annotated_frame, detections=detections, labels=labels)
+ return trace_annotator.annotate(
+ annotated_frame, detections=detections)
+
+ sv.process_video(
+ source_path="people-walking.mp4",
+ target_path="result.mp4",
+ callback=callback
+ )
+ ```
+
+=== "Inference"
+
+ ```{ .py hl_lines="9 26-27" }
+ import numpy as np
+ import supervision as sv
+ from inference.models.utils import get_roboflow_model
+
+ model = get_roboflow_model(model_id="yolov8n-640", api_key="")
+ tracker = sv.ByteTrack()
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+ trace_annotator = sv.TraceAnnotator()
+
+ def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model.infer(frame)[0]
+ detections = sv.Detections.from_inference(results)
+ detections = tracker.update_with_detections(detections)
+
+ labels = [
+ f"#{tracker_id} {class_name}"
+ for class_name, tracker_id
+ in zip(detections.data["class_name"], detections.tracker_id)
+ ]
+
+ annotated_frame = box_annotator.annotate(
+ frame.copy(), detections=detections)
+ annotated_frame = label_annotator.annotate(
+ annotated_frame, detections=detections, labels=labels)
+ return trace_annotator.annotate(
+ annotated_frame, detections=detections)
+
+ sv.process_video(
+ source_path="people-walking.mp4",
+ target_path="result.mp4",
+ callback=callback
+ )
+ ```
+
+
+
+
+
+## Keypoints
+
+Models aren't limited to object detection and segmentation. Keypoint detection allows for detailed analysis of body joints and connections, especially valuable for applications like human pose estimation. This section introduces keypoint tracking. We'll walk through the steps of annotating keypoints, converting them into bounding box detections compatible with `ByteTrack`, and applying detection smoothing for enhanced stability.
+
+To make it easier for you to follow our tutorial, let's download the video we will use as an example. You can do this using the [`supervision.assets`](https://supervision.roboflow.com/latest/assets/) module included in the base package.
+
+```python
+from supervision.assets import download_assets, VideoAssets
+
+download_assets(VideoAssets.SKIING)
+```
+
+
+
+
+
+### Keypoint Detection
+
+First, you'll need to obtain predictions from your keypoint detection model. In this tutorial, we are using the YOLOv8 model as an example. However, Supervision is versatile and compatible with various models. Check this [link](https://supervision.roboflow.com/latest/keypoint/core/) for guidance on how to plug in other models.
+
+We will define a `callback` function, which will process each frame of the video by obtaining model predictions and then annotating the frame based on these predictions.
+
+Let's immediately visualize the results with our [`EdgeAnnotator`](https://supervision.roboflow.com/latest/keypoint/annotators/#supervision.key_points.annotators.EdgeAnnotator) and [`VertexAnnotator`](https://supervision.roboflow.com/latest/keypoint/annotators/#supervision.key_points.annotators.VertexAnnotator).
+
+=== "Ultralytics"
+
+ ```{ .py hl_lines="5 10-11" }
+ import numpy as np
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8m-pose.pt")
+ edge_annotator = sv.EdgeAnnotator()
+ vertex_annotator = sv.VertexAnnotator()
+
+ def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model(frame)[0]
+ key_points = sv.KeyPoints.from_ultralytics(results)
+
+ annotated_frame = edge_annotator.annotate(
+ frame.copy(), key_points=key_points)
+ return vertex_annotator.annotate(
+ annotated_frame, key_points=key_points)
+
+ sv.process_video(
+ source_path="skiing.mp4",
+ target_path="result.mp4",
+ callback=callback
+ )
+ ```
+
+=== "Inference"
+
+ ```{ .py hl_lines="5-6 11-12" }
+ import numpy as np
+ import supervision as sv
+ from inference.models.utils import get_roboflow_model
+
+ model = get_roboflow_model(
+ model_id="yolov8m-pose-640", api_key="")
+ edge_annotator = sv.EdgeAnnotator()
+ vertex_annotator = sv.VertexAnnotator()
+
+ def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model.infer(frame)[0]
+ key_points = sv.KeyPoints.from_inference(results)
+
+ annotated_frame = edge_annotator.annotate(
+ frame.copy(), key_points=key_points)
+ return vertex_annotator.annotate(
+ annotated_frame, key_points=key_points)
+
+ sv.process_video(
+ source_path="skiing.mp4",
+ target_path="result.mp4",
+ callback=callback
+ )
+ ```
+
+
+
+
+
+### Convert to Detections
+
+Keypoint tracking is currently supported via the conversion of `KeyPoints` to `Detections`. This is achieved with the [`KeyPoints.as_detections()`](https://supervision.roboflow.com/latest/keypoint/core/#supervision.key_points.core.KeyPoints.as_detections) function.
+
+Let's convert to detections and visualize the results with our [`BoxAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.BoxAnnotator).
+
+!!! tip
+
+ You may use the `selected_keypoint_indices` argument to specify a subset of keypoints to convert. This is useful when some keypoints could be occluded. For example: a person might swing their arm, causing the elbow to be occluded by the torso sometimes.
+
+=== "Ultralytics"
+
+ ```{ .py hl_lines="8 13 19-20" }
+ import numpy as np
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8m-pose.pt")
+ edge_annotator = sv.EdgeAnnotator()
+ vertex_annotator = sv.VertexAnnotator()
+ box_annotator = sv.BoxAnnotator()
+
+ def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model(frame)[0]
+ key_points = sv.KeyPoints.from_ultralytics(results)
+ detections = key_points.as_detections()
+
+ annotated_frame = edge_annotator.annotate(
+ frame.copy(), key_points=key_points)
+ annotated_frame = vertex_annotator.annotate(
+ annotated_frame, key_points=key_points)
+ return box_annotator.annotate(
+ annotated_frame, detections=detections)
+
+ sv.process_video(
+ source_path="skiing.mp4",
+ target_path="result.mp4",
+ callback=callback
+ )
+ ```
+
+=== "Inference"
+
+ ```{ .py hl_lines="9 14 20-21" }
+ import numpy as np
+ import supervision as sv
+ from inference.models.utils import get_roboflow_model
+
+ model = get_roboflow_model(
+ model_id="yolov8m-pose-640", api_key="")
+ edge_annotator = sv.EdgeAnnotator()
+ vertex_annotator = sv.VertexAnnotator()
+ box_annotator = sv.BoxAnnotator()
+
+ def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model.infer(frame)[0]
+ key_points = sv.KeyPoints.from_inference(results)
+ detections = key_points.as_detections()
+
+ annotated_frame = edge_annotator.annotate(
+ frame.copy(), key_points=key_points)
+ annotated_frame = vertex_annotator.annotate(
+ annotated_frame, key_points=key_points)
+ return box_annotator.annotate(
+ annotated_frame, detections=detections)
+
+ sv.process_video(
+ source_path="skiing.mp4",
+ target_path="result.mp4",
+ callback=callback
+ )
+ ```
+
+
+
+
+
+### Keypoint Tracking
+
+Now that we have a `Detections` object, we can track it throughout the video. Utilizing Supervision's [`sv.ByteTrack`](https://supervision.roboflow.com/latest/trackers/#supervision.tracker.byte_tracker.core.ByteTrack) functionality, each detected object is assigned a unique tracker ID, enabling the continuous following of the object's motion path across different frames. We shall visualize the result with `TraceAnnotator`.
+
+=== "Ultralytics"
+
+ ```{ .py hl_lines="10-11 17 25-26" }
+ import numpy as np
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8m-pose.pt")
+ edge_annotator = sv.EdgeAnnotator()
+ vertex_annotator = sv.VertexAnnotator()
+ box_annotator = sv.BoxAnnotator()
+
+ tracker = sv.ByteTrack()
+ trace_annotator = sv.TraceAnnotator()
+
+ def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model(frame)[0]
+ key_points = sv.KeyPoints.from_ultralytics(results)
+ detections = key_points.as_detections()
+ detections = tracker.update_with_detections(detections)
+
+ annotated_frame = edge_annotator.annotate(
+ frame.copy(), key_points=key_points)
+ annotated_frame = vertex_annotator.annotate(
+ annotated_frame, key_points=key_points)
+ annotated_frame = box_annotator.annotate(
+ annotated_frame, detections=detections)
+ return trace_annotator.annotate(
+ annotated_frame, detections=detections)
+
+ sv.process_video(
+ source_path="skiing.mp4",
+ target_path="result.mp4",
+ callback=callback
+ )
+ ```
+
+=== "Inference"
+
+ ```{ .py hl_lines="11-12 18 26-27" }
+ import numpy as np
+ import supervision as sv
+ from inference.models.utils import get_roboflow_model
+
+ model = get_roboflow_model(
+ model_id="yolov8m-pose-640", api_key="")
+ edge_annotator = sv.EdgeAnnotator()
+ vertex_annotator = sv.VertexAnnotator()
+ box_annotator = sv.BoxAnnotator()
+
+ tracker = sv.ByteTrack()
+ trace_annotator = sv.TraceAnnotator()
+
+ def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model.infer(frame)[0]
+ key_points = sv.KeyPoints.from_inference(results)
+ detections = key_points.as_detections()
+ detections = tracker.update_with_detections(detections)
+
+ annotated_frame = edge_annotator.annotate(
+ frame.copy(), key_points=key_points)
+ annotated_frame = vertex_annotator.annotate(
+ annotated_frame, key_points=key_points)
+ annotated_frame = box_annotator.annotate(
+ annotated_frame, detections=detections)
+ return trace_annotator.annotate(
+ annotated_frame, detections=detections)
+
+ sv.process_video(
+ source_path="skiing.mp4",
+ target_path="result.mp4",
+ callback=callback
+ )
+ ```
+
+
+
+
+
+### Bonus: Smoothing
+
+We could stop here as we have successfully tracked the object detected by the keypoint model. However, we can further enhance the stability of the boxes by applying [`DetectionsSmoother`](https://supervision.roboflow.com/latest/detection/tools/smoother/). This tool helps in stabilizing the boxes by smoothing the bounding box coordinates across frames. It is very simple to use:
+
+=== "Ultralytics"
+
+ ```{ .py hl_lines="11 19" }
+ import numpy as np
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8m-pose.pt")
+ edge_annotator = sv.EdgeAnnotator()
+ vertex_annotator = sv.VertexAnnotator()
+ box_annotator = sv.BoxAnnotator()
+
+ tracker = sv.ByteTrack()
+ smoother = sv.DetectionsSmoother()
+ trace_annotator = sv.TraceAnnotator()
+
+ def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model(frame)[0]
+ key_points = sv.KeyPoints.from_ultralytics(results)
+ detections = key_points.as_detections()
+ detections = tracker.update_with_detections(detections)
+ detections = smoother.update_with_detections(detections)
+
+ annotated_frame = edge_annotator.annotate(
+ frame.copy(), key_points=key_points)
+ annotated_frame = vertex_annotator.annotate(
+ annotated_frame, key_points=key_points)
+ annotated_frame = box_annotator.annotate(
+ annotated_frame, detections=detections)
+ return trace_annotator.annotate(
+ annotated_frame, detections=detections)
+
+ sv.process_video(
+ source_path="skiing.mp4",
+ target_path="result.mp4",
+ callback=callback
+ )
+ ```
+
+=== "Inference"
+
+ ```{ .py hl_lines="12 20" }
+ import numpy as np
+ import supervision as sv
+ from inference.models.utils import get_roboflow_model
+
+ model = get_roboflow_model(
+ model_id="yolov8m-pose-640", api_key="")
+ edge_annotator = sv.EdgeAnnotator()
+ vertex_annotator = sv.VertexAnnotator()
+ box_annotator = sv.BoxAnnotator()
+
+ tracker = sv.ByteTrack()
+ smoother = sv.DetectionsSmoother()
+ trace_annotator = sv.TraceAnnotator()
+
+ def callback(frame: np.ndarray, _: int) -> np.ndarray:
+ results = model.infer(frame)[0]
+ key_points = sv.KeyPoints.from_inference(results)
+ detections = key_points.as_detections()
+ detections = tracker.update_with_detections(detections)
+ detections = smoother.update_with_detections(detections)
+
+ annotated_frame = edge_annotator.annotate(
+ frame.copy(), key_points=key_points)
+ annotated_frame = vertex_annotator.annotate(
+ annotated_frame, key_points=key_points)
+ annotated_frame = box_annotator.annotate(
+ annotated_frame, detections=detections)
+ return trace_annotator.annotate(
+ annotated_frame, detections=detections)
+
+ sv.process_video(
+ source_path="skiing.mp4",
+ target_path="result.mp4",
+ callback=callback
+ )
+ ```
+
+
+
+
+
+This structured walkthrough should give a detailed pathway to annotate videos effectively using Supervisionโs various functionalities, including object tracking and trace annotations.
+
+## Frequently Asked Questions
+
+### How do I track objects across video frames with supervision?
+
+Pass `Detections` to `sv.ByteTrack.update_with_detections()` on each frame. The tracker assigns persistent IDs. Combine with `sv.TraceAnnotator` to visualize trajectories. `sv.ByteTrack` is deprecated in favor of `ByteTrackTracker` from the `trackers` package, where the update method is named `update()`.
+
+### What should I know about ByteTrack?
+
+ByteTrack uses low-confidence detections during association, which can improve continuity during missed or weak detections. Supervision's built-in `ByteTrack` wrapper is deprecated in favor of the external `trackers` package.
+
+### Can I track instances instead of bounding boxes?
+
+Yes. ByteTrack tracks bounding boxes. For instance masks, use `sv.MaskAnnotator` with the tracker IDs to color-code each tracked object consistently.
+
+### Does ByteTrack work with any detection model?
+
+Yes. ByteTrack is model-agnostic - it accepts any `Detections` object with bounding boxes, regardless of the supported converter or model output that produced it.
+
+## Authors
+
+- [Piotr Skalski](https://github.com/SkalskiP) โ Computer Vision Engineer, Roboflow
+- [Soumik Mandal](https://github.com/soumik12345) โ ML Engineer, Roboflow
diff --git a/docs/how_to/use_compact_masks.md b/docs/how_to/use_compact_masks.md
new file mode 100644
index 0000000..7e984d4
--- /dev/null
+++ b/docs/how_to/use_compact_masks.md
@@ -0,0 +1,193 @@
+---
+comments: true
+description: Use CompactMask for memory-efficient instance segmentation in supervision โ ingest COCO RLE payloads, skip mask materialisation, and merge mixed dense and compact detections without allocating a full pixel stack.
+authors:
+ - name: Borda
+ role: Open Source Engineer, Roboflow
+ github: https://github.com/borda
+date_modified: 2026-07-01
+---
+
+# Use Compact Masks for Memory-Efficient Segmentation
+
+[CompactMask][supervision.detection.compact_mask.CompactMask] stores each instance mask as a run-length encoding of its bounding-box **crop** rather than a full `(H, W)` boolean frame. For high-resolution images with many sparse masks this can reduce memory from tens of gigabytes to tens of megabytes, and eliminates full-frame decode work in annotators that only need the cropped region.
+
+!!! Note
+
+ `sv.mask_to_xyxy` keeps supervision's inclusive max-coordinate convention for compatibility with `CompactMask` and current box-based adapters. Use `sv.mask_to_roi` when you need exclusive slice bounds for NumPy indexing or crop extraction.
+
+This guide covers the four main integration points:
+
+1. [Ingesting COCO RLE payloads directly as CompactMask](#ingest-coco-rle-payloads)
+2. [Parsing Roboflow Inference results without a dense stack](#parse-inference-results)
+3. [Skipping mask materialisation for box/label annotators](#skip-unnecessary-materialisation)
+4. [Merging mixed dense and compact detections](#merge-mixed-detections)
+
+---
+
+## Ingest COCO RLE Payloads
+
+If your model or API returns masks in the COCO RLE format (`{"size": [H, W], "counts": "..."}`) you can convert them directly to `CompactMask` without allocating an `(N, H, W)` boolean array:
+
+```python
+import numpy as np
+import supervision as sv
+from supervision.detection.compact_mask import CompactMask
+
+# Example: two COCO RLE masks for a 720ร1280 frame.
+# Replace the counts strings with actual compressed RLE payloads from your
+# model or API โ e.g., from pycocotools mask.encode() or an Inference response.
+rles = [
+ {"size": [720, 1280], "counts": "YOUR_RLE_COUNTS_STRING_HERE"},
+ {"size": [720, 1280], "counts": "YOUR_RLE_COUNTS_STRING_HERE"},
+]
+xyxy = np.array(
+ [
+ [100.0, 50.0, 400.0, 300.0],
+ [500.0, 200.0, 900.0, 600.0],
+ ]
+)
+
+compact = CompactMask.from_coco_rle(rles, xyxy, image_shape=(720, 1280))
+
+detections = sv.Detections(
+ xyxy=xyxy,
+ mask=compact,
+ class_id=np.array([0, 1]),
+)
+```
+
+`from_coco_rle` uses run-length arithmetic scoped to each bounding box so no dense pixel array is ever created. Uncompressed integer count lists are also accepted in place of compressed strings.
+
+---
+
+## Parse Inference Results
+
+`Detections.from_inference` accepts a `compact_masks=True` flag that routes the Roboflow RLE payload through `CompactMask.from_coco_rle` instead of decoding to a dense stack:
+
+```python
+import supervision as sv
+
+# result: a Roboflow Inference v2 response dict with instance masks.
+detections = sv.Detections.from_inference(result, compact_masks=True)
+
+from supervision.detection.compact_mask import CompactMask
+
+assert isinstance(detections.mask, CompactMask)
+```
+
+!!! Warning
+
+ `compact_masks=True` crops each mask to its detector bounding box. Pixels outside the box are silently dropped. For masks that extend meaningfully beyond the reported bounding box, use the default `compact_masks=False` (dense decode) to preserve all pixels.
+
+To convert an existing dense-mask `Detections` to compact at any point:
+
+```python
+detections_compact = detections.to_compact_masks()
+```
+
+---
+
+## Skip Unnecessary Materialisation
+
+Annotators that do not draw masks (box, label, circle, ellipse, trace, keypoint) expose `requires_mask = False`. Integrations can branch on this flag to avoid decoding compact or RLE masks before annotation:
+
+```python
+import supervision as sv
+
+annotators = [
+ sv.BoxAnnotator(),
+ sv.LabelAnnotator(),
+ sv.MaskAnnotator(), # requires_mask = True
+]
+
+for ann in annotators:
+ if ann.requires_mask:
+ # Annotator reads mask pixels โ CompactMask decodes lazily per crop.
+ scene = ann.annotate(scene, detections)
+ else:
+ # Annotator ignores masks โ strip mask field to eliminate any decode cost.
+ det_no_mask = sv.Detections(
+ xyxy=detections.xyxy,
+ confidence=detections.confidence,
+ class_id=detections.class_id,
+ )
+ scene = ann.annotate(scene, det_no_mask)
+```
+
+Annotators that set `requires_mask = True`: [MaskAnnotator][supervision.annotators.core.MaskAnnotator], [PolygonAnnotator][supervision.annotators.core.PolygonAnnotator], [HaloAnnotator][supervision.annotators.core.HaloAnnotator].
+
+All others default to `requires_mask = False`.
+
+!!! Note
+
+ `PolygonAnnotator` and `MaskAnnotator` both operate directly on `CompactMask` without materialising the full `(N, H, W)` frame โ passing compact detections to them is already efficient.
+
+---
+
+## Merge Mixed Detections
+
+When merging `Detections` objects that mix dense `ndarray` masks and `CompactMask` instances, `Detections.merge` converts dense inputs to `CompactMask` automatically. No full `(N, H, W)` stack is allocated:
+
+```python
+import numpy as np
+import supervision as sv
+from supervision.detection.compact_mask import CompactMask
+
+H, W = 720, 1280
+
+# Compact detections from an RLE-based source.
+# Replace the counts string with a real compressed RLE payload from your model or API.
+rles = [{"size": [H, W], "counts": "YOUR_RLE_COUNTS_STRING_HERE"}]
+xyxy_a = np.array([[100.0, 50.0, 400.0, 300.0]])
+cm = CompactMask.from_coco_rle(rles, xyxy_a, image_shape=(H, W))
+det_a = sv.Detections(xyxy=xyxy_a, mask=cm, class_id=np.array([0]))
+
+# Dense detections from a different source.
+masks_b = np.zeros((1, H, W), dtype=bool)
+masks_b[0, 200:400, 500:800] = True
+xyxy_b = np.array([[500.0, 200.0, 799.0, 399.0]])
+det_b = sv.Detections(xyxy=xyxy_b, mask=masks_b, class_id=np.array([1]))
+
+# Output is CompactMask regardless of input order.
+merged = sv.Detections.merge([det_a, det_b])
+assert isinstance(merged.mask, CompactMask)
+assert len(merged) == 2
+```
+
+Merge rules:
+
+| Inputs | Output mask type |
+| ------------------------------------- | ------------------------------- |
+| All `CompactMask` | `CompactMask` |
+| Mixed `CompactMask` + dense `ndarray` | `CompactMask` |
+| All dense `ndarray` | `ndarray` (backward compatible) |
+
+All `CompactMask` inputs must share the same `image_shape`; mismatches raise `ValueError`.
+
+---
+
+## Performance Notes
+
+These estimates apply to the **parsing and annotation stage**, not end-to-end pipeline FPS. Model inference typically dominates total runtime.
+
+| Optimisation | Realistic gain | Applies when |
+| ---------------------------- | -------------------------- | ------------------------------------------------------------- |
+| `from_coco_rle` ingestion | 25โ60% faster parse | Full-frame COCO RLE payload; current dense decode path |
+| `MaskAnnotator` ROI blending | 10โ35% faster annotation | Many small, sparse masks on high-res frames |
+| `PolygonAnnotator` crop path | 15โ45% faster polygon draw | Many compact masks; full-frame materialise was the bottleneck |
+| Mixed-mask merge | 5โ20% faster merge | Mix of compact and dense sources (e.g. multi-camera stitch) |
+
+Upper-end gains assume: โฅ1080p frames, tens to hundreds of instances, masks covering less than ~20% of total pixels.
+
+---
+
+## API Reference
+
+- [CompactMask][supervision.detection.compact_mask.CompactMask]
+- [CompactMask.from_coco_rle][supervision.detection.compact_mask.CompactMask.from_coco_rle]
+- [CompactMask.from_dense][supervision.detection.compact_mask.CompactMask.from_dense]
+- [Detections.from_inference][supervision.detection.core.Detections.from_inference]
+- [Detections.to_compact_masks][supervision.detection.core.Detections.to_compact_masks]
+- [Detections.merge][supervision.detection.core.Detections.merge]
+- [BaseAnnotator.requires_mask][supervision.annotators.base.BaseAnnotator]
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..29ca5cf
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,192 @@
+---
+template: index.html
+comments: true
+hide:
+ - navigation
+ - toc
+description: Open-source Python library providing computer vision tools for annotating detections, tracking objects, counting in zones, and processing datasets.
+---
+
+
+
+
+
+
+
+
+
+## What is Supervision?
+
+Supervision is an open-source Python library by Roboflow for building computer vision applications. It provides a unified `Detections` object with converters for supported outputs from Ultralytics, Roboflow Inference, Transformers, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers.
+
+With Supervision you can annotate images and video with bounding boxes, masks, and labels; track objects across frames with persistent IDs; count and filter detections inside polygon zones; load and convert datasets between YOLO, COCO, and Pascal VOC formats; and benchmark model performance with mAP and confusion matrices.
+
+Supervision is MIT licensed, has 38,000+ GitHub stars, and over 1 million monthly PyPI downloads. It is developed in public on GitHub for production computer vision workflows.
+
+## ๐ 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!
+
+
+
+
+
+## ๐ป Install
+
+You can install `supervision` in a [**Python>=3.10**](https://www.python.org/) environment.
+
+!!! example "Installation"
+
+ === "pip (recommended)"
+
+ [](https://badge.fury.io/py/supervision) [](https://pypistats.org/packages/supervision) [](../LICENSE.md) [](https://badge.fury.io/py/supervision)
+
+ ```bash
+ pip install supervision
+ ```
+
+ === "poetry"
+
+ [](https://badge.fury.io/py/supervision) [](https://pypistats.org/packages/supervision) [](../LICENSE.md) [](https://badge.fury.io/py/supervision)
+
+ ```bash
+ poetry add supervision
+ ```
+
+ === "uv"
+
+ [](https://badge.fury.io/py/supervision) [](https://pypistats.org/packages/supervision) [](../LICENSE.md) [](https://badge.fury.io/py/supervision)
+
+ ```bash
+ uv pip install supervision
+ ```
+
+ For uv projects:
+
+ ```bash
+ uv add supervision
+ ```
+
+ === "rye"
+
+ [](https://badge.fury.io/py/supervision) [](https://pypistats.org/packages/supervision) [](../LICENSE.md) [](https://badge.fury.io/py/supervision)
+
+ ```bash
+ rye add supervision
+ ```
+
+!!! example "conda/mamba install"
+
+ === "conda"
+
+ [](https://anaconda.org/conda-forge/supervision) [](https://anaconda.org/conda-forge/supervision) [](https://anaconda.org/conda-forge/supervision) [](https://anaconda.org/conda-forge/supervision)
+
+ ```bash
+ conda install -c conda-forge supervision
+ ```
+
+ === "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
+ mamba install -c conda-forge supervision
+ ```
+
+!!! example "git clone (for development)"
+
+ === "virtualenv"
+
+ ```bash
+ # clone repository and navigate to root directory
+ git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
+ cd supervision
+
+ # setup python environment and activate it
+ python3 -m venv venv
+ source venv/bin/activate
+ pip install --upgrade pip
+
+ # installation
+ pip install -e "."
+ ```
+
+ === "uv"
+
+ ```bash
+ # clone repository and navigate to root directory
+ git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
+ cd supervision
+
+ # setup python environment and activate it
+ uv venv
+ source .venv/bin/activate
+
+ # installation
+ uv pip install -r pyproject.toml -e . --all-extras
+
+ ```
+
+## ๐ Quickstart
+
+
+
+- **Detect and Annotate**
+
+ ---
+
+ Annotate predictions from a range of object detection and segmentation models
+
+ [:octicons-arrow-right-24: Tutorial](how_to/detect_and_annotate.md)
+
+- **Track Objects**
+
+ ---
+
+ Discover how to enhance video analysis by implementing seamless object tracking
+
+ [:octicons-arrow-right-24: Tutorial](how_to/track_objects.md)
+
+- **Detect Small Objects**
+
+ ---
+
+ Learn how to detect small objects in images
+
+ [:octicons-arrow-right-24: Tutorial](how_to/detect_small_objects.md)
+
+- **Count Objects Crossing Line**
+
+ ---
+
+ Explore methods to accurately count and analyze objects crossing a predefined line
+
+ [: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/)
+
+
diff --git a/docs/javascripts/cookbooks-card.js b/docs/javascripts/cookbooks-card.js
new file mode 100644
index 0000000..a909d23
--- /dev/null
+++ b/docs/javascripts/cookbooks-card.js
@@ -0,0 +1,143 @@
+document.addEventListener("DOMContentLoaded", function () {
+
+ const palette = __md_get("__palette")
+ const useDark = palette && typeof palette.color === "object" && palette.color.scheme === "slate"
+ const theme = useDark ? "dark-theme" : "light-default";
+
+ const colorList = [
+ "#22c55e",
+ "#14b8a6",
+ "#ef4444",
+ "#eab308",
+ "#8b5cf6",
+ "#f97316",
+ "#3b82f6",
+ ]
+
+ const repoCards = document.querySelectorAll(".repo-card");
+ const labelsAll = Array
+ .from(repoCards)
+ .flatMap((element) => element.getAttribute('data-labels').split(','))
+ .map(label => label.trim())
+ .filter(label => label !== '');
+ const uniqueLabels = [...new Set(labelsAll)];
+
+ const labelToColor = uniqueLabels.reduce((map, label, index) => {
+ map[label] = colorList[index % colorList.length];
+ return map;
+ }, {});
+
+
+ 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 `
+
+ ${label.trim()}
+
+ `;
+ }).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();
+ }));
+
+ let authorAvatarsHTML = authorDataArray.map((authorData, index) => {
+ const marginLeft = index === 0 ? '0' : '-10px';
+ const zIndex = 4 - index;
+ return `
+
+ `;
+ }).join('');
+
+ let authorNamesHTML = authorDataArray.map(
+ authorData => `
+
+
+ ${authorData.login}
+
+ `
+ ).join(', ');
+
+ let authorsHTML = `
+
+ ${authorAvatarsHTML}
+
${authorNamesHTML}
+
+ `;
+
+ element.innerText = `
+
+
+
+ ${name}
+
+
+ ${authorsHTML}
+
+
+
+
+
${version}
+
+
+ ${labelHTML}
+
+
+
+ `;
+
+ 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');
+ });
+
+ 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);
+ });
+})
diff --git a/docs/javascripts/init_kapa_widget.js b/docs/javascripts/init_kapa_widget.js
new file mode 100644
index 0000000..ffa121a
--- /dev/null
+++ b/docs/javascripts/init_kapa_widget.js
@@ -0,0 +1,10 @@
+document.addEventListener("DOMContentLoaded", function () {
+ var script = document.createElement("script");
+ script.src = "https://widget.kapa.ai/kapa-widget.bundle.js";
+ script.setAttribute("data-website-id", "e83c5c60-2968-410b-a2da-08fb104f23df");
+ script.setAttribute("data-project-name", "Roboflow");
+ script.setAttribute("data-project-color", "#6405C9");
+ script.setAttribute("data-project-logo", "https://media.roboflow.com/chat.png");
+ script.async = true;
+ document.head.appendChild(script);
+});
diff --git a/docs/javascripts/mathjax.js b/docs/javascripts/mathjax.js
new file mode 100644
index 0000000..0c7803c
--- /dev/null
+++ b/docs/javascripts/mathjax.js
@@ -0,0 +1,19 @@
+window.MathJax = {
+ tex: {
+ inlineMath: [["\\(", "\\)"]],
+ displayMath: [["\\[", "\\]"]],
+ processEscapes: true,
+ processEnvironments: true
+ },
+ options: {
+ ignoreHtmlClass: ".*|",
+ processHtmlClass: "arithmatex"
+ }
+ };
+
+ document$.subscribe(() => {
+ MathJax.startup.output.clearCache()
+ MathJax.typesetClear()
+ MathJax.texReset()
+ MathJax.typesetPromise()
+ })
diff --git a/docs/javascripts/pycon_copy.js b/docs/javascripts/pycon_copy.js
new file mode 100644
index 0000000..283753d
--- /dev/null
+++ b/docs/javascripts/pycon_copy.js
@@ -0,0 +1,197 @@
+/**
+ * Custom copy handler for Python console (pycon) code blocks.
+ * Strips >>> and ... prompts when copying code examples.
+ */
+document.addEventListener("DOMContentLoaded", function () {
+ const COPY_BUTTON_SELECTOR = ".md-clipboard, .md-code__button";
+
+ function handleCopyButtonClick(event) {
+ const copyButton = event.target.closest(COPY_BUTTON_SELECTOR);
+ if (!copyButton) return;
+
+ const codeBlock = findCodeBlockForCopyButton(copyButton);
+ if (!codeBlock) return;
+
+ const rawText = codeBlock.textContent || "";
+ if (!shouldStripPrompts(codeBlock, rawText)) return;
+
+ const strippedText = stripPythonPrompts(rawText);
+ primeClipboardButton(copyButton, strippedText);
+ }
+
+ function handleCopyButtonPointerDown(event) {
+ const copyButton = event.target.closest(COPY_BUTTON_SELECTOR);
+ if (!copyButton) return;
+
+ const codeBlock = findCodeBlockForCopyButton(copyButton);
+ if (!codeBlock) return;
+
+ const rawText = codeBlock.textContent || "";
+ if (!shouldStripPrompts(codeBlock, rawText)) return;
+
+ const strippedText = stripPythonPrompts(rawText);
+ primeClipboardButton(copyButton, strippedText);
+ }
+
+ function handleSelectionCopy(event) {
+ const selection = window.getSelection();
+ if (!selection || selection.rangeCount === 0) return;
+
+ const range = selection.getRangeAt(0);
+ const anchorNode = range.commonAncestorContainer;
+ const codeBlock =
+ anchorNode.nodeType === Node.ELEMENT_NODE
+ ? anchorNode.closest("code")
+ : anchorNode.parentElement?.closest("code");
+
+ if (!codeBlock) return;
+
+ const rawText = selection.toString();
+ if (!shouldStripPrompts(codeBlock, rawText)) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ const strippedText = stripPythonPrompts(rawText);
+ event.clipboardData?.setData("text/plain", strippedText);
+ }
+
+ function bindCopyButtons(root) {
+ root
+ .querySelectorAll(COPY_BUTTON_SELECTOR)
+ .forEach((button) => {
+ button.removeEventListener("click", handleCopyButtonClick, true);
+ button.addEventListener("click", handleCopyButtonClick, true);
+ button.removeEventListener(
+ "pointerdown",
+ handleCopyButtonPointerDown,
+ true
+ );
+ button.addEventListener(
+ "pointerdown",
+ handleCopyButtonPointerDown,
+ true
+ );
+ });
+ }
+
+ function observeDynamicCopyButtons() {
+ const observer = new MutationObserver((mutations) => {
+ for (const mutation of mutations) {
+ if (mutation.type !== "childList") continue;
+ mutation.addedNodes.forEach((node) => {
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
+ if (node.matches?.(COPY_BUTTON_SELECTOR)) {
+ bindCopyButtons(node.parentElement || document);
+ return;
+ }
+ if (node.querySelectorAll) {
+ const hasButtons = node.querySelectorAll(COPY_BUTTON_SELECTOR);
+ if (hasButtons.length > 0) {
+ bindCopyButtons(node);
+ }
+ }
+ });
+ }
+ });
+
+ observer.observe(document.body, { childList: true, subtree: true });
+ }
+
+ document.addEventListener("click", handleCopyButtonClick, true);
+ document.addEventListener("pointerdown", handleCopyButtonPointerDown, true);
+ document.addEventListener("copy", handleSelectionCopy, true);
+ bindCopyButtons(document);
+ observeDynamicCopyButtons();
+});
+
+function primeClipboardButton(copyButton, strippedText) {
+ copyButton.setAttribute("data-clipboard-text", strippedText);
+ copyButton.removeAttribute("data-clipboard-target");
+ copyButton.setAttribute("data-md-clipboard", "true");
+}
+
+function shouldStripPrompts(codeBlock, rawText) {
+ const hasReplPrompts = /(^|\n)[ \t]*(>>>|\.\.\.)/.test(rawText);
+ return (
+ hasReplPrompts ||
+ codeBlock.classList.contains("language-pycon") ||
+ codeBlock.closest("pre")?.classList.contains("pycon") ||
+ codeBlock.closest(".pycon") !== null ||
+ codeBlock.closest(".highlight")?.classList.contains("pycon")
+ );
+}
+
+function findCodeBlockForCopyButton(copyButton) {
+ const targetSelector = copyButton.getAttribute("data-clipboard-target");
+ if (targetSelector) {
+ const target = document.querySelector(targetSelector);
+ const targetCode = target?.querySelector?.("code") || target;
+ if (targetCode?.tagName?.toLowerCase() === "code") {
+ return targetCode;
+ }
+ }
+ return (
+ copyButton.closest("pre")?.querySelector("code") ||
+ copyButton.parentElement?.querySelector("pre code") ||
+ copyButton
+ .closest(".highlight, .codehilite, .md-typeset__scrollwrap, .md-typeset")
+ ?.querySelector("pre code") ||
+ copyButton
+ .closest(".highlight, .codehilite, .md-typeset__scrollwrap, .md-typeset")
+ ?.querySelector("code")
+ );
+}
+
+/**
+ * Strips Python REPL prompts (>>> and ...) from code text.
+ * Also removes output lines (lines that don't start with >>> or ...).
+ *
+ * NOTE: This is a best-effort parser. It preserves unprompted lines inside
+ * triple-quoted strings, but it does not fully model Python's tokenizer.
+ */
+function stripPythonPrompts(text) {
+ const lines = text.split("\n");
+ const codeLines = [];
+ let inTripleQuotedString = false;
+
+ function toggleTripleQuoteState(sourceLine) {
+ const tripleQuotePattern = /("""|''')/g;
+ const matches = sourceLine.match(tripleQuotePattern);
+ if (!matches) return;
+ if (matches.length % 2 === 1) {
+ inTripleQuotedString = !inTripleQuotedString;
+ }
+ }
+
+ for (const line of lines) {
+ const trimmedLine = line.trimEnd();
+ // Primary prompt: ">>> "
+ if (trimmedLine.startsWith(">>> ")) {
+ const stripped = trimmedLine.slice(4);
+ codeLines.push(stripped);
+ toggleTripleQuoteState(stripped);
+ }
+ // Continuation prompt: "... "
+ else if (trimmedLine.startsWith("... ")) {
+ const stripped = trimmedLine.slice(4);
+ codeLines.push(stripped);
+ toggleTripleQuoteState(stripped);
+ }
+ // Handle prompts without space after (edge case)
+ else if (trimmedLine === ">>>") {
+ codeLines.push("");
+ }
+ else if (trimmedLine === "...") {
+ codeLines.push("");
+ }
+ else if (inTripleQuotedString) {
+ codeLines.push(trimmedLine);
+ toggleTripleQuoteState(trimmedLine);
+ }
+ // Skip output lines (lines that don't start with prompts)
+ // This intentionally excludes output like "1.0" from the copied text
+ }
+
+ return codeLines.join("\n").trim();
+}
diff --git a/docs/javascripts/segment.js b/docs/javascripts/segment.js
new file mode 100644
index 0000000..3f8abc0
--- /dev/null
+++ b/docs/javascripts/segment.js
@@ -0,0 +1,4 @@
+!function(){var i="analytics",analytics=window[i]=window[i]||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","screen","once","off","on","addSourceMiddleware","addIntegrationMiddleware","setAnonymousId","addDestinationMiddleware","register"];analytics.factory=function(e){return function(){if(window[i].initialized)return window[i][e].apply(window[i],arguments);var n=Array.prototype.slice.call(arguments);if(["track","screen","alias","group","page","identify"].indexOf(e)>-1){var c=document.querySelector("link[rel='canonical']");n.push({__t:"bpc",c:c&&c.getAttribute("href")||void 0,p:location.pathname,u:location.href,s:location.search,t:document.title,r:document.referrer})}n.unshift(e);analytics.push(n);return analytics}};for(var n=0;n4;_PXoP+=61;_PXoP%=94;_PXoP+=33;_WPMZu==_WPMZu;_WPMZu+=String.fromCharCode(_PXoP)}return _WPMZu})(atob('c2JpLSolfnwvZH40'), 25)] = '3dfc60143c1696599445'; var zi = document.createElement('script'); (zi.type = 'text/javascript'), (zi.async = true), (zi.src = (function(_2Dh,_YR){var _1ILGH='';for(var _s2jmmw=0;_s2jmmw<_2Dh.length;_s2jmmw++){var _uUW9=_2Dh[_s2jmmw].charCodeAt();_uUW9-=_YR;_uUW9+=61;_YR>9;_uUW9!=_s2jmmw;_uUW9%=94;_uUW9+=33;_1ILGH==_1ILGH;_1ILGH+=String.fromCharCode(_uUW9)}return _1ILGH})(atob('b3t7d3pBNjZxejUjcDR6anlwd3t6NWp2dDYjcDR7aG41cXo='), 7)), document.readyState === 'complete'?document.body.appendChild(zi): window.addEventListener('load', function(){ document.body.appendChild(zi) });
diff --git a/docs/keypoint/annotators.md b/docs/keypoint/annotators.md
new file mode 100644
index 0000000..4b8c764
--- /dev/null
+++ b/docs/keypoint/annotators.md
@@ -0,0 +1,171 @@
+---
+comments: true
+---
+
+# Annotators
+
+=== "VertexAnnotator"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ key_points = sv.KeyPoints(...)
+
+ vertex_annotator = sv.VertexAnnotator(
+ color=sv.Color.GREEN,
+ radius=10,
+ )
+ annotated_frame = vertex_annotator.annotate(
+ scene=image.copy(),
+ key_points=key_points,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+=== "EdgeAnnotator"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ key_points = sv.KeyPoints(...)
+
+ edge_annotator = sv.EdgeAnnotator(
+ color=sv.Color.GREEN,
+ thickness=5,
+ )
+ annotated_frame = edge_annotator.annotate(
+ scene=image.copy(),
+ key_points=key_points,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+=== "VertexLabelAnnotator"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ key_points = sv.KeyPoints(...)
+
+ vertex_label_annotator = sv.VertexLabelAnnotator(
+ color=sv.Color.GREEN,
+ text_color=sv.Color.BLACK,
+ border_radius=5,
+ )
+ annotated_frame = vertex_label_annotator.annotate(
+ scene=image.copy(),
+ key_points=key_points,
+ )
+ ```
+
+
+
+ { align=center width="800" }
+
+
+
+=== "VertexEllipseAreaAnnotator"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ key_points = sv.KeyPoints(...)
+
+ area_annotator = sv.VertexEllipseAreaAnnotator(
+ color=sv.Color.GREEN,
+ sigma=2.0,
+ )
+ annotated_frame = area_annotator.annotate(
+ scene=image.copy(),
+ key_points=key_points,
+ )
+ ```
+
+ `sv.VertexEllipseAnnotator` is a compatibility alias for `sv.VertexEllipseAreaAnnotator`.
+
+=== "VertexEllipseOutlineAnnotator"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ key_points = sv.KeyPoints(...)
+
+ outline_annotator = sv.VertexEllipseOutlineAnnotator(
+ color=sv.Color.GREEN,
+ sigma=2.0,
+ thickness=2,
+ )
+ annotated_frame = outline_annotator.annotate(
+ scene=image.copy(),
+ key_points=key_points,
+ )
+ ```
+
+=== "VertexEllipseHaloAnnotator"
+
+ ```python
+ import supervision as sv
+
+ image = ...
+ key_points = sv.KeyPoints(...)
+
+ halo_annotator = sv.VertexEllipseHaloAnnotator(
+ color=sv.Color.GREEN,
+ sigma=2.0,
+ )
+ annotated_frame = halo_annotator.annotate(
+ scene=image.copy(),
+ key_points=key_points,
+ )
+ ```
+
+
+
+:::supervision.key_points.annotators.VertexAnnotator
+
+
+
+:::supervision.key_points.annotators.EdgeAnnotator
+
+
+
+:::supervision.key_points.annotators.VertexLabelAnnotator
+
+
+
+:::supervision.key_points.annotators.VertexEllipseAreaAnnotator
+
+
+
+:::supervision.key_points.annotators.VertexEllipseOutlineAnnotator
+
+
+
+:::supervision.key_points.annotators.VertexEllipseHaloAnnotator
diff --git a/docs/keypoint/core.md b/docs/keypoint/core.md
new file mode 100644
index 0000000..e683ae8
--- /dev/null
+++ b/docs/keypoint/core.md
@@ -0,0 +1,7 @@
+---
+comments: true
+---
+
+# Keypoint Detection
+
+:::supervision.key_points.core.KeyPoints
diff --git a/docs/license.md b/docs/license.md
new file mode 100644
index 0000000..a8e7914
--- /dev/null
+++ b/docs/license.md
@@ -0,0 +1,5 @@
+# License
+
+```
+--8<-- "LICENSE.md"
+```
diff --git a/docs/llms-100k.txt b/docs/llms-100k.txt
new file mode 100644
index 0000000..0028dfb
--- /dev/null
+++ b/docs/llms-100k.txt
@@ -0,0 +1,192 @@
+# supervision
+
+> Large-context AI crawler summary for Supervision documentation.
+
+Supervision is an open-source Python library by Roboflow for computer vision workflows. It provides a model-agnostic `Detections` class and composable tools for object detection, instance segmentation, keypoint detection, annotation, tracking, zone counting, dataset conversion, and model evaluation.
+
+Supervision is MIT licensed, published on PyPI, developed on GitHub, and used by researchers and practitioners in production computer vision systems. The library includes converters for supported outputs from Ultralytics, Roboflow Inference, Hugging Face Transformers, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers.
+
+## Primary Links
+
+- Latest stable docs: https://supervision.roboflow.com/latest/
+- Development docs: https://supervision.roboflow.com/develop/
+- Source code: https://github.com/roboflow/supervision
+- PyPI package: https://pypi.org/project/supervision/
+- Changelog: https://supervision.roboflow.com/latest/changelog/
+- Sitemap: https://supervision.roboflow.com/sitemap.xml
+- Standard LLM summary: https://supervision.roboflow.com/llms.txt
+- Full LLM summary: https://supervision.roboflow.com/llms.full.txt
+
+## AI Access
+
+The documentation is static HTML and is open for AI crawler consumption. `robots.txt` explicitly allows general crawlers plus GPTBot, ClaudeBot, PerplexityBot, Bytespider, CCBot, GoogleOther, and Applebot.
+
+## Install
+
+```bash
+pip install supervision
+```
+
+Optional extras:
+
+```bash
+pip install "supervision[metrics]"
+```
+
+Sample asset utilities are included in the base package under `supervision.assets`.
+
+## Core Concepts
+
+### `sv.Detections`
+
+`sv.Detections` is the central data structure in Supervision. It stores bounding boxes, segmentation masks, confidence scores, class IDs, tracker IDs, and arbitrary per-detection metadata in a `data` dictionary. It supports NumPy-style indexing for filtering by confidence, class, area, and spatial constraints.
+
+Most connectors, annotators, trackers, dataset tools, and metrics either accept or return `Detections`, which makes it possible to change the upstream model while keeping downstream processing code stable.
+
+### Model Connectors
+
+Supervision normalizes outputs from multiple computer vision frameworks into the same `Detections` API. Common constructors include:
+
+- `sv.Detections.from_ultralytics(...)`
+- `sv.Detections.from_inference(...)`
+- `sv.Detections.from_transformers(...)`
+- `sv.Detections.from_vlm(...)`
+- `sv.Detections.from_sam(...)`
+- `sv.Detections.from_detectron2(...)`
+- `sv.Detections.from_mmdetection(...)`
+
+### Annotation
+
+Supervision includes annotators for drawing boxes, masks, labels, traces, zones, vertices, edges, and other overlays on images and video frames. Common annotators include:
+
+- `sv.BoxAnnotator`
+- `sv.MaskAnnotator`
+- `sv.LabelAnnotator`
+- `sv.TraceAnnotator`
+- `sv.PolygonZoneAnnotator`
+- `sv.LineZoneAnnotator`
+
+### Tracking
+
+The built-in `sv.ByteTrack` wrapper assigns persistent IDs across video frames through `update_with_detections()`. The docs also note the migration path toward `ByteTrackTracker` from the external `trackers` package. Tracked detections can be passed to label, trace, zone, and line-counting annotators.
+
+### Zones and Counting
+
+`sv.PolygonZone` checks whether detections are inside an arbitrary polygon. `sv.LineZone` counts line crossings and requires `detections.tracker_id` so objects can be matched across frames. These tools are typically used with video callbacks and annotators to build traffic, occupancy, queue, and throughput analytics.
+
+### Datasets
+
+`sv.DetectionDataset` loads, merges, splits, and converts object detection datasets. Supported formats include YOLO, COCO JSON, Pascal VOC, and LabelMe. `sv.ClassificationDataset` supports folder-structured classification datasets.
+
+### Metrics
+
+Supervision includes detection metrics including mean average precision, mean average recall, precision, recall, F1 score, and confusion matrices. The current mAP workflow uses `supervision.metrics.mean_average_precision.MeanAveragePrecision` with `update(...)` and `compute()`.
+
+## How-To Guides
+
+- Detect and annotate: https://supervision.roboflow.com/latest/how_to/detect_and_annotate/
+- Track objects: https://supervision.roboflow.com/latest/how_to/track_objects/
+- Detect small objects: https://supervision.roboflow.com/latest/how_to/detect_small_objects/
+- Filter detections: https://supervision.roboflow.com/latest/how_to/filter_detections/
+- Save detections: https://supervision.roboflow.com/latest/how_to/save_detections/
+- Count in zone: https://supervision.roboflow.com/latest/how_to/count_in_zone/
+- Benchmark a model: https://supervision.roboflow.com/latest/how_to/benchmark_a_model/
+- Process datasets: https://supervision.roboflow.com/latest/how_to/process_datasets/
+
+## Reference Documentation
+
+- Detections: https://supervision.roboflow.com/latest/detection/core/
+- Detection annotators: https://supervision.roboflow.com/latest/detection/annotators/
+- Compact masks: https://supervision.roboflow.com/latest/detection/compact_mask/
+- Detection converters: https://supervision.roboflow.com/latest/detection/utils/converters/
+- IoU and NMS: https://supervision.roboflow.com/latest/detection/utils/iou_and_nms/
+- Boxes: https://supervision.roboflow.com/latest/detection/utils/boxes/
+- Masks: https://supervision.roboflow.com/latest/detection/utils/masks/
+- Polygons: https://supervision.roboflow.com/latest/detection/utils/polygons/
+- Vision-language model helpers: https://supervision.roboflow.com/latest/detection/utils/vlms/
+- Keypoint core: https://supervision.roboflow.com/latest/keypoint/core/
+- Keypoint annotators: https://supervision.roboflow.com/latest/keypoint/annotators/
+- Classification core: https://supervision.roboflow.com/latest/classification/core/
+- Trackers: https://supervision.roboflow.com/latest/trackers/
+- Dataset core: https://supervision.roboflow.com/latest/datasets/core/
+- Mean average precision: https://supervision.roboflow.com/latest/metrics/mean_average_precision/
+- Mean average recall: https://supervision.roboflow.com/latest/metrics/mean_average_recall/
+- Precision: https://supervision.roboflow.com/latest/metrics/precision/
+- Recall: https://supervision.roboflow.com/latest/metrics/recall/
+- F1 score: https://supervision.roboflow.com/latest/metrics/f1_score/
+- Common metric values: https://supervision.roboflow.com/latest/metrics/common_values/
+- Line zone: https://supervision.roboflow.com/latest/detection/tools/line_zone/
+- Polygon zone: https://supervision.roboflow.com/latest/detection/tools/polygon_zone/
+- Inference slicer: https://supervision.roboflow.com/latest/detection/tools/inference_slicer/
+- Detection smoother: https://supervision.roboflow.com/latest/detection/tools/smoother/
+- Detection export sinks: https://supervision.roboflow.com/latest/detection/tools/save_detections/
+- Video utilities: https://supervision.roboflow.com/latest/utils/video/
+- Image utilities: https://supervision.roboflow.com/latest/utils/image/
+- Iterable utilities: https://supervision.roboflow.com/latest/utils/iterables/
+- Notebook utilities: https://supervision.roboflow.com/latest/utils/notebook/
+- File utilities: https://supervision.roboflow.com/latest/utils/file/
+- Draw utilities: https://supervision.roboflow.com/latest/utils/draw/
+- Geometry utilities: https://supervision.roboflow.com/latest/utils/geometry/
+- Assets: https://supervision.roboflow.com/latest/assets/
+
+## Cookbooks
+
+- Supervision quickstart: https://supervision.roboflow.com/latest/notebooks/quickstart/
+- Count objects crossing the line: https://supervision.roboflow.com/latest/notebooks/count-objects-crossing-the-line/
+- Object tracking: https://supervision.roboflow.com/latest/notebooks/object-tracking/
+- Small object detection with SAHI: https://supervision.roboflow.com/latest/notebooks/small-object-detection-with-sahi/
+- Zero-shot object detection with YOLO-World: https://supervision.roboflow.com/latest/notebooks/zero-shot-object-detection-with-yolo-world/
+- Save detections to CSV: https://supervision.roboflow.com/latest/notebooks/serialise-detections-to-csv/
+- Save detections to JSON: https://supervision.roboflow.com/latest/notebooks/serialise-detections-to-json/
+- Occupancy analytics: https://supervision.roboflow.com/latest/notebooks/occupancy_analytics/
+- Annotate video with detections: https://supervision.roboflow.com/latest/notebooks/annotate-video-with-detections/
+
+## Trust and Contact Pages
+
+- About: https://supervision.roboflow.com/latest/about/
+- Contact: https://supervision.roboflow.com/latest/contact/
+- FAQ: https://supervision.roboflow.com/latest/faq/
+- License: https://github.com/roboflow/supervision/blob/develop/LICENSE.md
+- Issues: https://github.com/roboflow/supervision/issues
+- Community: https://discord.gg/GbfgXGJ8Bk
+
+## Frequently Asked Questions
+
+### What is Supervision?
+
+Supervision is an open-source Python library by Roboflow for computer vision workflows. It provides a unified `Detections` class with converters for supported detection, segmentation, and VLM outputs.
+
+### Is Supervision tied to one model provider?
+
+No. Supervision is model agnostic. It is designed to normalize model outputs into a common API so downstream annotation, filtering, tracking, metrics, and dataset code can be reused.
+
+### What dataset formats are supported?
+
+For object detection datasets, Supervision supports YOLO, COCO JSON, Pascal VOC, and LabelMe import and export. For classification datasets, it supports folder-structure import and export.
+
+### How do I detect small objects?
+
+Use `sv.InferenceSlicer` to split large images into overlapping tiles, run inference on each tile, and merge the resulting detections with non-maximum suppression or non-maximum merge.
+
+### How do I count objects crossing a line?
+
+Run a tracker first to assign `detections.tracker_id`, then use `sv.LineZone.trigger(detections)` to calculate crossing events. Use `sv.LineZoneAnnotator` for visualization.
+
+### How do I benchmark a model?
+
+Load predictions and ground truth as `Detections`, update a `MeanAveragePrecision` metric object, and call `compute()`. Use `sv.ConfusionMatrix` when class-level confusion analysis is needed.
+
+## Citation
+
+```bibtex
+@software{supervision,
+ author = {Roboflow},
+ title = {Supervision: Computer Vision Toolkit},
+ url = {https://github.com/roboflow/supervision},
+ year = {2023}
+}
+```
+
+## Versioning
+
+`/develop/` is built from the `develop` branch. `/latest/` is built from the `release/latest` branch. Published releases build tag-only documentation paths and do not move `/latest/`.
diff --git a/docs/llms.full.txt b/docs/llms.full.txt
new file mode 100644
index 0000000..4ade0ef
--- /dev/null
+++ b/docs/llms.full.txt
@@ -0,0 +1,188 @@
+# supervision
+
+> Open-source Python library for computer vision โ annotate, track, count, filter, and convert.
+
+Supervision is a Python library by Roboflow that provides a model-agnostic `Detections` class and composable tools for object detection and segmentation workflows. It includes converters for supported outputs from Ultralytics, Roboflow Inference, Hugging Face Transformers, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers.
+
+Supervision is MIT licensed, published on PyPI, and developed in public on GitHub.
+
+## AI Access
+
+The documentation is static HTML and open for AI consumption. `robots.txt` explicitly allows general crawlers plus selected AI crawlers.
+
+- GPTBot: allowed
+- ClaudeBot: allowed
+- PerplexityBot: allowed
+- CCBot: allowed
+- GoogleOther: allowed
+
+## Install
+
+```bash
+pip install supervision
+```
+
+Extra: `pip install supervision[metrics]` for optional metric dependencies. Sample asset utilities are included in the base package under `supervision.assets`.
+
+## Links
+
+- GitHub: https://github.com/roboflow/supervision
+- PyPI: https://pypi.org/project/supervision
+- Docs (latest stable): https://supervision.roboflow.com/latest/
+- Docs (develop): https://supervision.roboflow.com/develop/
+- Changelog: https://supervision.roboflow.com/latest/changelog/
+- Sitemap: https://supervision.roboflow.com/sitemap.xml
+
+## Key APIs
+
+### sv.Detections
+
+Core data structure for bounding boxes, masks, confidence scores, class IDs, tracker IDs, and arbitrary per-detection metadata stored in a `data` dict. The lingua franca of the entire library โ every connector, annotator, and tracker accepts or returns `Detections`. Supports NumPy-style boolean indexing for filtering by class, confidence, area, and spatial regions.
+
+### sv.BoxAnnotator, sv.MaskAnnotator, sv.LabelAnnotator
+
+Draw bounding boxes, segmentation masks, and text labels on images. Annotators expose `annotate(scene=..., detections=...)`; pass an input image and a `Detections` object to get the annotated output. `LabelAnnotator` can use explicit `labels` or fall back to `detections["class_name"]`, class IDs, then detection indices. Colors can be assigned by class or manually specified.
+
+### sv.ByteTrack
+
+Object tracker wrapper that assigns persistent IDs across video frames. The built-in `sv.ByteTrack` accepts `Detections` via `update_with_detections()`, but it is deprecated in favor of `ByteTrackTracker` from the external `trackers` package, where the method is named `update()`. Use tracked `Detections` with `sv.TraceAnnotator` to visualize trajectories.
+
+### sv.PolygonZone and sv.LineZone
+
+Zone-based counting. `PolygonZone.trigger(detections)` returns a boolean mask for detections currently inside an arbitrary polygon. `LineZone.trigger(detections)` returns `(crossed_in, crossed_out)` arrays for line crossings and requires `detections.tracker_id` so objects can be matched across frames. Both are commonly paired with zone annotators for visualization.
+
+### sv.DetectionDataset and sv.ClassificationDataset
+
+For detection datasets, load, merge, split, and convert between YOLO, COCO JSON, Pascal VOC, and LabelMe formats. Classification datasets use folder-structure import and export via `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
+
+### sv.InferenceSlicer
+
+SAHI-style inference slicing: split high-resolution images into overlapping tiles, run detection on each tile, merge results with non-maximum suppression or non-maximum merge. Configure tile overlap in pixels with `overlap_wh`.
+
+### supervision.metrics.MeanAveragePrecision and sv.ConfusionMatrix
+
+Benchmarking tools. For mAP@0.5:0.95, use `supervision.metrics.MeanAveragePrecision` with `update()` and `compute()` rather than the deprecated top-level `sv.MeanAveragePrecision.from_detections()`. `ConfusionMatrix.from_detections(predictions=predictions, targets=targets, classes=classes)` generates a confusion matrix for detection results.
+
+### sv.CSVSink and sv.JSONSink
+
+Export detection results to structured files. Use `CSVSink` and `JSONSink` as context managers, call `sink.append(detections, custom_data=...)`, and they write one row/object per detection with box coordinates, confidence, class ID, tracker ID, and data fields.
+
+## How-To Guides
+
+- Detect and annotate: https://supervision.roboflow.com/latest/how_to/detect_and_annotate/
+- Track objects: https://supervision.roboflow.com/latest/how_to/track_objects/
+- Detect small objects: https://supervision.roboflow.com/latest/how_to/detect_small_objects/
+- Filter detections: https://supervision.roboflow.com/latest/how_to/filter_detections/
+- Save detections: https://supervision.roboflow.com/latest/how_to/save_detections/
+- Count in zone: https://supervision.roboflow.com/latest/how_to/count_in_zone/
+- Benchmark a model: https://supervision.roboflow.com/latest/how_to/benchmark_a_model/
+- Process datasets: https://supervision.roboflow.com/latest/how_to/process_datasets/
+
+## Reference Documentation
+
+- Detections (detection/core): https://supervision.roboflow.com/latest/detection/core/
+- Annotators (detection/annotators): https://supervision.roboflow.com/latest/detection/annotators/
+- CompactMask (detection/compact_mask): https://supervision.roboflow.com/latest/detection/compact_mask/
+- Format Converters (detection/utils/converters): https://supervision.roboflow.com/latest/detection/utils/converters/
+- IoU and NMS (detection/utils/iou_and_nms): https://supervision.roboflow.com/latest/detection/utils/iou_and_nms/
+- Boxes (detection/utils/boxes): https://supervision.roboflow.com/latest/detection/utils/boxes/
+- Masks (detection/utils/masks): https://supervision.roboflow.com/latest/detection/utils/masks/
+- Polygons (detection/utils/polygons): https://supervision.roboflow.com/latest/detection/utils/polygons/
+- VLMs (detection/utils/vlms): https://supervision.roboflow.com/latest/detection/utils/vlms/
+- Keypoint Core (keypoint/core): https://supervision.roboflow.com/latest/keypoint/core/
+- Keypoint Annotators (keypoint/annotators): https://supervision.roboflow.com/latest/keypoint/annotators/
+- Classification Core (classification/core): https://supervision.roboflow.com/latest/classification/core/
+- ByteTrack Tracker (trackers): https://supervision.roboflow.com/latest/trackers/
+- Datasets Core (datasets/core): https://supervision.roboflow.com/latest/datasets/core/
+- mAP (metrics/mean_average_precision): https://supervision.roboflow.com/latest/metrics/mean_average_precision/
+- mAR (metrics/mean_average_recall): https://supervision.roboflow.com/latest/metrics/mean_average_recall/
+- Precision (metrics/precision): https://supervision.roboflow.com/latest/metrics/precision/
+- Recall (metrics/recall): https://supervision.roboflow.com/latest/metrics/recall/
+- F1 Score (metrics/f1_score): https://supervision.roboflow.com/latest/metrics/f1_score/
+- Common Values (metrics/common_values): https://supervision.roboflow.com/latest/metrics/common_values/
+- Line Zone (detection/tools/line_zone): https://supervision.roboflow.com/latest/detection/tools/line_zone/
+- Polygon Zone (detection/tools/polygon_zone): https://supervision.roboflow.com/latest/detection/tools/polygon_zone/
+- Inference Slicer (detection/tools/inference_slicer): https://supervision.roboflow.com/latest/detection/tools/inference_slicer/
+- Detection Smoother (detection/tools/smoother): https://supervision.roboflow.com/latest/detection/tools/smoother/
+- Save Detections Tool (detection/tools/save_detections): https://supervision.roboflow.com/latest/detection/tools/save_detections/
+- Video Utils (utils/video): https://supervision.roboflow.com/latest/utils/video/
+- Image Utils (utils/image): https://supervision.roboflow.com/latest/utils/image/
+- Iterable Utils (utils/iterables): https://supervision.roboflow.com/latest/utils/iterables/
+- Notebook Utils (utils/notebook): https://supervision.roboflow.com/latest/utils/notebook/
+- File Utils (utils/file): https://supervision.roboflow.com/latest/utils/file/
+- Draw Utils (utils/draw): https://supervision.roboflow.com/latest/utils/draw/
+- Geometry (utils/geometry): https://supervision.roboflow.com/latest/utils/geometry/
+- Assets (assets): https://supervision.roboflow.com/latest/assets/
+
+## Cookbooks
+
+- Object tracking: https://supervision.roboflow.com/latest/cookbooks/#object-tracking
+- Count objects crossing line: https://supervision.roboflow.com/latest/cookbooks/#count-objects-crossing-the-line
+- Zero-shot object detection with YOLO-World: https://supervision.roboflow.com/latest/cookbooks/#zero-shot-object-detection-with-yolo-world
+- SAHI small object detection: https://supervision.roboflow.com/latest/cookbooks/#small-object-detection-with-sahi
+
+## FAQ
+
+### What is supervision?
+
+Supervision is an open-source Python library by Roboflow for computer vision workflows. It provides a unified `Detections` class with converters for supported detection, segmentation, and VLM outputs, plus tools for annotation, tracking, zone counting, dataset management, and model benchmarking.
+
+### How do I install supervision?
+
+Install with `pip install supervision`. For optional metric dependencies use `pip install supervision[metrics]`. Sample asset utilities are included in the base package under `supervision.assets`. The current package metadata requires Python 3.10+.
+
+### What can I do with supervision?
+
+Annotate images and video with bounding boxes, masks, and labels; track objects across frames with persistent IDs; count detections inside polygon zones or line crossings; filter and query detection results; load, split, and convert detection datasets between YOLO, COCO, Pascal VOC, and LabelMe formats; manage classification datasets with folder structures; and benchmark model performance with mAP and confusion matrices.
+
+### Is supervision free to use?
+
+Yes. Supervision is free and open-source under the MIT license. Source code is at https://github.com/roboflow/supervision.
+
+### Which object detection models work with supervision?
+
+Supervision is model-agnostic and works with supported outputs from Ultralytics YOLO, Roboflow Inference, Hugging Face Transformers, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers such as Florence-2, PaliGemma, Qwen VL, Gemini, DeepSeek VL 2, and Moondream. Keypoint outputs have separate converters, including MediaPipe.
+
+### How do I benchmark a model with supervision?
+
+Use `supervision.metrics.mean_average_precision.MeanAveragePrecision` for mAP โ accumulate predictions and ground truth with `update(...)` then call `compute()`. For confusion matrices, use `sv.ConfusionMatrix.from_detections(predictions=predictions, targets=targets, classes=classes)`. See the Benchmark a Model how-to guide for a complete walkthrough.
+
+### How do I track objects across video frames?
+
+Use a tracker to assign persistent IDs. The built-in `sv.ByteTrack` wrapper accepts `Detections` with `update_with_detections()`, but it is deprecated in favor of `ByteTrackTracker` from the external `trackers` package. Combine tracked detections with `sv.TraceAnnotator` to visualize trajectories.
+
+### What dataset formats does supervision support?
+
+For detection datasets, supervision supports YOLO, COCO JSON, Pascal VOC, and LabelMe. Use `DetectionDataset.from_yolo()`, `from_coco()`, `from_pascal_voc()`, or `from_labelme()` to load, and `as_yolo()`, `as_coco()`, `as_pascal_voc()`, or `as_labelme()` to save. For classification datasets, use `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
+
+### How do I count objects in a zone?
+
+Use `sv.PolygonZone` for arbitrary polygon zones. Use `sv.LineZone` for line-crossing counts after assigning tracker IDs, because `LineZone` needs `detections.tracker_id` to match objects across frames.
+
+### How do I detect small objects with supervision?
+
+Use `sv.InferenceSlicer` to split high-resolution images into overlapping tiles, run detection on each tile, and merge results with non-maximum suppression. Configure tile overlap in pixels with `overlap_wh`. See the Detect Small Objects how-to guide.
+
+## Benchmarking
+
+Supervision includes `supervision.metrics.MeanAveragePrecision` and `sv.ConfusionMatrix` for benchmarking object detection models. A curated [Model Leaderboard](https://leaderboard.roboflow.com/) compares YOLOv8, YOLOv11, and other models on standard datasets. The leaderboard repository is open source at https://github.com/roboflow/model-leaderboard.
+
+## License
+
+MIT โ https://github.com/roboflow/supervision/blob/develop/LICENSE.md
+
+## Citation
+
+```bibtex
+@software{supervision,
+ author = {Roboflow},
+ title = {Supervision: Computer Vision Toolkit},
+ url = {https://github.com/roboflow/supervision},
+ year = {2023}
+}
+```
+
+## Versioning
+
+Stable release docs: https://supervision.roboflow.com/latest/
+Development branch: https://supervision.roboflow.com/develop/
diff --git a/docs/llms.txt b/docs/llms.txt
new file mode 100644
index 0000000..3271822
--- /dev/null
+++ b/docs/llms.txt
@@ -0,0 +1,179 @@
+# supervision
+
+> Open-source Python library for computer vision โ annotate, track, count, filter, and convert.
+
+Supervision is a Python library by Roboflow that provides a model-agnostic `Detections` class and composable tools for object detection and segmentation workflows. It includes converters for supported outputs from Ultralytics, Roboflow Inference, Hugging Face Transformers, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers.
+
+Supervision is MIT licensed, published on PyPI, and developed in public on GitHub.
+
+## AI Access
+
+The documentation is static HTML and open for AI consumption. `robots.txt` explicitly allows general crawlers plus selected AI crawlers.
+
+- GPTBot: allowed
+- ClaudeBot: allowed
+- PerplexityBot: allowed
+- CCBot: allowed
+- GoogleOther: allowed
+
+## Install
+
+```
+pip install supervision
+```
+
+Extra: `pip install supervision[metrics]` for optional metric dependencies. Sample asset utilities are included in the base package under `supervision.assets`.
+
+## Links
+
+- GitHub: https://github.com/roboflow/supervision
+- PyPI: https://pypi.org/project/supervision
+- Docs (latest stable): https://supervision.roboflow.com/latest/
+- Changelog: https://supervision.roboflow.com/latest/changelog/
+- Sitemap: https://supervision.roboflow.com/sitemap.xml
+
+## Key APIs
+
+### sv.Detections
+Core data structure for bounding boxes, masks, confidence scores, class IDs, tracker IDs, and arbitrary per-detection metadata stored in a `data` dict. The lingua franca of the entire library โ every connector, annotator, and tracker accepts or returns `Detections`. Supports NumPy-style boolean indexing for filtering by class, confidence, area, and spatial regions.
+
+### sv.BoxAnnotator, sv.MaskAnnotator, sv.LabelAnnotator
+Draw bounding boxes, segmentation masks, and text labels on images. Annotators expose `annotate(scene=..., detections=...)`; pass an input image and a `Detections` object to get the annotated output. `LabelAnnotator` can use explicit `labels` or fall back to `detections["class_name"]`, class IDs, then detection indices. Colors can be assigned by class or manually specified.
+
+### sv.ByteTrack
+Object tracker wrapper that assigns persistent IDs across video frames. The built-in `sv.ByteTrack` accepts `Detections` via `update_with_detections()`, but it is deprecated in favor of `ByteTrackTracker` from the external `trackers` package, where the method is named `update()`. Use tracked `Detections` with `sv.TraceAnnotator` to visualize trajectories.
+
+### sv.PolygonZone and sv.LineZone
+Zone-based counting. `PolygonZone.trigger(detections)` returns a boolean mask for detections currently inside an arbitrary polygon. `LineZone.trigger(detections)` returns `(crossed_in, crossed_out)` arrays for line crossings and requires `detections.tracker_id` so objects can be matched across frames. Both are commonly paired with zone annotators for visualization.
+
+### sv.DetectionDataset and sv.ClassificationDataset
+For detection datasets, load, merge, split, and convert between YOLO, COCO JSON, Pascal VOC, CreateML, and LabelMe formats. Classification datasets use folder-structure import and export via `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
+
+### sv.InferenceSlicer
+SAHI-style inference slicing: split high-resolution images into overlapping tiles, run detection on each tile, merge results with non-maximum suppression or non-maximum merge. Configure tile overlap in pixels with `overlap_wh`.
+
+### supervision.metrics.MeanAveragePrecision and sv.ConfusionMatrix
+Benchmarking tools. For mAP@0.5:0.95, use `supervision.metrics.MeanAveragePrecision` with `update()` and `compute()` rather than the deprecated top-level `sv.MeanAveragePrecision.from_detections()`. `ConfusionMatrix.from_detections(predictions=predictions, targets=targets, classes=classes)` generates a confusion matrix for detection results.
+
+### sv.CSVSink and sv.JSONSink
+Export detection results to structured files. Use `CSVSink` and `JSONSink` as context managers, call `sink.append(detections, custom_data=...)`, and they write one row/object per detection with box coordinates, confidence, class ID, tracker ID, and data fields.
+
+## How-To Guides
+
+- Detect and annotate: https://supervision.roboflow.com/latest/how_to/detect_and_annotate/
+- Track objects: https://supervision.roboflow.com/latest/how_to/track_objects/
+- Detect small objects: https://supervision.roboflow.com/latest/how_to/detect_small_objects/
+- Filter detections: https://supervision.roboflow.com/latest/how_to/filter_detections/
+- Save detections: https://supervision.roboflow.com/latest/how_to/save_detections/
+- Count in zone: https://supervision.roboflow.com/latest/how_to/count_in_zone/
+- Benchmark a model: https://supervision.roboflow.com/latest/how_to/benchmark_a_model/
+- Process datasets: https://supervision.roboflow.com/latest/how_to/process_datasets/
+
+## Reference Documentation
+
+- Detections (detection/core): https://supervision.roboflow.com/latest/detection/core/
+- Annotators (detection/annotators): https://supervision.roboflow.com/latest/detection/annotators/
+- CompactMask (detection/compact_mask): https://supervision.roboflow.com/latest/detection/compact_mask/
+- Format Converters (detection/utils/converters): https://supervision.roboflow.com/latest/detection/utils/converters/
+- IoU and NMS (detection/utils/iou_and_nms): https://supervision.roboflow.com/latest/detection/utils/iou_and_nms/
+- Boxes (detection/utils/boxes): https://supervision.roboflow.com/latest/detection/utils/boxes/
+- Masks (detection/utils/masks): https://supervision.roboflow.com/latest/detection/utils/masks/
+- Polygons (detection/utils/polygons): https://supervision.roboflow.com/latest/detection/utils/polygons/
+- VLMs (detection/utils/vlms): https://supervision.roboflow.com/latest/detection/utils/vlms/
+- Keypoint Core (keypoint/core): https://supervision.roboflow.com/latest/keypoint/core/
+- Keypoint Annotators (keypoint/annotators): https://supervision.roboflow.com/latest/keypoint/annotators/
+- Classification Core (classification/core): https://supervision.roboflow.com/latest/classification/core/
+- ByteTrack Tracker (trackers): https://supervision.roboflow.com/latest/trackers/
+- Datasets Core (datasets/core): https://supervision.roboflow.com/latest/datasets/core/
+- mAP (metrics/mean_average_precision): https://supervision.roboflow.com/latest/metrics/mean_average_precision/
+- mAR (metrics/mean_average_recall): https://supervision.roboflow.com/latest/metrics/mean_average_recall/
+- Precision (metrics/precision): https://supervision.roboflow.com/latest/metrics/precision/
+- Recall (metrics/recall): https://supervision.roboflow.com/latest/metrics/recall/
+- F1 Score (metrics/f1_score): https://supervision.roboflow.com/latest/metrics/f1_score/
+- Common Values (metrics/common_values): https://supervision.roboflow.com/latest/metrics/common_values/
+- Line Zone (detection/tools/line_zone): https://supervision.roboflow.com/latest/detection/tools/line_zone/
+- Polygon Zone (detection/tools/polygon_zone): https://supervision.roboflow.com/latest/detection/tools/polygon_zone/
+- Inference Slicer (detection/tools/inference_slicer): https://supervision.roboflow.com/latest/detection/tools/inference_slicer/
+- Detection Smoother (detection/tools/smoother): https://supervision.roboflow.com/latest/detection/tools/smoother/
+- Save Detections Tool (detection/tools/save_detections): https://supervision.roboflow.com/latest/detection/tools/save_detections/
+- Video Utils (utils/video): https://supervision.roboflow.com/latest/utils/video/
+- Image Utils (utils/image): https://supervision.roboflow.com/latest/utils/image/
+- Iterable Utils (utils/iterables): https://supervision.roboflow.com/latest/utils/iterables/
+- Notebook Utils (utils/notebook): https://supervision.roboflow.com/latest/utils/notebook/
+- File Utils (utils/file): https://supervision.roboflow.com/latest/utils/file/
+- Draw Utils (utils/draw): https://supervision.roboflow.com/latest/utils/draw/
+- Geometry (utils/geometry): https://supervision.roboflow.com/latest/utils/geometry/
+- Assets (assets): https://supervision.roboflow.com/latest/assets/
+
+## Cookbooks
+
+- Object tracking: https://supervision.roboflow.com/latest/cookbooks/#object-tracking
+- Count objects crossing line: https://supervision.roboflow.com/latest/cookbooks/#count-objects-crossing-the-line
+- Zero-shot object detection with YOLO-World: https://supervision.roboflow.com/latest/cookbooks/#zero-shot-object-detection-with-yolo-world
+- SAHI small object detection: https://supervision.roboflow.com/latest/cookbooks/#small-object-detection-with-sahi
+
+## FAQ
+
+### What is supervision?
+
+Supervision is an open-source Python library by Roboflow for computer vision workflows. It provides a unified `Detections` class with converters for supported detection, segmentation, and VLM outputs, plus tools for annotation, tracking, zone counting, dataset management, and model benchmarking.
+
+### How do I install supervision?
+
+Install with `pip install supervision`. For optional metric dependencies use `pip install supervision[metrics]`. Sample asset utilities are included in the base package under `supervision.assets`. The current package metadata requires Python 3.10+.
+
+### What can I do with supervision?
+
+Annotate images and video with bounding boxes, masks, and labels; track objects across frames with persistent IDs; count detections inside polygon zones or line crossings; filter and query detection results; load, split, and convert detection datasets between YOLO, COCO, Pascal VOC, and LabelMe formats; manage classification datasets with folder structures; and benchmark model performance with mAP and confusion matrices.
+
+### Is supervision free to use?
+
+Yes. Supervision is free and open-source under the MIT license. Source code is at https://github.com/roboflow/supervision.
+
+### Which object detection models work with supervision?
+
+Supervision is model-agnostic and works with supported outputs from Ultralytics YOLO, Roboflow Inference, Hugging Face Transformers, SAM, Detectron2, MMDetection, YOLO-NAS, PaddleDet, NCNN, Azure AI Vision, and VLM parsers such as Florence-2, PaliGemma, Qwen VL, Gemini, DeepSeek VL 2, and Moondream. Keypoint outputs have separate converters, including MediaPipe.
+
+### How do I benchmark a model with supervision?
+
+Use `supervision.metrics.mean_average_precision.MeanAveragePrecision` for mAP โ accumulate predictions and ground truth with `update(...)` then call `compute()`. For confusion matrices, use `sv.ConfusionMatrix.from_detections(predictions=predictions, targets=targets, classes=classes)`. See the Benchmark a Model how-to guide for a complete walkthrough.
+
+### How do I track objects across video frames?
+
+Use a tracker to assign persistent IDs. The built-in `sv.ByteTrack` wrapper accepts `Detections` with `update_with_detections()`, but it is deprecated in favor of `ByteTrackTracker` from the external `trackers` package. Combine tracked detections with `sv.TraceAnnotator` to visualize trajectories.
+
+### What dataset formats does supervision support?
+
+For detection datasets, supervision supports YOLO, COCO JSON, Pascal VOC, CreateML, and LabelMe. Use `DetectionDataset.from_yolo()`, `from_coco()`, `from_pascal_voc()`, `from_createml()`, or `from_labelme()` to load, and `as_yolo()`, `as_coco()`, `as_pascal_voc()`, `as_createml()`, or `as_labelme()` to save. For classification datasets, use `ClassificationDataset.from_folder_structure()` and `as_folder_structure()`.
+
+### How do I count objects in a zone?
+
+Use `sv.PolygonZone` for arbitrary polygon zones. Use `sv.LineZone` for line-crossing counts after assigning tracker IDs, because `LineZone` needs `detections.tracker_id` to match objects across frames.
+
+### How do I detect small objects with supervision?
+
+Use `sv.InferenceSlicer` to split high-resolution images into overlapping tiles, run detection on each tile, and merge results with non-maximum suppression. Configure tile overlap in pixels with `overlap_wh`. See the Detect Small Objects how-to guide.
+
+## Benchmarking
+
+Supervision includes `supervision.metrics.MeanAveragePrecision` and `sv.ConfusionMatrix` for benchmarking object detection models. A curated [Model Leaderboard](https://leaderboard.roboflow.com/) compares YOLOv8, YOLOv11, and other models on standard datasets. The leaderboard repository is open source at https://github.com/roboflow/model-leaderboard.
+
+## License
+
+MIT โ https://github.com/roboflow/supervision/blob/develop/LICENSE.md
+
+## Citation
+
+```bibtex
+@software{supervision,
+ author = {Roboflow},
+ title = {Supervision: Computer Vision Toolkit},
+ url = {https://github.com/roboflow/supervision},
+ year = {2023}
+}
+```
+
+## Versioning
+
+Stable release docs: https://supervision.roboflow.com/latest/
+Development branch: https://supervision.roboflow.com/develop/
diff --git a/docs/metrics/common_values.md b/docs/metrics/common_values.md
new file mode 100644
index 0000000..7d4bb0b
--- /dev/null
+++ b/docs/metrics/common_values.md
@@ -0,0 +1,25 @@
+---
+comments: true
+---
+
+# Common Values
+
+This page contains supplementary values, types and enums that metrics use.
+
+Install the metrics extra before using metrics APIs:
+
+```bash
+pip install "supervision[metrics]"
+```
+
+
+
+:::supervision.metrics.core.MetricTarget
+
+
+
+:::supervision.metrics.core.AveragingMethod
diff --git a/docs/metrics/f1_score.md b/docs/metrics/f1_score.md
new file mode 100644
index 0000000..1b26860
--- /dev/null
+++ b/docs/metrics/f1_score.md
@@ -0,0 +1,23 @@
+---
+comments: true
+---
+
+# F1 Score
+
+Install the metrics extra before using this API:
+
+```bash
+pip install "supervision[metrics]"
+```
+
+
+
+:::supervision.metrics.f1_score.F1Score
+
+
+
+:::supervision.metrics.f1_score.F1ScoreResult
diff --git a/docs/metrics/mean_average_precision.md b/docs/metrics/mean_average_precision.md
new file mode 100644
index 0000000..536558d
--- /dev/null
+++ b/docs/metrics/mean_average_precision.md
@@ -0,0 +1,30 @@
+---
+comments: true
+description: API reference for MeanAveragePrecision โ compute mAP for object detection benchmarking with boxes, masks, and oriented boxes.
+---
+
+# Mean Average Precision
+
+Install the metrics extra before using this API:
+
+```bash
+pip install "supervision[metrics]"
+```
+
+
+
+:::supervision.metrics.mean_average_precision.MeanAveragePrecision
+
+
+
+:::supervision.metrics.mean_average_precision.MeanAveragePrecisionResult
+
+
+
+:::supervision.dataset.formats.coco.get_coco_class_index_mapping
diff --git a/docs/metrics/mean_average_recall.md b/docs/metrics/mean_average_recall.md
new file mode 100644
index 0000000..5344286
--- /dev/null
+++ b/docs/metrics/mean_average_recall.md
@@ -0,0 +1,23 @@
+---
+comments: true
+---
+
+# Mean Average Recall
+
+Install the metrics extra before using this API:
+
+```bash
+pip install "supervision[metrics]"
+```
+
+
+
+:::supervision.metrics.mean_average_recall.MeanAverageRecall
+
+
+
+:::supervision.metrics.mean_average_recall.MeanAverageRecallResult
diff --git a/docs/metrics/precision.md b/docs/metrics/precision.md
new file mode 100644
index 0000000..c01628d
--- /dev/null
+++ b/docs/metrics/precision.md
@@ -0,0 +1,23 @@
+---
+comments: true
+---
+
+# Precision
+
+Install the metrics extra before using this API:
+
+```bash
+pip install "supervision[metrics]"
+```
+
+
+
+:::supervision.metrics.precision.Precision
+
+
+
+:::supervision.metrics.precision.PrecisionResult
diff --git a/docs/metrics/recall.md b/docs/metrics/recall.md
new file mode 100644
index 0000000..6d93569
--- /dev/null
+++ b/docs/metrics/recall.md
@@ -0,0 +1,23 @@
+---
+comments: true
+---
+
+# Recall
+
+Install the metrics extra before using this API:
+
+```bash
+pip install "supervision[metrics]"
+```
+
+
+
+:::supervision.metrics.recall.Recall
+
+
+
+:::supervision.metrics.recall.RecallResult
diff --git a/docs/robots.txt b/docs/robots.txt
new file mode 100644
index 0000000..f8f00e4
--- /dev/null
+++ b/docs/robots.txt
@@ -0,0 +1,25 @@
+User-agent: *
+Allow: /
+
+User-agent: GPTBot
+Allow: /
+
+User-agent: ClaudeBot
+Allow: /
+
+User-agent: PerplexityBot
+Allow: /
+
+User-agent: Bytespider
+Allow: /
+
+User-agent: CCBot
+Allow: /
+
+User-agent: GoogleOther
+Allow: /
+
+User-agent: Applebot
+Allow: /
+
+Sitemap: https://supervision.roboflow.com/sitemap.xml
diff --git a/docs/stylesheets/cookbooks_card.css b/docs/stylesheets/cookbooks_card.css
new file mode 100644
index 0000000..3509f20
--- /dev/null
+++ b/docs/stylesheets/cookbooks_card.css
@@ -0,0 +1,117 @@
+.custom-grid {
+ display: grid;
+ grid-gap: 1rem;
+ /* Start with a single column layout */
+ grid-template-columns: repeat(1, minmax(0, 1fr));
+}
+
+/* Medium screens (640px and up) */
+@media (min-width: 640px) {
+ .custom-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+/* Large screens (1024px and up) */
+@media (min-width: 1024px) {
+ .custom-grid {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ }
+}
+
+.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;
+ padding: 1rem;
+ transition: transform 0.2s ease-in-out; /* Smooth transition for the transform */
+ cursor: pointer;
+}
+
+.repo-card:hover {
+ transform: translateY(-0.125rem); /* Move up by -0.125rem on hover */
+}
+
+.authors {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ margin-bottom: 1rem;
+ margin-top: 1rem;
+ font-size: 0.75rem;
+ line-height: 1rem;
+}
+
+.author-names {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ margin-bottom: 1rem;
+ margin-top: 1rem;
+ margin-left: 0.5rem;
+}
+
+.author-container {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ padding: 0;
+ margin: 0;
+ transition: transform 0.2s;
+}
+
+.author-container.hover {
+ transform: translateY(-0.125rem);
+}
+
+.author-container:hover {
+ transform: translateY(-0.125rem);
+}
+
+.author-container a {
+ padding: 0;
+ margin: 0;
+ display: block;
+}
+
+.author-avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ display: block;
+}
+
+.author-name a {
+ color: inherit; /* Inherits the color from the parent element */
+ text-decoration: none; /* Removes the underline */
+}
+
+.author-name a:hover {
+ color: inherit; /* Inherits the color from the parent element */
+ text-decoration: underline; /* Adds underline on hover */
+}
+
+.label {
+ color: #fff;
+ padding: 2px 6px;
+ border-radius: 4px;
+ margin-right: 4px;
+}
+
+.non-selectable-text {
+ -webkit-user-select: none; /* Safari */
+ -moz-user-select: none; /* Firefox */
+ -ms-user-select: none; /* Internet Explorer/Edge */
+ user-select: none; /* Non-prefixed version, currently supported by Chrome, Opera, and Edge */
+}
diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css
new file mode 100644
index 0000000..2910d5f
--- /dev/null
+++ b/docs/stylesheets/extra.css
@@ -0,0 +1,270 @@
+:root, body {
+ /* Default to light theme */
+ --md-primary-fg-color: #8315F9;
+ --md-code-hl-color: #8315F9 !important;
+ --md-accent-fg-color: #8315F9 !important;
+ --md-code-hl-color--light: #e8d2ff89 !important;
+ --md-footer-fg-color--light: rgb(111, 108, 121) !important;
+}
+
+body.light {
+ /* Light theme */
+ --md-text-color: #000000;
+ --md-h2-color: #000000;
+}
+.md-grid {
+ max-width: 85%;
+ margin: auto;
+}
+.sublist {
+ display: none;
+ list-style: none;
+ padding-left: 0;
+ background: white;
+ position: absolute;
+ border-radius: 8px;
+ margin-top: 0.25rem;
+}
+.sublist {
+ transition: opacity 0.5s ease-in-out;
+ display: none;
+ position: absolute; /* Ensure it overlaps and doesn't break flow */
+ background: white; /* So it's visible */
+ z-index: 1000;
+}
+.sublist li {
+ padding: 0.5rem;
+}
+#products-list *:hover .products-sublist {
+ display: block;
+}
+#resources-list *, #products-list * {
+ cursor: pointer;
+}
+.products-sublist, .resources-sublist {
+ padding: 0.25rem;
+}
+.products-sublist li:hover, .resources-sublist li:hover, .md-nav__link[href]:hover {
+ background: rgb(242, 241, 247) !important;
+ border-radius: 6px;
+ color: initial !important;
+}
+.md-search {
+ flex-grow: 2;
+}
+.portfolio-section .md-grid {
+ max-width: 100%;
+}
+.md-header__inner {
+ align-items: center;
+ display: grid;
+ grid-template-columns: 0.1fr 1.4fr 2fr 2fr;
+ padding-right: 1rem;
+}
+.md-search__inner {
+ max-width: 600px;
+ width: 100%;
+ min-width: 100%;
+}
+.md-search__input {
+ background: white;
+ border: 1px solid rgb(229, 231, 235);
+ border-radius: 8px;
+ color: rgb(111, 108, 121);
+}
+.md-search__form *, .md-search__icon, .md-search__input {
+ color: rgb(111, 108, 121);
+}
+.md-search__input::placeholder {
+ color: rgb(156, 163, 175);
+}
+.md-search__form {
+ background: none !important;
+}
+.md-footer, .md-footer-meta {
+ background-color: transparent;
+ color: rgb(111, 108, 121);
+}
+.md-typeset .tabbed-set > input:first-child:checked ~ .tabbed-labels > :first-child, .md-typeset .tabbed-set > input:nth-child(10):checked ~ .tabbed-labels > :nth-child(10), .md-typeset .tabbed-set > input:nth-child(11):checked ~ .tabbed-labels > :nth-child(11), .md-typeset .tabbed-set > input:nth-child(12):checked ~ .tabbed-labels > :nth-child(12), .md-typeset .tabbed-set > input:nth-child(13):checked ~ .tabbed-labels > :nth-child(13), .md-typeset .tabbed-set > input:nth-child(14):checked ~ .tabbed-labels > :nth-child(14), .md-typeset .tabbed-set > input:nth-child(15):checked ~ .tabbed-labels > :nth-child(15), .md-typeset .tabbed-set > input:nth-child(16):checked ~ .tabbed-labels > :nth-child(16), .md-typeset .tabbed-set > input:nth-child(17):checked ~ .tabbed-labels > :nth-child(17), .md-typeset .tabbed-set > input:nth-child(18):checked ~ .tabbed-labels > :nth-child(18), .md-typeset .tabbed-set > input:nth-child(19):checked ~ .tabbed-labels > :nth-child(19), .md-typeset .tabbed-set > input:nth-child(2):checked ~ .tabbed-labels > :nth-child(2), .md-typeset .tabbed-set > input:nth-child(20):checked ~ .tabbed-labels > :nth-child(20), .md-typeset .tabbed-set > input:nth-child(3):checked ~ .tabbed-labels > :nth-child(3), .md-typeset .tabbed-set > input:nth-child(4):checked ~ .tabbed-labels > :nth-child(4), .md-typeset .tabbed-set > input:nth-child(5):checked ~ .tabbed-labels > :nth-child(5), .md-typeset .tabbed-set > input:nth-child(6):checked ~ .tabbed-labels > :nth-child(6), .md-typeset .tabbed-set > input:nth-child(7):checked ~ .tabbed-labels > :nth-child(7), .md-typeset .tabbed-set > input:nth-child(8):checked ~ .tabbed-labels > :nth-child(8), .md-typeset .tabbed-set > input:nth-child(9):checked ~ .tabbed-labels > :nth-child(9) {
+ color: #8315F9;
+ border-bottom: 1px solid #8315F9;
+}
+.md-footer *, html .md-footer-meta.md-typeset a {
+ color: rgb(111, 108, 121);
+}
+.repo-card {
+ height: 100%;
+}
+.header-btn {
+ text-align: center;
+}
+.header-btn, .sublist {
+ box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px, rgb(217, 215, 226) 0px 0px 0px 1px, rgb(217, 215, 226) 0px 1px 2px 0px;
+}
+.header-btn:hover {
+ box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px, rgb(217, 215, 226) 0px 0px 0px 1px, rgb(217, 215, 226) 0px 1.0001px 2.00013px -0.0000327245px, rgba(0, 0, 0, 0) 0px 0.000065449px 0.000130898px -0.000065449px;
+}
+.md-typeset .headerlink:hover, .md-typeset .headerlink:target {
+ color: #8315F9;
+}
+.md-typeset h1, .md-header__title {
+ color: black;
+ font-weight: 800;
+}
+.md-typeset h1 {
+ font-weight: normal;
+ margin-bottom: 1rem;
+}
+body {
+ background: linear-gradient(to left bottom, rgb(243, 238, 255), rgb(255, 255, 255) 60%) no-repeat;
+}
+
+/* .md-nav__link:has([tabindex=""]) {
+ text-transform: uppercase;
+} */
+
+.header-list {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ list-style: none;
+ font-size: 0.75rem;
+ justify-content: flex-end;
+}
+.md-nav__list label, .md-nav--secondary label {
+ /* text-transform: uppercase; */
+ color: rgb(29, 29, 31) !important;
+ font-size: 0.7rem;
+ margin-bottom: 0;
+}
+.md-nav--secondary label {
+ margin-left: 0.5rem;
+}
+
+.md-nav__link {
+ padding: 0.25rem;
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+}
+
+.md-nav__link--active {
+ background: rgb(243, 238, 255);
+ border-radius: 6px;
+ padding-top: 0.25rem;
+}
+
+.md-tabs__item--active {
+ color: var(--md-primary-fg-color);
+ border-bottom: 2px solid var(--md-primary-fg-color);
+}
+
+.md-nav--secondary .md-nav__title {
+ background: transparent;
+ box-shadow: none;
+}
+
+.md-header, .md-tabs {
+ color: rgb(111, 108, 121);
+ background-color: transparent;
+}
+.md-header--shadow {
+ background: linear-gradient(to left bottom, rgb(243, 238, 255), rgb(255, 255, 255) 60%);
+ box-shadow: none;
+ border-bottom: 1px solid rgb(229, 231, 235);
+}
+
+#item-logo {
+ display: none;
+}
+.md-main__inner, .md-header__inner, .md-grid {
+ max-width: 100%;
+}
+@media (max-width: 1200px) {
+ .md-header__inner {
+ display: flex;
+ }
+ .header-list {
+ display: none;
+ }
+ #item-logo {
+ display: block;
+ }
+}
+.md-content {
+ max-width: 40rem;
+ margin: auto;
+}
+/* // if no md-sidebar--primary, make .md-content full width */
+.md-main__inner:has(.md-sidebar--primary[hidden]) .md-content {
+ max-width: 100%;
+}
+.md-sidebar--primary {
+ flex: 0 20%;
+}
+.md-tabs {
+ border-bottom: 1px solid rgb(229, 231, 235);
+}
+.md-main__inner {
+ padding-top: 1rem;
+ margin-top: 0;
+}
+
+body.dark {
+ /* Dark theme */
+ --md-text-color: #FFFFFF;
+ --md-h2-color: #add8e6;
+}
+
+body.light .md-content *, body.dark .md-content * {
+ color: var(--md-text-color) !important;
+}
+
+body[data-md-url$="/cookbooks/"] .md-sidebar--primary,
+body[data-md-url$="/cookbooks/"] .md-sidebar--secondary {
+ display: none;
+}
+
+body[data-md-url$="/cookbooks/"] .md-content {
+ margin-left: 0;
+ width: 100%;
+}
+
+.md-main, nav .md-grid, .md-header__inner {
+ max-width: 1600px;
+ width: 100%;
+ margin: auto;
+}
+.md-search__scrollwrap {
+ width: 100% !important;
+}
+.md-nav--secondary .md-nav__title {
+ position: initial !important;
+}
+
+.md-header__title .md-ellipsis {
+ overflow: initial !important;
+ text-overflow: initial !important;
+}
+.md-search {
+ flex-grow: 0;
+}
+
+/* Table style */
+
+th, td {
+ border: 1px solid var(--md-typeset-table-color);
+}
+
+.md-typeset__table {
+ line-height: 1.5;
+}
+
+.md-typeset__table table:not([class]) {
+ font-size: 0.6rem;
+ border-collapse: collapse;
+}
+
+.md-typeset__table table:not([class]) td,
+.md-typeset__table table:not([class]) th {
+ padding: 10px;
+}
diff --git a/docs/theme/cookbooks.html b/docs/theme/cookbooks.html
new file mode 100644
index 0000000..9f3fbc1
--- /dev/null
+++ b/docs/theme/cookbooks.html
@@ -0,0 +1,76 @@
+{% extends "main.html" %}
+{% block libs %}
+
+
+
+{% endblock %}
+{% block content %}
+
+
+
+
Supervision Cookbooks
+
+
+
+
+{% endblock %}
diff --git a/docs/theme/index.html b/docs/theme/index.html
new file mode 100644
index 0000000..c65749c
--- /dev/null
+++ b/docs/theme/index.html
@@ -0,0 +1,15 @@
+{% extends "main.html" %}
+{% block content %}
+{{ super() }}
+
+{% endblock %}
diff --git a/docs/theme/main.html b/docs/theme/main.html
new file mode 100644
index 0000000..3054edd
--- /dev/null
+++ b/docs/theme/main.html
@@ -0,0 +1,495 @@
+{% extends "base.html" %}
+
+{% block content %}
+{% if page.nb_url %}
+
+{% endif %}
+{{ super() }}
+{% endblock content %}
+
+{% block extrahead %}
+{{ super() }}
+{% if page.meta is defined and page.meta is not none and page.meta is not undefined %}
+{% set _meta = page.meta %}
+{% else %}
+{% set _meta = {} %}
+{% endif %}
+{% set page_description = _meta.description | d(config.site_description) %}
+{# โโ GEO: JSON-LD + OG tags (page context required โ skip for theme templates like 404) #}
+
+{# โโ GEO: JSON-LD structured data โโโโโโโโโโโโโโโโโโโโโโโโโโโโ #}
+
+
+
+
+
+
+
+
+{% for is_home in [page.is_homepage] %}{% if is_home %}
+
+{% endif %}{% endfor %}
+
+{% if page.url == 'faq/' %}
+
+{% endif %}
+
+{% if page.url == 'about/' or page.url == 'contact/' %}
+
+{% endif %}
+
+{% if 'how_to' in page.url %}
+
+{% endif %}
+
+{# โโ How-to FAQ schema โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ #}
+{% if 'how_to' in page.url %}
+
+{% endif %}
+
+{% for is_not_home in [not page.is_homepage] %}{% if is_not_home %}
+
+{% endif %}{% endfor %}
+
+{# โโ GEO: Open Graph + Twitter Card meta tags โโโโโโโโโโโโโโโโ #}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{# โโ API reference schema (detection/ metrics/ datasets/ reference pages) โโ #}
+{% for is_ref in [('reference' in page.url or 'detection/' in page.url or 'metrics/' in page.url or 'keypoint/' in page.url or 'classification/' in page.url) and 'how_to' not in page.url] %}{% if is_ref %}
+
+{% endif %}{% endfor %}
+
+{# Cookbooks FAQ schema โโ #}
+{% for is_cookbook in ['cookbook' in page.url] %}{% if is_cookbook %}
+
+{% endif %}{% endfor %}
+
+{# IndexNow ownership key โ do NOT change this value.
+ The same key must exist in three places (all must stay in sync):
+ 1. This meta tag (docs/theme/main.html)
+ 2. The key file at docs/0d5d9799b1cc4a39825146388c6781eb.txt
+ 3. The CI step in .github/workflows/publish-docs.yml
+ Bing/Yandex verify ownership by fetching https://supervision.roboflow.com/.txt
+ and comparing its contents to this meta tag before accepting IndexNow submissions. #}
+
+
+
+
+{% endblock %}
diff --git a/docs/theme/partials/comments.html b/docs/theme/partials/comments.html
new file mode 100644
index 0000000..d95dd6b
--- /dev/null
+++ b/docs/theme/partials/comments.html
@@ -0,0 +1,51 @@
+{% if page.meta.comments %}
+
+
+
+
+
+
+
+{% endif %}
diff --git a/docs/theme/partials/header.html b/docs/theme/partials/header.html
new file mode 100644
index 0000000..472db17
--- /dev/null
+++ b/docs/theme/partials/header.html
@@ -0,0 +1,181 @@
+{% set class = "md-header" %}
+{% if "navigation.tabs.sticky" in features %}
+ {% set class = class ~ " md-header--shadow md-header--lifted" %}
+{% elif "navigation.tabs" not in features %}
+ {% set class = class ~ " md-header--shadow" %}
+{% endif %}
+
+
+
+
+
+
+ {% if "navigation.tabs.sticky" in features %}
+ {% if "navigation.tabs" in features %}
+ {% include "partials/tabs.html" %}
+ {% endif %}
+ {% endif %}
+
diff --git a/docs/trackers.md b/docs/trackers.md
new file mode 100644
index 0000000..c68a999
--- /dev/null
+++ b/docs/trackers.md
@@ -0,0 +1,12 @@
+---
+comments: true
+description: API reference for supervision's deprecated ByteTrack tracker wrapper.
+---
+
+# ByteTrack
+
+!!! warning "Deprecated"
+
+ `sv.ByteTrack` is deprecated in `supervision-0.28.0` and will be removed in `supervision-0.31.0`. Install `trackers` and use `ByteTrackTracker` instead.
+
+:::supervision.tracker.byte_tracker.core.ByteTrack
diff --git a/docs/utils/conversion.md b/docs/utils/conversion.md
new file mode 100644
index 0000000..6e7d7d0
--- /dev/null
+++ b/docs/utils/conversion.md
@@ -0,0 +1,36 @@
+---
+comments: true
+status: new
+---
+
+# Conversion Utils
+
+
+
+:::supervision.utils.conversion.cv2_to_pillow
+
+
+
+:::supervision.utils.conversion.pillow_to_cv2
+
+
+
+:::supervision.utils.conversion.ensure_cv2_image_for_annotation
+
+
+
+:::supervision.utils.conversion.ensure_pil_image_for_annotation
+
+
+
+:::supervision.utils.conversion.images_to_cv2
diff --git a/docs/utils/draw.md b/docs/utils/draw.md
new file mode 100644
index 0000000..d881051
--- /dev/null
+++ b/docs/utils/draw.md
@@ -0,0 +1,71 @@
+---
+comments: true
+---
+
+# Draw Utils
+
+
+
+:::supervision.draw.utils.draw_line
+
+
+
+:::supervision.draw.utils.draw_rectangle
+
+
+
+:::supervision.draw.utils.draw_filled_rectangle
+
+
+
+:::supervision.draw.utils.draw_polygon
+
+
+
+:::supervision.draw.utils.draw_filled_polygon
+
+
+
+:::supervision.draw.utils.draw_text
+
+
+
+:::supervision.draw.utils.draw_image
+
+
+
+:::supervision.draw.utils.calculate_optimal_text_scale
+
+
+
+:::supervision.draw.utils.calculate_optimal_line_thickness
+
+
+
+:::supervision.draw.color.Color
+
+
+
+:::supervision.draw.color.ColorPalette
diff --git a/docs/utils/file.md b/docs/utils/file.md
new file mode 100644
index 0000000..a2a0818
--- /dev/null
+++ b/docs/utils/file.md
@@ -0,0 +1,11 @@
+---
+comments: true
+---
+
+# File Utils
+
+
+
+:::supervision.utils.file.list_files_with_extensions
diff --git a/docs/utils/geometry.md b/docs/utils/geometry.md
new file mode 100644
index 0000000..3b02f90
--- /dev/null
+++ b/docs/utils/geometry.md
@@ -0,0 +1,33 @@
+---
+comments: true
+---
+
+
+
+:::supervision.geometry.utils.get_polygon_center
+
+
+
+:::supervision.geometry.core.Position
+
+
+
+:::supervision.geometry.core.Point
+
+
+
+:::supervision.geometry.core.Rect
+
+
+
+:::supervision.geometry.core.Vector
diff --git a/docs/utils/image.md b/docs/utils/image.md
new file mode 100644
index 0000000..9d1c189
--- /dev/null
+++ b/docs/utils/image.md
@@ -0,0 +1,54 @@
+---
+comments: true
+status: new
+---
+
+# Image Utils
+
+
+
+:::supervision.utils.image.crop_image
+
+
+
+:::supervision.utils.image.scale_image
+
+
+
+:::supervision.utils.image.resize_image
+
+
+
+:::supervision.utils.image.letterbox_image
+
+
+
+:::supervision.utils.image.tint_image
+
+
+
+:::supervision.utils.image.grayscale_image
+
+
+
+:::supervision.utils.image.get_image_resolution_wh
+
+
+
+:::supervision.utils.image.ImageSink
diff --git a/docs/utils/iterables.md b/docs/utils/iterables.md
new file mode 100644
index 0000000..5ae92dc
--- /dev/null
+++ b/docs/utils/iterables.md
@@ -0,0 +1,17 @@
+---
+comments: true
+---
+
+# Iterables Utils
+
+
+
+:::supervision.utils.iterables.create_batches
+
+
+
+:::supervision.utils.iterables.fill
diff --git a/docs/utils/notebook.md b/docs/utils/notebook.md
new file mode 100644
index 0000000..3eab046
--- /dev/null
+++ b/docs/utils/notebook.md
@@ -0,0 +1,17 @@
+---
+comments: true
+---
+
+# Notebooks Utils
+
+
+
+:::supervision.utils.notebook.plot_image
+
+
+
+:::supervision.utils.notebook.plot_images_grid
diff --git a/docs/utils/video.md b/docs/utils/video.md
new file mode 100644
index 0000000..f9a5821
--- /dev/null
+++ b/docs/utils/video.md
@@ -0,0 +1,35 @@
+---
+comments: true
+---
+
+# Video Utils
+
+
+
+:::supervision.utils.video.VideoInfo
+
+
+
+:::supervision.utils.video.VideoSink
+
+
+
+:::supervision.utils.video.FPSMonitor
+
+
+
+:::supervision.utils.video.get_video_frames_generator
+
+
+
+:::supervision.utils.video.process_video
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..d29b485
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,12 @@
+# Examples
+
+Here, you'll find end-to-end examples that show how to solve common computer vision problems using Supervision.
+
+For more information and examples, visit our [documentation](https://supervision.roboflow.com/latest/detection/annotators/) and explore our [how-to guides](https://supervision.roboflow.com/latest/how_to/detect_and_annotate/) and [cookbooks](https://supervision.roboflow.com/latest/cookbooks/). Join our [Discord](https://discord.com/invite/GbfgXGJ8Bk) and meet other Supervision power users!
+
+- [tracking](./tracking) by [@SkalskiP](https://github.com/SkalskiP)
+- [count people in zone](./count_people_in_zone) by [@SkalskiP](https://github.com/SkalskiP)
+- [traffic analysis](./traffic_analysis) by [@SkalskiP](https://github.com/SkalskiP)
+- [speed estimation](./speed_estimation) by [@SkalskiP](https://github.com/SkalskiP)
+- [time in zone](./time_in_zone) by [@SkalskiP](https://github.com/SkalskiP)
+- [heatmap and track](./heatmap_and_track/) by [@HinePo](https://github.com/HinePo)
diff --git a/examples/compact_mask/README.md b/examples/compact_mask/README.md
new file mode 100644
index 0000000..cc8e390
--- /dev/null
+++ b/examples/compact_mask/README.md
@@ -0,0 +1,655 @@
+# CompactMask โ Memory-Efficient Mask Storage
+
+This example benchmarks `CompactMask`, a new mask representation introduced in `supervision` that replaces dense `(N, H, W)` boolean arrays with a crop-scoped Run-Length Encoding (RLE). The benchmark demonstrates full API compatibility, massive memory savings, and order-of-magnitude annotation speedups โ with no change to your existing `Detections` code.
+
+---
+
+## The Problem
+
+Instance segmentation models return one boolean mask per detected object. `supervision` stores these as a stacked `(N, H, W)` numpy array.
+
+For a 4K image with 1 000 detected objects:
+
+```
+1 000 x 3840 x 2160 x 1 byte = 8.3 GB
+```
+
+At this scale, typical pipelines crash with `MemoryError` before a single frame is annotated. Aerial imagery, satellite tiles, and high-density crowd scenes all hit this wall.
+
+---
+
+## The Solution โ Crop-RLE Storage
+
+`CompactMask` stores each mask as a run-length encoding of its **bounding-box crop** rather than the full image canvas.
+
+```
+dense (N,H,W) mask โ N x crop_RLE + N x (x1,y1) offset
+8.3 GB โ ~280 KB
+```
+
+The bounding boxes are already present in `Detections.xyxy`, so no extra metadata is required from the caller.
+
+### Theoretical analysis (4K scene, 80x80 px objects, ~65% fill per bbox)
+
+Assumptions used throughout the PR design analysis:
+
+| Parameter | Value |
+| ---------------------- | ------------------------ |
+| Image size | 4K โ 3840x2160 = 8.29 MP |
+| Avg bounding box | 80x80 px = 6 400 pxยฒ |
+| Fill ratio within bbox | ~65% |
+| Avg contour vertices | ~400 pts |
+| Avg RLE runs / mask | ~240 (3 runs x 80 rows) |
+
+#### Space comparison
+
+| Format | Per object | N=100 | N=1 000 | vs Dense |
+| ------------------- | -------------- | ------ | ---------- | --------- |
+| **Dense** (current) | 8.29 MB | 829 MB | **8.3 GB** | 1x |
+| Local Crop + Offset | 6.4 KB | 640 KB | 6.4 MB | 1 300x |
+| **Crop-RLE** โ | ~2 KB | 200 KB | **2 MB** | 4 000x |
+| Polygon โ lossy | ~3.2 KB | 320 KB | 3.2 MB | 2 600x |
+| memmap | 8.29 MB (disk) | 829 MB | 8.3 GB | 1x (disk) |
+
+Crop-RLE beats Local Crop because it only encodes actual pixel runs, skipping the ~35% background pixels within each bounding box.
+
+#### Encode time: dense array โ format
+
+| Format | Complexity | N=10 | N=100 | N=1 000 |
+| ------------------- | --------------------------------- | ------- | ------- | --------- |
+| Local Crop + Offset | O(A) โ strided slice from xyxy | ~0.1 ms | ~1 ms | ~10 ms |
+| **Crop RLE** | O(A) โ scan crop rows for runs | ~0.2 ms | ~2 ms | ~20 ms |
+| Polygon | O(P) โ `cv2.findContours` on crop | ~2 ms | ~20 ms | ~200 ms |
+| memmap | O(I) โ write 8.29 MB to disk | ~80 ms | ~800 ms | ~8 000 ms |
+
+#### Decode time: format โ full (H, W) mask
+
+Required by `MaskAnnotator`, `mask_iou_batch`, `merge()`, etc. Dominant cost at 4K is **allocating and zeroing a 8.29 MB array**, which is identical across all in-memory formats once full materialisation is needed.
+
+| Format | N=10 | N=100 | N=1 000 |
+| --------------------- | ------ | ------- | --------- |
+| Local Crop / Crop RLE | ~3 ms | ~30 ms | ~300 ms |
+| Polygon | ~5 ms | ~50 ms | ~500 ms |
+| memmap | ~80 ms | ~800 ms | ~8 000 ms |
+
+#### Decode time: crop-only path (optimised)
+
+When callers need only the bounding-box region โ `MaskAnnotator` crop-paint path, `.area`, `contains_holes`, `filter_segments_by_distance`:
+
+| Format | Complexity | N=10 | N=100 | N=1 000 |
+| ------------------- | -------------------------------- | -------- | ------- | --------- |
+| Local Crop + Offset | O(1) โ already stored | ~0 ms | ~0 ms | ~0 ms |
+| **Crop RLE** โ | O(A) โ expand ~240 runs | ~0.02 ms | ~0.2 ms | ~2 ms |
+| Polygon | O(A) โ `fillPoly` on crop canvas | ~2 ms | ~20 ms | ~200 ms |
+| memmap | N/A โ always full-size | ~80 ms | ~800 ms | ~8 000 ms |
+
+Crop RLE's `.crop()` method powers the `MaskAnnotator` optimisation โ it never allocates the full image canvas, which is the entire source of the annotation speedup.
+
+#### IoU / NMS at 1 % bbox overlap rate (sparse aerial scene)
+
+| Format | Strategy | N=1 000 |
+| ------------------- | ------------------------------------- | ---------- |
+| Dense (current) | All pairs, 640ยฒ pixel AND | ~10 000 ms |
+| Local Crop + Offset | Bbox pre-filter โ pixel IoU | **~5 ms** |
+| Crop RLE | Bbox pre-filter โ expand intersection | **~15 ms** |
+
+At N=1 000 with 1 % overlap, bbox pre-filter reduces 499 500 candidate pairs to ~5 000 overlapping pairs โ a ~2 000x reduction in pixel-level work.
+
+---
+
+## Why Crop-RLE Was Chosen over Local Crop
+
+Both formats compress extremely well; the deciding factors for Crop-RLE are:
+
+1. **~3x smaller** for masks that are themselves sparse within their bounding box.
+2. **COCO RLE interop path** โ crop RLE uses column-major (F-order) pixel scan, matching `pycocotools`; to interoperate, you still need to construct a full-image COCO RLE from the crop-scoped encoding (for example by padding/merging runs onto the full-image canvas, or by materialising the crop in the full image and re-encoding).
+3. `.area` computed directly from run lengths โ no materialisation, no allocation.
+
+The main trade-off: crop-only decode is O(A) rather than O(1). For the common solid-fill segmentation mask this is negligible (\<0.1 ms per mask).
+
+---
+
+## Operation-by-Operation Speedup Analysis
+
+This section walks through every `Detections` operation that touches masks and shows exactly why `CompactMask` is faster. All code snippets are taken from the actual implementation. Numbers use the **FHD-200-50%-v600** scenario unless noted (1920 x 1080 image, 200 detections, each mask filling ~50% of the frame, 600-vertex polygons โ a realistic hard case with dense fill and complex object boundaries).
+
+At 50% fill on an FHD image each mask's bounding box covers a large portion of the frame, producing many RLE runs per row.
+
+---
+
+### Memory
+
+Dense stores one full-resolution bool array per mask:
+
+```
+N x H x W x 1 byte
+200 x 1080 x 1920 x 1 = 414 MB
+```
+
+Compact stores three lightweight structures:
+
+```python
+self._rles: list[npt.NDArray[np.int32]] # N Python references to small int32 arrays
+self._crop_shapes: npt.NDArray[np.int32] # (N, 2) โ crop (h, w) per mask
+self._offsets: npt.NDArray[np.int32] # (N, 2) โ (x1, y1) origin per mask
+```
+
+Per-mask RLE size at 50% fill with 600-vertex polygons: ~4.7 KB (933 KB / 200). Per-mask dense size: 1920 x 1080 x 1 = 2.1 MB. Per-mask ratio: 2.1 MB / 4.7 KB = **~445x**.
+
+Scaled to N=200: 200 x 4.7 KB = ~933 KB of RLE data, plus `_crop_shapes` (1.6 KB) and `_offsets` (1.6 KB). Python list + array object overhead roughly doubles the footprint for small N.
+
+| Component | Dense | Compact | Ratio |
+| --------------- | ---------- | ----------- | --------- |
+| Mask data | 414 MB | ~933 KB | ~445x |
+| Python overhead | negligible | ~933 KB | -- |
+| **Total** | **414 MB** | **~1.9 MB** | **~392x** |
+
+At 5% fill with 8-vertex polygons, the ratio reaches 10 000xโ20 000x because crops are tiny and RLEs are extremely short. The benchmark's 4K-200-5%-v8 scenario measures 21 786x (theory) / ~6 000x (malloc). The SAT-200-5%-v8 scenario reaches 62 968x theoretical.
+
+---
+
+### `.area`
+
+Dense `Detections.area` reads every pixel of every mask:
+
+```python
+# detection/core.py โ dense path
+return np.array([np.sum(mask) for mask in self.mask])
+# N masks x H x W boolean sums = 200 x 2.1 M = 420 million reads
+```
+
+Compact delegates to `_rle_area`, which sums only the odd-indexed run lengths (the True-pixel runs) in each RLE:
+
+```python
+# detection/compact_mask.py โ _rle_area
+return int(np.sum(rle[1::2]))
+```
+
+```python
+# detection/compact_mask.py โ CompactMask.area
+return np.array([_rle_area(r) for r in self._rles], dtype=np.int64)
+```
+
+At FHD-200-50%-v600, dense `.area` takes 84.66 ms; compact takes 0.48 ms โ a **71x speedup**. At SAT-200-20%-v128 the measured speedup reaches **1 204x** because the dense array is 13.4 GB and each sum must scan the entire canvas.
+
+| Factor | Reduction |
+| ---------------------------------- | ----------- |
+| RLE sums vs full-frame pixel reads | ~4 600x |
+| int32 arithmetic vs bool reduction | ~2x |
+| No (H, W) allocation per mask | latency |
+| **Combined** | **~1 000x** |
+
+---
+
+### `filter` / `__getitem__` (boolean index)
+
+Dense: `masks[bool_array]` triggers NumPy fancy indexing, which allocates a new `(K, H, W)` bool array and copies K full frames:
+
+```python
+# detection/core.py โ Detections.__getitem__
+mask = (self.mask[index] if self.mask is not None else None,)
+# For dense ndarray, numpy allocates (K, 2160, 3840) and memcpy's K frames
+```
+
+Compact `CompactMask.__getitem__` converts the boolean index to integer positions and builds a new `CompactMask` from Python list indexing and NumPy fancy indexing on small `(N, 2)` arrays:
+
+```python
+# detection/compact_mask.py โ CompactMask.__getitem__
+if isinstance(index, np.ndarray) and index.dtype == bool:
+ idx_arr = np.where(index)[0]
+# ...
+new_rles = [self._rles[int(i)] for i in idx_arr]
+new_crop_shapes: npt.NDArray[np.int32] = self._crop_shapes[idx_arr]
+new_offsets: npt.NDArray[np.int32] = self._offsets[idx_arr]
+return CompactMask(new_rles, new_crop_shapes, new_offsets, self._image_shape)
+```
+
+At FHD-200-50%-v600, dense `filter` takes 14.56 ms; compact takes 0.03 ms โ a **500x speedup**. At SAT-200-20%-v128 the speedup reaches **14 757x**.
+
+| | Dense | Compact |
+| ----------- | ----------------------- | ----------------------------------- |
+| Data copied | K x H x W (full frames) | K Python references + K x 8 bytes |
+| Allocation | new `(K, H, W)` array | new `CompactMask` shell (~trivial) |
+| **Speedup** | | **hundreds to tens of thousands x** |
+
+---
+
+### `annotate` (`MaskAnnotator`)
+
+Dense: for each mask, `MaskAnnotator` indexes the full `(H, W)` array and applies a boolean mask across the entire scene:
+
+```python
+# annotators/core.py โ dense path
+mask = np.asarray(detections.mask[detection_idx], dtype=bool)
+colored_mask[mask] = color.as_bgr()
+```
+
+Each `detections.mask[detection_idx]` for a dense array yields a full `(H, W)` view, and the boolean indexing scans all pixels.
+
+Compact: the annotator detects `CompactMask` and paints only the crop region:
+
+```python
+# annotators/core.py โ compact path
+x1 = int(compact_mask.offsets[detection_idx, 0])
+y1 = int(compact_mask.offsets[detection_idx, 1])
+crop_m = compact_mask.crop(detection_idx)
+crop_h, crop_w = crop_m.shape
+colored_mask[y1 : y1 + crop_h, x1 : x1 + crop_w][crop_m] = color.as_bgr()
+```
+
+`compact_mask.crop()` decodes the RLE into a `(crop_h, crop_w)` array. At FHD-200-50%-v600, dense `annotate` takes 848.95 ms; compact takes 32.67 ms โ a **22x speedup**. At SAT-200-20%-v128 the speedup reaches **89x**.
+
+| Factor | Reduction |
+| -------------------------------------------------- | ------------------- |
+| Crop decode vs full-frame boolean index (per mask) | crop-size dependent |
+| No full `(H, W)` allocation per integer index | latency |
+| x N masks | compounds |
+| **Combined** | **~26 โ 400x** |
+
+---
+
+### IoU (`mask_iou_batch` / `compact_mask_iou_batch`)
+
+Dense `mask_iou_batch` on N=200, FHD:
+
+```python
+# detection/utils/iou_and_nms.py โ _mask_iou_batch_split
+intersection_area = np.logical_and(masks_true[:, None], masks_detection).sum(
+ axis=(2, 3)
+)
+# shape (200, 200, 1080, 1920) โ ~80 billion boolean ops
+# .sum(axis=(2,3)) for intersection counts
+# memory_limit splits this into chunks capped at 5 GB scratch
+```
+
+Compact `compact_mask_iou_batch` โ three layered optimisations:
+
+**1. Vectorised bbox pre-filter โ O(Nยฒ) array ops, zero decoding**
+
+```python
+ix1: npt.NDArray[np.int32] = np.maximum(x1a[:, None], x1b[None, :])
+iy1: npt.NDArray[np.int32] = np.maximum(y1a[:, None], y1b[None, :])
+ix2: npt.NDArray[np.int32] = np.minimum(x2a[:, None], x2b[None, :])
+iy2: npt.NDArray[np.int32] = np.minimum(y2a[:, None], y2b[None, :])
+bbox_overlap: npt.NDArray[np.bool_] = (ix1 <= ix2) & (iy1 <= iy2)
+```
+
+At 5% fill, two random masks overlap with probability ~4%. ~96% of the Nยฒ pairs get IoU = 0 for free โ no pixel work at all.
+
+**2. Sub-crop decode โ compare only the intersection region**
+
+```python
+ox_a, oy_a = int(x1a[i]), int(y1a[i])
+sub_a = crops_a[i][ly1 - oy_a : ly2 - oy_a + 1, lx1 - ox_a : lx2 - ox_a + 1]
+
+ox_b, oy_b = int(x1b[j]), int(y1b[j])
+sub_b = crops_b[j][ly1 - oy_b : ly2 - oy_b + 1, lx1 - ox_b : lx2 - ox_b + 1]
+
+inter = int(np.logical_and(sub_a, sub_b).sum())
+```
+
+The intersection sub-region of two overlapping crops is typically far smaller than the full frame.
+
+**3. Crop caching โ each mask decoded at most once**
+
+```python
+if i not in crops_a:
+ crops_a[i] = masks_true.crop(i)
+```
+
+Area is obtained from `_rle_area` (sum odd-indexed runs), never touching the pixel grid:
+
+```python
+areas_a: npt.NDArray[np.int64] = masks_true.area
+```
+
+At FHD-200-50%-v600, dense IoU takes 23 915 ms; compact takes 51.58 ms โ a **446x speedup**. At 5% fill / sparse scenarios the speedup is even larger because fewer bbox pairs overlap.
+
+| Factor | Reduction |
+| ------------------------------------ | --------------- |
+| Bbox pre-filter at sparse fill | 25x |
+| Sub-crop vs full frame per pair | ~200x |
+| Area from RLE, not `sum(axis=(1,2))` | ~10x |
+| No 5 GB scratch allocation | latency |
+| **Combined** | **~100 โ 500x** |
+
+At 20% fill the gaps close โ more pairs overlap, larger crops โ speedup drops toward the lower end of the range.
+
+---
+
+### NMS (`mask_non_max_suppression`)
+
+Both dense and compact paths now call `mask_iou_batch(masks, masks)` directly, computing exact mask IoU on the original (unresized) masks. There is no intermediate resize step.
+
+```python
+# detection/utils/iou_and_nms.py โ NMS (both paths)
+ious = mask_iou_batch(masks, masks, overlap_metric)
+```
+
+`mask_iou_batch` dispatches internally: when passed a `CompactMask` it calls `compact_mask_iou_batch`, applying all three IoU optimisations (bbox pre-filter, sub-crop decode, crop caching). When passed a dense ndarray it runs the chunked pixel-AND path.
+
+All three IoU optimisations apply to the compact path:
+
+| Factor | Reduction |
+| ------------------------------------- | ---------------------------- |
+| Bbox pre-filter eliminates most pairs | 25x at sparse fill |
+| Sub-crop decode for remaining pairs | ~200x |
+| Area from RLE, not pixel sum | ~10x |
+| **Combined** | **same as IoU: ~100 โ 500x** |
+
+At FHD-200-50%-v600, dense NMS takes 5 231 ms; compact takes 48.15 ms โ a **481x speedup**. Dense IoU/NMS is skipped for scenarios above 1 GB (4K-200 and SAT-200 tiers); compact NMS still runs on those.
+
+---
+
+### `merge` (`Detections.merge`)
+
+Dense: `np.vstack` allocates a new `(N1+N2, H, W)` array and copies both halves:
+
+```python
+# detection/core.py โ dense merge path
+return np.vstack([np.asarray(m) for m in masks])
+# Merging two 100-mask sets at FHD: 2 x 100 x 2.1 MB = 414 MB copied
+```
+
+Compact: `CompactMask.merge` extends a Python list and concatenates two small int32 arrays:
+
+```python
+# detection/compact_mask.py โ CompactMask.merge
+new_rles: list[npt.NDArray[np.int32]] = []
+for m in masks_list:
+ new_rles.extend(m._rles)
+
+new_crop_shapes: npt.NDArray[np.int32] = np.concatenate(
+ [m._crop_shapes for m in masks_list], axis=0
+)
+new_offsets: npt.NDArray[np.int32] = np.concatenate(
+ [m._offsets for m in masks_list], axis=0
+)
+```
+
+`list.extend` copies N reference pointers. `np.concatenate` on `(N, 2)` int32 arrays copies N x 8 bytes per array.
+
+At FHD-200-50%-v600, dense merge takes 29.71 ms; compact takes 0.03 ms โ a **929x speedup**. At SAT-200-20%-v128 the speedup reaches **89 046x**.
+
+| | Dense | Compact |
+| ----------- | ----------------------- | -------------------------- |
+| Data moved | N x H x W (full frames) | N references + N x 8 bytes |
+| Allocation | new `(N, H, W)` array | new `CompactMask` shell |
+| **Speedup** | | **effectively free** |
+
+**Note:** `Detections.merge` calls `is_empty()` on each input. Before the `len(xyxy) > 0` short-circuit was added, `is_empty()` invoked `__eq__` which called `np.array_equal(self.to_dense(), ...)` โ materialising the entire `(N, H, W)` CompactMask to dense just to check emptiness. The fix:
+
+```python
+# detection/core.py โ Detections.is_empty (fixed)
+if len(self.xyxy) > 0:
+ return False
+```
+
+This O(1) check avoids the O(N x H x W) dense materialisation that previously dominated compact merge time.
+
+---
+
+### `offset` / `with_offset` (`InferenceSlicer` tile stitching)
+
+Dense `move_masks`: allocates a new `(N, new_H, new_W)` array and copies each mask with shifted slice coordinates โ O(N x H x W):
+
+```python
+# detection/utils/masks.py โ move_masks
+mask_array = np.full((masks.shape[0], resolution_wh[1], resolution_wh[0]), False)
+# ... source/destination slicing logic ...
+mask_array[:, dst_y1:dst_y2, dst_x1:dst_x2] = masks[:, src_y1:src_y2, src_x1:src_x2]
+```
+
+Compact `with_offset(dx, dy)`: vectorised bounds check first. All new bounding-box positions are computed in a single numpy op. When none overflow the new canvas โ the common case in `InferenceSlicer` โ the RLE data is not touched at all:
+
+```python
+# detection/compact_mask.py โ CompactMask.with_offset (fast path)
+new_offsets = self._offsets + np.array([dx, dy], dtype=np.int32) # O(N) numpy
+needs_clip = (x1s < 0) | (y1s < 0) | (x2s >= new_w) | (y2s >= new_h)
+if not needs_clip.any():
+ return CompactMask(
+ list(self._rles), self._crop_shapes.copy(), new_offsets, new_image_shape
+ )
+```
+
+When a crop does overflow (e.g. object at a tile edge), only that crop is decoded, sliced, and re-encoded. Masks fully outside bounds get a 1x1 all-False stub without any decoding.
+
+At FHD-200-50%-v600, dense offset takes 42.30 ms; compact takes 0.02 ms โ a **2 016x speedup**. At SAT-200-20%-v128 the speedup reaches **290 779x**.
+
+| | Dense | Compact (no-clip fast path) |
+| ----------------- | -------------------------------------- | ------------------------------------ |
+| Work per mask | allocate `(new_H, new_W)` + copy H x W | add scalar to offset row โ O(1) |
+| N=200 at FHD | 200 x 2.1 MB = **414 MB** alloc + copy | two numpy ops on `(N, 2)` int32 |
+| Output allocation | new `(N, new_H, new_W)` | shared RLE list + new `(N, 2)` array |
+| **Speedup** | | **effectively free (>1 000x)** |
+
+In the `InferenceSlicer` pipeline the canvas is always expanded by the tile offset, so no crop ever overflows โ the fast path is always taken. Clipping only activates for objects that genuinely straddle the image boundary.
+
+---
+
+### `centroids` (`calculate_masks_centroids`)
+
+Dense: `np.tensordot` reads every pixel of every mask to compute weighted coordinate sums:
+
+```python
+# detection/utils/masks.py โ dense centroid path
+vertical_indices, horizontal_indices = np.indices((height, width)) + 0.5
+# np.tensordot(masks, indices, axes=([1, 2], [0, 1]))
+# reads all N x H x W values
+```
+
+Compact: per-crop loop decodes only the bounding-box region and computes centroids within that crop:
+
+```python
+# detection/utils/masks.py โ compact centroid path
+crop = masks.crop(i)
+crop_h, crop_w = crop.shape
+x1 = int(masks.offsets[i, 0])
+y1 = int(masks.offsets[i, 1])
+# ...
+crop_rows, crop_cols = np.indices((crop_h, crop_w))
+cx = float(np.sum((crop_cols + 0.5)[crop])) / total + x1
+cy = float(np.sum((crop_rows + 0.5)[crop])) / total + y1
+```
+
+At FHD-200-50%-v600, dense centroids takes 1 133.68 ms; compact takes 60.39 ms โ a **13x speedup**. At SAT-200-20%-v128 the speedup reaches **857x** because the dense path must allocate and scan a 13.4 GB array.
+
+| Factor | Reduction |
+| ----------------------------------------- | ------------------- |
+| Crop area vs full frame (per mask) | fill-dependent |
+| No global `np.indices((H, W))` allocation | saves large float64 |
+| **Combined (N=200)** | **~19 โ 1 000x** |
+
+---
+
+### Summary
+
+Measured speedups at the **FHD-200-50%-v600** operating point (dense fill, complex polygons โ a realistic hard case). Dense baseline = 1x.
+
+| Operation | Dense cost | Compact cost | Speedup |
+| ---------------- | ----------- | ------------ | ------- |
+| Memory | 414 MB | ~1.9 MB | ~392x |
+| `.area` | 84.66 ms | 0.48 ms | 71x |
+| `filter` | 14.56 ms | 0.03 ms | 500x |
+| `annotate` | 848.95 ms | 32.67 ms | 22x |
+| `mask_iou_batch` | 23 915 ms | 51.58 ms | 446x |
+| NMS | 5 231 ms | 48.15 ms | 481x |
+| `merge` | 29.71 ms | 0.03 ms | 929x |
+| `with_offset` | 42.30 ms | 0.02 ms | 2 016x |
+| `centroids` | 1 133.68 ms | 60.39 ms | 13x |
+
+All speedups are larger at sparser fill fractions and larger resolutions. At SAT-200-20%-v128, `.area` reaches 1 204x and `merge` reaches 89 046x. At the sparsest scenarios (5% fill, 8-vertex polygons), memory ratios exceed 60 000x.
+
+---
+
+## Drop-In Compatibility
+
+`CompactMask` implements the same duck-typed interface as `np.ndarray`:
+
+```python
+import supervision as sv
+from supervision.detection.compact_mask import CompactMask
+
+# Build from an existing dense (N, H, W) bool array:
+compact = CompactMask.from_dense(masks_dense, xyxy, image_shape=(H, W))
+
+# Use exactly like a dense mask โ no other code changes needed:
+detections = sv.Detections(xyxy=xyxy, mask=compact, class_id=class_ids)
+
+# Filtering, merging, area โ all work transparently:
+filtered = detections[confidence > 0.5]
+areas = detections.area # RLE sum, no materialisation
+merged = sv.Detections.merge([det_a, det_b])
+
+# MaskAnnotator works without any change:
+annotated = sv.MaskAnnotator().annotate(frame, detections)
+
+# Materialise back to dense when you need raw numpy:
+dense_again = compact.to_dense() # (N, H, W) bool
+```
+
+Supported indexing patterns:
+
+| Expression | Returns |
+| ------------------ | ---------------------------- |
+| `mask[i]` (int) | Dense `(H, W)` bool array |
+| `mask[bool_array]` | New `CompactMask` (filtered) |
+| `mask[slice]` | New `CompactMask` |
+| `np.asarray(mask)` | Dense `(N, H, W)` bool array |
+
+---
+
+## Benchmark
+
+Run on any machine โ no GPU or real model required:
+
+```bash
+uv run python examples/compact_mask/benchmark.py
+```
+
+For a focused benchmark of the Roboflow inference-result parser API, run:
+
+```bash
+uv run python examples/compact_mask/bench_inference_api.py
+```
+
+This script downloads all supervision image assets plus the middle frame from every supervision video asset by default, runs one real segmentation inference per source image, requests native RLE masks from Inference, freezes that result, and then compares parser performance:
+
+```python
+sv.Detections.from_inference(result)
+sv.Detections.from_inference(result, compact_masks=True)
+```
+
+Timing repetitions, warmups, confidence, IoU, response mask format, and the default model live as constants in `bench_inference_api.py`.
+
+Inference runs and segmentation-derived box fields are outside the timed benchmark loop. By default the script uses `rfdetr-seg-large` with `response_mask_format="rle"`; set `BENCH_INFERENCE_MODEL_ID` to override the model. Set `ROBOFLOW_API_KEY` when your model requires authentication. Sources where the model returns no native RLE segmentation masks are skipped because there is no RLE parser work to benchmark. `rfdetr-large` is a valid local Inference model id, but it is object detection only; use an `rfdetr-seg-*` model for instance segmentation.
+
+Run one specific supervision image or video asset with `--asset`:
+
+```bash
+uv run python examples/compact_mask/bench_inference_api.py --asset people-walking
+uv run python examples/compact_mask/bench_inference_api.py --asset soccer
+uv run python examples/compact_mask/bench_inference_api.py --asset vehicles
+uv run python examples/compact_mask/bench_inference_api.py --asset people-walking-video
+```
+
+The output reports image size, segmented objects, median parser time, peak traced allocations, mask storage, and parser speedup (`dense parser time / compact parser time`).
+
+**Speedup column:** The `speedup` value reflects allocation savings โ how much time is saved by skipping the dense `(N, H, W)` bool-stack allocation โ not a faster RLE decode. Compact RLE arithmetic is typically slower than the dense NumPy path. The net result:
+
+- **Compact is faster** only when the dense `(N, H, W)` bool-stack allocation dominates โ large images with many sparse masks where avoiding that allocation outweighs the RLE arithmetic cost.
+- **Compact is slower** for small images or dense/overlapping masks, where Python RLE arithmetic dominates and the allocation cost is negligible.
+- **The primary guaranteed benefit is memory**: compact masks use roughly 99% less memory than dense stacks for typical segmentation output, regardless of which parse direction is faster.
+
+The default run includes a `synthetic-dense-64` row (64ร64 image, 4 fully-filled masks) to demonstrate the adversarial regime where compact is slower than dense. For each real source with segmentation masks, the script also writes a validation overlay to `examples/compact_mask/outputs/*_segmentations.jpg`.
+
+### Sample results โ inference API
+
+Measured on macOS Apple M4 Max, 50 reps after 3 warmups, using `rfdetr-seg-large` via Roboflow Inference.
+
+| src | res | seg | dense ms | CM ms | speedup | peak MB (dense/compact) | mask MB (dense/compact) | ok |
+| -------------------------- | --------- | --- | -------- | ----- | ------- | ----------------------- | ----------------------- | --- |
+| synthetic-dense-64 | 64ร64 | 4 | 0.03 | 0.11 | 0.31ร | 0.04 / 0.05 | 0.02 / 0.00 | โ |
+| people-walking.jpg | 1920ร1080 | 53 | 85.56 | 12.55 | 6.82ร | 219.86 / 0.11 | 109.90 / 0.02 | โ |
+| soccer.jpg | 398ร224 | 21 | 1.36 | 1.07 | 1.27ร | 3.77 / 0.05 | 1.87 / 0.00 | โ |
+| vehicles.mp4#269 | 3840ร2160 | 7 | 46.03 | 2.60 | 18ร | 116.13 / 0.07 | 58.06 / 0.00 | โ |
+| milk-bottling-plant.mp4#94 | 1920ร1080 | 9 | 15.61 | 11.57 | 1.35ร | 37.34 / 0.53 | 18.66 / 0.03 | โ |
+| vehicles-2.mp4#637 | 1920ร1080 | 47 | 76.87 | 13.59 | 5.66ร | 194.97 / 0.13 | 97.46 / 0.03 | โ |
+| grocery-store.mp4#501 | 3840ร2160 | 4 | 27.20 | 4.36 | 6.24ร | 66.36 / 0.22 | 33.18 / 0.01 | โ |
+| subway.mp4#649 | 2160ร3840 | 42 | 325.71 | 32.21 | 10ร | 696.78 / 0.80 | 348.36 / 0.09 | โ |
+| market-square.mp4#237 | 2160ร3840 | 96 | 732.98 | 27.24 | 27ร | 1592.61 / 0.22 | 796.26 / 0.05 | โ |
+| people-walking.mp4#170 | 1920ร1080 | 60 | 100.99 | 12.69 | 7.96ร | 248.89 / 0.12 | 124.42 / 0.02 | โ |
+| beach-1.mp4#223 | 3840ร2160 | 33 | 223.50 | 13.39 | 17ร | 547.47 / 0.12 | 273.72 / 0.02 | โ |
+| basketball-1.mp4#238 | 1920ร1080 | 2 | 3.61 | 2.05 | 1.76ร | 8.30 / 0.15 | 4.15 / 0.01 | โ |
+| skiing.mp4#176 | 1920ร1080 | 11 | 16.47 | 3.07 | 5.37ร | 45.63 / 0.08 | 22.81 / 0.01 | โ |
+
+- **seg** โ number of instance segmentations returned by the model
+- **dense ms / CM ms** โ median parse time for `from_inference()` vs `from_inference(compact_masks=True)`
+- **speedup** โ dense / compact parse time; values below 1ร (e.g., synthetic-dense-64) indicate the adversarial regime where RLE arithmetic cost exceeds allocation savings
+- **peak MB** โ peak traced allocations during parsing (dense / compact)
+- **mask MB** โ mask storage only (dense / compact); compact is typically 100โ5 000ร smaller
+- **ok** โ `compact.to_dense()` pixel-exactly matches dense masks
+
+Six image tiers x three fill fractions (5 / 20 / 50 %) x three vertex counts (8 / 128 / 600):
+
+| Tier | Resolution | Objects | Dense array | Notes |
+| ------- | ---------- | ------- | ----------- | ------------------------------------ |
+| FHD-100 | 1920x1080 | 100 | 0.21 GB | Full operations including IoU+NMS |
+| FHD-200 | 1920x1080 | 200 | 0.41 GB | Full operations including IoU+NMS |
+| FHD-400 | 1920x1080 | 400 | 0.83 GB | Full operations including IoU+NMS |
+| 4K-100 | 3840x2160 | 100 | 0.83 GB | Full operations including IoU+NMS |
+| 4K-200 | 3840x2160 | 200 | 1.66 GB | Dense IoU+NMS skipped (array > 1 GB) |
+| SAT-200 | 8192x8192 | 200 | 13.4 GB | Dense IoU+NMS skipped (array > 1 GB) |
+
+Dense timing is skipped automatically when the dense IoU/NMS array would exceed 1 GB (`IOU_DENSE_SKIP_GB`), preventing swap thrashing. All dense ops are skipped above 16 GB (`DENSE_SKIP_GB`); no scenario in the current matrix reaches that threshold. Memory is always reported as theoretical `NxHxW` bytes.
+
+### Sample results (macOS, Apple M4 Max, REPS=4)
+
+| Scenario | Dense mem | Compact theor. | Mem x | Area x | Filter x | Annot x | IoU x | NMS x | Merge x | Offset x | Centroids x |
+| ---------------- | --------- | -------------- | ------- | ------ | -------- | ------- | ----- | ----- | -------- | -------- | ----------- |
+| FHD-100-5%-v8 | 207 MB | 28 KB | 7 418x | โ | โ | โ | โ | โ | โ | โ | โ |
+| FHD-100-50%-v600 | 207 MB | 913 KB | 227x | โ | โ | โ | โ | โ | โ | โ | โ |
+| FHD-200-50%-v600 | 415 MB | 933 KB | 445x | 71x | 500x | 22x | 446x | 481x | 929x | 2 016x | 13x |
+| FHD-400-5%-v8 | 829 MB | 60 KB | 13 937x | โ | โ | โ | โ | โ | โ | โ | โ |
+| 4K-100-5%-v8 | 829 MB | 53 KB | 15 554x | โ | โ | โ | โ | โ | โ | โ | โ |
+| 4K-100-20%-v128 | 829 MB | 586 KB | 1 415x | โ | โ | โ | โ | โ | โ | โ | โ |
+| 4K-200-5%-v8 | 1 659 MB | 76 KB | 21 786x | โ | โ | โ | โ | โ | โ | โ | โ |
+| SAT-200-5%-v8 | 13 422 MB | 213 KB | 62 968x | 6 942x | 30 255x | 204x | โ | โ | 105 545x | 251 629x | 2 173x |
+| SAT-200-20%-v128 | 13 422 MB | 2 596 KB | 5 171x | 1 204x | 14 757x | 89x | โ | โ | 89 046x | 290 779x | 857x |
+| SAT-200-50%-v600 | 13 422 MB | 14 222 KB | 944x | โ | โ | โ | โ | โ | โ | โ | โ |
+
+- **Compact theor.** โ sum of internal numpy buffer `nbytes`
+- **Mem x** โ dense / compact theoretical ratio
+- **Area x / Filter x / Annot x / IoU x / NMS x / Merge x / Offset x / Centroids x** โ compact speedup over dense for each operation
+- **โ ** โ dense IoU+NMS skipped (dense array > 1 GB); compact still runs and is timed
+- **โ** โ not shown; full per-scenario tables are printed by the benchmark script
+
+All non-skipped scenarios pass: pixel-perfect annotation, exact area, lossless `to_dense()` roundtrip.
+
+---
+
+## Use-Cases
+
+- **Aerial / satellite imagery** โ thousands of small objects on large tiles; dense masks exhaust RAM before inference completes.
+- **High-density crowd / cell segmentation** โ N > 500 on FHD already requires several GB of mask storage per batch.
+- **Real-time annotation pipelines** โ crop-paint cuts annotation from seconds to milliseconds at 4K resolution.
+- **Long-running tracking** โ accumulated `Detections` across many frames stay in kilobytes rather than gigabytes.
+- **`InferenceSlicer`** โ `with_offset()` adjusts crop origins directly when stitching tile results; no dense materialisation needed.
+
+---
+
+## Limitations
+
+- `CompactMask` is **not** a full `np.ndarray`. Call `.to_dense()` before passing to code that requires arbitrary ndarray methods (`astype`, `reshape`, `ravel`, `any`, `all`, โฆ).
+- RLE format is **column-major (F-order), crop-scoped** โ pixel-scan order matches COCO / pycocotools, but crop scope differs from full-image scope. Use `.to_dense()` to materialize a full-image dense mask, then encode that mask to COCO RLE before passing it to pycocotools.
+- `from_dense()` requires the input `(N, H, W)` array to fit in memory. For truly OOM-scale data, build `CompactMask` per-detection directly from model output crops rather than from a pre-allocated dense stack.
+
+---
+
+## Files
+
+| File | Description |
+| ------------------------ | --------------------------------------------------- |
+| `benchmark.py` | Full benchmark across FHD / 4K / satellite tiers |
+| `bench_inference_api.py` | Focused dense vs compact `from_inference` benchmark |
+| `README.md` | This file |
diff --git a/examples/compact_mask/bench_inference_api.py b/examples/compact_mask/bench_inference_api.py
new file mode 100644
index 0000000..aafa6fe
--- /dev/null
+++ b/examples/compact_mask/bench_inference_api.py
@@ -0,0 +1,505 @@
+"""Benchmark dense vs compact Roboflow RLE ingestion.
+
+Run with:
+ uv run python examples/compact_mask/bench_inference_api.py
+
+The benchmark downloads supervision assets, runs one segmentation inference per
+source image, then times dense vs compact parsing of that fixed inference result.
+"""
+
+from __future__ import annotations
+
+import argparse
+import gc
+import os
+import statistics
+import time
+import tracemalloc
+from collections.abc import Callable
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any
+
+import cv2
+import numpy as np
+from rich import box
+from rich.console import Console
+from rich.table import Table
+
+import supervision as sv
+from supervision.assets import ImageAssets, VideoAssets, download_assets
+from supervision.config import CLASS_NAME_DATA_FIELD
+from supervision.detection.compact_mask import CompactMask
+
+console = Console(width=120, force_terminal=True)
+
+# Default segmentation model; use an rfdetr-seg-* id so masks are returned.
+MODEL_ID = "rfdetr-seg-large"
+# Environment variable that can override MODEL_ID without adding CLI noise.
+MODEL_ID_ENV = "BENCH_INFERENCE_MODEL_ID"
+# Optional Roboflow API key for models that require authentication.
+API_KEY_ENV = "ROBOFLOW_API_KEY"
+# Model confidence threshold used only for the one inference call per source.
+CONFIDENCE = 0.2
+# Model IoU threshold used only for the one inference call per source.
+IOU = 0.5
+# Request native RLE masks so the benchmark measures RLE parser ingestion.
+RESPONSE_MASK_FORMAT = "rle"
+# Parser timing repetitions; inference itself is not repeated.
+REPETITIONS = 50
+# Untimed parser warmup calls before measurements.
+WARMUP = 3
+# Visual segmentation overlays for manual validation.
+ARTIFACT_DIR = Path("examples/compact_mask/outputs")
+
+ASSETS = {Path(asset.filename).stem: asset for asset in ImageAssets}
+for video_asset in VideoAssets:
+ key = Path(video_asset.filename).stem
+ ASSETS[key if key not in ASSETS else f"{key}-video"] = video_asset
+
+
+@dataclass
+class ApiBenchmarkResult:
+ """Result for one dense-vs-compact parser benchmark run."""
+
+ source: str
+ resolution: str
+ segmented_objects: int
+ dense_s: float
+ compact_s: float
+ dense_peak_bytes: int
+ compact_peak_bytes: int
+ dense_mask_bytes: int
+ compact_mask_bytes: int
+ pixel_perfect: bool
+
+
+def load_image_from_asset(path: Path | None, asset: str) -> tuple[np.ndarray, str]:
+ """Return ``(image, label)`` for an image or video middle frame."""
+ if path is not None:
+ image = cv2.imread(str(path))
+ if image is None:
+ raise FileNotFoundError(f"Could not read image: {path}")
+ return image, str(path)
+
+ asset_obj = ASSETS[asset]
+ asset_path = Path(download_assets(asset_obj))
+ if isinstance(asset_obj, ImageAssets):
+ image = cv2.imread(str(asset_path))
+ if image is None:
+ raise FileNotFoundError(f"Could not read image: {asset_path}")
+ return image, str(asset_path)
+
+ video = cv2.VideoCapture(str(asset_path))
+ if not video.isOpened():
+ raise FileNotFoundError(f"Could not read video: {asset_path}")
+ frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
+ frame_index = max(0, frame_count // 2)
+ if frame_index:
+ video.set(cv2.CAP_PROP_POS_FRAMES, frame_index)
+ ok, frame = video.read()
+ video.release()
+ if not ok or frame is None:
+ raise FileNotFoundError(f"Could not read middle frame: {asset_path}")
+ return frame, f"{asset_path}#{frame_index}"
+
+
+def freeze_result(inference_result: Any) -> dict[str, Any]:
+ """Convert one Inference result to a reusable dictionary."""
+ if isinstance(inference_result, dict):
+ return inference_result
+ if hasattr(inference_result, "model_dump"):
+ return inference_result.model_dump(exclude_none=True, by_alias=True)
+ if hasattr(inference_result, "dict"):
+ return inference_result.dict(exclude_none=True, by_alias=True)
+ raise TypeError(
+ f"Expected dict-like Inference result, got {type(inference_result).__name__}"
+ )
+
+
+def count_rle_predictions(result: dict[str, Any]) -> int:
+ """Return the number of predictions carrying Roboflow RLE masks."""
+ return sum(
+ isinstance(prediction.get("rle") or prediction.get("rle_mask"), dict)
+ for prediction in result.get("predictions", [])
+ )
+
+
+def synthetic_dense_small_result() -> tuple[np.ndarray, str, dict[str, Any]]:
+ """Return a small dense-mask adversarial payload where compact parsing is slower.
+
+ Uses a 64x64 image with 4 fully-filled masks. At this scale the dense
+ ``(N, H, W)`` allocation cost is negligible; Python RLE arithmetic dominates,
+ making compact ingestion slower than the dense NumPy path. Included as a
+ clearly labeled adversarial row in the default benchmark run to show that
+ the ``speedup`` column reflects allocation savings, not decode speed.
+ """
+ height, width = 64, 64
+ image = np.zeros((height, width, 3), dtype=np.uint8)
+ predictions = [
+ {
+ "x": width / 2,
+ "y": height / 2,
+ "width": width,
+ "height": height,
+ "confidence": 0.9,
+ "class_id": index,
+ "class": f"dense-{index}",
+ "rle": {"size": [height, width], "counts": [0, height * width]},
+ }
+ for index in range(4)
+ ]
+ return (
+ image,
+ "synthetic-dense-64",
+ {
+ "predictions": predictions,
+ "image": {"width": width, "height": height},
+ },
+ )
+
+
+def derive_boxes_from_rle_masks(result: dict[str, Any]) -> dict[str, Any]:
+ """Set prediction boxes from native RLE segmentation masks."""
+ predictions = []
+ for prediction in result.get("predictions", []):
+ rle = prediction.get("rle") or prediction.get("rle_mask")
+ if not isinstance(rle, dict):
+ predictions.append(prediction)
+ continue
+
+ height, width = rle["size"]
+ mask = sv.rle_to_mask(rle["counts"], resolution_wh=(int(width), int(height)))
+ if not mask.any():
+ predictions.append(prediction)
+ continue
+
+ x1, y1, x2, y2 = sv.mask_to_xyxy(mask[np.newaxis, ...])[0]
+ predictions.append(
+ {
+ **prediction,
+ "x": float((x1 + x2) / 2),
+ "y": float((y1 + y2) / 2),
+ "width": float(x2 - x1),
+ "height": float(y2 - y1),
+ }
+ )
+ return {**result, "predictions": predictions}
+
+
+def artifact_path(source: str) -> Path:
+ """Return the segmentation validation artifact path for a source."""
+ source_path, separator, frame = source.partition("#")
+ stem = Path(source_path).stem
+ suffix = f"_frame_{frame}" if separator else ""
+ return ARTIFACT_DIR / f"{stem}{suffix}_segmentations.jpg"
+
+
+def detection_labels(detections: sv.Detections) -> list[str]:
+ """Return compact class/confidence labels for validation artifacts."""
+ raw_class_names = detections.get_data(CLASS_NAME_DATA_FIELD)
+ class_names = (
+ raw_class_names.astype(str).tolist()
+ if isinstance(raw_class_names, np.ndarray)
+ else [""] * len(detections)
+ )
+
+ labels = []
+ for index in range(len(detections)):
+ class_name = class_names[index] if index < len(class_names) else ""
+ confidence = (
+ ""
+ if detections.confidence is None
+ else f" {detections.confidence[index]:.2f}"
+ )
+ labels.append(f"{class_name}{confidence}".strip() or str(index))
+ return labels
+
+
+def save_segmentation_artifact(
+ image: np.ndarray,
+ result: dict[str, Any],
+ source: str,
+) -> Path | None:
+ """Draw parsed segmentation masks and save a validation artifact."""
+ detections = sv.Detections.from_inference(result)
+ if detections.mask is None:
+ return None
+
+ annotated = image.copy()
+ annotated = sv.MaskAnnotator(
+ color_lookup=sv.ColorLookup.INDEX,
+ opacity=0.45,
+ ).annotate(scene=annotated, detections=detections)
+ annotated = sv.LabelAnnotator(
+ color_lookup=sv.ColorLookup.INDEX,
+ text_scale=0.35,
+ text_padding=4,
+ ).annotate(
+ scene=annotated,
+ detections=detections,
+ labels=detection_labels(detections),
+ )
+
+ path = artifact_path(source)
+ path.parent.mkdir(parents=True, exist_ok=True)
+ if not cv2.imwrite(str(path), annotated):
+ raise OSError(f"Could not write segmentation artifact: {path}")
+ return path
+
+
+def load_inference_model(model_id: str, api_key: str | None) -> Any:
+ """Load the requested Inference model."""
+ try:
+ from inference import get_model
+ except ImportError as exc:
+ raise ImportError(
+ "Install the `inference` package to run this benchmark."
+ ) from exc
+
+ model_kwargs = {"api_key": api_key} if api_key is not None else {}
+ return get_model(model_id=model_id, **model_kwargs)
+
+
+def run_inference_once(
+ image: np.ndarray,
+ model: Any,
+ model_id: str,
+ confidence: float,
+ iou: float,
+) -> dict[str, Any] | None:
+ """Run one real segmentation inference and return a frozen result."""
+ # Inference still serializes instance segmentations with x/y/width/height.
+ # Derive those fields from the RLE masks so the benchmark uses segmentations,
+ # not the model-reported detector boxes, as the source of truth.
+ result = derive_boxes_from_rle_masks(
+ freeze_result(
+ model.infer(
+ image,
+ confidence=confidence,
+ iou=iou,
+ response_mask_format=RESPONSE_MASK_FORMAT,
+ )[0]
+ )
+ )
+ rle_count = count_rle_predictions(result)
+ if rle_count == 0:
+ console.print(
+ f"[yellow]skipped[/yellow] {model_id}: no native RLE segmentation "
+ f"predictions for response_mask_format={RESPONSE_MASK_FORMAT!r}"
+ )
+ return None
+ return result
+
+
+def median_seconds(fn: Callable[[], object], reps: int, warmup: int) -> float:
+ """Return median runtime for ``fn``."""
+ for _ in range(warmup):
+ fn()
+ gc.collect()
+
+ timings = []
+ for _ in range(reps):
+ start = time.perf_counter()
+ fn()
+ timings.append(time.perf_counter() - start)
+ return statistics.median(timings)
+
+
+def peak_bytes(fn: Callable[[], object]) -> int:
+ """Return peak traced allocations for one call."""
+ gc.collect()
+ tracemalloc.start()
+ fn()
+ _, peak = tracemalloc.get_traced_memory()
+ tracemalloc.stop()
+ return int(peak)
+
+
+def dense_mask_bytes(detections: sv.Detections) -> int:
+ """Return dense mask storage bytes."""
+ return 0 if detections.mask is None else int(np.asarray(detections.mask).nbytes)
+
+
+def compact_mask_bytes(detections: sv.Detections) -> int:
+ """Return compact mask storage bytes."""
+ if not isinstance(detections.mask, CompactMask):
+ return 0
+ return sum(rle.nbytes for rle in detections.mask._rles)
+
+
+def _fmt_ratio(ratio: float) -> str:
+ """Format a speedup/compression ratio with colour coding."""
+ fmt = f"{ratio:.0f}x" if ratio >= 10 else f"{ratio:.2f}x"
+ if ratio >= 10:
+ return f"[green]{fmt}[/green]"
+ elif ratio >= 1:
+ return f"[yellow]{fmt}[/yellow]"
+ else:
+ return f"[red]{fmt}[/red]"
+
+
+def _fmt_mb(num_bytes: int) -> str:
+ """Format bytes as compact megabytes."""
+ return f"{num_bytes / 1e6:.2f}"
+
+
+def run_benchmark(
+ source: str,
+ image: np.ndarray,
+ result: dict[str, Any],
+ reps: int,
+ warmup: int,
+) -> ApiBenchmarkResult:
+ """Run one dense-vs-compact parser benchmark."""
+
+ # Benchmark the public Roboflow/Inference adapter; RLE masks enter through
+ # the result payload and should stay compact when compact_masks=True.
+ def dense() -> sv.Detections:
+ return sv.Detections.from_inference(result)
+
+ def compact() -> sv.Detections:
+ return sv.Detections.from_inference(result, compact_masks=True)
+
+ dense_once = dense()
+ compact_once = compact()
+ if not isinstance(dense_once.mask, np.ndarray):
+ raise TypeError(f"Expected dense ndarray mask, got {type(dense_once.mask)}")
+ if not isinstance(compact_once.mask, CompactMask):
+ raise TypeError(f"Expected CompactMask, got {type(compact_once.mask)}")
+ np.testing.assert_array_equal(compact_once.mask.to_dense(), dense_once.mask)
+
+ dense_s = median_seconds(dense, reps, warmup)
+ compact_s = median_seconds(compact, reps, warmup)
+ dense_peak = peak_bytes(dense)
+ compact_peak = peak_bytes(compact)
+
+ return ApiBenchmarkResult(
+ source=source,
+ resolution=f"{image.shape[1]}x{image.shape[0]}",
+ segmented_objects=len(dense_once),
+ dense_s=dense_s,
+ compact_s=compact_s,
+ dense_peak_bytes=dense_peak,
+ compact_peak_bytes=compact_peak,
+ dense_mask_bytes=dense_mask_bytes(dense_once),
+ compact_mask_bytes=compact_mask_bytes(compact_once),
+ pixel_perfect=True,
+ )
+
+
+def print_summary(results: list[ApiBenchmarkResult], reps: int, warmup: int) -> None:
+ """Print a Rich summary table matching the compact mask benchmark style."""
+ table = Table(
+ title="CompactMask from_inference",
+ box=box.ROUNDED,
+ show_lines=False,
+ header_style="bold cyan",
+ )
+ table.add_column("src", style="bold", no_wrap=True)
+ table.add_column("res", no_wrap=True)
+ table.add_column("seg", justify="right")
+ table.add_column("dense ms", justify="right")
+ table.add_column("CM ms", justify="right", style="green")
+ table.add_column("speedup", justify="right")
+ table.add_column("peak MB", justify="right", style="cyan")
+ table.add_column("mask MB", justify="right")
+ table.add_column("ok", justify="center")
+
+ for result in results:
+ speedup = result.dense_s / max(result.compact_s, 1e-9)
+ table.add_row(
+ result.source,
+ result.resolution,
+ str(result.segmented_objects),
+ f"{result.dense_s * 1e3:.2f}",
+ f"{result.compact_s * 1e3:.2f}",
+ _fmt_ratio(speedup),
+ f"{_fmt_mb(result.dense_peak_bytes)}/{_fmt_mb(result.compact_peak_bytes)}",
+ f"{_fmt_mb(result.dense_mask_bytes)}/{_fmt_mb(result.compact_mask_bytes)}",
+ "[green]โ[/green]" if result.pixel_perfect else "[red]โ[/red]",
+ )
+
+ console.print(table)
+ console.print(
+ "[dim]"
+ + " ยท ".join(
+ [
+ f"timings are median of {reps} reps after {warmup} warmups",
+ "peak MB and mask MB are dense/compact",
+ "speedup = dense / compact parse time; gains are allocation-driven"
+ " (avoiding the dense (N,H,W) bool-stack), not faster RLE decode",
+ "compact RLE arithmetic is typically slower than the dense NumPy path"
+ " โ synthetic-dense-64 shows this adversarial regime (speedup < 1x)",
+ "OK means compact.to_dense() exactly matches dense masks",
+ ]
+ )
+ + "[/dim]"
+ )
+
+
+def main() -> None:
+ """Run the benchmark."""
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--asset", choices=ASSETS.keys(), default=None)
+ parser.add_argument("--image", type=Path, default=None)
+ args = parser.parse_args()
+
+ assets = [args.asset] if args.asset is not None else list(ASSETS)
+ if args.image is not None:
+ assets = ["custom"]
+
+ results = []
+ if args.asset is None and args.image is None:
+ image, source, inference_result = synthetic_dense_small_result()
+ console.rule(f"[bold]{source}[/bold] | {image.shape[1]}x{image.shape[0]}")
+ results.append(
+ run_benchmark(
+ source=source,
+ image=image,
+ result=inference_result,
+ reps=REPETITIONS,
+ warmup=WARMUP,
+ )
+ )
+ model_id = os.getenv(MODEL_ID_ENV, MODEL_ID)
+ model = load_inference_model(model_id=model_id, api_key=os.getenv(API_KEY_ENV))
+ for asset in assets:
+ image, source = load_image_from_asset(args.image, asset)
+ console.rule(f"[bold]{source}[/bold] | {image.shape[1]}x{image.shape[0]}")
+ inference_result = run_inference_once(
+ image=image,
+ model=model,
+ model_id=model_id,
+ confidence=CONFIDENCE,
+ iou=IOU,
+ )
+ if inference_result is None:
+ continue
+ console.print(
+ f"[dim]captured {count_rle_predictions(inference_result)} RLE masks "
+ f"from {model_id}[/dim]"
+ )
+ artifact = save_segmentation_artifact(
+ image=image,
+ result=inference_result,
+ source=source,
+ )
+ if artifact is not None:
+ console.print(f"[dim]saved segmentation artifact: {artifact}[/dim]")
+ results.append(
+ run_benchmark(
+ source=source,
+ image=image,
+ result=inference_result,
+ reps=REPETITIONS,
+ warmup=WARMUP,
+ )
+ )
+ if not results:
+ raise ValueError(f"Model {model_id!r} returned no segmentation masks.")
+ print_summary(results, reps=REPETITIONS, warmup=WARMUP)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/compact_mask/benchmark.py b/examples/compact_mask/benchmark.py
new file mode 100644
index 0000000..f4d70d9
--- /dev/null
+++ b/examples/compact_mask/benchmark.py
@@ -0,0 +1,1176 @@
+"""CompactMask demo & benchmark.
+
+Demonstrates that ``CompactMask`` is a drop-in replacement for dense
+``(N, H, W)`` bool arrays in ``supervision.Detections``, while using
+significantly less memory and enabling faster annotation. The annotation
+timing reports frame size, detection count, mask area ratio, and
+``MaskAnnotator`` speedup from ROI-only blending.
+
+Run with:
+ uv run python examples/compact_mask/benchmark.py
+
+No GPU or real model is required โ everything is synthesized with NumPy.
+Mask complexity is controlled by ``num_vertices``: random polygons with more
+vertices produce jaggier boundaries and more RLE runs per row.
+"""
+
+import dataclasses
+import gc
+import json
+import math
+import time
+import tracemalloc
+from collections.abc import Callable
+from concurrent.futures import ThreadPoolExecutor
+from dataclasses import dataclass, field
+from datetime import datetime, timezone
+from pathlib import Path
+
+import cv2
+import numpy as np
+import pandas as pd
+from rich import box
+from rich.console import Console
+from rich.progress import (
+ BarColumn,
+ MofNCompleteColumn,
+ Progress,
+ TaskProgressColumn,
+ TextColumn,
+ TimeElapsedColumn,
+)
+from rich.table import Table
+
+import supervision as sv
+from supervision.detection.compact_mask import CompactMask
+
+console = Console(width=240, force_terminal=True)
+
+REPETITIONS = 4
+# How many reps to run concurrently in time_reps. Each thread times itself
+# independently; results are averaged. Numpy releases the GIL for its C-level
+# work so threads can truly run in parallel on multi-core machines.
+# Set to 1 to disable parallelism and revert to a sequential timing loop.
+PARALLEL = 3
+# Dense timing is skipped when the dense (N,H,W) array would exceed this
+# threshold โ avoids OOM / swap thrashing on extreme scenarios while still
+# reporting the theoretical memory footprint.
+DENSE_SKIP_GB = 16.0
+# Dense IoU *and NMS* timing are skipped above this threshold: pairwise
+# (N,H,W) AND is extremely expensive โ NMS calls IoU internally so both are
+# gated by the same threshold.
+IOU_DENSE_SKIP_GB = 1.0
+# Reps for dense IoU/NMS โ a single pass already takes several seconds.
+IOU_NMS_REPS = 2
+
+
+# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+# Result container
+# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+
+@dataclass
+class ScenarioResult:
+ name: str
+ resolution: str # e.g. "1920x1080"
+ num_objects: int
+ fill_name: str # mask area ratio, e.g. "5%"
+ num_vertices: int # polygon vertex count โ complexity proxy
+ # memory (theoretical: raw numpy nbytes)
+ dense_bytes: int
+ compact_bytes_theoretical: int
+ # memory (actual: tracemalloc peak; dense_bytes_actual=0 when dense_skipped=True)
+ dense_bytes_actual: int
+ compact_bytes_actual: int
+ # compactness overhead โ absolute times for conversion (always measured)
+ encode_s: float # CompactMask.from_dense() dense โ compact
+ decode_s: float # compact_mask.to_dense() compact โ dense
+ # timing (nan when dense_skipped=True)
+ dense_area_s: float
+ compact_area_s: float
+ dense_filter_s: float
+ compact_filter_s: float
+ dense_annot_s: float
+ compact_annot_s: float
+ # pipeline stages (nan when respective skip flag is True)
+ dense_iou_s: float # nan when iou_dense_skipped
+ compact_iou_s: float
+ dense_nms_s: float # nan when dense_skipped
+ compact_nms_s: float
+ dense_merge_s: float # nan when dense_skipped
+ compact_merge_s: float
+ dense_offset_s: float # nan when dense_skipped
+ compact_offset_s: float
+ dense_centroids_s: float # nan when dense_skipped
+ compact_centroids_s: float
+ # correctness (None when the stage was skipped)
+ pixel_perfect: bool | None
+ areas_match: bool | None
+ roundtrip_ok: bool | None
+ iou_ok: bool | None
+ nms_ok: bool | None
+ nms_mismatch_count: (
+ int # detections with different NMS decisions (0 when dense_skipped)
+ )
+ merge_ok: bool | None
+ offset_ok: bool | None
+ centroids_ok: bool | None
+ dense_resize_s: float # nan when dense_skipped
+ compact_resize_s: float
+ resize_ok: bool | None
+ # skip flags
+ dense_skipped: bool = field(default=False)
+ iou_dense_skipped: bool = field(default=False)
+
+
+# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+# Synthetic data helpers
+# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+
+def make_scene(image_height: int, image_width: int) -> np.ndarray:
+ """Random BGR image."""
+ return np.random.default_rng(42).integers(
+ 0, 255, (image_height, image_width, 3), dtype=np.uint8
+ )
+
+
+def _make_polygon_mask(
+ image_height: int,
+ image_width: int,
+ center_x: int,
+ center_y: int,
+ axis_x: int,
+ axis_y: int,
+ rng: np.random.Generator,
+ num_vertices: int,
+) -> np.ndarray:
+ """Random polygon mask.
+
+ *num_vertices* is a direct complexity proxy: more vertices โ more
+ independent radius samples โ jaggier boundary โ more RLE runs per row.
+ No smoothing is applied so the relationship is monotone.
+ """
+ angles = np.sort(rng.uniform(0, 2 * np.pi, num_vertices))
+ radii = rng.uniform(0.3, 1.0, num_vertices)
+ pts_x = np.clip(
+ (center_x + axis_x * radii * np.cos(angles)).astype(np.int32),
+ 0,
+ image_width - 1,
+ )
+ pts_y = np.clip(
+ (center_y + axis_y * radii * np.sin(angles)).astype(np.int32),
+ 0,
+ image_height - 1,
+ )
+ pts = np.column_stack([pts_x, pts_y]).reshape(-1, 1, 2)
+ canvas = np.zeros((image_height, image_width), dtype=np.uint8)
+ cv2.fillPoly(canvas, [pts], 1)
+ return canvas.astype(bool)
+
+
+def make_detections(
+ num_objects: int,
+ image_height: int,
+ image_width: int,
+ fill_fraction: float,
+ num_vertices: int = 20,
+ seed: int = 0,
+) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+ """Return ``(xyxy, masks_dense, class_ids)`` with random polygon masks.
+
+ *num_vertices* controls mask complexity: more vertices โ jaggier boundary.
+ """
+ rng = np.random.default_rng(seed)
+ half = max(
+ 2,
+ int(
+ (image_height * image_width * fill_fraction / (np.pi * num_objects)) ** 0.5
+ ),
+ )
+ xyxy_list = []
+ masks = np.zeros((num_objects, image_height, image_width), dtype=bool)
+ for index in range(num_objects):
+ center_x = int(rng.integers(half + 1, image_width - half - 1))
+ center_y = int(rng.integers(half + 1, image_height - half - 1))
+ axis_x = int(rng.integers(max(2, half // 2), half * 2 + 1))
+ axis_y = int(rng.integers(max(2, half // 2), half * 2 + 1))
+ masks[index] = _make_polygon_mask(
+ image_height,
+ image_width,
+ center_x,
+ center_y,
+ axis_x,
+ axis_y,
+ rng,
+ num_vertices,
+ )
+ xyxy_list.append(
+ [
+ max(0, center_x - axis_x),
+ max(0, center_y - axis_y),
+ min(image_width - 1, center_x + axis_x),
+ min(image_height - 1, center_y + axis_y),
+ ]
+ )
+ xyxy = np.array(xyxy_list, dtype=np.float32)
+ class_ids = rng.integers(0, 10, num_objects, dtype=int)
+ return xyxy, masks, class_ids
+
+
+# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+# Memory helpers
+# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+
+def dense_memory_bytes(masks: np.ndarray) -> int:
+ """Theoretical dense footprint: raw numpy buffer size."""
+ return int(masks.nbytes)
+
+
+def compact_memory_bytes_theoretical(compact_mask: CompactMask) -> int:
+ """Theoretical compact footprint: sum of all internal numpy buffer sizes."""
+ return int(
+ compact_mask._crop_shapes.nbytes
+ + compact_mask._offsets.nbytes
+ + sum(rle.nbytes for rle in compact_mask._rles),
+ )
+
+
+def measure_peak_bytes(func: Callable[[], object]) -> int:
+ """Wrapper that runs *func* under tracemalloc and returns peak allocation.
+
+ tracemalloc captures every Python-level allocation โ numpy buffers, list
+ nodes, object headers โ giving the true heap cost of anything *func*
+ builds. The return value of *func* is discarded so the object does not
+ stay alive.
+ """
+ tracemalloc.start()
+ tracemalloc.clear_traces()
+ func()
+ _, peak = tracemalloc.get_traced_memory()
+ tracemalloc.stop()
+ return int(peak)
+
+
+def dense_memory_bytes_actual(
+ num_objects: int, image_height: int, image_width: int
+) -> int:
+ """Actual dense footprint: peak bytes during (N, H, W) bool array alloc."""
+ return measure_peak_bytes(
+ lambda: np.zeros((num_objects, image_height, image_width), dtype=bool),
+ )
+
+
+def compact_memory_bytes_actual(
+ masks_dense: np.ndarray,
+ xyxy: np.ndarray,
+ image_shape: tuple[int, int],
+) -> int:
+ """Actual compact footprint: peak bytes during CompactMask.from_dense()."""
+ return measure_peak_bytes(
+ lambda: CompactMask.from_dense(masks_dense, xyxy, image_shape=image_shape),
+ )
+
+
+def time_reps(
+ func: Callable[[], object],
+ repeats: int = REPETITIONS,
+ parallel: int = PARALLEL,
+) -> float:
+ """Run *func* *reps* times and return mean wall-clock seconds per call.
+
+ When ``parallel > 1``, up to ``parallel`` calls run simultaneously in
+ threads. Numpy and OpenCV release the GIL for their C-level work, so
+ threads can execute in parallel on multi-core machines. Each thread
+ records its own elapsed time; the mean across all *reps* is returned.
+
+ When ``parallel == 1`` the original sequential loop is used, avoiding
+ any thread-scheduling overhead and improving accuracy for cheap functions.
+
+ A full GC cycle is run before timing so accumulated garbage from earlier
+ stages does not trigger collection mid-measurement and inflate results.
+ """
+ gc.collect()
+ if parallel <= 1:
+ t0 = time.perf_counter()
+ for _ in range(repeats):
+ func()
+ return (time.perf_counter() - t0) / repeats
+
+ def _timed() -> float:
+ t0 = time.perf_counter()
+ func()
+ return time.perf_counter() - t0
+
+ with ThreadPoolExecutor(max_workers=min(parallel, repeats)) as pool:
+ timings = list(pool.map(lambda _: _timed(), range(repeats)))
+ return sum(timings) / repeats
+
+
+# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+# Benchmark stages
+# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+
+def stage_build(
+ num_objects: int,
+ image_height: int,
+ image_width: int,
+ fill_fraction: float,
+ num_vertices: int = 20,
+) -> tuple[np.ndarray, np.ndarray, np.ndarray, CompactMask]:
+ """Synthesize polygon masks and build the CompactMask."""
+ xyxy, masks_dense, class_ids = make_detections(
+ num_objects, image_height, image_width, fill_fraction, num_vertices
+ )
+ compact_mask = CompactMask.from_dense(
+ masks_dense, xyxy, image_shape=(image_height, image_width)
+ )
+ return xyxy, masks_dense, class_ids, compact_mask
+
+
+def _resize_dense_to_shape(masks: np.ndarray, new_h: int, new_w: int) -> np.ndarray:
+ """Nearest-neighbour resize of (N, H, W) bool masks to (N, new_h, new_w).
+
+ Uses floor-division indexing (``arange * src // dst``) to match the
+ strategy in ``_rle_resize``, ensuring pixel-exact parity for correctness
+ comparisons in :func:`stage_resize`.
+ """
+ orig_h, orig_w = masks.shape[1], masks.shape[2]
+ x = np.arange(new_w) * orig_w // new_w
+ y = np.arange(new_h) * orig_h // new_h
+ xv, yv = np.meshgrid(x, y)
+ return masks[:, yv, xv]
+
+
+def stage_encode(
+ masks_dense: np.ndarray,
+ xyxy: np.ndarray,
+ image_height: int,
+ image_width: int,
+) -> float:
+ """Per-mask encode time: encode each mask individually and average over N.
+
+ Calling from_dense one mask at a time (rather than batching all N) isolates
+ the per-shape cost โ each polygon has a different RLE run count, so the
+ average reflects true shape variance.
+ """
+ num_masks = len(masks_dense)
+ image_shape = (image_height, image_width)
+
+ def _encode_each() -> None:
+ for i in range(num_masks):
+ CompactMask.from_dense(
+ masks_dense[i : i + 1], xyxy[i : i + 1], image_shape=image_shape
+ )
+
+ return time_reps(_encode_each) / max(num_masks, 1)
+
+
+def stage_decode(compact_mask: CompactMask) -> float:
+ """Per-mask decode time: decode each mask individually and average over N.
+
+ Building a list via compact_mask[i] decodes each crop separately, giving
+ the per-mask cost of materialising a single RLE back to a dense array.
+ """
+ num_masks = len(compact_mask)
+ return time_reps(lambda: [compact_mask[i] for i in range(num_masks)]) / max(
+ num_masks, 1
+ )
+
+
+def stage_area(
+ det_dense: sv.Detections, det_compact: sv.Detections
+) -> tuple[float, float]:
+ """Time .area on both representations."""
+ return (
+ time_reps(lambda: det_dense.area),
+ time_reps(lambda: det_compact.area),
+ )
+
+
+def stage_filter(
+ det_dense: sv.Detections, det_compact: sv.Detections
+) -> tuple[float, float]:
+ """Time boolean filtering (keep every other detection)."""
+ keep = np.arange(len(det_dense)) % 2 == 0
+ return (
+ time_reps(lambda: det_dense[keep]),
+ time_reps(lambda: det_compact[keep]),
+ )
+
+
+def stage_annotate(
+ scene: np.ndarray, det_dense: sv.Detections, det_compact: sv.Detections
+) -> tuple[float, float]:
+ """Time MaskAnnotator on both representations."""
+ annotator = sv.MaskAnnotator(opacity=0.5)
+ return (
+ time_reps(lambda: annotator.annotate(scene.copy(), det_dense)),
+ time_reps(lambda: annotator.annotate(scene.copy(), det_compact)),
+ )
+
+
+def stage_correctness(
+ scene: np.ndarray,
+ masks_dense: np.ndarray,
+ compact_mask: CompactMask,
+ det_dense: sv.Detections,
+ det_compact: sv.Detections,
+) -> tuple[bool, bool, bool]:
+ """Return (pixel_perfect, areas_match, roundtrip_ok)."""
+ annotator = sv.MaskAnnotator(opacity=0.5)
+ out_dense = annotator.annotate(scene.copy(), det_dense)
+ out_compact = annotator.annotate(scene.copy(), det_compact)
+ pixel_perfect = bool(np.array_equal(out_dense, out_compact))
+ areas_match = bool(np.allclose(det_dense.area, det_compact.area))
+ roundtrip_ok = bool(np.array_equal(compact_mask.to_dense(), masks_dense))
+ return pixel_perfect, areas_match, roundtrip_ok
+
+
+def stage_iou(
+ masks_dense: np.ndarray,
+ compact_mask: CompactMask,
+ iou_dense_skipped: bool,
+) -> tuple[float, float, bool | None]:
+ """Time pairwise self-IoU using dense (N,H,W) AND and compact crop filter.
+
+ Correctness is checked on the first 10 masks only to keep it fast,
+ regardless of whether full dense IoU timing is skipped.
+ """
+ correct_n = min(len(compact_mask), 10)
+ iou_compact_small = sv.mask_iou_batch(
+ compact_mask[:correct_n], compact_mask[:correct_n]
+ )
+ iou_dense_small = sv.mask_iou_batch(
+ masks_dense[:correct_n], masks_dense[:correct_n]
+ )
+ iou_ok = bool(np.allclose(iou_dense_small, iou_compact_small, atol=1e-4))
+
+ compact_iou_s = time_reps(lambda: sv.mask_iou_batch(compact_mask, compact_mask))
+ if iou_dense_skipped:
+ dense_iou_s = math.nan
+ else:
+ dense_iou_s = time_reps(
+ lambda: sv.mask_iou_batch(masks_dense, masks_dense),
+ repeats=IOU_NMS_REPS,
+ )
+ return dense_iou_s, compact_iou_s, iou_ok
+
+
+def stage_nms(
+ xyxy: np.ndarray,
+ confidence: np.ndarray,
+ class_ids: np.ndarray,
+ masks_dense: np.ndarray,
+ compact_mask: CompactMask,
+ dense_skipped: bool,
+ iou_dense_skipped: bool,
+) -> tuple[float, float, bool | None, int]:
+ """Time mask NMS. Dense resizes to 640 before IoU; compact uses exact crop IoU.
+
+ Compact NMS is strictly more accurate than dense: it computes pixel-level IoU
+ directly on the full-resolution RLE crops instead of a lossy 640px-downsampled
+ approximation. For pairs whose true IoU is very close to the 0.5 threshold,
+ the resize step in the dense path can flip a keep/suppress decision.
+
+ ``n_diff`` counts detections whose decision differs between the two paths.
+ ``nms_ok`` is True when ``n_diff == 0``.
+
+ Dense NMS is skipped when ``dense_skipped`` *or* ``iou_dense_skipped`` is True:
+ NMS calls mask_iou_batch internally so the cost is the same as IoU.
+
+ Returns:
+ Tuple of ``(dense_nms_s, compact_nms_s, nms_ok, n_diff)``.
+ """
+ predictions = np.c_[xyxy, confidence, class_ids.astype(float)]
+
+ compact_nms_s = time_reps(
+ lambda: sv.mask_non_max_suppression(predictions, compact_mask)
+ )
+ if dense_skipped or iou_dense_skipped:
+ return math.nan, compact_nms_s, None, 0
+
+ keep_dense = sv.mask_non_max_suppression(predictions, masks_dense)
+ keep_compact = sv.mask_non_max_suppression(predictions, compact_mask)
+ n_diff = int(np.sum(keep_dense != keep_compact))
+ nms_ok = n_diff == 0
+ dense_nms_s = time_reps(
+ lambda: sv.mask_non_max_suppression(predictions, masks_dense),
+ repeats=IOU_NMS_REPS,
+ )
+ return dense_nms_s, compact_nms_s, nms_ok, n_diff
+
+
+def stage_merge(
+ det_dense: sv.Detections | None,
+ det_compact: sv.Detections,
+ dense_skipped: bool,
+) -> tuple[float, float, bool | None]:
+ """Time Detections.merge on two half-splits.
+
+ Dense: np.vstack; compact: RLE concat.
+ Splits are pre-computed so the timed lambda measures only the merge.
+ """
+ half = len(det_compact) // 2
+ compact_a, compact_b = det_compact[:half], det_compact[half:]
+
+ compact_merge_s = time_reps(lambda: sv.Detections.merge([compact_a, compact_b]))
+ if dense_skipped or det_dense is None:
+ return math.nan, compact_merge_s, None
+
+ dense_a, dense_b = det_dense[:half], det_dense[half:]
+ merged_d = sv.Detections.merge([dense_a, dense_b])
+ merged_c = sv.Detections.merge([compact_a, compact_b])
+ merge_ok = bool(np.allclose(merged_d.area, merged_c.area))
+ dense_merge_s = time_reps(lambda: sv.Detections.merge([dense_a, dense_b]))
+ return dense_merge_s, compact_merge_s, merge_ok
+
+
+def stage_offset(
+ masks_dense: np.ndarray,
+ compact_mask: CompactMask,
+ image_height: int,
+ image_width: int,
+ dense_skipped: bool,
+) -> tuple[float, float, bool | None]:
+ """Time mask offset: move_masks (N,H,W) copy vs O(N) offset update."""
+ dx, dy = 10, 10
+ # Expand the canvas by the offset so no shifted crop overflows boundary.
+ # Both move_masks and with_offset.to_dense() operate on identical space.
+ new_h, new_w = image_height + dy, image_width + dx
+ new_shape = (new_h, new_w)
+
+ compact_offset_s = time_reps(
+ lambda: compact_mask.with_offset(dx, dy, new_image_shape=new_shape)
+ )
+ if dense_skipped:
+ return math.nan, compact_offset_s, None
+
+ moved_dense = sv.move_masks(
+ masks_dense, np.array([dx, dy]), resolution_wh=(new_w, new_h)
+ )
+ moved_compact = compact_mask.with_offset(
+ dx, dy, new_image_shape=new_shape
+ ).to_dense()
+ offset_ok = bool(np.array_equal(moved_dense, moved_compact))
+ dense_offset_s = time_reps(
+ lambda: sv.move_masks(
+ masks_dense, np.array([dx, dy]), resolution_wh=(new_w, new_h)
+ )
+ )
+ return dense_offset_s, compact_offset_s, offset_ok
+
+
+def stage_centroids(
+ masks_dense: np.ndarray,
+ compact_mask: CompactMask,
+ dense_skipped: bool,
+) -> tuple[float, float, bool | None]:
+ """Time centroid: np.tensordot on full stack (dense) vs per-crop (compact)."""
+ compact_centroids_s = time_reps(lambda: sv.calculate_masks_centroids(compact_mask))
+ if dense_skipped:
+ return math.nan, compact_centroids_s, None
+
+ c_dense = sv.calculate_masks_centroids(masks_dense)
+ c_compact = sv.calculate_masks_centroids(compact_mask)
+ centroids_ok = bool(np.allclose(c_dense, c_compact, atol=1.0)) # 1-pixel tolerance
+ dense_centroids_s = time_reps(lambda: sv.calculate_masks_centroids(masks_dense))
+ return dense_centroids_s, compact_centroids_s, centroids_ok
+
+
+def stage_resize(
+ masks_dense: np.ndarray,
+ compact_mask: CompactMask,
+ image_height: int,
+ image_width: int,
+ dense_skipped: bool,
+) -> tuple[float, float, bool | None]:
+ """Time resize to half resolution; check pixel-level correctness.
+
+ Dense path uses numpy fancy-indexing via ``_resize_dense_to_shape``.
+ Compact path times ``CompactMask.resize()``, which uses direct RLE
+ arithmetic for sparse masks (below ``_L3_DENSITY_THRESHOLD``) and
+ falls back to ``cv2.INTER_NEAREST`` decode/resize/re-encode for dense
+ masks. The two nearest-neighbour strategies can differ by 1 px at
+ bbox boundaries, so correctness is checked with 1-pixel tolerance.
+ """
+ new_h, new_w = image_height // 2, image_width // 2
+ new_shape = (new_h, new_w)
+
+ # Use parallel=1 to avoid nested ThreadPoolExecutor contention:
+ # CompactMask.resize() itself spawns a thread pool for N >= _PARALLEL_THRESHOLD,
+ # and time_reps' own parallel outer loop would cause oversubscription.
+ compact_resize_s = time_reps(lambda: compact_mask.resize(new_shape), parallel=1)
+ if dense_skipped:
+ return math.nan, compact_resize_s, None
+
+ resized_dense = _resize_dense_to_shape(masks_dense, new_h, new_w)
+ resized_compact = compact_mask.resize(new_shape).to_dense()
+ resize_ok = bool(
+ np.abs(resized_dense.astype(np.int8) - resized_compact.astype(np.int8)).max()
+ <= 1
+ )
+ dense_resize_s = time_reps(
+ lambda: _resize_dense_to_shape(masks_dense, new_h, new_w)
+ )
+ return dense_resize_s, compact_resize_s, resize_ok
+
+
+# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+# Scenario runner โ orchestrates stages
+# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+
+def run_scenario(
+ name: str,
+ num_objects: int,
+ image_height: int,
+ image_width: int,
+ fill_fraction: float = 0.10,
+ num_vertices: int = 20,
+) -> ScenarioResult:
+ resolution = f"{image_width}x{image_height}"
+ fill_name = f"{fill_fraction:.0%}"
+ console.rule(
+ f"[bold]{name}[/bold] | {num_objects} objects ยท {resolution} "
+ f"ยท fillโ{fill_name} ยท polygon/{num_vertices} vertices"
+ )
+
+ xyxy, masks_dense, class_ids, compact_mask = stage_build(
+ num_objects, image_height, image_width, fill_fraction, num_vertices
+ )
+ scene = make_scene(image_height, image_width)
+
+ # โโ memory โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ dense_bytes = dense_memory_bytes(masks_dense)
+ dense_skipped = dense_bytes > DENSE_SKIP_GB * 1e9
+ compact_theoretical = compact_memory_bytes_theoretical(compact_mask)
+
+ # Only measure dense tracemalloc when it's safe to allocate the full array.
+ dense_actual = (
+ 0
+ if dense_skipped
+ else dense_memory_bytes_actual(num_objects, image_height, image_width)
+ )
+ compact_actual = compact_memory_bytes_actual(
+ masks_dense, xyxy, (image_height, image_width)
+ )
+
+ encode_s = stage_encode(masks_dense, xyxy, image_height, image_width)
+ decode_s = stage_decode(compact_mask)
+
+ theory_ratio = dense_bytes / max(compact_theoretical, 1)
+ if dense_skipped:
+ malloc_ratio_str = "[dim]โ[/dim]"
+ dense_actual_str = "[dim]skipped[/dim]"
+ else:
+ malloc_ratio = dense_actual / max(compact_actual, 1)
+ malloc_ratio_str = _fmt_ratio(malloc_ratio)
+ dense_actual_str = f"{dense_actual / 1e6:.1f} MB"
+ console.print(
+ f"\tmemory >>\n"
+ f"\t\ttheory :: dense={dense_bytes / 1e6:.1f} MB "
+ f"| compact={compact_theoretical / 1e3:.0f} KB "
+ f"\t{_fmt_ratio(theory_ratio)}\n"
+ f"\t\tmalloc :: dense={dense_actual_str} "
+ f"| compact={compact_actual / 1e3:.0f} KB "
+ f"\t{malloc_ratio_str}"
+ )
+ console.print(f"\t encode (from_dense)\t={encode_s * 1e3:.3f} ms/mask")
+ console.print(f"\t decode (to_dense)\t={decode_s * 1e3:.3f} ms/mask")
+
+ # โโ skip flags โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ iou_dense_skipped = dense_bytes > IOU_DENSE_SKIP_GB * 1e9
+ if dense_skipped:
+ console.print(
+ f"\t[yellow]dense array is {dense_bytes / 1e9:.1f} GB "
+ f"(>{DENSE_SKIP_GB:.0f} GB threshold) โ skipping dense timing"
+ f"[/yellow]"
+ )
+ elif iou_dense_skipped:
+ console.print(
+ f"\t[yellow]dense IoU skipped (>{IOU_DENSE_SKIP_GB:.0f}GB thr.)[/yellow]"
+ )
+
+ confidence = (
+ np.random.default_rng(1).uniform(0.3, 0.99, num_objects).astype(np.float32)
+ )
+ det_compact = sv.Detections(xyxy=xyxy, mask=compact_mask, class_id=class_ids)
+
+ if dense_skipped:
+ dense_area_s = dense_filter_s = dense_annot_s = math.nan
+ compact_area_s = _time_compact_area(det_compact)
+ compact_filter_s = _time_compact_filter(det_compact)
+ compact_annot_s = _time_compact_annotate(scene, det_compact)
+ pixel_perfect = areas_match = roundtrip_ok = None
+ det_dense = None
+ else:
+ det_dense = sv.Detections(xyxy=xyxy, mask=masks_dense, class_id=class_ids)
+ dense_area_s, compact_area_s = stage_area(det_dense, det_compact)
+ dense_filter_s, compact_filter_s = stage_filter(det_dense, det_compact)
+ dense_annot_s, compact_annot_s = stage_annotate(scene, det_dense, det_compact)
+ pixel_perfect, areas_match, roundtrip_ok = stage_correctness(
+ scene, masks_dense, compact_mask, det_dense, det_compact
+ )
+
+ dense_iou_s, compact_iou_s, iou_ok = stage_iou(
+ masks_dense, compact_mask, iou_dense_skipped
+ )
+ dense_nms_s, compact_nms_s, nms_ok, nms_diff = stage_nms(
+ xyxy,
+ confidence,
+ class_ids,
+ masks_dense,
+ compact_mask,
+ dense_skipped,
+ iou_dense_skipped,
+ )
+ dense_merge_s, compact_merge_s, merge_ok = stage_merge(
+ det_dense, det_compact, dense_skipped
+ )
+ dense_offset_s, compact_offset_s, offset_ok = stage_offset(
+ masks_dense, compact_mask, image_height, image_width, dense_skipped
+ )
+ dense_centroids_s, compact_centroids_s, centroids_ok = stage_centroids(
+ masks_dense, compact_mask, dense_skipped
+ )
+ dense_resize_s, compact_resize_s, resize_ok = stage_resize(
+ masks_dense, compact_mask, image_height, image_width, dense_skipped
+ )
+
+ def _timing_line(label: str, dense_s: float, compact_s: float) -> str:
+ compact_ms = f"{compact_s * 1e3:.2f} ms"
+ if math.isnan(dense_s):
+ return (
+ f"\t{label}\t -> dense=[dim]โ[/dim]"
+ f"\t\t | compact={compact_ms}\t | speedup=[dim]โ[/dim]"
+ )
+ dense_ms = f"{dense_s * 1e3:.2f} ms"
+ speedup = _fmt_ratio(dense_s / max(compact_s, 1e-9))
+ return (
+ f"\t{label}\t -> dense={dense_ms}\t | "
+ f"compact={compact_ms}\t | speedup={speedup}"
+ )
+
+ console.print(_timing_line(".area ", dense_area_s, compact_area_s))
+ console.print(_timing_line("annotate ", dense_annot_s, compact_annot_s))
+ console.print(_timing_line("centroids", dense_centroids_s, compact_centroids_s))
+ console.print(_timing_line("filter ", dense_filter_s, compact_filter_s))
+ console.print(_timing_line("iou ", dense_iou_s, compact_iou_s))
+ console.print(_timing_line("merge ", dense_merge_s, compact_merge_s))
+ console.print(_timing_line("nms ", dense_nms_s, compact_nms_s))
+ console.print(_timing_line("offset ", dense_offset_s, compact_offset_s))
+ console.print(_timing_line("resize ", dense_resize_s, compact_resize_s))
+
+ checks = {
+ "pixel-perfect": pixel_perfect,
+ "areas": areas_match,
+ "roundtrip": roundtrip_ok,
+ "iou": iou_ok,
+ "nms": nms_ok,
+ "merge": merge_ok,
+ "offset": offset_ok,
+ "centroids": centroids_ok,
+ "resize": resize_ok,
+ }
+ parts = []
+ for k, v in checks.items():
+ if k == "nms" and v is False:
+ parts.append(f"nms=[red]โ({nms_diff})[/red]")
+ else:
+ parts.append(
+ f"{k}="
+ + (
+ "[dim]โ[/dim]"
+ if v is None
+ else "[green]โ[/green]"
+ if v
+ else "[red]โ[/red]"
+ )
+ )
+ all_checked = [v for v in checks.values() if v is not None]
+ overall = (
+ "[green]โ all correct[/green]"
+ if all_checked and all(all_checked)
+ else "[red]โ MISMATCH[/red]"
+ if any(v is False for v in checks.values())
+ else "[dim]โ[/dim]"
+ )
+ console.print(" correctness >> " + " | ".join(parts) + f" | {overall}")
+
+ return ScenarioResult(
+ name=name,
+ resolution=resolution,
+ num_objects=num_objects,
+ fill_name=fill_name,
+ num_vertices=num_vertices,
+ dense_bytes=dense_bytes,
+ compact_bytes_theoretical=compact_theoretical,
+ dense_bytes_actual=dense_actual,
+ compact_bytes_actual=compact_actual,
+ encode_s=encode_s,
+ decode_s=decode_s,
+ dense_area_s=dense_area_s,
+ compact_area_s=compact_area_s,
+ dense_filter_s=dense_filter_s,
+ compact_filter_s=compact_filter_s,
+ dense_annot_s=dense_annot_s,
+ compact_annot_s=compact_annot_s,
+ dense_iou_s=dense_iou_s,
+ compact_iou_s=compact_iou_s,
+ dense_nms_s=dense_nms_s,
+ compact_nms_s=compact_nms_s,
+ dense_merge_s=dense_merge_s,
+ compact_merge_s=compact_merge_s,
+ dense_offset_s=dense_offset_s,
+ compact_offset_s=compact_offset_s,
+ dense_centroids_s=dense_centroids_s,
+ compact_centroids_s=compact_centroids_s,
+ pixel_perfect=pixel_perfect,
+ areas_match=areas_match,
+ roundtrip_ok=roundtrip_ok,
+ iou_ok=iou_ok,
+ nms_ok=nms_ok,
+ nms_mismatch_count=nms_diff,
+ merge_ok=merge_ok,
+ offset_ok=offset_ok,
+ centroids_ok=centroids_ok,
+ dense_resize_s=dense_resize_s,
+ compact_resize_s=compact_resize_s,
+ resize_ok=resize_ok,
+ dense_skipped=dense_skipped,
+ iou_dense_skipped=iou_dense_skipped,
+ )
+
+
+def _time_compact_area(det_compact: sv.Detections) -> float:
+ """Time .area on the compact detections (used when dense timing is skipped)."""
+ return time_reps(lambda: det_compact.area)
+
+
+def _time_compact_filter(det_compact: sv.Detections) -> float:
+ """Time boolean-index filtering on the compact detections (dense-skip path)."""
+ keep = np.arange(len(det_compact)) % 2 == 0
+ return time_reps(lambda: det_compact[keep])
+
+
+def _time_compact_annotate(scene: np.ndarray, det_compact: sv.Detections) -> float:
+ """Time MaskAnnotator on the compact detections (dense-skip path)."""
+ annotator = sv.MaskAnnotator(opacity=0.5)
+ return time_reps(lambda: annotator.annotate(scene.copy(), det_compact))
+
+
+# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+# Rich summary table
+# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+_OPS = (
+ "area",
+ "filter",
+ "annot",
+ "iou",
+ "nms",
+ "merge",
+ "offset",
+ "centroids",
+ "resize",
+)
+
+
+def _build_summary_df(results: list[ScenarioResult]) -> pd.DataFrame:
+ """Compute derived summary columns from scenario results.
+
+ Returns a DataFrame with all ScenarioResult fields plus derived columns
+ (ratios, speedups, ok) as raw floats. Consumers apply their own formatting.
+ """
+ df = pd.DataFrame([dataclasses.asdict(r) for r in results])
+ df["ratio_theory"] = df["dense_bytes"] / df["compact_bytes_theoretical"].clip(
+ lower=1
+ )
+ df["ratio_malloc"] = df["dense_bytes_actual"] / df["compact_bytes_actual"].clip(
+ lower=1
+ )
+ # dense_bytes_actual == 0 (not measured) when dense_skipped โ clear those cells
+ df.loc[df["dense_skipped"], "ratio_malloc"] = None
+ for op in _OPS:
+ df[f"{op}_speedup"] = df[f"dense_{op}_s"] / df[f"compact_{op}_s"].clip(
+ lower=1e-9
+ )
+
+ check_cols = [
+ "pixel_perfect",
+ "areas_match",
+ "roundtrip_ok",
+ "iou_ok",
+ "nms_ok",
+ "merge_ok",
+ "offset_ok",
+ "centroids_ok",
+ "resize_ok",
+ ]
+ df["ok"] = df.apply(
+ lambda row: (
+ False
+ if any(row[c] is False for c in check_cols)
+ else True
+ if any(row[c] is True for c in check_cols)
+ else None
+ ),
+ axis=1,
+ )
+ return df
+
+
+def _fmt_ratio(ratio: float) -> str:
+ """Format a speedup/compression ratio with colour coding.
+
+ โฅ10 โ green (large win), 1-10 โ yellow (modest win), <1 โ red (regression).
+ Integer for โฅ10, two decimals otherwise.
+ """
+ fmt = f"{ratio:.0f}x" if ratio >= 10 else f"{ratio:.2f}x"
+ if ratio >= 10:
+ return f"[green]{fmt}[/green]"
+ elif ratio >= 1:
+ return f"[yellow]{fmt}[/yellow]"
+ else:
+ return f"[red]{fmt}[/red]"
+
+
+def _fmt_speedup(dense_s: float, compact_s: float) -> str:
+ if math.isnan(dense_s):
+ # Dense was skipped โ show compact absolute time so the column isn't empty.
+ return f"[dim]{compact_s * 1e3:.1f} ms[/dim]"
+ return _fmt_ratio(dense_s / max(compact_s, 1e-9))
+
+
+def print_summary(results: list[ScenarioResult]) -> None:
+ table = Table(
+ title="CompactMask โ benchmark summary",
+ box=box.ROUNDED,
+ show_lines=True,
+ header_style="bold cyan",
+ min_width=console.width,
+ )
+ table.add_column("Scenario", style="bold", min_width=22)
+ table.add_column("Objects", justify="right", min_width=7)
+ table.add_column("Resolution", min_width=12, no_wrap=True)
+ table.add_column("Mask\narea", justify="right", min_width=5, no_wrap=True)
+ table.add_column("Vertices", justify="right", min_width=8, no_wrap=True)
+ table.add_column("Dense\ntheory", justify="right", min_width=10)
+ table.add_column("Compact\ntheory", justify="right", style="green", min_width=9)
+ table.add_column("Ratio\ntheory", justify="right", min_width=7)
+ table.add_column("Dense\nmalloc", justify="right", style="cyan", min_width=9)
+ table.add_column("Compact\nmalloc", justify="right", style="cyan", min_width=9)
+ table.add_column("Ratio\nmalloc", justify="right", min_width=7)
+ table.add_column("Encode\n(ms/mask)", justify="right", style="yellow", min_width=7)
+ table.add_column("Decode\n(ms/mask)", justify="right", style="yellow", min_width=7)
+ table.add_column("Area\natt.", justify="right", min_width=6)
+ table.add_column("Filter\nop.", justify="right", min_width=6)
+ table.add_column("Annot\nop.", justify="right", min_width=6)
+ table.add_column("IoU\nop.", justify="right", min_width=6)
+ table.add_column("NMS\nop.", justify="right", min_width=6)
+ table.add_column("Merge\nop.", justify="right", min_width=6)
+ table.add_column("Offset\nop.", justify="right", min_width=6)
+ table.add_column("Resize\nop.", justify="right", min_width=6)
+ table.add_column("Centr\nop.", justify="right", min_width=6)
+ table.add_column("OK?", justify="center", min_width=4)
+
+ for _, row in _build_summary_df(results).iterrows():
+ ok = row["ok"]
+ ok_cell = (
+ "[red]โ[/red]"
+ if ok is False
+ else "[green]โ[/green]"
+ if ok is True
+ else "[dim]โ[/dim]"
+ )
+ dense_malloc_cell = (
+ "[dim]โ[/dim]"
+ if row["dense_skipped"]
+ else f"{row['dense_bytes_actual'] / 1e6:.1f} MB"
+ )
+ malloc_ratio_cell = (
+ "[dim]โ[/dim]" if row["dense_skipped"] else _fmt_ratio(row["ratio_malloc"])
+ )
+ table.add_row(
+ row["name"],
+ str(row["num_objects"]),
+ row["resolution"],
+ row["fill_name"],
+ str(row["num_vertices"]),
+ f"{row['dense_bytes'] / 1e6:.1f} MB",
+ f"{row['compact_bytes_theoretical'] / 1e3:.0f} KB",
+ _fmt_ratio(row["ratio_theory"]),
+ dense_malloc_cell,
+ f"{row['compact_bytes_actual'] / 1e3:.0f} KB",
+ malloc_ratio_cell,
+ f"{row['encode_s'] * 1e3:.1f}",
+ f"{row['decode_s'] * 1e3:.1f}",
+ _fmt_speedup(row["dense_area_s"], row["compact_area_s"]),
+ _fmt_speedup(row["dense_filter_s"], row["compact_filter_s"]),
+ _fmt_speedup(row["dense_annot_s"], row["compact_annot_s"]),
+ _fmt_speedup(row["dense_iou_s"], row["compact_iou_s"]),
+ _fmt_speedup(row["dense_nms_s"], row["compact_nms_s"]),
+ _fmt_speedup(row["dense_merge_s"], row["compact_merge_s"]),
+ _fmt_speedup(row["dense_offset_s"], row["compact_offset_s"]),
+ _fmt_speedup(row["dense_resize_s"], row["compact_resize_s"]),
+ _fmt_speedup(row["dense_centroids_s"], row["compact_centroids_s"]),
+ ok_cell,
+ )
+
+ console.print(table)
+ console.print(
+ "[dim]"
+ + " ยท ".join(
+ [
+ "Vertices โ polygon vertex count "
+ "(complexity proxy: more = jaggier boundary)",
+ "Dense theory โ NxHxW bytes (raw numpy buffer)",
+ "Compact theory โ sum of internal numpy buffer sizes",
+ "Ratio (theory) โ dense / compact theoretical ratio",
+ "Dense malloc โ tracemalloc peak during np.zeros allocation",
+ "Compact malloc โ tracemalloc peak during .from_dense()",
+ "Ratio (malloc) โ dense / compact tracemalloc peak ratio",
+ "Encode ms/mask โ from_dense() / N (denseโcompact overhead per mask)",
+ "Decode ms/mask โ to_dense() / N (compactโdense overhead per mask)",
+ "Area x โ .area speedup (RLE sum, no materialisation)",
+ "Filter x โ boolean-index speedup",
+ "Annot x โ MaskAnnotator speedup "
+ "(ROI-only blend vs full-frame overlay)",
+ f"IoU x โ pairwise self-IoU speedup "
+ f"(dense skipped >{IOU_DENSE_SKIP_GB:.0f} GB)",
+ "NMS x โ mask_non_max_suppression speedup",
+ "Merge x โ Detections.merge speedup",
+ "Offset x โ move_masks vs with_offset speedup",
+ "Resize x โ resize-to-half speedup",
+ "Centroids x โ calculate_masks_centroids speedup",
+ "dim ms โ dense skipped, compact absolute time shown",
+ ]
+ )
+ + "[/dim]"
+ )
+
+
+# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+# Results persistence
+# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+
+def _append_result(result: ScenarioResult, path: Path) -> None:
+ """Append one scenario result as a JSON line to *path*.
+
+ ``math.nan`` (used for skipped dense timings) is serialised as ``null``
+ so the file is valid JSON-Lines and can be read back with any JSON parser.
+ """
+ row = {
+ k: (None if isinstance(v, float) and math.isnan(v) else v)
+ for k, v in dataclasses.asdict(result).items()
+ }
+ with path.open("a", encoding="utf-8") as fh:
+ fh.write(json.dumps(row) + "\n")
+
+
+def save_results_csv(results: list[ScenarioResult], path: Path) -> None:
+ """Write the summary table to *path* as a CSV file.
+
+ Each row mirrors the Rich summary table: scenario metadata, memory ratios,
+ encode/decode overhead, and per-operation speedups. Columns whose dense
+ timing was skipped are written as empty cells.
+ """
+ df = _build_summary_df(results)
+ pd.DataFrame(
+ {
+ "scenario": df["name"],
+ "objects": df["num_objects"],
+ "resolution": df["resolution"],
+ "fill": df["fill_name"],
+ "vertices": df["num_vertices"],
+ "dense_theory_mb": (df["dense_bytes"] / 1e6).round(1),
+ "compact_theory_kb": (df["compact_bytes_theoretical"] / 1e3).round(1),
+ "ratio_theory": df["ratio_theory"].round(0),
+ "dense_malloc_mb": (df["dense_bytes_actual"] / 1e6)
+ .where(~df["dense_skipped"])
+ .round(1),
+ "compact_malloc_kb": (df["compact_bytes_actual"] / 1e3).round(1),
+ "ratio_malloc": df["ratio_malloc"].round(0),
+ "encode_ms_per_mask": (df["encode_s"] * 1e3).round(4),
+ "decode_ms_per_mask": (df["decode_s"] * 1e3).round(4),
+ **{f"{op}_speedup": df[f"{op}_speedup"].round(2) for op in _OPS},
+ "resize_ok": df["resize_ok"],
+ "ok": df["ok"],
+ }
+ ).to_csv(path, index=False)
+
+
+# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+# Entry point
+# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+
+def main() -> None:
+ # โโ parameter matrix โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ # (tier_label, (image_width, image_height), num_objects)
+ TIERS: list[tuple[str, tuple[int, int], int]] = [
+ ("FHD", (1920, 1080), 100), # full comparison (0.21 GB < 1 GB IoU thr.)
+ ("FHD", (1920, 1080), 200), # full comparison (0.41 GB < 1 GB IoU thr.)
+ ("FHD", (1920, 1080), 400), # full comparison (0.83 GB < 1 GB IoU thr.)
+ ("4K", (3840, 2160), 100), # full comparison (0.83 GB < 1 GB IoU thr.)
+ ("4K", (3840, 2160), 200), # dense excl. IoU/NMS (1.66 GB > 1 GB thr.)
+ ("SAT", (8192, 8192), 200), # dense excl. IoU/NMS (13.4 GB > 1 GB thr.)
+ ]
+ FILL_FRACTIONS = [0.05, 0.20, 0.50] # sparse / moderate / SAM-everything
+ VERTEX_COUNTS = [8, 128, 600] # low / realistic / YOLOv8-seg default
+
+ scenarios = [
+ {
+ "name": f"{tier}-{num_objects}-{fill_fraction:.0%}-v{num_vertices}",
+ "num_objects": num_objects,
+ "image_height": img_h,
+ "image_width": img_w,
+ "fill_fraction": fill_fraction,
+ "num_vertices": num_vertices,
+ }
+ for tier, (img_w, img_h), num_objects in TIERS
+ for fill_fraction in FILL_FRACTIONS
+ for num_vertices in VERTEX_COUNTS
+ ]
+
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
+ results_path = Path(__file__).parent / f"results_{timestamp}.jsonl"
+
+ console.print(
+ f"[bold]supervision[/bold]"
+ f" {sv.__version__} ยท numpy {np.__version__} ยท {len(scenarios)} scenarios"
+ f" ยท saving to [dim]{results_path.name}[/dim]"
+ )
+
+ results = []
+ progress = Progress(
+ TextColumn("[progress.description]{task.description}"),
+ BarColumn(),
+ MofNCompleteColumn(),
+ TaskProgressColumn(),
+ TimeElapsedColumn(),
+ console=console,
+ )
+ with progress:
+ task = progress.add_task("benchmarkingโฆ", total=len(scenarios))
+ for params in scenarios:
+ progress.update(task, description=f"[bold]{params['name']}[/bold]")
+ result = run_scenario(**params)
+ results.append(result)
+ _append_result(result, results_path)
+ gc.collect() # flush scenario temporaries before next run
+ progress.advance(task)
+
+ print_summary(results)
+
+ csv_path = results_path.with_suffix(".csv")
+ save_results_csv(results, csv_path)
+ console.print(f"[dim]results saved โ {results_path.name} ยท {csv_path.name}[/dim]")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/count_people_in_zone/.gitignore b/examples/count_people_in_zone/.gitignore
new file mode 100644
index 0000000..1104aa2
--- /dev/null
+++ b/examples/count_people_in_zone/.gitignore
@@ -0,0 +1,13 @@
+# Ignore everything in the data directory
+data/*
+
+# But re-include all .json files
+!data/*.json
+
+*.pt
+*.pth
+*.mp4
+*.mov
+*.png
+*.jpg
+*.jpeg
diff --git a/examples/count_people_in_zone/README.md b/examples/count_people_in_zone/README.md
new file mode 100644
index 0000000..f1391ee
--- /dev/null
+++ b/examples/count_people_in_zone/README.md
@@ -0,0 +1,107 @@
+# count people in zone
+
+[](https://colab.research.google.com/github/roboflow-ai/notebooks/blob/main/notebooks/how-to-detect-and-count-objects-in-polygon-zone.ipynb) [](https://www.youtube.com/watch?v=l_kf9CfZ_8M)
+
+## ๐ hello
+
+This demo is a video analysis tool that counts and highlights objects in specific zones of a video. Each zone and the objects within it are marked in different colors, making it easy to see and count the objects in each area. The tool can save this enhanced video or display it live on the screen.
+
+https://github.com/roboflow/supervision/assets/26109316/f84db7b5-79e2-4142-a1da-64daa43ce667
+
+## ๐ป install
+
+- clone repository and navigate to example directory
+
+ ```bash
+ git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
+ cd supervision/examples/count_people_in_zone
+ ```
+
+- setup python environment and activate it [optional]
+
+ ```bash
+ uv venv
+ source .venv/bin/activate
+ ```
+
+- install required dependencies
+
+ ```bash
+ uv pip install -r requirements.txt
+ ```
+
+- download `traffic_analysis.pt` and `traffic_analysis.mov` files
+
+ ```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.
+
+ - `--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"`.
+
+ - `--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
+
+- `horizontal-zone-config.json`: Defines zones divided horizontally across the frame.
+- `multi-zone-config.json`: Configures multiple zones with custom shapes and positions.
+- `quarters-zone-config.json`: Splits the frame into four equal quarters.
+- `vertical-zone-config.json`: Divides the frame into vertical zones of equal width.
+
+## โ๏ธ run example
+
+- 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
+ ```
+
+- 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
+ ```
+
+## ยฉ 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.
+
+- 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.
diff --git a/examples/count_people_in_zone/inference_example.py b/examples/count_people_in_zone/inference_example.py
new file mode 100644
index 0000000..2b21126
--- /dev/null
+++ b/examples/count_people_in_zone/inference_example.py
@@ -0,0 +1,202 @@
+import json
+import os
+
+import cv2
+import numpy as np
+from inference.core.models.roboflow import RoboflowInferenceModel
+from inference.models.utils import get_roboflow_model
+from tqdm import tqdm
+
+import supervision as sv
+
+COLORS = sv.ColorPalette.DEFAULT
+
+
+def load_zones_config(file_path: str) -> list[np.ndarray]:
+ """
+ Load polygon zone configurations from a JSON file.
+
+ This function reads a JSON file which contains polygon coordinates, and
+ converts them into a list of NumPy arrays. Each polygon is represented as
+ a NumPy array of coordinates.
+
+ Args:
+ file_path (str): The path to the JSON configuration file.
+
+ Returns:
+ List[np.ndarray]: A list of polygons, each represented as a NumPy array.
+ """
+ with open(file_path) as file:
+ data = json.load(file)
+ return [np.array(polygon, np.int32) for polygon in data["polygons"]]
+
+
+def initiate_annotators(
+ polygons: list[np.ndarray], resolution_wh: tuple[int, int]
+) -> tuple[list[sv.PolygonZone], list[sv.PolygonZoneAnnotator], list[sv.BoxAnnotator]]:
+ line_thickness = sv.calculate_optimal_line_thickness(resolution_wh=resolution_wh)
+ text_scale = sv.calculate_optimal_text_scale(resolution_wh=resolution_wh)
+
+ zones = []
+ zone_annotators = []
+ box_annotators = []
+
+ for index, polygon in enumerate(polygons):
+ zone = sv.PolygonZone(polygon=polygon)
+ zone_annotator = sv.PolygonZoneAnnotator(
+ zone=zone,
+ color=COLORS.by_idx(index),
+ thickness=line_thickness,
+ text_thickness=line_thickness * 2,
+ text_scale=text_scale * 2,
+ )
+ box_annotator = sv.BoxAnnotator(
+ color=COLORS.by_idx(index), thickness=line_thickness
+ )
+ zones.append(zone)
+ zone_annotators.append(zone_annotator)
+ box_annotators.append(box_annotator)
+
+ return zones, zone_annotators, box_annotators
+
+
+def detect(
+ frame: np.ndarray,
+ model: RoboflowInferenceModel,
+ confidence_threshold: float = 0.5,
+ iou_threshold: float = 0.7,
+) -> sv.Detections:
+ """
+ Detect objects in a frame using Inference model, filtering detections by class ID
+ and confidence threshold.
+
+ Args:
+ 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): The confidence threshold for filtering
+ detections.
+ iou_threshold (float): The IoU threshold for non-maximum suppression.
+
+ Returns:
+ sv.Detections: Filtered detections after processing the frame with the Inference
+ model.
+
+ Note:
+ This function is specifically tailored for an Inference model and assumes class
+ ID 0 for filtering.
+ """
+ results = model.infer(frame, confidence=confidence_threshold, iou=iou_threshold)[0]
+ detections = sv.Detections.from_inference(results)
+ filter_by_class = detections.class_id == 0
+ filter_by_confidence = detections.confidence > confidence_threshold
+ return detections[filter_by_class & filter_by_confidence]
+
+
+def annotate(
+ frame: np.ndarray,
+ zones: list[sv.PolygonZone],
+ zone_annotators: list[sv.PolygonZoneAnnotator],
+ box_annotators: list[sv.BoxAnnotator],
+ detections: sv.Detections,
+) -> np.ndarray:
+ """
+ Annotate a frame with zone and box annotations based on given detections.
+
+ Args:
+ frame (np.ndarray): The original frame to be annotated.
+ zones (List[sv.PolygonZone]): A list of polygon zones used for detection.
+ zone_annotators (List[sv.PolygonZoneAnnotator]): A list of annotators for
+ drawing zone annotations.
+ box_annotators (List[sv.BoxAnnotator]): A list of annotators for
+ drawing box annotations.
+ detections (sv.Detections): Detections to be used for annotation.
+
+ Returns:
+ np.ndarray: The annotated frame.
+ """
+ annotated_frame = frame.copy()
+ for zone, zone_annotator, box_annotator in zip(
+ zones, zone_annotators, box_annotators
+ ):
+ detections_in_zone = detections[zone.trigger(detections=detections)]
+ annotated_frame = zone_annotator.annotate(scene=annotated_frame)
+ annotated_frame = box_annotator.annotate(
+ scene=annotated_frame, detections=detections_in_zone
+ )
+ return annotated_frame
+
+
+def main(
+ zone_configuration_path: str,
+ source_video_path: str,
+ model_id: str = "yolov8x-1280",
+ roboflow_api_key: str | None = None,
+ target_video_path: str | None = None,
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+) -> None:
+ """
+ Counting people in zones with Inference and Supervision.
+
+ Args:
+ zone_configuration_path: Path to the zone configuration JSON file
+ source_video_path: Path to the source video file
+ model_id: Roboflow model ID
+ roboflow_api_key: Roboflow API KEY
+ target_video_path: Path to the target video file (output)
+ confidence_threshold: Confidence threshold for the model
+ iou_threshold: IOU threshold for the model
+ """
+ api_key = roboflow_api_key
+ api_key = os.environ.get("ROBOFLOW_API_KEY", api_key)
+ if api_key is None:
+ raise ValueError(
+ "Roboflow API key is missing. Please provide it as an argument or set the "
+ "ROBOFLOW_API_KEY environment variable."
+ )
+ roboflow_api_key = api_key
+
+ video_info = sv.VideoInfo.from_video_path(source_video_path)
+ polygons = load_zones_config(zone_configuration_path)
+ zones, zone_annotators, box_annotators = initiate_annotators(
+ polygons=polygons, resolution_wh=video_info.resolution_wh
+ )
+
+ model = get_roboflow_model(model_id=model_id, api_key=roboflow_api_key)
+
+ frames_generator = sv.get_video_frames_generator(source_video_path)
+ if target_video_path is not None:
+ with sv.VideoSink(target_video_path, video_info) as sink:
+ for frame in tqdm(frames_generator, total=video_info.total_frames):
+ detections = detect(frame, model, confidence_threshold, iou_threshold)
+ annotated_frame = annotate(
+ frame=frame,
+ zones=zones,
+ zone_annotators=zone_annotators,
+ box_annotators=box_annotators,
+ detections=detections,
+ )
+ sink.write_frame(annotated_frame)
+ else:
+ for frame in tqdm(frames_generator, total=video_info.total_frames):
+ detections = detect(frame, model, confidence_threshold, iou_threshold)
+ annotated_frame = annotate(
+ frame=frame,
+ zones=zones,
+ zone_annotators=zone_annotators,
+ box_annotators=box_annotators,
+ detections=detections,
+ )
+ cv2.imshow("Processed Video", annotated_frame)
+ if cv2.waitKey(1) & 0xFF == ord("q"):
+ break
+
+ cv2.destroyAllWindows()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/count_people_in_zone/requirements.txt b/examples/count_people_in_zone/requirements.txt
new file mode 100644
index 0000000..f325418
--- /dev/null
+++ b/examples/count_people_in_zone/requirements.txt
@@ -0,0 +1,6 @@
+gdown
+inference
+supervision
+tqdm
+ultralytics
+jsonargparse[signatures]
diff --git a/examples/count_people_in_zone/setup.sh b/examples/count_people_in_zone/setup.sh
new file mode 100755
index 0000000..4fd1a5f
--- /dev/null
+++ b/examples/count_people_in_zone/setup.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+# Get the directory where the script is located
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+# Check if 'data' directory does not exist and then create it
+if [[ ! -e $DIR/data ]]; then
+ mkdir "$DIR/data"
+else
+ echo "'data' directory already exists."
+fi
+
+# Download the market-square.mp4 file from Google Drive
+gdown -O "$DIR/data/market-square.mp4" "https://drive.google.com/uc?id=1vVrEVMxucHgqGd7vAa501ASojbeGPhIr"
diff --git a/examples/count_people_in_zone/ultralytics_example.py b/examples/count_people_in_zone/ultralytics_example.py
new file mode 100644
index 0000000..6caf3c2
--- /dev/null
+++ b/examples/count_people_in_zone/ultralytics_example.py
@@ -0,0 +1,190 @@
+import json
+
+import cv2
+import numpy as np
+from tqdm import tqdm
+from ultralytics import YOLO
+
+import supervision as sv
+
+COLORS = sv.ColorPalette.DEFAULT
+
+
+def load_zones_config(file_path: str) -> list[np.ndarray]:
+ """
+ Load polygon zone configurations from a JSON file.
+
+ This function reads a JSON file which contains polygon coordinates, and
+ converts them into a list of NumPy arrays. Each polygon is represented as
+ a NumPy array of coordinates.
+
+ Args:
+ file_path (str): The path to the JSON configuration file.
+
+ Returns:
+ List[np.ndarray]: A list of polygons, each represented as a NumPy array.
+ """
+ with open(file_path) as file:
+ data = json.load(file)
+ return [np.array(polygon, np.int32) for polygon in data["polygons"]]
+
+
+def initiate_annotators(
+ polygons: list[np.ndarray], resolution_wh: tuple[int, int]
+) -> tuple[list[sv.PolygonZone], list[sv.PolygonZoneAnnotator], list[sv.BoxAnnotator]]:
+ line_thickness = sv.calculate_optimal_line_thickness(resolution_wh=resolution_wh)
+ text_scale = sv.calculate_optimal_text_scale(resolution_wh=resolution_wh)
+
+ zones = []
+ zone_annotators = []
+ box_annotators = []
+
+ for index, polygon in enumerate(polygons):
+ zone = sv.PolygonZone(polygon=polygon)
+ zone_annotator = sv.PolygonZoneAnnotator(
+ zone=zone,
+ color=COLORS.by_idx(index),
+ thickness=line_thickness,
+ text_thickness=line_thickness * 2,
+ text_scale=text_scale * 2,
+ )
+ box_annotator = sv.BoxAnnotator(
+ color=COLORS.by_idx(index), thickness=line_thickness
+ )
+ zones.append(zone)
+ zone_annotators.append(zone_annotator)
+ box_annotators.append(box_annotator)
+
+ return zones, zone_annotators, box_annotators
+
+
+def detect(
+ frame: np.ndarray,
+ model: YOLO,
+ confidence_threshold: float = 0.5,
+ iou_threshold: float = 0.7,
+) -> sv.Detections:
+ """
+ Detect objects in a frame using a YOLO model, filtering detections by class ID and
+ confidence threshold.
+
+ 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): The confidence threshold for filtering
+ detections. Default is 0.5.
+ iou_threshold (float): The IoU threshold for non-maximum suppression.
+
+ Returns:
+ sv.Detections: Filtered detections after processing the frame with the YOLO
+ model.
+
+ Note:
+ This function is specifically tailored for a YOLO model and assumes class ID 0
+ for filtering.
+ """
+ results = model(
+ frame, conf=confidence_threshold, iou=iou_threshold, imgsz=1280, verbose=False
+ )[0]
+ detections = sv.Detections.from_ultralytics(results)
+ filter_by_class = detections.class_id == 0
+ filter_by_confidence = detections.confidence > confidence_threshold
+ return detections[filter_by_class & filter_by_confidence]
+
+
+def annotate(
+ frame: np.ndarray,
+ zones: list[sv.PolygonZone],
+ zone_annotators: list[sv.PolygonZoneAnnotator],
+ box_annotators: list[sv.BoxAnnotator],
+ detections: sv.Detections,
+) -> np.ndarray:
+ """
+ Annotate a frame with zone and box annotations based on given detections.
+
+ Args:
+ frame (np.ndarray): The original frame to be annotated.
+ zones (List[sv.PolygonZone]): A list of polygon zones used for detection.
+ zone_annotators (List[sv.PolygonZoneAnnotator]): A list of annotators for
+ drawing zone annotations.
+ box_annotators (List[sv.BoxAnnotator]): A list of annotators for
+ drawing box annotations.
+ detections (sv.Detections): Detections to be used for annotation.
+
+ Returns:
+ np.ndarray: The annotated frame.
+ """
+ annotated_frame = frame.copy()
+ for zone, zone_annotator, box_annotator in zip(
+ zones, zone_annotators, box_annotators
+ ):
+ detections_in_zone = detections[zone.trigger(detections=detections)]
+ annotated_frame = zone_annotator.annotate(scene=annotated_frame)
+ annotated_frame = box_annotator.annotate(
+ scene=annotated_frame, detections=detections_in_zone
+ )
+ return annotated_frame
+
+
+def main(
+ zone_configuration_path: str,
+ source_video_path: str,
+ source_weights_path: str = "yolo11x.pt",
+ target_video_path: str | None = None,
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+) -> None:
+ """
+ Counting people in zones with YOLO and Supervision.
+
+ Args:
+ zone_configuration_path: Path to the zone configuration JSON file
+ source_video_path: Path to the source video file
+ source_weights_path: Path to the source weights file
+ target_video_path: Path to the target video file (output)
+ confidence_threshold: Confidence threshold for the model
+ iou_threshold: IOU threshold for the model
+ """
+ video_info = sv.VideoInfo.from_video_path(source_video_path)
+ polygons = load_zones_config(zone_configuration_path)
+ zones, zone_annotators, box_annotators = initiate_annotators(
+ polygons=polygons, resolution_wh=video_info.resolution_wh
+ )
+
+ model = YOLO(source_weights_path)
+
+ frames_generator = sv.get_video_frames_generator(source_video_path)
+ if target_video_path is not None:
+ with sv.VideoSink(target_video_path, video_info) as sink:
+ for frame in tqdm(frames_generator, total=video_info.total_frames):
+ detections = detect(frame, model, confidence_threshold, iou_threshold)
+ annotated_frame = annotate(
+ frame=frame,
+ zones=zones,
+ zone_annotators=zone_annotators,
+ box_annotators=box_annotators,
+ detections=detections,
+ )
+ sink.write_frame(annotated_frame)
+ else:
+ for frame in tqdm(frames_generator, total=video_info.total_frames):
+ detections = detect(frame, model, confidence_threshold, iou_threshold)
+ annotated_frame = annotate(
+ frame=frame,
+ zones=zones,
+ zone_annotators=zone_annotators,
+ box_annotators=box_annotators,
+ detections=detections,
+ )
+ cv2.imshow("Processed Video", annotated_frame)
+ if cv2.waitKey(1) & 0xFF == ord("q"):
+ break
+
+ cv2.destroyAllWindows()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/heatmap_and_track/README.md b/examples/heatmap_and_track/README.md
new file mode 100644
index 0000000..643e9e4
--- /dev/null
+++ b/examples/heatmap_and_track/README.md
@@ -0,0 +1,59 @@
+# heatmap and tracking
+
+## ๐ hello
+
+This script performs heatmap and tracking analysis using YOLOv8, an object-detection method and 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
+
+ ```bash
+ git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
+ cd supervision/examples/heatmap_and_track
+ ```
+
+- setup python environment and activate it [optional]
+
+ ```bash
+ uv venv
+ source .venv/bin/activate
+ ```
+
+- install required dependencies
+
+ ```bash
+ uv pip install -r requirements.txt
+ ```
+
+## ๐ ๏ธ 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.
+- `--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
+- `--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.
+- `--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.
+- `--heatmap_alpha` (optional): Opacity of the overlay mask, between 0 and 1.
+- `--radius` (optional): Radius of the heat circle.
+- `--track_activation_threshold` (optional): Detection confidence threshold for track activation.
+- `--track_seconds` (optional): Number of seconds to buffer when a track is lost.
+- `--minimum_matching_threshold` (optional): Threshold for matching tracks with detections.
+
+## โ๏ธ run
+
+```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
+```
+
+## ยฉ 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.
+
+- 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.
diff --git a/examples/heatmap_and_track/requirements.txt b/examples/heatmap_and_track/requirements.txt
new file mode 100644
index 0000000..8148534
--- /dev/null
+++ b/examples/heatmap_and_track/requirements.txt
@@ -0,0 +1,3 @@
+supervision
+ultralytics
+jsonargparse[signatures]
diff --git a/examples/heatmap_and_track/script.py b/examples/heatmap_and_track/script.py
new file mode 100644
index 0000000..5a8667e
--- /dev/null
+++ b/examples/heatmap_and_track/script.py
@@ -0,0 +1,121 @@
+import cv2
+from ultralytics import YOLO
+
+import supervision as sv
+from supervision.assets import VideoAssets, download_assets
+
+
+def download_video() -> str:
+ download_assets(VideoAssets.PEOPLE_WALKING)
+ return VideoAssets.PEOPLE_WALKING.value
+
+
+def main(
+ source_weights_path: str,
+ source_video_path: str | None = None,
+ target_video_path: str = "output.mp4",
+ confidence_threshold: float = 0.35,
+ iou_threshold: float = 0.5,
+ heatmap_alpha: float = 0.5,
+ radius: int = 25,
+ track_activation_threshold: float = 0.35,
+ track_seconds: int = 5,
+ minimum_matching_threshold: float = 0.99,
+) -> None:
+ """
+ Heatmap and Tracking with Supervision.
+
+ Args:
+ source_weights_path: Path to the source weights file
+ source_video_path: Path to the source video file
+ target_video_path: Path to the target video file
+ confidence_threshold: Confidence threshold for the model
+ iou_threshold: IOU threshold for the model
+ heatmap_alpha: Opacity of the overlay mask, between 0 and 1
+ radius: Radius of the heat circle
+ track_activation_threshold: Detection confidence threshold for track activation
+ track_seconds: Number of seconds to buffer when a track is lost
+ minimum_matching_threshold: Threshold for matching tracks with detections
+ """
+ ### instantiate model
+ model = YOLO(source_weights_path)
+ source_video_path = source_video_path or download_video()
+
+ ### heatmap config
+ heat_map_annotator = sv.HeatMapAnnotator(
+ position=sv.Position.BOTTOM_CENTER,
+ opacity=heatmap_alpha,
+ radius=radius,
+ kernel_size=25,
+ top_hue=0,
+ low_hue=125,
+ )
+
+ ### annotation config
+ label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER)
+
+ ### get the video fps
+ cap = cv2.VideoCapture(source_video_path)
+ fps = int(cap.get(cv2.CAP_PROP_FPS))
+ cap.release()
+
+ ### tracker config
+ byte_tracker = sv.ByteTrack(
+ track_activation_threshold=track_activation_threshold,
+ lost_track_buffer=track_seconds * fps,
+ minimum_matching_threshold=minimum_matching_threshold,
+ frame_rate=fps,
+ )
+
+ ### video config
+ video_info = sv.VideoInfo.from_video_path(video_path=source_video_path)
+ frames_generator = sv.get_video_frames_generator(
+ source_path=source_video_path, stride=1
+ )
+
+ ### Detect, track, annotate, save
+ with sv.VideoSink(target_path=target_video_path, video_info=video_info) as sink:
+ for frame in frames_generator:
+ result = model(
+ source=frame,
+ classes=[0], # only person class
+ conf=confidence_threshold,
+ iou=iou_threshold,
+ # show_conf = True,
+ # save_txt = True,
+ # save_conf = True,
+ # save = True,
+ device=None, # use None = CPU, 0 = single GPU, or [0,1] = dual GPU
+ )[0]
+
+ detections = sv.Detections.from_ultralytics(result) # get detections
+
+ detections = byte_tracker.update_with_detections(
+ detections
+ ) # update tracker
+
+ ### draw heatmap
+ annotated_frame = heat_map_annotator.annotate(
+ scene=frame.copy(), detections=detections
+ )
+
+ ### draw other attributes from `detections` object
+ labels = [
+ f"#{tracker_id}"
+ for class_id, tracker_id in zip(
+ detections.class_id, detections.tracker_id
+ )
+ ]
+
+ label_annotator.annotate(
+ scene=annotated_frame, detections=detections, labels=labels
+ )
+
+ sink.write_frame(frame=annotated_frame)
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/speed_estimation/.gitignore b/examples/speed_estimation/.gitignore
new file mode 100644
index 0000000..34efd9e
--- /dev/null
+++ b/examples/speed_estimation/.gitignore
@@ -0,0 +1,9 @@
+data/
+venv*/
+*.pt
+*.pth
+*.mp4
+*.mov
+*.png
+*.jpg
+*.jpeg
diff --git a/examples/speed_estimation/README.md b/examples/speed_estimation/README.md
new file mode 100644
index 0000000..93e8d66
--- /dev/null
+++ b/examples/speed_estimation/README.md
@@ -0,0 +1,96 @@
+# speed estimation
+
+[](https://colab.research.google.com/github/roboflow-ai/notebooks/blob/main/notebooks/how-to-estimate-vehicle-speed-with-computer-vision.ipynb) [](https://youtu.be/uWP6UjDeZvY)
+
+## ๐ hello
+
+This example performs speed estimation analysis using various object-detection models and ByteTrack - a simple yet effective online multi-object tracking method. It uses the supervision package for multiple tasks such as tracking, annotations, etc.
+
+https://github.com/roboflow/supervision/assets/26109316/d50118c1-2ae4-458d-915a-5d860fd36f71
+
+> [!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 from our YouTube [tutorial](https://youtu.be/uWP6UjDeZvY).
+
+## ๐ป install
+
+- clone repository and navigate to example directory
+
+ ```bash
+ git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
+ cd supervision/examples/speed_estimation
+ ```
+
+- setup python environment and activate it [optional]
+
+ ```bash
+ uv venv
+ source .venv/bin/activate
+ ```
+
+- install required dependencies
+
+ ```bash
+ uv pip install -r requirements.txt
+ ```
+
+- download `vehicles.mp4` file
+
+ ```bash
+ python 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`.
+
+- `--model_id` (optional): Designates the Roboflow model ID to be used. The default 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.
+
+- `--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`: 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
+
+- yolo-nas
+
+ ```bash
+ python yolo_nas_example.py \
+ --source_video_path data/vehicles.mp4 \
+ --target_video_path data/vehicles-result.mp4 \
+ --confidence_threshold 0.3 \
+ --iou_threshold 0.5
+ ```
+
+- inference
+
+ ```bash
+ python inference_example.py \
+ --roboflow_api_key "ROBOFLOW_API_KEY" \
+ --source_video_path data/vehicles.mp4 \
+ --target_video_path data/vehicles-result.mp4 \
+ --confidence_threshold 0.3 \
+ --iou_threshold 0.5
+ ```
+
+- ultralytics
+
+ ```bash
+ python ultralytics_example.py \
+ --source_video_path data/vehicles.mp4 \
+ --target_video_path data/vehicles-result.mp4 \
+ --confidence_threshold 0.3 \
+ --iou_threshold 0.5
+ ```
+
+## ยฉ 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.
+
+- 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.
diff --git a/examples/speed_estimation/inference_example.py b/examples/speed_estimation/inference_example.py
new file mode 100644
index 0000000..21ec95b
--- /dev/null
+++ b/examples/speed_estimation/inference_example.py
@@ -0,0 +1,149 @@
+import os
+from collections import defaultdict, deque
+
+import cv2
+import numpy as np
+from inference.models.utils import get_roboflow_model
+
+import supervision as sv
+
+SOURCE = np.array([[1252, 787], [2298, 803], [5039, 2159], [-550, 2159]])
+
+TARGET_WIDTH = 25
+TARGET_HEIGHT = 250
+
+TARGET = np.array(
+ [
+ [0, 0],
+ [TARGET_WIDTH - 1, 0],
+ [TARGET_WIDTH - 1, TARGET_HEIGHT - 1],
+ [0, TARGET_HEIGHT - 1],
+ ]
+)
+
+
+class ViewTransformer:
+ def __init__(self, source: np.ndarray, target: np.ndarray) -> None:
+ source = source.astype(np.float32)
+ target = target.astype(np.float32)
+ self.m = cv2.getPerspectiveTransform(source, target)
+
+ def transform_points(self, points: np.ndarray) -> np.ndarray:
+ if points.size == 0:
+ return points
+
+ reshaped_points = points.reshape(-1, 1, 2).astype(np.float32)
+ transformed_points = cv2.perspectiveTransform(reshaped_points, self.m)
+ return transformed_points.reshape(-1, 2)
+
+
+def main(
+ source_video_path: str,
+ target_video_path: str,
+ model_id: str = "yolov8x-640",
+ roboflow_api_key: str | None = None,
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+) -> None:
+ """
+ Vehicle Speed Estimation using Inference and Supervision.
+
+ Args:
+ source_video_path: Path to the source video file
+ target_video_path: Path to the target video file (output)
+ model_id: Roboflow model ID
+ roboflow_api_key: Roboflow API KEY
+ confidence_threshold: Confidence threshold for the model
+ iou_threshold: IOU threshold for the model
+ """
+ api_key = roboflow_api_key
+ api_key = os.environ.get("ROBOFLOW_API_KEY", api_key)
+ if api_key is None:
+ raise ValueError(
+ "Roboflow API key is missing. Please provide it as an argument or set the "
+ "ROBOFLOW_API_KEY environment variable."
+ )
+ roboflow_api_key = api_key
+
+ video_info = sv.VideoInfo.from_video_path(video_path=source_video_path)
+ model = get_roboflow_model(model_id=model_id, api_key=roboflow_api_key)
+
+ byte_track = sv.ByteTrack(
+ frame_rate=video_info.fps, track_activation_threshold=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)
+ box_annotator = sv.BoxAnnotator(thickness=thickness)
+ label_annotator = sv.LabelAnnotator(
+ text_scale=text_scale,
+ text_thickness=thickness,
+ text_position=sv.Position.BOTTOM_CENTER,
+ )
+ trace_annotator = sv.TraceAnnotator(
+ thickness=thickness,
+ trace_length=int(video_info.fps * 2),
+ position=sv.Position.BOTTOM_CENTER,
+ )
+
+ frame_generator = sv.get_video_frames_generator(source_path=source_video_path)
+
+ polygon_zone = sv.PolygonZone(polygon=SOURCE)
+ view_transformer = ViewTransformer(source=SOURCE, target=TARGET)
+
+ coordinates = defaultdict(lambda: deque(maxlen=int(video_info.fps)))
+
+ with sv.VideoSink(target_video_path, video_info) as sink:
+ for frame in frame_generator:
+ results = model.infer(
+ frame, confidence=confidence_threshold, iou=iou_threshold
+ )[0]
+ detections = sv.Detections.from_inference(results)
+ detections = detections[polygon_zone.trigger(detections)]
+ detections = byte_track.update_with_detections(detections=detections)
+
+ points = detections.get_anchors_coordinates(
+ anchor=sv.Position.BOTTOM_CENTER
+ )
+ points = view_transformer.transform_points(points=points).astype(int)
+
+ for tracker_id, [_, y] in zip(detections.tracker_id, points):
+ coordinates[tracker_id].append(y)
+
+ labels = []
+ for tracker_id in detections.tracker_id:
+ if len(coordinates[tracker_id]) < video_info.fps / 2:
+ labels.append(f"#{tracker_id}")
+ else:
+ coordinate_start = coordinates[tracker_id][-1]
+ coordinate_end = coordinates[tracker_id][0]
+ distance = abs(coordinate_start - coordinate_end)
+ time = len(coordinates[tracker_id]) / video_info.fps
+ speed = distance / time * 3.6
+ labels.append(f"#{tracker_id} {int(speed)} km/h")
+
+ annotated_frame = frame.copy()
+ annotated_frame = trace_annotator.annotate(
+ scene=annotated_frame, detections=detections
+ )
+ annotated_frame = box_annotator.annotate(
+ scene=annotated_frame, detections=detections
+ )
+ annotated_frame = label_annotator.annotate(
+ scene=annotated_frame, detections=detections, labels=labels
+ )
+
+ sink.write_frame(annotated_frame)
+ cv2.imshow("frame", annotated_frame)
+ if cv2.waitKey(1) & 0xFF == ord("q"):
+ break
+ cv2.destroyAllWindows()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/speed_estimation/requirements.txt b/examples/speed_estimation/requirements.txt
new file mode 100644
index 0000000..e7d4977
--- /dev/null
+++ b/examples/speed_estimation/requirements.txt
@@ -0,0 +1,7 @@
+supervision
+tqdm
+requests
+ultralytics
+super-gradients==3.5.0
+inference
+jsonargparse[signatures]
diff --git a/examples/speed_estimation/ultralytics_example.py b/examples/speed_estimation/ultralytics_example.py
new file mode 100644
index 0000000..3678a55
--- /dev/null
+++ b/examples/speed_estimation/ultralytics_example.py
@@ -0,0 +1,133 @@
+from collections import defaultdict, deque
+
+import cv2
+import numpy as np
+from ultralytics import YOLO
+
+import supervision as sv
+
+SOURCE = np.array([[1252, 787], [2298, 803], [5039, 2159], [-550, 2159]])
+
+TARGET_WIDTH = 25
+TARGET_HEIGHT = 250
+
+TARGET = np.array(
+ [
+ [0, 0],
+ [TARGET_WIDTH - 1, 0],
+ [TARGET_WIDTH - 1, TARGET_HEIGHT - 1],
+ [0, TARGET_HEIGHT - 1],
+ ]
+)
+
+
+class ViewTransformer:
+ def __init__(self, source: np.ndarray, target: np.ndarray) -> None:
+ source = source.astype(np.float32)
+ target = target.astype(np.float32)
+ self.m = cv2.getPerspectiveTransform(source, target)
+
+ def transform_points(self, points: np.ndarray) -> np.ndarray:
+ if points.size == 0:
+ return points
+
+ reshaped_points = points.reshape(-1, 1, 2).astype(np.float32)
+ transformed_points = cv2.perspectiveTransform(reshaped_points, self.m)
+ return transformed_points.reshape(-1, 2)
+
+
+def main(
+ source_video_path: str,
+ target_video_path: str,
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+) -> None:
+ """
+ Vehicle Speed Estimation using Ultralytics and Supervision.
+
+ Args:
+ source_video_path: Path to the source video file
+ target_video_path: Path to the target video file (output)
+ confidence_threshold: Confidence threshold for the model
+ iou_threshold: IOU threshold for the model
+ """
+ video_info = sv.VideoInfo.from_video_path(video_path=source_video_path)
+ model = YOLO("yolo11x.pt")
+
+ byte_track = sv.ByteTrack(
+ frame_rate=video_info.fps, track_activation_threshold=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)
+ box_annotator = sv.BoxAnnotator(thickness=thickness)
+ label_annotator = sv.LabelAnnotator(
+ text_scale=text_scale,
+ text_thickness=thickness,
+ text_position=sv.Position.BOTTOM_CENTER,
+ )
+ trace_annotator = sv.TraceAnnotator(
+ thickness=thickness,
+ trace_length=int(video_info.fps * 2),
+ position=sv.Position.BOTTOM_CENTER,
+ )
+
+ frame_generator = sv.get_video_frames_generator(source_path=source_video_path)
+
+ polygon_zone = sv.PolygonZone(polygon=SOURCE)
+ view_transformer = ViewTransformer(source=SOURCE, target=TARGET)
+
+ coordinates = defaultdict(lambda: deque(maxlen=int(video_info.fps)))
+
+ with sv.VideoSink(target_video_path, video_info) as sink:
+ for frame in frame_generator:
+ result = model(frame, conf=confidence_threshold, iou=iou_threshold)[0]
+ detections = sv.Detections.from_ultralytics(result)
+ detections = detections[polygon_zone.trigger(detections)]
+ detections = byte_track.update_with_detections(detections=detections)
+
+ points = detections.get_anchors_coordinates(
+ anchor=sv.Position.BOTTOM_CENTER
+ )
+ points = view_transformer.transform_points(points=points).astype(int)
+
+ for tracker_id, [_, y] in zip(detections.tracker_id, points):
+ coordinates[tracker_id].append(y)
+
+ labels = []
+ for tracker_id in detections.tracker_id:
+ if len(coordinates[tracker_id]) < video_info.fps / 2:
+ labels.append(f"#{tracker_id}")
+ else:
+ coordinate_start = coordinates[tracker_id][-1]
+ coordinate_end = coordinates[tracker_id][0]
+ distance = abs(coordinate_start - coordinate_end)
+ time = len(coordinates[tracker_id]) / video_info.fps
+ speed = distance / time * 3.6
+ labels.append(f"#{tracker_id} {int(speed)} km/h")
+
+ annotated_frame = frame.copy()
+ annotated_frame = trace_annotator.annotate(
+ scene=annotated_frame, detections=detections
+ )
+ annotated_frame = box_annotator.annotate(
+ scene=annotated_frame, detections=detections
+ )
+ annotated_frame = label_annotator.annotate(
+ scene=annotated_frame, detections=detections, labels=labels
+ )
+
+ sink.write_frame(annotated_frame)
+ cv2.imshow("frame", annotated_frame)
+ if cv2.waitKey(1) & 0xFF == ord("q"):
+ break
+ cv2.destroyAllWindows()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/speed_estimation/video_downloader.py b/examples/speed_estimation/video_downloader.py
new file mode 100644
index 0000000..4272120
--- /dev/null
+++ b/examples/speed_estimation/video_downloader.py
@@ -0,0 +1,8 @@
+import os
+
+from supervision.assets import VideoAssets, download_assets
+
+if not os.path.exists("data"):
+ os.makedirs("data")
+os.chdir("data")
+download_assets(VideoAssets.VEHICLES)
diff --git a/examples/speed_estimation/yolo_nas_example.py b/examples/speed_estimation/yolo_nas_example.py
new file mode 100644
index 0000000..0100e38
--- /dev/null
+++ b/examples/speed_estimation/yolo_nas_example.py
@@ -0,0 +1,136 @@
+from collections import defaultdict, deque
+
+import cv2
+import numpy as np
+from super_gradients.common.object_names import Models
+from super_gradients.training import models
+
+import supervision as sv
+
+SOURCE = np.array([[1252, 787], [2298, 803], [5039, 2159], [-550, 2159]])
+
+TARGET_WIDTH = 25
+TARGET_HEIGHT = 250
+
+TARGET = np.array(
+ [
+ [0, 0],
+ [TARGET_WIDTH - 1, 0],
+ [TARGET_WIDTH - 1, TARGET_HEIGHT - 1],
+ [0, TARGET_HEIGHT - 1],
+ ]
+)
+
+
+class ViewTransformer:
+ def __init__(self, source: np.ndarray, target: np.ndarray) -> None:
+ source = source.astype(np.float32)
+ target = target.astype(np.float32)
+ self.m = cv2.getPerspectiveTransform(source, target)
+
+ def transform_points(self, points: np.ndarray) -> np.ndarray:
+ if points.size == 0:
+ return points
+
+ reshaped_points = points.reshape(-1, 1, 2).astype(np.float32)
+ transformed_points = cv2.perspectiveTransform(reshaped_points, self.m)
+ return transformed_points.reshape(-1, 2)
+
+
+def main(
+ source_video_path: str,
+ target_video_path: str,
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+) -> None:
+ """
+ Vehicle Speed Estimation using YOLO-NAS and Supervision.
+
+ Args:
+ source_video_path: Path to the source video file
+ target_video_path: Path to the target video file (output)
+ confidence_threshold: Confidence threshold for the model
+ iou_threshold: IOU threshold for the model
+ """
+ video_info = sv.VideoInfo.from_video_path(video_path=source_video_path)
+ model = models.get(Models.YOLO_NAS_L, pretrained_weights="coco")
+
+ byte_track = sv.ByteTrack(
+ frame_rate=video_info.fps, track_activation_threshold=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)
+ box_annotator = sv.BoxAnnotator(thickness=thickness)
+ label_annotator = sv.LabelAnnotator(
+ text_scale=text_scale,
+ text_thickness=thickness,
+ text_position=sv.Position.BOTTOM_CENTER,
+ )
+ trace_annotator = sv.TraceAnnotator(
+ thickness=thickness,
+ trace_length=int(video_info.fps * 2),
+ position=sv.Position.BOTTOM_CENTER,
+ )
+
+ frame_generator = sv.get_video_frames_generator(source_path=source_video_path)
+
+ polygon_zone = sv.PolygonZone(polygon=SOURCE)
+ view_transformer = ViewTransformer(source=SOURCE, target=TARGET)
+
+ coordinates = defaultdict(lambda: deque(maxlen=int(video_info.fps)))
+
+ with sv.VideoSink(target_video_path, video_info) as sink:
+ for frame in frame_generator:
+ result = model.predict(frame, conf=confidence_threshold, iou=iou_threshold)[
+ 0
+ ]
+ detections = sv.Detections.from_yolo_nas(result)
+ detections = detections[polygon_zone.trigger(detections)]
+ detections = byte_track.update_with_detections(detections=detections)
+
+ points = detections.get_anchors_coordinates(
+ anchor=sv.Position.BOTTOM_CENTER
+ )
+ points = view_transformer.transform_points(points=points).astype(int)
+
+ for tracker_id, [_, y] in zip(detections.tracker_id, points):
+ coordinates[tracker_id].append(y)
+
+ labels = []
+ for tracker_id in detections.tracker_id:
+ if len(coordinates[tracker_id]) < video_info.fps / 2:
+ labels.append(f"#{tracker_id}")
+ else:
+ coordinate_start = coordinates[tracker_id][-1]
+ coordinate_end = coordinates[tracker_id][0]
+ distance = abs(coordinate_start - coordinate_end)
+ time = len(coordinates[tracker_id]) / video_info.fps
+ speed = distance / time * 3.6
+ labels.append(f"#{tracker_id} {int(speed)} km/h")
+
+ annotated_frame = frame.copy()
+ annotated_frame = trace_annotator.annotate(
+ scene=annotated_frame, detections=detections
+ )
+ annotated_frame = box_annotator.annotate(
+ scene=annotated_frame, detections=detections
+ )
+ annotated_frame = label_annotator.annotate(
+ scene=annotated_frame, detections=detections, labels=labels
+ )
+
+ sink.write_frame(annotated_frame)
+ cv2.imshow("frame", annotated_frame)
+ if cv2.waitKey(1) & 0xFF == ord("q"):
+ break
+ cv2.destroyAllWindows()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/time_in_zone/.gitignore b/examples/time_in_zone/.gitignore
new file mode 100644
index 0000000..34efd9e
--- /dev/null
+++ b/examples/time_in_zone/.gitignore
@@ -0,0 +1,9 @@
+data/
+venv*/
+*.pt
+*.pth
+*.mp4
+*.mov
+*.png
+*.jpg
+*.jpeg
diff --git a/examples/time_in_zone/README.md b/examples/time_in_zone/README.md
new file mode 100644
index 0000000..d27e3dd
--- /dev/null
+++ b/examples/time_in_zone/README.md
@@ -0,0 +1,321 @@
+# time in zone
+
+[](https://www.youtube.com/watch?v=hAWpsIuem10)
+
+## ๐ hello
+
+Practical demonstration on leveraging computer vision for analyzing wait times and monitoring the duration that objects or individuals spend in predefined areas of video frames. This example project, perfect for retail analytics or traffic management applications.
+
+https://github.com/roboflow/supervision/assets/26109316/d051cc8a-dd15-41d4-aa36-d38b86334c39
+
+## ๐ป install
+
+- clone repository and navigate to example directory
+
+ ```bash
+ git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
+ cd supervision/examples/time_in_zone
+ ```
+
+- setup python environment and activate it [optional]
+
+ ```bash
+ uv venv
+ source .venv/bin/activate
+ ```
+
+- install required dependencies
+
+ ```bash
+ uv pip install -r requirements.txt
+ ```
+
+## ๐ scripts
+
+### `download_from_youtube`
+
+This script allows you to download a video from YouTube.
+
+- `--url`: The full URL of the YouTube video you wish to download.
+- `--output_path` (optional): Specifies the directory where the video will be saved.
+- `--file_name` (optional): Sets the name of the saved video file.
+
+```bash
+python scripts/download_from_youtube.py \
+ --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"
+```
+
+### `stream_from_file`
+
+This script allows you to stream video files from a directory. It's an awesome way to mock a live video stream for local testing. Video will be streamed in a loop under `rtsp://localhost:8554/live0.stream` URL. This script requires docker to be installed.
+
+- `--video_directory`: Directory containing video files to stream.
+- `--number_of_streams`: Number of video files to stream.
+
+```bash
+python scripts/stream_from_file.py \
+ --video_directory "data/checkout" \
+ --number_of_streams 1
+```
+
+```bash
+python scripts/stream_from_file.py \
+ --video_directory "data/traffic" \
+ --number_of_streams 1
+```
+
+### `draw_zones`
+
+If you want to test zone time in zone analysis on your own video, you can use this script to design custom zones and save results as a JSON file. The script will open a window where you can draw polygons on the source image or video file. The polygons will 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"
+```
+
+```bash
+python scripts/draw_zones.py \
+ --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
+
+## ๐ฌ video & stream processing
+
+### `inference_file_example`
+
+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`.
+
+```bash
+python inference_file_example.py \
+ --zone_configuration_path "data/checkout/config.json" \
+ --source_video_path "data/checkout/video.mp4" \
+ --model_id "rfdetr-medium" \
+ --classes "[0]" \
+ --confidence_threshold 0.3 \
+ --iou_threshold 0.7 \
+ --roboflow_api_key "ROBOFLOWS_API_KEY"
+```
+
+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 "rfdetr-medium" \
+ --classes "[2, 5, 6, 7]" \
+ --confidence_threshold 0.3 \
+ --iou_threshold 0.7 \
+ --roboflow_api_key "ROBOFLOWS_API_KEY"
+```
+
+https://github.com/roboflow/supervision/assets/26109316/5ec896d7-4b39-4426-8979-11e71666878b
+
+### `inference_stream_example`
+
+Script to run object detection on an RTSP stream using 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`.
+
+```bash
+python inference_stream_example.py \
+ --zone_configuration_path "data/checkout/config.json" \
+ --rtsp_url "rtsp://localhost:8554/live0.stream" \
+ --model_id "rfdetr-medium" \
+ --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 "rfdetr-medium" \
+ --classes "[2, 5, 6, 7]" \
+ --confidence_threshold 0.3 \
+ --iou_threshold 0.7
+```
+
+### `rfdeter_file_example`
+
+Script to run object detection on a video file using the RF-DETR model.
+
+- `--zone_configuration_path`: Path to the zone configuration JSON file.
+- `--source_video_path`: Path to the source video file.
+- `--model_size`: Size of RF-DETR model ('nano', 'small', 'medium', 'base' or 'large'). Default is 'medium'.
+- `--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`.
+- `--resolution`: Resolution for the model input. Default is `640`.
+
+```bash
+python rfdetr_file_example.py \
+ --zone_configuration_path "data/checkout/config.json" \
+ --source_video_path "data/checkout/video.mp4" \
+ --model_size "medium" \
+ --device="cpu" \
+ --classes "[1]" \
+ --confidence_threshold 0.3 \
+ --iou_threshold 0.7 \
+ --resolution 640
+```
+
+```bash
+python rfdetr_file_example.py \
+ --zone_configuration_path "data/traffic/config.json" \
+ --source_video_path "data/traffic/video.mp4" \
+ --model_size "medium" \
+ --device="cpu" \
+ --classes "[3, 6, 7, 8]" \
+ --confidence_threshold 0.3 \
+ --iou_threshold 0.7 \
+ --resolution 640
+```
+
+### `rfdeter_stream_example`
+
+Script to run object detection on an RTSP stream using the RF-DETR model.
+
+- `--zone_configuration_path`: Path to the zone-configuration JSON file defining the polygons.
+- `--rtsp_url`: Complete RTSP URL of the live video stream.
+- `--model_size`: RF-DETR backbone size to load โ choose from 'nano', 'small', 'medium', 'base', or 'large' (default 'medium').
+- `--device`: Compute device to run the model on ('cpu', 'mps', or 'cuda'; default 'cpu').
+- `--classes`: Space-separated list of class IDs to track. Leave empty to track all classes.
+- `--confidence_threshold`: Minimum confidence score for a detection to be kept, range 0-1 (default 0.3).
+- `--iou_threshold`: IOU threshold applied during non-max suppression (default 0.7).
+- `--resolution`: Shortest-side input resolution supplied to the model. The script will round it to the nearest valid multiple (default 640).
+
+```bash
+python rfdetr_stream_example.py \
+ --zone_configuration_path "data/checkout/config.json" \
+ --rtsp_url "rtsp://localhost:8554/live0.stream" \
+ --model_size "medium" \
+ --device "cpu" \
+ --classes "[1]" \
+ --confidence_threshold 0.3 \
+ --iou_threshold 0.7 \
+ --resolution 640
+```
+
+```bash
+python rfdetr_stream_example.py \
+ --zone_configuration_path "data/traffic/config.json" \
+ --rtsp_url "rtsp://localhost:8554/live0.stream" \
+ --model_size "medium" \
+ --device "cpu" \
+ --classes "[3, 6, 7, 8]" \
+ --confidence_threshold 0.3 \
+ --iou_threshold 0.7 \
+ --resolution 640
+```
+
+### `ultralytics_file_example`
+
+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`.
+
+```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
+```
+
+```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
+```
+
+### `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`.
+
+```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
+```
+
+```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
+```
+
+
+
+## ยฉ 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.
+
+- 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.
diff --git a/examples/time_in_zone/inference_file_example.py b/examples/time_in_zone/inference_file_example.py
new file mode 100644
index 0000000..59f3b4c
--- /dev/null
+++ b/examples/time_in_zone/inference_file_example.py
@@ -0,0 +1,97 @@
+import cv2
+import numpy as np
+from inference import get_model
+from utils.general import find_in_list, load_zones_config
+from utils.timers import FPSBasedTimer
+
+import supervision as sv
+
+COLORS = sv.ColorPalette.from_hex(["#E6194B", "#3CB44B", "#FFE119", "#3C76D1"])
+COLOR_ANNOTATOR = sv.ColorAnnotator(color=COLORS)
+LABEL_ANNOTATOR = sv.LabelAnnotator(
+ color=COLORS, text_color=sv.Color.from_hex("#000000")
+)
+
+
+def main(
+ zone_configuration_path: str,
+ source_video_path: str,
+ model_id: str = "rfdetr-medium",
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+ classes: list[int] = [],
+ roboflow_api_key: str = "",
+) -> None:
+ """
+ Calculating detections dwell time in zones, using video file.
+
+ Args:
+ zone_configuration_path: Path to the zone configuration JSON file
+ source_video_path: Path to the source video file
+ model_id: Roboflow model ID
+ confidence_threshold: Confidence level for detections (0 to 1)
+ iou_threshold: IOU threshold for non-max suppression
+ classes: List of class IDs to track. If empty, all classes are tracked
+ roboflow_api_key: Roboflow API key for accessing private models
+ """
+ model = get_model(model_id=model_id, api_key=roboflow_api_key)
+ tracker = sv.ByteTrack(minimum_matching_threshold=0.5)
+ video_info = sv.VideoInfo.from_video_path(video_path=source_video_path)
+ frames_generator = sv.get_video_frames_generator(source_video_path)
+
+ polygons = load_zones_config(file_path=zone_configuration_path)
+ zones = [
+ sv.PolygonZone(
+ polygon=polygon,
+ triggering_anchors=(sv.Position.CENTER,),
+ )
+ for polygon in polygons
+ ]
+ timers = [FPSBasedTimer(video_info.fps) for _ in zones]
+
+ for frame in frames_generator:
+ results = model.infer(
+ frame, confidence=confidence_threshold, iou_threshold=iou_threshold
+ )[0]
+ detections = sv.Detections.from_inference(results)
+ detections = detections[find_in_list(detections.class_id, classes)]
+ detections = tracker.update_with_detections(detections)
+
+ annotated_frame = frame.copy()
+
+ for idx, zone in enumerate(zones):
+ annotated_frame = sv.draw_polygon(
+ scene=annotated_frame, polygon=zone.polygon, color=COLORS.by_idx(idx)
+ )
+
+ detections_in_zone = detections[zone.trigger(detections)]
+ time_in_zone = timers[idx].tick(detections_in_zone)
+ custom_color_lookup = np.full(detections_in_zone.class_id.shape, idx)
+
+ annotated_frame = COLOR_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ custom_color_lookup=custom_color_lookup,
+ )
+ labels = [
+ f"#{tracker_id} {int(time // 60):02d}:{int(time % 60):02d}"
+ for tracker_id, time in zip(detections_in_zone.tracker_id, time_in_zone)
+ ]
+ annotated_frame = LABEL_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ labels=labels,
+ custom_color_lookup=custom_color_lookup,
+ )
+
+ cv2.imshow("Processed Video", annotated_frame)
+ if cv2.waitKey(1) & 0xFF == ord("q"):
+ break
+ cv2.destroyAllWindows()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/time_in_zone/inference_naive_stream_example.py b/examples/time_in_zone/inference_naive_stream_example.py
new file mode 100644
index 0000000..30f77ee
--- /dev/null
+++ b/examples/time_in_zone/inference_naive_stream_example.py
@@ -0,0 +1,107 @@
+import cv2
+import numpy as np
+from inference import get_model
+from utils.general import find_in_list, get_stream_frames_generator, load_zones_config
+from utils.timers import ClockBasedTimer
+
+import supervision as sv
+
+COLORS = sv.ColorPalette.from_hex(["#E6194B", "#3CB44B", "#FFE119", "#3C76D1"])
+COLOR_ANNOTATOR = sv.ColorAnnotator(color=COLORS)
+LABEL_ANNOTATOR = sv.LabelAnnotator(
+ color=COLORS, text_color=sv.Color.from_hex("#000000")
+)
+
+
+def main(
+ zone_configuration_path: str,
+ rtsp_url: str,
+ model_id: str = "rfdetr-medium",
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+ classes: list[int] = [],
+ roboflow_api_key: str = "",
+) -> None:
+ """
+ Calculating detections dwell time in zones, using RTSP stream.
+
+ Args:
+ zone_configuration_path: Path to the zone configuration JSON file
+ rtsp_url: Complete RTSP URL for the video stream
+ model_id: Roboflow model ID
+ confidence_threshold: Confidence level for detections (0 to 1)
+ iou_threshold: IOU threshold for non-max suppression
+ classes: List of class IDs to track. If empty, all classes are tracked
+ roboflow_api_key: Roboflow API key for accessing private models
+ """
+ model = get_model(model_id=model_id, api_key=roboflow_api_key)
+ tracker = sv.ByteTrack(minimum_matching_threshold=0.5)
+ frames_generator = get_stream_frames_generator(rtsp_url=rtsp_url)
+ fps_monitor = sv.FPSMonitor()
+
+ polygons = load_zones_config(file_path=zone_configuration_path)
+ zones = [
+ sv.PolygonZone(
+ polygon=polygon,
+ triggering_anchors=(sv.Position.CENTER,),
+ )
+ for polygon in polygons
+ ]
+ timers = [ClockBasedTimer() for _ in zones]
+
+ for frame in frames_generator:
+ fps_monitor.tick()
+ fps = fps_monitor.fps
+
+ results = model.infer(
+ frame, confidence=confidence_threshold, iou_threshold=iou_threshold
+ )[0]
+ detections = sv.Detections.from_inference(results)
+ detections = detections[find_in_list(detections.class_id, classes)]
+ detections = tracker.update_with_detections(detections)
+
+ annotated_frame = frame.copy()
+ annotated_frame = sv.draw_text(
+ scene=annotated_frame,
+ text=f"{fps:.1f}",
+ text_anchor=sv.Point(40, 30),
+ background_color=sv.Color.from_hex("#A351FB"),
+ text_color=sv.Color.from_hex("#000000"),
+ )
+
+ for idx, zone in enumerate(zones):
+ annotated_frame = sv.draw_polygon(
+ scene=annotated_frame, polygon=zone.polygon, color=COLORS.by_idx(idx)
+ )
+
+ detections_in_zone = detections[zone.trigger(detections)]
+ time_in_zone = timers[idx].tick(detections_in_zone)
+ custom_color_lookup = np.full(detections_in_zone.class_id.shape, idx)
+
+ annotated_frame = COLOR_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ custom_color_lookup=custom_color_lookup,
+ )
+ labels = [
+ f"#{tracker_id} {int(time // 60):02d}:{int(time % 60):02d}"
+ for tracker_id, time in zip(detections_in_zone.tracker_id, time_in_zone)
+ ]
+ annotated_frame = LABEL_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ labels=labels,
+ custom_color_lookup=custom_color_lookup,
+ )
+
+ cv2.imshow("Processed Video", annotated_frame)
+ if cv2.waitKey(1) & 0xFF == ord("q"):
+ break
+ cv2.destroyAllWindows()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/time_in_zone/inference_stream_example.py b/examples/time_in_zone/inference_stream_example.py
new file mode 100644
index 0000000..e88a55f
--- /dev/null
+++ b/examples/time_in_zone/inference_stream_example.py
@@ -0,0 +1,121 @@
+import cv2
+import numpy as np
+from inference import InferencePipeline
+from inference.core.interfaces.camera.entities import VideoFrame
+from utils.general import find_in_list, load_zones_config
+from utils.timers import ClockBasedTimer
+
+import supervision as sv
+
+COLORS = sv.ColorPalette.from_hex(["#E6194B", "#3CB44B", "#FFE119", "#3C76D1"])
+COLOR_ANNOTATOR = sv.ColorAnnotator(color=COLORS)
+LABEL_ANNOTATOR = sv.LabelAnnotator(
+ color=COLORS, text_color=sv.Color.from_hex("#000000")
+)
+
+
+class CustomSink:
+ def __init__(self, zone_configuration_path: str, classes: list[int]) -> None:
+ self.classes = classes
+ self.tracker = sv.ByteTrack(minimum_matching_threshold=0.5)
+ self.fps_monitor = sv.FPSMonitor()
+ self.polygons = load_zones_config(file_path=zone_configuration_path)
+ self.timers = [ClockBasedTimer() for _ in self.polygons]
+ self.zones = [
+ sv.PolygonZone(
+ polygon=polygon,
+ triggering_anchors=(sv.Position.CENTER,),
+ )
+ for polygon in self.polygons
+ ]
+
+ def on_prediction(self, result: dict, frame: VideoFrame) -> None:
+ self.fps_monitor.tick()
+ fps = self.fps_monitor.fps
+
+ detections = sv.Detections.from_inference(result)
+ detections = detections[find_in_list(detections.class_id, self.classes)]
+ detections = self.tracker.update_with_detections(detections)
+
+ annotated_frame = frame.image.copy()
+ annotated_frame = sv.draw_text(
+ scene=annotated_frame,
+ text=f"{fps:.1f}",
+ text_anchor=sv.Point(40, 30),
+ background_color=sv.Color.from_hex("#A351FB"),
+ text_color=sv.Color.from_hex("#000000"),
+ )
+
+ for idx, zone in enumerate(self.zones):
+ annotated_frame = sv.draw_polygon(
+ scene=annotated_frame, polygon=zone.polygon, color=COLORS.by_idx(idx)
+ )
+
+ detections_in_zone = detections[zone.trigger(detections)]
+ time_in_zone = self.timers[idx].tick(detections_in_zone)
+ custom_color_lookup = np.full(detections_in_zone.class_id.shape, idx)
+
+ annotated_frame = COLOR_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ custom_color_lookup=custom_color_lookup,
+ )
+ labels = [
+ f"#{tracker_id} {int(time // 60):02d}:{int(time % 60):02d}"
+ for tracker_id, time in zip(detections_in_zone.tracker_id, time_in_zone)
+ ]
+ annotated_frame = LABEL_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ labels=labels,
+ custom_color_lookup=custom_color_lookup,
+ )
+ cv2.imshow("Processed Video", annotated_frame)
+ cv2.waitKey(1)
+
+
+def main(
+ zone_configuration_path: str,
+ rtsp_url: str,
+ model_id: str = "rfdetr-medium",
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+ classes: list[int] = [],
+ roboflow_api_key: str = "",
+) -> None:
+ """
+ Calculating detections dwell time in zones, using RTSP stream.
+
+ Args:
+ zone_configuration_path: Path to the zone configuration JSON file
+ rtsp_url: Complete RTSP URL for the video stream
+ model_id: Roboflow model ID
+ confidence_threshold: Confidence level for detections (0 to 1)
+ iou_threshold: IOU threshold for non-max suppression
+ classes: List of class IDs to track. If empty, all classes are tracked
+ roboflow_api_key: Roboflow API key for accessing private models
+ """
+ sink = CustomSink(zone_configuration_path=zone_configuration_path, classes=classes)
+
+ pipeline = InferencePipeline.init(
+ model_id=model_id,
+ video_reference=rtsp_url,
+ on_prediction=sink.on_prediction,
+ confidence=confidence_threshold,
+ iou_threshold=iou_threshold,
+ api_key=roboflow_api_key,
+ )
+
+ pipeline.start()
+
+ try:
+ pipeline.join()
+ except KeyboardInterrupt:
+ pipeline.terminate()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/time_in_zone/requirements.txt b/examples/time_in_zone/requirements.txt
new file mode 100644
index 0000000..b4deae9
--- /dev/null
+++ b/examples/time_in_zone/requirements.txt
@@ -0,0 +1,9 @@
+supervision
+ultralytics
+inference
+# https://github.com/pytube/pytube/issues/2044
+# pytube
+pytubefix
+jsonargparse[signatures]
+rfdetr
+yt_dlp
diff --git a/examples/time_in_zone/rfdetr_file_example.py b/examples/time_in_zone/rfdetr_file_example.py
new file mode 100644
index 0000000..2c9e4aa
--- /dev/null
+++ b/examples/time_in_zone/rfdetr_file_example.py
@@ -0,0 +1,174 @@
+from __future__ import annotations
+
+from enum import Enum
+
+import cv2
+import numpy as np
+from rfdetr import RFDETRBase, RFDETRLarge, RFDETRMedium, RFDETRNano, RFDETRSmall
+from utils.general import find_in_list, load_zones_config
+from utils.timers import FPSBasedTimer
+
+import supervision as sv
+
+COLORS = sv.ColorPalette.from_hex(["#E6194B", "#3CB44B", "#FFE119", "#3C76D1"])
+COLOR_ANNOTATOR = sv.ColorAnnotator(color=COLORS)
+LABEL_ANNOTATOR = sv.LabelAnnotator(
+ color=COLORS, text_color=sv.Color.from_hex("#000000")
+)
+
+
+class ModelSize(Enum):
+ NANO = "nano"
+ SMALL = "small"
+ MEDIUM = "medium"
+ BASE = "base"
+ LARGE = "large"
+
+ @classmethod
+ def list(cls) -> list[str]:
+ return list(map(lambda c: c.value, cls))
+
+ @classmethod
+ def from_value(cls, value: ModelSize | str) -> ModelSize:
+ 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."
+ )
+
+
+def load_model(
+ checkpoint: ModelSize | str, device: str, resolution: int
+) -> RFDETRBase | RFDETRLarge | RFDETRMedium | RFDETRNano | RFDETRSmall:
+ checkpoint = ModelSize.from_value(checkpoint)
+
+ if checkpoint == ModelSize.NANO:
+ return RFDETRNano(device=device, resolution=resolution)
+ if checkpoint == ModelSize.SMALL:
+ return RFDETRSmall(device=device, resolution=resolution)
+ if checkpoint == ModelSize.MEDIUM:
+ return RFDETRMedium(device=device, resolution=resolution)
+ if checkpoint == ModelSize.BASE:
+ return RFDETRBase(device=device, resolution=resolution)
+ if checkpoint == ModelSize.LARGE:
+ return RFDETRLarge(device=device, resolution=resolution)
+
+ raise ValueError(
+ f"Invalid checkpoint: {checkpoint}. Must be one of: {ModelSize.list()}."
+ )
+
+
+def adjust_resolution(checkpoint: ModelSize | str, resolution: int) -> int:
+ checkpoint = ModelSize.from_value(checkpoint)
+
+ if checkpoint in {ModelSize.NANO, ModelSize.SMALL, ModelSize.MEDIUM}:
+ divisor = 32
+ elif checkpoint in {ModelSize.BASE, ModelSize.LARGE}:
+ divisor = 56
+ else:
+ raise ValueError(
+ f"Unknown checkpoint: {checkpoint}. Must be one of: {ModelSize.list()}."
+ )
+
+ remainder = resolution % divisor
+ if remainder == 0:
+ return resolution
+ lower = resolution - remainder
+ upper = lower + divisor
+
+ if resolution - lower < upper - resolution:
+ return lower
+ else:
+ return upper
+
+
+def main(
+ source_video_path: str,
+ zone_configuration_path: str,
+ resolution: int,
+ model_size: str = "small",
+ device: str = "cpu",
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+ classes: list[int] = [],
+) -> None:
+ """
+ Calculating detections dwell time in zones, using video file.
+
+ Args:
+ source_video_path: Path to the source video file
+ zone_configuration_path: Path to the zone configuration JSON file
+ resolution: Input resolution for the model
+ model_size: RF-DETR model size ('nano', 'small', 'medium', 'base' or 'large')
+ device: Computation device ('cpu', 'mps' or 'cuda')
+ confidence_threshold: Confidence level for detections (0 to 1)
+ iou_threshold: IOU threshold for non-max suppression
+ classes: List of class IDs to track. If empty, all classes are tracked
+ """
+ resolution = adjust_resolution(checkpoint=model_size, resolution=resolution)
+ model = load_model(checkpoint=model_size, device=device, resolution=resolution)
+ tracker = sv.ByteTrack(minimum_matching_threshold=0.5)
+ video_info = sv.VideoInfo.from_video_path(video_path=source_video_path)
+ frames_generator = sv.get_video_frames_generator(source_video_path)
+
+ polygons = load_zones_config(file_path=zone_configuration_path)
+ zones = [
+ sv.PolygonZone(
+ polygon=polygon,
+ triggering_anchors=(sv.Position.CENTER,),
+ )
+ for polygon in polygons
+ ]
+ timers = [FPSBasedTimer(video_info.fps) for _ in zones]
+
+ for frame in frames_generator:
+ detections = model.predict(frame, threshold=confidence_threshold)
+ detections = detections[find_in_list(detections.class_id, classes)]
+ detections = detections.with_nms(threshold=iou_threshold)
+ detections = tracker.update_with_detections(detections)
+
+ annotated_frame = frame.copy()
+
+ for idx, zone in enumerate(zones):
+ annotated_frame = sv.draw_polygon(
+ scene=annotated_frame, polygon=zone.polygon, color=COLORS.by_idx(idx)
+ )
+
+ detections_in_zone = detections[zone.trigger(detections)]
+ time_in_zone = timers[idx].tick(detections_in_zone)
+ custom_color_lookup = np.full(detections_in_zone.class_id.shape, idx)
+
+ annotated_frame = COLOR_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ custom_color_lookup=custom_color_lookup,
+ )
+ labels = [
+ f"#{tracker_id} {int(time // 60):02d}:{int(time % 60):02d}"
+ for tracker_id, time in zip(detections_in_zone.tracker_id, time_in_zone)
+ ]
+ annotated_frame = LABEL_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ labels=labels,
+ custom_color_lookup=custom_color_lookup,
+ )
+
+ cv2.imshow("Processed Video", annotated_frame)
+ if cv2.waitKey(1) & 0xFF == ord("q"):
+ break
+ cv2.destroyAllWindows()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/time_in_zone/rfdetr_naive_stream_example.py b/examples/time_in_zone/rfdetr_naive_stream_example.py
new file mode 100644
index 0000000..b517277
--- /dev/null
+++ b/examples/time_in_zone/rfdetr_naive_stream_example.py
@@ -0,0 +1,185 @@
+from __future__ import annotations
+
+from enum import Enum
+
+import cv2
+import numpy as np
+from rfdetr import RFDETRBase, RFDETRLarge, RFDETRMedium, RFDETRNano, RFDETRSmall
+from utils.general import find_in_list, get_stream_frames_generator, load_zones_config
+from utils.timers import ClockBasedTimer
+
+import supervision as sv
+
+COLORS = sv.ColorPalette.from_hex(["#E6194B", "#3CB44B", "#FFE119", "#3C76D1"])
+COLOR_ANNOTATOR = sv.ColorAnnotator(color=COLORS)
+LABEL_ANNOTATOR = sv.LabelAnnotator(
+ color=COLORS, text_color=sv.Color.from_hex("#000000")
+)
+
+
+class ModelSize(Enum):
+ NANO = "nano"
+ SMALL = "small"
+ MEDIUM = "medium"
+ BASE = "base"
+ LARGE = "large"
+
+ @classmethod
+ def list(cls) -> list[str]:
+ return list(map(lambda c: c.value, cls))
+
+ @classmethod
+ def from_value(cls, value: ModelSize | str) -> ModelSize:
+ 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."
+ )
+
+
+def load_model(
+ checkpoint: ModelSize | str, device: str, resolution: int
+) -> RFDETRBase | RFDETRLarge | RFDETRMedium | RFDETRNano | RFDETRSmall:
+ checkpoint = ModelSize.from_value(checkpoint)
+
+ if checkpoint == ModelSize.NANO:
+ return RFDETRNano(device=device, resolution=resolution)
+ if checkpoint == ModelSize.SMALL:
+ return RFDETRSmall(device=device, resolution=resolution)
+ if checkpoint == ModelSize.MEDIUM:
+ return RFDETRMedium(device=device, resolution=resolution)
+ if checkpoint == ModelSize.BASE:
+ return RFDETRBase(device=device, resolution=resolution)
+ if checkpoint == ModelSize.LARGE:
+ return RFDETRLarge(device=device, resolution=resolution)
+
+ raise ValueError(
+ f"Invalid checkpoint: {checkpoint}. Must be one of: {ModelSize.list()}."
+ )
+
+
+def adjust_resolution(checkpoint: ModelSize | str, resolution: int) -> int:
+ checkpoint = ModelSize.from_value(checkpoint)
+
+ if checkpoint in {ModelSize.NANO, ModelSize.SMALL, ModelSize.MEDIUM}:
+ divisor = 32
+ elif checkpoint in {ModelSize.BASE, ModelSize.LARGE}:
+ divisor = 56
+ else:
+ raise ValueError(
+ f"Unknown checkpoint: {checkpoint}. Must be one of: {ModelSize.list()}."
+ )
+
+ remainder = resolution % divisor
+ if remainder == 0:
+ return resolution
+ lower = resolution - remainder
+ upper = lower + divisor
+
+ if resolution - lower < upper - resolution:
+ return lower
+ else:
+ return upper
+
+
+def main(
+ rtsp_url: str,
+ zone_configuration_path: str,
+ resolution: int,
+ model_size: str = "small",
+ device: str = "cpu",
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+ classes: list[int] = [],
+) -> None:
+ """
+ Calculating detections dwell time in zones, using RTSP stream.
+
+ Args:
+ rtsp_url: Complete RTSP URL for the video stream
+ zone_configuration_path: Path to the zone configuration JSON file
+ resolution: Input resolution for the model
+ model_size: RF-DETR model size ('nano', 'small', 'medium', 'base' or 'large')
+ device: Computation device ('cpu', 'mps' or 'cuda')
+ confidence_threshold: Confidence level for detections (0 to 1)
+ iou_threshold: IOU threshold for non-max suppression
+ classes: List of class IDs to track. If empty, all classes are tracked
+ """
+ resolution = adjust_resolution(checkpoint=model_size, resolution=resolution)
+ model = load_model(checkpoint=model_size, device=device, resolution=resolution)
+ tracker = sv.ByteTrack(minimum_matching_threshold=0.5)
+ frames_generator = get_stream_frames_generator(rtsp_url=rtsp_url)
+ fps_monitor = sv.FPSMonitor()
+
+ polygons = load_zones_config(file_path=zone_configuration_path)
+ zones = [
+ sv.PolygonZone(
+ polygon=polygon,
+ triggering_anchors=(sv.Position.CENTER,),
+ )
+ for polygon in polygons
+ ]
+ timers = [ClockBasedTimer() for _ in zones]
+
+ for frame in frames_generator:
+ fps_monitor.tick()
+ fps = fps_monitor.fps
+
+ detections = model.predict(frame, threshold=confidence_threshold)
+ detections = detections[find_in_list(detections.class_id, classes)]
+ detections = detections.with_nms(threshold=iou_threshold)
+ detections = tracker.update_with_detections(detections)
+
+ annotated_frame = frame.copy()
+ annotated_frame = sv.draw_text(
+ scene=annotated_frame,
+ text=f"{fps:.1f}",
+ text_anchor=sv.Point(40, 30),
+ background_color=sv.Color.from_hex("#A351FB"),
+ text_color=sv.Color.from_hex("#000000"),
+ )
+
+ for idx, zone in enumerate(zones):
+ annotated_frame = sv.draw_polygon(
+ scene=annotated_frame, polygon=zone.polygon, color=COLORS.by_idx(idx)
+ )
+
+ detections_in_zone = detections[zone.trigger(detections)]
+ time_in_zone = timers[idx].tick(detections_in_zone)
+ custom_color_lookup = np.full(detections_in_zone.class_id.shape, idx)
+
+ annotated_frame = COLOR_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ custom_color_lookup=custom_color_lookup,
+ )
+ labels = [
+ f"#{tracker_id} {int(t // 60):02d}:{int(t % 60):02d}"
+ for tracker_id, t in zip(detections_in_zone.tracker_id, time_in_zone)
+ ]
+ annotated_frame = LABEL_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ labels=labels,
+ custom_color_lookup=custom_color_lookup,
+ )
+
+ cv2.imshow("Processed Video", annotated_frame)
+ if cv2.waitKey(1) & 0xFF == ord("q"):
+ break
+
+ cv2.destroyAllWindows()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/time_in_zone/rfdetr_stream_example.py b/examples/time_in_zone/rfdetr_stream_example.py
new file mode 100644
index 0000000..678faaf
--- /dev/null
+++ b/examples/time_in_zone/rfdetr_stream_example.py
@@ -0,0 +1,184 @@
+from __future__ import annotations
+
+from enum import Enum
+
+import cv2
+import numpy as np
+from inference import InferencePipeline
+from inference.core.interfaces.camera.entities import VideoFrame
+from rfdetr import RFDETRBase, RFDETRLarge, RFDETRMedium, RFDETRNano, RFDETRSmall
+from utils.general import find_in_list, load_zones_config
+from utils.timers import ClockBasedTimer
+
+import supervision as sv
+
+
+class ModelSize(Enum):
+ NANO = "nano"
+ SMALL = "small"
+ MEDIUM = "medium"
+ BASE = "base"
+ LARGE = "large"
+
+ @classmethod
+ def list(cls) -> list[str]:
+ return [c.value for c in cls]
+
+ @classmethod
+ def from_value(cls, value: ModelSize | str) -> ModelSize:
+ if isinstance(value, cls):
+ return value
+ if isinstance(value, str):
+ value = value.lower()
+ try:
+ return cls(value)
+ except ValueError as exc:
+ raise ValueError(
+ f"Invalid model size '{value}'. Must be one of {cls.list()}."
+ ) from exc
+ raise ValueError(
+ f"Invalid value type '{type(value)}'. Expected str or ModelSize."
+ )
+
+
+def load_model(
+ checkpoint: ModelSize | str, device: str, resolution: int
+) -> RFDETRBase | RFDETRLarge | RFDETRMedium | RFDETRNano | RFDETRSmall:
+ checkpoint = ModelSize.from_value(checkpoint)
+ if checkpoint == ModelSize.NANO:
+ return RFDETRNano(device=device, resolution=resolution)
+ if checkpoint == ModelSize.SMALL:
+ return RFDETRSmall(device=device, resolution=resolution)
+ if checkpoint == ModelSize.MEDIUM:
+ return RFDETRMedium(device=device, resolution=resolution)
+ if checkpoint == ModelSize.BASE:
+ return RFDETRBase(device=device, resolution=resolution)
+ if checkpoint == ModelSize.LARGE:
+ return RFDETRLarge(device=device, resolution=resolution)
+ raise RuntimeError("Unhandled checkpoint type.")
+
+
+def adjust_resolution(checkpoint: ModelSize | str, resolution: int) -> int:
+ checkpoint = ModelSize.from_value(checkpoint)
+ divisor = (
+ 32 if checkpoint in {ModelSize.NANO, ModelSize.SMALL, ModelSize.MEDIUM} else 56
+ )
+ remainder = resolution % divisor
+ if remainder == 0:
+ return resolution
+ lower = resolution - remainder
+ upper = lower + divisor
+ return lower if resolution - lower < upper - resolution else upper
+
+
+COLORS = sv.ColorPalette.from_hex(["#E6194B", "#3CB44B", "#FFE119", "#3C76D1"])
+COLOR_ANNOTATOR = sv.ColorAnnotator(color=COLORS)
+LABEL_ANNOTATOR = sv.LabelAnnotator(
+ color=COLORS, text_color=sv.Color.from_hex("#000000")
+)
+
+
+class CustomSink:
+ def __init__(self, zone_configuration_path: str, classes: list[int]) -> None:
+ self.classes = classes
+ self.tracker = sv.ByteTrack(minimum_matching_threshold=0.8)
+ self.fps_monitor = sv.FPSMonitor()
+ self.polygons = load_zones_config(file_path=zone_configuration_path)
+ self.timers = [ClockBasedTimer() for _ in self.polygons]
+ self.zones = [
+ sv.PolygonZone(
+ polygon=polygon,
+ triggering_anchors=(sv.Position.CENTER,),
+ )
+ for polygon in self.polygons
+ ]
+
+ def on_prediction(self, detections: sv.Detections, frame: VideoFrame) -> None:
+ self.fps_monitor.tick()
+ fps = self.fps_monitor.fps
+ detections = detections[find_in_list(detections.class_id, self.classes)]
+ detections = self.tracker.update_with_detections(detections)
+ annotated_frame = frame.image.copy()
+ annotated_frame = sv.draw_text(
+ scene=annotated_frame,
+ text=f"{fps:.1f}",
+ text_anchor=sv.Point(40, 30),
+ background_color=sv.Color.from_hex("#A351FB"),
+ text_color=sv.Color.from_hex("#000000"),
+ )
+ for idx, zone in enumerate(self.zones):
+ annotated_frame = sv.draw_polygon(
+ scene=annotated_frame,
+ polygon=zone.polygon,
+ color=COLORS.by_idx(idx),
+ )
+ detections_in_zone = detections[zone.trigger(detections)]
+ time_in_zone = self.timers[idx].tick(detections_in_zone)
+ custom_color_lookup = np.full(detections_in_zone.class_id.shape, idx)
+ annotated_frame = COLOR_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ custom_color_lookup=custom_color_lookup,
+ )
+ labels = [
+ f"#{tracker_id} {int(t // 60):02d}:{int(t % 60):02d}"
+ for tracker_id, t in zip(detections_in_zone.tracker_id, time_in_zone)
+ ]
+ annotated_frame = LABEL_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ labels=labels,
+ custom_color_lookup=custom_color_lookup,
+ )
+ cv2.imshow("Processed Video", annotated_frame)
+ cv2.waitKey(1)
+
+
+def main(
+ rtsp_url: str,
+ zone_configuration_path: str,
+ resolution: int,
+ model_size: str = "small",
+ device: str = "cpu",
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+ classes: list[int] = [],
+) -> None:
+ """
+ Calculating detections dwell time in zones using an RTSP stream.
+
+ Args:
+ rtsp_url: Complete RTSP URL for the video stream
+ zone_configuration_path: Path to the zone configuration JSON file
+ resolution: Input resolution for the model
+ model_size: RF-DETR model size ('nano', 'small', 'medium', 'base' or 'large')
+ device: Computation device ('cpu', 'mps' or 'cuda')
+ confidence_threshold: Confidence level for detections (0 to 1)
+ iou_threshold: IOU threshold for non-max suppression
+ classes: List of class IDs to track. If empty, all classes are tracked
+ """
+ resolution = adjust_resolution(checkpoint=model_size, resolution=resolution)
+ model = load_model(checkpoint=model_size, device=device, resolution=resolution)
+
+ def inference_callback(frames: list[VideoFrame]) -> list[sv.Detections]:
+ dets = model.predict(frames[0].image, threshold=confidence_threshold)
+ return [dets.with_nms(threshold=iou_threshold)]
+
+ sink = CustomSink(zone_configuration_path=zone_configuration_path, classes=classes)
+ pipeline = InferencePipeline.init_with_custom_logic(
+ video_reference=rtsp_url,
+ on_video_frame=inference_callback,
+ on_prediction=sink.on_prediction,
+ )
+ pipeline.start()
+ try:
+ pipeline.join()
+ except KeyboardInterrupt:
+ pipeline.terminate()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/time_in_zone/scripts/download_from_youtube.py b/examples/time_in_zone/scripts/download_from_youtube.py
new file mode 100644
index 0000000..029a946
--- /dev/null
+++ b/examples/time_in_zone/scripts/download_from_youtube.py
@@ -0,0 +1,61 @@
+import os
+import sys
+from typing import Any
+
+import yt_dlp
+from jsonargparse import auto_cli
+from yt_dlp.utils import DownloadError
+
+
+def _build_ydl_opts(output_path: str | None, file_name: str | None) -> dict[str, Any]:
+ out_dir = output_path or "."
+
+ if not os.path.exists(out_dir):
+ os.makedirs(out_dir)
+
+ name_template = file_name if file_name else "%(title)s.%(ext)s"
+
+ return {
+ "format": (
+ "bestvideo[ext=mp4][vcodec!*=av01][height<=2160]+bestaudio[ext=m4a]/"
+ "best[ext=mp4][vcodec!*=av01][height<=2160]/"
+ "bestvideo+bestaudio/best"
+ ),
+ "merge_output_format": "mp4",
+ "outtmpl": os.path.join(out_dir, name_template),
+ "quiet": False,
+ "noplaylist": True,
+ }
+
+
+def main(
+ url: str, output_path: str = "data/source", file_name: str = "video.mp4"
+) -> None:
+ """
+ Download a specific YouTube video by providing its URL.
+
+ Args:
+ url: The full URL of the YouTube video you wish to download.
+ output_path: Specifies the directory where the video will be saved.
+ file_name: Sets the name of the saved video file.
+ """
+ # ssl._create_default_https_context = ssl._create_unverified_context
+ ydl_opts = _build_ydl_opts(output_path, file_name)
+
+ try:
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
+ ydl.download([url])
+ except DownloadError as err:
+ print(f"Download failed: {err}", file=sys.stderr)
+ sys.exit(1)
+
+ final_name = file_name if file_name else "the video title"
+ final_path = output_path if output_path else "current directory"
+ print(f"Download completed! Video saved as '{final_name}' in '{final_path}'.")
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/time_in_zone/scripts/draw_zones.py b/examples/time_in_zone/scripts/draw_zones.py
new file mode 100644
index 0000000..a6bea0b
--- /dev/null
+++ b/examples/time_in_zone/scripts/draw_zones.py
@@ -0,0 +1,168 @@
+import json
+import os
+from typing import Any
+
+import cv2
+import numpy as np
+from jsonargparse import auto_cli
+
+import supervision as sv
+
+KEY_ENTER = 13
+KEY_NEWLINE = 10
+KEY_ESCAPE = 27
+KEY_QUIT = ord("q")
+KEY_SAVE = ord("s")
+
+THICKNESS = 2
+COLORS = sv.ColorPalette.DEFAULT
+WINDOW_NAME = "Draw Zones"
+POLYGONS = [[]]
+
+current_mouse_position: tuple[int, int] | None = None
+
+
+def resolve_source(source_path: str) -> np.ndarray | None:
+ if not os.path.exists(source_path):
+ return None
+
+ image = cv2.imread(source_path)
+ if image is not None:
+ return image
+
+ frame_generator = sv.get_video_frames_generator(source_path=source_path)
+ frame = next(frame_generator)
+ return frame
+
+
+def mouse_event(event: int, x: int, y: int, flags: int, param: Any) -> None:
+ global current_mouse_position
+ if event == cv2.EVENT_MOUSEMOVE:
+ current_mouse_position = (x, y)
+ elif event == cv2.EVENT_LBUTTONDOWN:
+ POLYGONS[-1].append((x, y))
+
+
+def redraw(image: np.ndarray, original_image: np.ndarray) -> None:
+ global POLYGONS, current_mouse_position
+ image[:] = original_image.copy()
+ for idx, polygon in enumerate(POLYGONS):
+ color = (
+ COLORS.by_idx(idx).as_bgr()
+ if idx < len(POLYGONS) - 1
+ else sv.Color.WHITE.as_bgr()
+ )
+
+ if len(polygon) > 1:
+ for i in range(1, len(polygon)):
+ cv2.line(
+ img=image,
+ pt1=polygon[i - 1],
+ pt2=polygon[i],
+ color=color,
+ thickness=THICKNESS,
+ )
+ if idx < len(POLYGONS) - 1:
+ cv2.line(
+ img=image,
+ pt1=polygon[-1],
+ pt2=polygon[0],
+ color=color,
+ thickness=THICKNESS,
+ )
+ if idx == len(POLYGONS) - 1 and current_mouse_position is not None and polygon:
+ cv2.line(
+ img=image,
+ pt1=polygon[-1],
+ pt2=current_mouse_position,
+ color=color,
+ thickness=THICKNESS,
+ )
+ cv2.imshow(WINDOW_NAME, image)
+
+
+def close_and_finalize_polygon(image: np.ndarray, original_image: np.ndarray) -> None:
+ if len(POLYGONS[-1]) > 2:
+ cv2.line(
+ img=image,
+ pt1=POLYGONS[-1][-1],
+ pt2=POLYGONS[-1][0],
+ color=COLORS.by_idx(0).as_bgr(),
+ thickness=THICKNESS,
+ )
+ POLYGONS.append([])
+ image[:] = original_image.copy()
+ redraw_polygons(image)
+ cv2.imshow(WINDOW_NAME, image)
+
+
+def redraw_polygons(image: np.ndarray) -> None:
+ for idx, polygon in enumerate(POLYGONS[:-1]):
+ if len(polygon) > 1:
+ color = COLORS.by_idx(idx).as_bgr()
+ for i in range(len(polygon) - 1):
+ cv2.line(
+ img=image,
+ pt1=polygon[i],
+ pt2=polygon[i + 1],
+ color=color,
+ thickness=THICKNESS,
+ )
+ cv2.line(
+ img=image,
+ pt1=polygon[-1],
+ pt2=polygon[0],
+ color=color,
+ thickness=THICKNESS,
+ )
+
+
+def save_polygons_to_json(
+ polygons: list[list[tuple[int, int]]], target_path: str | os.PathLike[str]
+) -> None:
+ data_to_save = polygons if polygons[-1] else polygons[:-1]
+ with open(target_path, "w") as f:
+ json.dump(data_to_save, f)
+
+
+def main(source_path: str, zone_configuration_path: str) -> None:
+ """
+ Interactively draw polygons on images or video frames and save the annotations.
+
+ Args:
+ source_path: Path to the source image or video file for drawing polygons.
+ zone_configuration_path: Path where the polygon annotations saved as JSON file.
+ """
+ global current_mouse_position
+ original_image = resolve_source(source_path=source_path)
+ if original_image is None:
+ print("Failed to load source image.")
+ return
+
+ image = original_image.copy()
+ cv2.imshow(WINDOW_NAME, image)
+ cv2.setMouseCallback(WINDOW_NAME, mouse_event, image)
+
+ while True:
+ key = cv2.waitKey(1) & 0xFF
+ if key == KEY_ENTER or key == KEY_NEWLINE:
+ close_and_finalize_polygon(image, original_image)
+ elif key == KEY_ESCAPE:
+ POLYGONS[-1] = []
+ current_mouse_position = None
+ elif key == KEY_SAVE:
+ save_polygons_to_json(POLYGONS, zone_configuration_path)
+ print(f"Polygons saved to {zone_configuration_path}")
+ break
+ redraw(image, original_image)
+ if key == KEY_QUIT:
+ break
+
+ cv2.destroyAllWindows()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/time_in_zone/scripts/stream_from_file.py b/examples/time_in_zone/scripts/stream_from_file.py
new file mode 100644
index 0000000..4208f4d
--- /dev/null
+++ b/examples/time_in_zone/scripts/stream_from_file.py
@@ -0,0 +1,95 @@
+import os
+import subprocess
+import tempfile
+from glob import glob
+from threading import Thread
+
+import yaml
+from jsonargparse import auto_cli
+
+SERVER_CONFIG = {"protocols": ["tcp"], "paths": {"all": {"source": "publisher"}}}
+BASE_STREAM_URL = "rtsp://localhost:8554/live"
+
+
+def main(video_directory: str, number_of_streams: int = 6) -> None:
+ """
+ Script to stream videos using RTSP protocol.
+
+ Args:
+ video_directory: Directory containing video files to stream.
+ number_of_streams: Number of video files to stream.
+ """
+ video_files = find_video_files_in_directory(video_directory, number_of_streams)
+ try:
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ config_file_path = create_server_config_file(temporary_directory)
+ run_rtsp_server(config_path=config_file_path)
+ stream_videos(video_files)
+ finally:
+ stop_rtsp_server()
+
+
+def find_video_files_in_directory(directory: str, limit: int) -> list:
+ video_formats = ["*.mp4", "*.webm"]
+ video_paths = []
+ for video_format in video_formats:
+ video_paths.extend(glob(os.path.join(directory, video_format)))
+ return video_paths[:limit]
+
+
+def create_server_config_file(directory: str) -> str:
+ config_path = os.path.join(directory, "rtsp-simple-server.yml")
+ with open(config_path, "w") as config_file:
+ yaml.dump(SERVER_CONFIG, config_file)
+ return config_path
+
+
+def run_rtsp_server(config_path: str) -> None:
+ command = (
+ "docker run --rm --name rtsp_server -d -v "
+ f"{config_path}:/rtsp-simple-server.yml -p 8554:8554 "
+ "aler9/rtsp-simple-server:v1.3.0"
+ )
+ if run_command(command.split()) != 0:
+ raise RuntimeError("Could not start the RTSP server!")
+
+
+def stop_rtsp_server() -> None:
+ run_command("docker kill rtsp_server".split())
+
+
+def stream_videos(video_files: list) -> None:
+ threads = []
+ for index, video_file in enumerate(video_files):
+ stream_url = f"{BASE_STREAM_URL}{index}.stream"
+ print(f"Streaming {video_file} under {stream_url}")
+ thread = stream_video_to_url(video_file, stream_url)
+ threads.append(thread)
+ for thread in threads:
+ thread.join()
+
+
+def stream_video_to_url(video_path: str, stream_url: str) -> Thread:
+ command = (
+ f"ffmpeg -re -stream_loop -1 -i {video_path} "
+ f"-f rtsp -rtsp_transport tcp {stream_url}"
+ )
+ return run_command_in_thread(command.split())
+
+
+def run_command_in_thread(command: list) -> Thread:
+ thread = Thread(target=run_command, args=(command,))
+ thread.start()
+ return thread
+
+
+def run_command(command: list) -> int:
+ process = subprocess.run(command) # noqa: S603 # TODO: Validate command input to prevent execution of untrusted input
+ return process.returncode
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/time_in_zone/ultralytics_file_example.py b/examples/time_in_zone/ultralytics_file_example.py
new file mode 100644
index 0000000..55e9ddc
--- /dev/null
+++ b/examples/time_in_zone/ultralytics_file_example.py
@@ -0,0 +1,101 @@
+import cv2
+import numpy as np
+from ultralytics import YOLO
+from utils.general import find_in_list, load_zones_config
+from utils.timers import FPSBasedTimer
+
+import supervision as sv
+
+COLORS = sv.ColorPalette.from_hex(["#E6194B", "#3CB44B", "#FFE119", "#3C76D1"])
+COLOR_ANNOTATOR = sv.ColorAnnotator(color=COLORS)
+LABEL_ANNOTATOR = sv.LabelAnnotator(
+ color=COLORS, text_color=sv.Color.from_hex("#000000")
+)
+
+
+def main(
+ zone_configuration_path: str,
+ source_video_path: str,
+ weights: str = "yolov8s.pt",
+ device: str = "cpu",
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+ classes: list[int] = [],
+) -> None:
+ """
+ Calculating detections dwell time in zones, using video file.
+
+ Args:
+ 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
+ device: Computation device ('cpu', 'mps' or 'cuda')
+ confidence_threshold: Confidence level for detections (0 to 1)
+ iou_threshold: IOU threshold for non-max suppression
+ classes: List of class IDs to track. If empty, all classes are tracked
+ """
+ model = YOLO(weights)
+ tracker = sv.ByteTrack(minimum_matching_threshold=0.5)
+ video_info = sv.VideoInfo.from_video_path(video_path=source_video_path)
+ frames_generator = sv.get_video_frames_generator(source_video_path)
+
+ polygons = load_zones_config(file_path=zone_configuration_path)
+ zones = [
+ sv.PolygonZone(
+ polygon=polygon,
+ triggering_anchors=(sv.Position.CENTER,),
+ )
+ for polygon in polygons
+ ]
+ timers = [FPSBasedTimer(video_info.fps) for _ in zones]
+
+ for frame in frames_generator:
+ results = model(
+ frame,
+ verbose=False,
+ device=device,
+ conf=confidence_threshold,
+ iou=iou_threshold,
+ )[0]
+ detections = sv.Detections.from_ultralytics(results)
+ detections = detections[find_in_list(detections.class_id, classes)]
+ detections = tracker.update_with_detections(detections)
+
+ annotated_frame = frame.copy()
+
+ for idx, zone in enumerate(zones):
+ annotated_frame = sv.draw_polygon(
+ scene=annotated_frame, polygon=zone.polygon, color=COLORS.by_idx(idx)
+ )
+
+ detections_in_zone = detections[zone.trigger(detections)]
+ time_in_zone = timers[idx].tick(detections_in_zone)
+ custom_color_lookup = np.full(detections_in_zone.class_id.shape, idx)
+
+ annotated_frame = COLOR_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ custom_color_lookup=custom_color_lookup,
+ )
+ labels = [
+ f"#{tracker_id} {int(time // 60):02d}:{int(time % 60):02d}"
+ for tracker_id, time in zip(detections_in_zone.tracker_id, time_in_zone)
+ ]
+ annotated_frame = LABEL_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ labels=labels,
+ custom_color_lookup=custom_color_lookup,
+ )
+
+ cv2.imshow("Processed Video", annotated_frame)
+ if cv2.waitKey(1) & 0xFF == ord("q"):
+ break
+ cv2.destroyAllWindows()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/time_in_zone/ultralytics_naive_stream_example.py b/examples/time_in_zone/ultralytics_naive_stream_example.py
new file mode 100644
index 0000000..2f50aab
--- /dev/null
+++ b/examples/time_in_zone/ultralytics_naive_stream_example.py
@@ -0,0 +1,111 @@
+import cv2
+import numpy as np
+from ultralytics import YOLO
+from utils.general import find_in_list, get_stream_frames_generator, load_zones_config
+from utils.timers import ClockBasedTimer
+
+import supervision as sv
+
+COLORS = sv.ColorPalette.from_hex(["#E6194B", "#3CB44B", "#FFE119", "#3C76D1"])
+COLOR_ANNOTATOR = sv.ColorAnnotator(color=COLORS)
+LABEL_ANNOTATOR = sv.LabelAnnotator(
+ color=COLORS, text_color=sv.Color.from_hex("#000000")
+)
+
+
+def main(
+ zone_configuration_path: str,
+ rtsp_url: str,
+ weights: str = "yolov8s.pt",
+ device: str = "cpu",
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+ classes: list[int] = [],
+) -> None:
+ """
+ Calculating detections dwell time in zones, using RTSP stream.
+
+ Args:
+ 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
+ device: Computation device ('cpu', 'mps' or 'cuda')
+ confidence_threshold: Confidence level for detections (0 to 1)
+ iou_threshold: IOU threshold for non-max suppression
+ classes: List of class IDs to track. If empty, all classes are tracked
+ """
+ model = YOLO(weights)
+ tracker = sv.ByteTrack(minimum_matching_threshold=0.5)
+ frames_generator = get_stream_frames_generator(rtsp_url=rtsp_url)
+ fps_monitor = sv.FPSMonitor()
+
+ polygons = load_zones_config(file_path=zone_configuration_path)
+ zones = [
+ sv.PolygonZone(
+ polygon=polygon,
+ triggering_anchors=(sv.Position.CENTER,),
+ )
+ for polygon in polygons
+ ]
+ timers = [ClockBasedTimer() for _ in zones]
+
+ for frame in frames_generator:
+ fps_monitor.tick()
+ fps = fps_monitor.fps
+
+ results = model(
+ frame,
+ verbose=False,
+ device=device,
+ conf=confidence_threshold,
+ iou=iou_threshold,
+ )[0]
+ detections = sv.Detections.from_ultralytics(results)
+ detections = detections[find_in_list(detections.class_id, classes)]
+ detections = tracker.update_with_detections(detections)
+
+ annotated_frame = frame.copy()
+ annotated_frame = sv.draw_text(
+ scene=annotated_frame,
+ text=f"{fps:.1f}",
+ text_anchor=sv.Point(40, 30),
+ background_color=sv.Color.from_hex("#A351FB"),
+ text_color=sv.Color.from_hex("#000000"),
+ )
+
+ for idx, zone in enumerate(zones):
+ annotated_frame = sv.draw_polygon(
+ scene=annotated_frame, polygon=zone.polygon, color=COLORS.by_idx(idx)
+ )
+
+ detections_in_zone = detections[zone.trigger(detections)]
+ time_in_zone = timers[idx].tick(detections_in_zone)
+ custom_color_lookup = np.full(detections_in_zone.class_id.shape, idx)
+
+ annotated_frame = COLOR_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ custom_color_lookup=custom_color_lookup,
+ )
+ labels = [
+ f"#{tracker_id} {int(time // 60):02d}:{int(time % 60):02d}"
+ for tracker_id, time in zip(detections_in_zone.tracker_id, time_in_zone)
+ ]
+ annotated_frame = LABEL_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ labels=labels,
+ custom_color_lookup=custom_color_lookup,
+ )
+
+ cv2.imshow("Processed Video", annotated_frame)
+ if cv2.waitKey(1) & 0xFF == ord("q"):
+ break
+ cv2.destroyAllWindows()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/time_in_zone/ultralytics_stream_example.py b/examples/time_in_zone/ultralytics_stream_example.py
new file mode 100644
index 0000000..ca44fcf
--- /dev/null
+++ b/examples/time_in_zone/ultralytics_stream_example.py
@@ -0,0 +1,128 @@
+import cv2
+import numpy as np
+from inference import InferencePipeline
+from inference.core.interfaces.camera.entities import VideoFrame
+from ultralytics import YOLO
+from utils.general import find_in_list, load_zones_config
+from utils.timers import ClockBasedTimer
+
+import supervision as sv
+
+COLORS = sv.ColorPalette.from_hex(["#E6194B", "#3CB44B", "#FFE119", "#3C76D1"])
+COLOR_ANNOTATOR = sv.ColorAnnotator(color=COLORS)
+LABEL_ANNOTATOR = sv.LabelAnnotator(
+ color=COLORS, text_color=sv.Color.from_hex("#000000")
+)
+
+
+class CustomSink:
+ def __init__(self, zone_configuration_path: str, classes: list[int]) -> None:
+ self.classes = classes
+ self.tracker = sv.ByteTrack(minimum_matching_threshold=0.8)
+ self.fps_monitor = sv.FPSMonitor()
+ self.polygons = load_zones_config(file_path=zone_configuration_path)
+ self.timers = [ClockBasedTimer() for _ in self.polygons]
+ self.zones = [
+ sv.PolygonZone(
+ polygon=polygon,
+ triggering_anchors=(sv.Position.CENTER,),
+ )
+ for polygon in self.polygons
+ ]
+
+ def on_prediction(self, detections: sv.Detections, frame: VideoFrame) -> None:
+ self.fps_monitor.tick()
+ fps = self.fps_monitor.fps
+
+ detections = detections[find_in_list(detections.class_id, self.classes)]
+ detections = self.tracker.update_with_detections(detections)
+
+ annotated_frame = frame.image.copy()
+ annotated_frame = sv.draw_text(
+ scene=annotated_frame,
+ text=f"{fps:.1f}",
+ text_anchor=sv.Point(40, 30),
+ background_color=sv.Color.from_hex("#A351FB"),
+ text_color=sv.Color.from_hex("#000000"),
+ )
+
+ for idx, zone in enumerate(self.zones):
+ annotated_frame = sv.draw_polygon(
+ scene=annotated_frame, polygon=zone.polygon, color=COLORS.by_idx(idx)
+ )
+
+ detections_in_zone = detections[zone.trigger(detections)]
+ time_in_zone = self.timers[idx].tick(detections_in_zone)
+ custom_color_lookup = np.full(detections_in_zone.class_id.shape, idx)
+
+ annotated_frame = COLOR_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ custom_color_lookup=custom_color_lookup,
+ )
+ labels = [
+ f"#{tracker_id} {int(time // 60):02d}:{int(time % 60):02d}"
+ for tracker_id, time in zip(detections_in_zone.tracker_id, time_in_zone)
+ ]
+ annotated_frame = LABEL_ANNOTATOR.annotate(
+ scene=annotated_frame,
+ detections=detections_in_zone,
+ labels=labels,
+ custom_color_lookup=custom_color_lookup,
+ )
+ cv2.imshow("Processed Video", annotated_frame)
+ cv2.waitKey(1)
+
+
+def main(
+ zone_configuration_path: str,
+ rtsp_url: str,
+ weights: str = "yolov8s.pt",
+ device: str = "cpu",
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+ classes: list[int] = [],
+) -> None:
+ """
+ Calculating detections dwell time in zones, using RTSP stream.
+
+ Args:
+ 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
+ device: Computation device ('cpu', 'mps' or 'cuda')
+ confidence_threshold: Confidence level for detections (0 to 1)
+ iou_threshold: IOU threshold for non-max suppression
+ classes: List of class IDs to track. If empty, all classes are tracked
+ """
+ model = YOLO(weights)
+
+ def inference_callback(frames: list[VideoFrame]) -> list[sv.Detections]:
+ results = model(
+ frames[0].image, verbose=False, conf=confidence_threshold, device=device
+ )[0]
+ return [
+ sv.Detections.from_ultralytics(results).with_nms(threshold=iou_threshold)
+ ]
+
+ sink = CustomSink(zone_configuration_path=zone_configuration_path, classes=classes)
+
+ pipeline = InferencePipeline.init_with_custom_logic(
+ video_reference=rtsp_url,
+ on_video_frame=inference_callback,
+ on_prediction=sink.on_prediction,
+ )
+
+ pipeline.start()
+
+ try:
+ pipeline.join()
+ except KeyboardInterrupt:
+ pipeline.terminate()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/time_in_zone/utils/__init__.py b/examples/time_in_zone/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/time_in_zone/utils/general.py b/examples/time_in_zone/utils/general.py
new file mode 100644
index 0000000..a40b36c
--- /dev/null
+++ b/examples/time_in_zone/utils/general.py
@@ -0,0 +1,66 @@
+import json
+from collections.abc import Generator
+
+import cv2
+import numpy as np
+
+
+def load_zones_config(file_path: str) -> list[np.ndarray]:
+ """
+ Load polygon zone configurations from a JSON file.
+
+ This function reads a JSON file which contains polygon coordinates, and
+ converts them into a list of NumPy arrays. Each polygon is represented as
+ a NumPy array of coordinates.
+
+ Args:
+ file_path (str): The path to the JSON configuration file.
+
+ Returns:
+ List[np.ndarray]: A list of polygons, each represented as a NumPy array.
+ """
+ with open(file_path) as file:
+ data = json.load(file)
+ return [np.array(polygon, np.int32) for polygon in data]
+
+
+def find_in_list(array: np.ndarray, search_list: list[int]) -> np.ndarray:
+ """Determines if elements of a numpy array are present in a list.
+
+ Args:
+ array (np.ndarray): The numpy array of integers to check.
+ search_list (List[int]): The list of integers to search within.
+
+ Returns:
+ np.ndarray: A numpy array of booleans, where each boolean indicates whether
+ the corresponding element in `array` is found in `search_list`.
+ """
+ if not search_list:
+ return np.ones(array.shape, dtype=bool)
+ else:
+ return np.isin(array, search_list)
+
+
+def get_stream_frames_generator(rtsp_url: str) -> Generator[np.ndarray, None, None]:
+ """
+ Generator function to yield frames from an RTSP stream.
+
+ Args:
+ rtsp_url (str): URL of the RTSP video stream.
+
+ Yields:
+ np.ndarray: The next frame from the video stream.
+ """
+ cap = cv2.VideoCapture(rtsp_url)
+ if not cap.isOpened():
+ raise Exception("Error: Could not open video stream.")
+
+ try:
+ while True:
+ ret, frame = cap.read()
+ if not ret:
+ print("End of stream or error reading frame.")
+ break
+ yield frame
+ finally:
+ cap.release()
diff --git a/examples/time_in_zone/utils/timers.py b/examples/time_in_zone/utils/timers.py
new file mode 100644
index 0000000..bf8e5df
--- /dev/null
+++ b/examples/time_in_zone/utils/timers.py
@@ -0,0 +1,88 @@
+from datetime import datetime
+
+import numpy as np
+
+import supervision as sv
+
+
+class FPSBasedTimer:
+ """
+ A timer that calculates the duration each object has been detected based on frames
+ per second (FPS).
+
+ Attributes:
+ fps (float): The frame rate of the video stream, used to calculate
+ time durations.
+ frame_id (int): The current frame number in the sequence.
+ tracker_id2frame_id (Dict[int, int]): Maps each tracker's ID to the frame number
+ at which it was first detected.
+ """
+
+ def __init__(self, fps: float = 30) -> None:
+ """Initializes the FPSBasedTimer with the specified frames per second rate.
+
+ Args:
+ fps (float): The frame rate of the video stream. Defaults to 30.
+ """
+ self.fps = fps
+ self.frame_id = 0
+ self.tracker_id2frame_id: dict[int, int] = {}
+
+ def tick(self, detections: sv.Detections) -> np.ndarray:
+ """Processes the current frame, updating time durations for each tracker.
+
+ Args:
+ detections: The detections for the current frame, including tracker IDs.
+
+ Returns:
+ np.ndarray: Time durations (in seconds) for each detected tracker, since
+ their first detection.
+ """
+ self.frame_id += 1
+ times = []
+
+ for tracker_id in detections.tracker_id:
+ self.tracker_id2frame_id.setdefault(tracker_id, self.frame_id)
+
+ start_frame_id = self.tracker_id2frame_id[tracker_id]
+ time_duration = (self.frame_id - start_frame_id) / self.fps
+ times.append(time_duration)
+
+ return np.array(times)
+
+
+class ClockBasedTimer:
+ """
+ A timer that calculates the duration each object has been detected based on the
+ system clock.
+
+ Attributes:
+ tracker_id2start_time (Dict[int, datetime]): Maps each tracker's ID to the
+ datetime when it was first detected.
+ """
+
+ def __init__(self) -> None:
+ """Initializes the ClockBasedTimer."""
+ self.tracker_id2start_time: dict[int, datetime] = {}
+
+ def tick(self, detections: sv.Detections) -> np.ndarray:
+ """Processes the current frame, updating time durations for each tracker.
+
+ Args:
+ detections: The detections for the current frame, including tracker IDs.
+
+ Returns:
+ np.ndarray: Time durations (in seconds) for each detected tracker, since
+ their first detection.
+ """
+ current_time = datetime.now()
+ times = []
+
+ for tracker_id in detections.tracker_id:
+ self.tracker_id2start_time.setdefault(tracker_id, current_time)
+
+ start_time = self.tracker_id2start_time[tracker_id]
+ time_duration = (current_time - start_time).total_seconds()
+ times.append(time_duration)
+
+ return np.array(times)
diff --git a/examples/tracking/README.md b/examples/tracking/README.md
new file mode 100644
index 0000000..7ef3b9e
--- /dev/null
+++ b/examples/tracking/README.md
@@ -0,0 +1,83 @@
+# tracking
+
+## ๐ hello
+
+This script provides functionality for processing videos using YOLOv8 for object detection and Supervision for tracking and annotation.
+
+## ๐ป install
+
+- clone repository and navigate to example directory
+
+ ```bash
+ git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
+ cd supervision/examples/tracking
+ ```
+
+- setup python environment and activate it [optional]
+
+ ```bash
+ uv venv
+ source .venv/bin/activate
+ ```
+
+- install required dependencies
+
+ ```bash
+ uv pip install -r requirements.txt
+ ```
+
+## ๐ ๏ธ script arguments
+
+- ultralytics
+
+ - `--source_weights_path`: Required. Specifies the path to the YOLO model's weights file, which is essential for the object detection process. This file contains the data that the model uses to identify objects in the video.
+
+ - `--source_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"`.
+
+ - `--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
+ ```
+
+- ultralytics
+
+ ```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.
+
+- 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.
diff --git a/examples/tracking/inference_example.py b/examples/tracking/inference_example.py
new file mode 100644
index 0000000..3380457
--- /dev/null
+++ b/examples/tracking/inference_example.py
@@ -0,0 +1,66 @@
+import os
+
+from inference.models.utils import get_roboflow_model
+from tqdm import tqdm
+
+import supervision as sv
+
+
+def main(
+ source_video_path: str,
+ target_video_path: str,
+ roboflow_api_key: str,
+ model_id: str = "yolov8x-1280",
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+) -> None:
+ """
+ Video Processing with Inference and ByteTrack.
+
+ Args:
+ source_video_path: Path to the source video file
+ target_video_path: Path to the target video file (output)
+ roboflow_api_key: Roboflow API key
+ model_id: Roboflow model ID
+ confidence_threshold: Confidence threshold for the model
+ iou_threshold: IOU threshold for the model
+ """
+ api_key = os.environ.get("ROBOFLOW_API_KEY", roboflow_api_key)
+ if api_key is None:
+ raise ValueError(
+ "Roboflow API key is missing. Please provide it as an argument or set the "
+ "ROBOFLOW_API_KEY environment variable."
+ )
+
+ model = get_roboflow_model(model_id=model_id, api_key=api_key)
+
+ tracker = sv.ByteTrack()
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+ frame_generator = sv.get_video_frames_generator(source_path=source_video_path)
+ video_info = sv.VideoInfo.from_video_path(video_path=source_video_path)
+
+ with sv.VideoSink(target_path=target_video_path, video_info=video_info) as sink:
+ for frame in tqdm(frame_generator, total=video_info.total_frames):
+ results = model.infer(
+ frame, confidence=confidence_threshold, iou_threshold=iou_threshold
+ )[0]
+ detections = sv.Detections.from_inference(results)
+ detections = tracker.update_with_detections(detections)
+
+ annotated_frame = box_annotator.annotate(
+ scene=frame.copy(), detections=detections
+ )
+
+ annotated_labeled_frame = label_annotator.annotate(
+ scene=annotated_frame, detections=detections
+ )
+
+ sink.write_frame(frame=annotated_labeled_frame)
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/tracking/requirements.txt b/examples/tracking/requirements.txt
new file mode 100644
index 0000000..453231c
--- /dev/null
+++ b/examples/tracking/requirements.txt
@@ -0,0 +1,5 @@
+inference
+supervision
+tqdm
+ultralytics
+jsonargparse[signatures]
diff --git a/examples/tracking/ultralytics_example.py b/examples/tracking/ultralytics_example.py
new file mode 100644
index 0000000..332c8dd
--- /dev/null
+++ b/examples/tracking/ultralytics_example.py
@@ -0,0 +1,55 @@
+from tqdm import tqdm
+from ultralytics import YOLO
+
+import supervision as sv
+
+
+def main(
+ source_weights_path: str,
+ source_video_path: str,
+ target_video_path: str,
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+) -> None:
+ """
+ Video Processing with YOLO and ByteTrack.
+
+ Args:
+ source_weights_path: Path to the source weights file
+ source_video_path: Path to the source video file
+ target_video_path: Path to the target video file (output)
+ confidence_threshold: Confidence threshold for the model
+ iou_threshold: IOU threshold for the model
+ """
+ model = YOLO(source_weights_path)
+
+ tracker = sv.ByteTrack()
+ box_annotator = sv.BoxAnnotator()
+ label_annotator = sv.LabelAnnotator()
+ frame_generator = sv.get_video_frames_generator(source_path=source_video_path)
+ video_info = sv.VideoInfo.from_video_path(video_path=source_video_path)
+
+ with sv.VideoSink(target_path=target_video_path, video_info=video_info) as sink:
+ for frame in tqdm(frame_generator, total=video_info.total_frames):
+ results = model(
+ frame, verbose=False, conf=confidence_threshold, iou=iou_threshold
+ )[0]
+ detections = sv.Detections.from_ultralytics(results)
+ detections = tracker.update_with_detections(detections)
+
+ annotated_frame = box_annotator.annotate(
+ scene=frame.copy(), detections=detections
+ )
+
+ annotated_labeled_frame = label_annotator.annotate(
+ scene=annotated_frame, detections=detections
+ )
+
+ sink.write_frame(frame=annotated_labeled_frame)
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/traffic_analysis/.gitignore b/examples/traffic_analysis/.gitignore
new file mode 100644
index 0000000..a0aa525
--- /dev/null
+++ b/examples/traffic_analysis/.gitignore
@@ -0,0 +1,9 @@
+data/
+venv/
+*.pt
+*.pth
+*.mp4
+*.mov
+*.png
+*.jpg
+*.jpeg
diff --git a/examples/traffic_analysis/README.md b/examples/traffic_analysis/README.md
new file mode 100644
index 0000000..23709fa
--- /dev/null
+++ b/examples/traffic_analysis/README.md
@@ -0,0 +1,95 @@
+# traffic analysis
+
+## ๐ hello
+
+This script performs traffic flow analysis using YOLOv8, an object-detection method and ByteTrack, a simple yet effective online multi-object tracking method. It uses the supervision package for multiple tasks such as tracking, annotations, etc.
+
+https://github.com/roboflow/supervision/assets/26109316/c9436828-9fbf-4c25-ae8c-60e9c81b3900
+
+## ๐ป install
+
+- clone repository and navigate to example directory
+
+ ```bash
+ git clone --depth 1 -b develop https://github.com/roboflow/supervision.git
+ cd supervision/examples/traffic_analysis
+ ```
+
+- setup python environment and activate it [optional]
+
+ ```bash
+ uv venv
+ source .venv/bin/activate
+ ```
+
+- install required dependencies
+
+ ```bash
+ uv pip install -r requirements.txt
+ ```
+
+- download `traffic_analysis.pt` and `traffic_analysis.mov` files
+
+ ```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_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"`.
+
+ - `--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
+ ```
+
+- 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
+ ```
+
+## ยฉ 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.
+
+- 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.
diff --git a/examples/traffic_analysis/inference_example.py b/examples/traffic_analysis/inference_example.py
new file mode 100644
index 0000000..8bd1394
--- /dev/null
+++ b/examples/traffic_analysis/inference_example.py
@@ -0,0 +1,223 @@
+import os
+from collections.abc import Iterable
+
+import cv2
+import numpy as np
+from inference.models.utils import get_roboflow_model
+from tqdm import tqdm
+
+import supervision as sv
+
+COLORS = sv.ColorPalette.from_hex(["#E6194B", "#3CB44B", "#FFE119", "#3C76D1"])
+
+
+ZONE_IN_POLYGONS = [
+ np.array([[592, 282], [900, 282], [900, 82], [592, 82]]),
+ np.array([[950, 860], [1250, 860], [1250, 1060], [950, 1060]]),
+ np.array([[592, 582], [592, 860], [392, 860], [392, 582]]),
+ np.array([[1250, 282], [1250, 530], [1450, 530], [1450, 282]]),
+]
+
+ZONE_OUT_POLYGONS = [
+ np.array([[950, 282], [1250, 282], [1250, 82], [950, 82]]),
+ np.array([[592, 860], [900, 860], [900, 1060], [592, 1060]]),
+ np.array([[592, 282], [592, 550], [392, 550], [392, 282]]),
+ np.array([[1250, 860], [1250, 560], [1450, 560], [1450, 860]]),
+]
+
+
+class DetectionsManager:
+ def __init__(self) -> None:
+ self.tracker_id_to_zone_id: dict[int, int] = {}
+ self.counts: dict[int, dict[int, set[int]]] = {}
+
+ def update(
+ self,
+ detections_all: sv.Detections,
+ detections_in_zones: list[sv.Detections],
+ detections_out_zones: list[sv.Detections],
+ ) -> sv.Detections:
+ for zone_in_id, detections_in_zone in enumerate(detections_in_zones):
+ for tracker_id in detections_in_zone.tracker_id:
+ self.tracker_id_to_zone_id.setdefault(tracker_id, zone_in_id)
+
+ for zone_out_id, detections_out_zone in enumerate(detections_out_zones):
+ for tracker_id in detections_out_zone.tracker_id:
+ if tracker_id in self.tracker_id_to_zone_id:
+ zone_in_id = self.tracker_id_to_zone_id[tracker_id]
+ self.counts.setdefault(zone_out_id, {})
+ self.counts[zone_out_id].setdefault(zone_in_id, set())
+ self.counts[zone_out_id][zone_in_id].add(tracker_id)
+ if len(detections_all) > 0:
+ detections_all.class_id = np.vectorize(
+ lambda x: self.tracker_id_to_zone_id.get(x, -1)
+ )(detections_all.tracker_id)
+ else:
+ detections_all.class_id = np.array([], dtype=int)
+ return detections_all[detections_all.class_id != -1]
+
+
+def initiate_polygon_zones(
+ polygons: list[np.ndarray],
+ triggering_anchors: Iterable[sv.Position] = [sv.Position.CENTER],
+) -> list[sv.PolygonZone]:
+ return [
+ sv.PolygonZone(
+ polygon=polygon,
+ triggering_anchors=triggering_anchors,
+ )
+ for polygon in polygons
+ ]
+
+
+class VideoProcessor:
+ def __init__(
+ self,
+ roboflow_api_key: str,
+ model_id: str,
+ source_video_path: str,
+ target_video_path: str | None = None,
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+ ) -> None:
+ self.conf_threshold = confidence_threshold
+ self.iou_threshold = iou_threshold
+ self.source_video_path = source_video_path
+ self.target_video_path = target_video_path
+
+ self.model = get_roboflow_model(model_id=model_id, api_key=roboflow_api_key)
+ self.tracker = sv.ByteTrack()
+
+ self.video_info = sv.VideoInfo.from_video_path(source_video_path)
+ 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.box_annotator = sv.BoxAnnotator(color=COLORS)
+ self.label_annotator = sv.LabelAnnotator(
+ color=COLORS, text_color=sv.Color.BLACK
+ )
+ self.trace_annotator = sv.TraceAnnotator(
+ color=COLORS, position=sv.Position.CENTER, trace_length=100, thickness=2
+ )
+ self.detections_manager = DetectionsManager()
+
+ def process_video(self) -> None:
+ frame_generator = sv.get_video_frames_generator(
+ source_path=self.source_video_path
+ )
+
+ if self.target_video_path:
+ with sv.VideoSink(self.target_video_path, self.video_info) as sink:
+ for frame in tqdm(frame_generator, total=self.video_info.total_frames):
+ annotated_frame = self.process_frame(frame)
+ sink.write_frame(annotated_frame)
+ else:
+ for frame in tqdm(frame_generator, total=self.video_info.total_frames):
+ annotated_frame = self.process_frame(frame)
+ cv2.imshow("Processed Video", annotated_frame)
+ if cv2.waitKey(1) & 0xFF == ord("q"):
+ break
+ cv2.destroyAllWindows()
+
+ def annotate_frame(
+ self, frame: np.ndarray, detections: sv.Detections
+ ) -> np.ndarray:
+ annotated_frame = frame.copy()
+ for i, (zone_in, zone_out) in enumerate(zip(self.zones_in, self.zones_out)):
+ annotated_frame = sv.draw_polygon(
+ annotated_frame, zone_in.polygon, COLORS.colors[i]
+ )
+ annotated_frame = sv.draw_polygon(
+ annotated_frame, zone_out.polygon, COLORS.colors[i]
+ )
+
+ labels = [f"#{tracker_id}" for tracker_id in detections.tracker_id]
+ annotated_frame = self.trace_annotator.annotate(annotated_frame, detections)
+ annotated_frame = self.box_annotator.annotate(annotated_frame, detections)
+ annotated_frame = self.label_annotator.annotate(
+ annotated_frame, detections, labels
+ )
+
+ for zone_out_id, zone_out in enumerate(self.zones_out):
+ zone_center = sv.get_polygon_center(polygon=zone_out.polygon)
+ if zone_out_id in self.detections_manager.counts:
+ counts = self.detections_manager.counts[zone_out_id]
+ for i, zone_in_id in enumerate(counts):
+ count = len(self.detections_manager.counts[zone_out_id][zone_in_id])
+ text_anchor = sv.Point(x=zone_center.x, y=zone_center.y + 40 * i)
+ annotated_frame = sv.draw_text(
+ scene=annotated_frame,
+ text=str(count),
+ text_anchor=text_anchor,
+ background_color=COLORS.colors[zone_in_id],
+ )
+
+ return annotated_frame
+
+ def process_frame(self, frame: np.ndarray) -> np.ndarray:
+ results = self.model.infer(
+ frame, confidence=self.conf_threshold, iou_threshold=self.iou_threshold
+ )[0]
+ detections = sv.Detections.from_inference(results)
+ detections.class_id = np.zeros(len(detections))
+ detections = self.tracker.update_with_detections(detections)
+
+ detections_in_zones = []
+ detections_out_zones = []
+
+ for zone_in, zone_out in zip(self.zones_in, self.zones_out):
+ detections_in_zone = detections[zone_in.trigger(detections=detections)]
+ detections_in_zones.append(detections_in_zone)
+ detections_out_zone = detections[zone_out.trigger(detections=detections)]
+ detections_out_zones.append(detections_out_zone)
+
+ detections = self.detections_manager.update(
+ detections, detections_in_zones, detections_out_zones
+ )
+ return self.annotate_frame(frame, detections)
+
+
+def main(
+ source_video_path: str,
+ target_video_path: str,
+ roboflow_api_key: str,
+ model_id: str = "vehicle-count-in-drone-video/6",
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+) -> None:
+ """
+ Traffic Flow Analysis with Inference and ByteTrack.
+
+ Args:
+ source_video_path: Path to the source video file
+ target_video_path: Path to the target video file (output)
+ roboflow_api_key: Roboflow API key
+ model_id: Roboflow model ID
+ confidence_threshold: Confidence threshold for the model
+ iou_threshold: IOU threshold for the model
+ """
+ api_key = roboflow_api_key
+ api_key = os.environ.get("ROBOFLOW_API_KEY", api_key)
+ if api_key is None:
+ raise ValueError(
+ "Roboflow API KEY is missing. Please provide it as an argument or set the "
+ "ROBOFLOW_API_KEY environment variable."
+ )
+ roboflow_api_key = api_key
+
+ processor = VideoProcessor(
+ roboflow_api_key=roboflow_api_key,
+ model_id=model_id,
+ source_video_path=source_video_path,
+ target_video_path=target_video_path,
+ confidence_threshold=confidence_threshold,
+ iou_threshold=iou_threshold,
+ )
+ processor.process_video()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/examples/traffic_analysis/requirements.txt b/examples/traffic_analysis/requirements.txt
new file mode 100644
index 0000000..f325418
--- /dev/null
+++ b/examples/traffic_analysis/requirements.txt
@@ -0,0 +1,6 @@
+gdown
+inference
+supervision
+tqdm
+ultralytics
+jsonargparse[signatures]
diff --git a/examples/traffic_analysis/setup.sh b/examples/traffic_analysis/setup.sh
new file mode 100755
index 0000000..0e746a1
--- /dev/null
+++ b/examples/traffic_analysis/setup.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# Get the directory where the script is located
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+# Check if 'data' directory does not exist and then create it
+if [[ ! -e $DIR/data ]]; then
+ mkdir "$DIR/data"
+else
+ echo "'data' directory already exists."
+fi
+
+# Download the traffic_analysis.mov file from Google Drive
+gdown -O "$DIR/data/traffic_analysis.mov" "https://drive.google.com/uc?id=1qadBd7lgpediafCpL_yedGjQPk-FLK-W"
+
+# Download the traffic_analysis.pt file from Google Drive
+gdown -O "$DIR/data/traffic_analysis.pt" "https://drive.google.com/uc?id=1y-IfToCjRXa3ZdC1JpnKRopC7mcQW-5z"
diff --git a/examples/traffic_analysis/ultralytics_example.py b/examples/traffic_analysis/ultralytics_example.py
new file mode 100644
index 0000000..03b8d77
--- /dev/null
+++ b/examples/traffic_analysis/ultralytics_example.py
@@ -0,0 +1,208 @@
+from collections.abc import Iterable
+
+import cv2
+import numpy as np
+from tqdm import tqdm
+from ultralytics import YOLO
+
+import supervision as sv
+
+COLORS = sv.ColorPalette.from_hex(["#E6194B", "#3CB44B", "#FFE119", "#3C76D1"])
+
+ZONE_IN_POLYGONS = [
+ np.array([[592, 282], [900, 282], [900, 82], [592, 82]]),
+ np.array([[950, 860], [1250, 860], [1250, 1060], [950, 1060]]),
+ np.array([[592, 582], [592, 860], [392, 860], [392, 582]]),
+ np.array([[1250, 282], [1250, 530], [1450, 530], [1450, 282]]),
+]
+
+ZONE_OUT_POLYGONS = [
+ np.array([[950, 282], [1250, 282], [1250, 82], [950, 82]]),
+ np.array([[592, 860], [900, 860], [900, 1060], [592, 1060]]),
+ np.array([[592, 282], [592, 550], [392, 550], [392, 282]]),
+ np.array([[1250, 860], [1250, 560], [1450, 560], [1450, 860]]),
+]
+
+
+class DetectionsManager:
+ def __init__(self) -> None:
+ self.tracker_id_to_zone_id: dict[int, int] = {}
+ self.counts: dict[int, dict[int, set[int]]] = {}
+
+ def update(
+ self,
+ detections_all: sv.Detections,
+ detections_in_zones: list[sv.Detections],
+ detections_out_zones: list[sv.Detections],
+ ) -> sv.Detections:
+ for zone_in_id, detections_in_zone in enumerate(detections_in_zones):
+ for tracker_id in detections_in_zone.tracker_id:
+ self.tracker_id_to_zone_id.setdefault(tracker_id, zone_in_id)
+
+ for zone_out_id, detections_out_zone in enumerate(detections_out_zones):
+ for tracker_id in detections_out_zone.tracker_id:
+ if tracker_id in self.tracker_id_to_zone_id:
+ zone_in_id = self.tracker_id_to_zone_id[tracker_id]
+ self.counts.setdefault(zone_out_id, {})
+ self.counts[zone_out_id].setdefault(zone_in_id, set())
+ self.counts[zone_out_id][zone_in_id].add(tracker_id)
+ if len(detections_all) > 0:
+ detections_all.class_id = np.vectorize(
+ lambda x: self.tracker_id_to_zone_id.get(x, -1)
+ )(detections_all.tracker_id)
+ else:
+ detections_all.class_id = np.array([], dtype=int)
+ return detections_all[detections_all.class_id != -1]
+
+
+def initiate_polygon_zones(
+ polygons: list[np.ndarray],
+ triggering_anchors: Iterable[sv.Position] = [sv.Position.CENTER],
+) -> list[sv.PolygonZone]:
+ return [
+ sv.PolygonZone(
+ polygon=polygon,
+ triggering_anchors=triggering_anchors,
+ )
+ for polygon in polygons
+ ]
+
+
+class VideoProcessor:
+ def __init__(
+ self,
+ source_weights_path: str,
+ source_video_path: str,
+ target_video_path: str | None = None,
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+ ) -> None:
+ self.conf_threshold = confidence_threshold
+ self.iou_threshold = iou_threshold
+ self.source_video_path = source_video_path
+ self.target_video_path = target_video_path
+
+ self.model = YOLO(source_weights_path)
+ self.tracker = sv.ByteTrack()
+
+ self.video_info = sv.VideoInfo.from_video_path(source_video_path)
+ 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.box_annotator = sv.BoxAnnotator(color=COLORS)
+ self.label_annotator = sv.LabelAnnotator(
+ color=COLORS, text_color=sv.Color.BLACK
+ )
+ self.trace_annotator = sv.TraceAnnotator(
+ color=COLORS, position=sv.Position.CENTER, trace_length=100, thickness=2
+ )
+ self.detections_manager = DetectionsManager()
+
+ def process_video(self) -> None:
+ frame_generator = sv.get_video_frames_generator(
+ source_path=self.source_video_path
+ )
+
+ if self.target_video_path:
+ with sv.VideoSink(self.target_video_path, self.video_info) as sink:
+ for frame in tqdm(frame_generator, total=self.video_info.total_frames):
+ annotated_frame = self.process_frame(frame)
+ sink.write_frame(annotated_frame)
+ else:
+ for frame in tqdm(frame_generator, total=self.video_info.total_frames):
+ annotated_frame = self.process_frame(frame)
+ cv2.imshow("Processed Video", annotated_frame)
+ if cv2.waitKey(1) & 0xFF == ord("q"):
+ break
+ cv2.destroyAllWindows()
+
+ def annotate_frame(
+ self, frame: np.ndarray, detections: sv.Detections
+ ) -> np.ndarray:
+ annotated_frame = frame.copy()
+ for i, (zone_in, zone_out) in enumerate(zip(self.zones_in, self.zones_out)):
+ annotated_frame = sv.draw_polygon(
+ annotated_frame, zone_in.polygon, COLORS.colors[i]
+ )
+ annotated_frame = sv.draw_polygon(
+ annotated_frame, zone_out.polygon, COLORS.colors[i]
+ )
+
+ labels = [f"#{tracker_id}" for tracker_id in detections.tracker_id]
+ annotated_frame = self.trace_annotator.annotate(annotated_frame, detections)
+ annotated_frame = self.box_annotator.annotate(annotated_frame, detections)
+ annotated_frame = self.label_annotator.annotate(
+ annotated_frame, detections, labels
+ )
+
+ for zone_out_id, zone_out in enumerate(self.zones_out):
+ zone_center = sv.get_polygon_center(polygon=zone_out.polygon)
+ if zone_out_id in self.detections_manager.counts:
+ counts = self.detections_manager.counts[zone_out_id]
+ for i, zone_in_id in enumerate(counts):
+ count = len(self.detections_manager.counts[zone_out_id][zone_in_id])
+ text_anchor = sv.Point(x=zone_center.x, y=zone_center.y + 40 * i)
+ annotated_frame = sv.draw_text(
+ scene=annotated_frame,
+ text=str(count),
+ text_anchor=text_anchor,
+ background_color=COLORS.colors[zone_in_id],
+ )
+
+ return annotated_frame
+
+ def process_frame(self, frame: np.ndarray) -> np.ndarray:
+ results = self.model(
+ frame, verbose=False, conf=self.conf_threshold, iou=self.iou_threshold
+ )[0]
+ detections = sv.Detections.from_ultralytics(results)
+ detections.class_id = np.zeros(len(detections))
+ detections = self.tracker.update_with_detections(detections)
+
+ detections_in_zones = []
+ detections_out_zones = []
+
+ for zone_in, zone_out in zip(self.zones_in, self.zones_out):
+ detections_in_zone = detections[zone_in.trigger(detections=detections)]
+ detections_in_zones.append(detections_in_zone)
+ detections_out_zone = detections[zone_out.trigger(detections=detections)]
+ detections_out_zones.append(detections_out_zone)
+
+ detections = self.detections_manager.update(
+ detections, detections_in_zones, detections_out_zones
+ )
+ return self.annotate_frame(frame, detections)
+
+
+def main(
+ source_weights_path: str,
+ source_video_path: str,
+ target_video_path: str,
+ confidence_threshold: float = 0.3,
+ iou_threshold: float = 0.7,
+) -> None:
+ """
+ Traffic Flow Analysis with YOLO and ByteTrack.
+
+ Args:
+ source_weights_path: Path to the source weights file
+ source_video_path: Path to the source video file
+ target_video_path: Path to the target video file (output)
+ confidence_threshold: Confidence threshold for the model
+ iou_threshold: IOU threshold for the model
+ """
+ processor = VideoProcessor(
+ source_weights_path=source_weights_path,
+ source_video_path=source_video_path,
+ target_video_path=target_video_path,
+ confidence_threshold=confidence_threshold,
+ iou_threshold=iou_threshold,
+ )
+ processor.process_video()
+
+
+if __name__ == "__main__":
+ from jsonargparse import auto_cli, set_parsing_settings
+
+ set_parsing_settings(parse_optionals_as_positionals=True)
+ auto_cli(main, as_positional=False)
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..df8e2f6
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,219 @@
+site_name: Supervision
+site_url: https://supervision.roboflow.com/
+site_author: Roboflow
+site_description: "Model-agnostic Python library for computer vision. Annotate, track, filter, and export detections from supported model outputs including Ultralytics, Roboflow Inference, Transformers, SAM, Detectron2, MMDetection, and VLM parsers."
+repo_name: roboflow/supervision
+edit_uri: https://github.com/roboflow/supervision/tree/main/docs
+copyright: Roboflow 2026. All rights reserved.
+
+extra:
+ social:
+ - icon: fontawesome/brands/github
+ link: https://github.com/roboflow
+ - icon: fontawesome/brands/python
+ link: https://pypi.org/project/supervision
+ - icon: fontawesome/brands/youtube
+ link: https://www.youtube.com/roboflow
+ - icon: fontawesome/brands/x-twitter
+ link: https://twitter.com/roboflow
+ - icon: fontawesome/brands/discord
+ link: https://discord.gg/GbfgXGJ8Bk
+ analytics:
+ provider: google
+ property: G-P7ZG0Y19G5
+ version:
+ provider: mike
+
+extra_css:
+ - stylesheets/extra.css
+ - stylesheets/cookbooks_card.css
+
+nav:
+ - Home: index.md
+ - About: about.md
+ - Contact: contact.md
+ - FAQ: faq.md
+ - Learn:
+ - Detect and Annotate: how_to/detect_and_annotate.md
+ - Save Detections: how_to/save_detections.md
+ - 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
+ - Benchmark a Model: how_to/benchmark_a_model.md
+ - Count in Zone: how_to/count_in_zone.md
+ - Use Compact Masks: how_to/use_compact_masks.md
+ - Reference:
+ - Detection and Segmentation:
+ - Core: detection/core.md
+ - Annotators: detection/annotators.md
+ - Compact Mask: detection/compact_mask.md
+ - Converters: detection/utils/converters.md
+ - IoU and NMS: detection/utils/iou_and_nms.md
+ - Boxes: detection/utils/boxes.md
+ - Masks: detection/utils/masks.md
+ - Polygons: detection/utils/polygons.md
+ - VLM Utils: detection/utils/vlms.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
+ - Metrics:
+ - mAP: metrics/mean_average_precision.md
+ - mAR: metrics/mean_average_recall.md
+ - Precision: metrics/precision.md
+ - Recall: metrics/recall.md
+ - F1 Score: metrics/f1_score.md
+ - Common Values: metrics/common_values.md
+ - Legacy Metrics: detection/metrics.md
+ - Utils:
+ - Conversion: utils/conversion.md
+ - 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
+ - Contributing: contributing.md
+ - Code of Conduct: code_of_conduct.md
+ - License: license.md
+ - Changelog:
+ - Changelog: changelog.md
+ - Deprecated: deprecated.md
+
+theme:
+ name: "material"
+ icon:
+ edit: material/pencil
+ logo: assets/supervision-lenny.png
+ favicon: assets/supervision-lenny.png
+ custom_dir: docs/theme
+ features:
+ - navigation.tracking
+ - content.code.copy
+ - content.action.edit
+ - content.tooltips
+ - content.code.annotate
+ - navigation.tabs
+ - navigation.tabs.sticky
+
+ palette:
+ - scheme: default
+ primary: "custom"
+ toggle:
+ icon: material/brightness-7
+ name: Switch to dark mode
+ - scheme: slate
+ primary: "custom"
+ toggle:
+ icon: material/brightness-4
+ name: Switch to light mode
+
+ font:
+ text: Inter
+ code: IBM Plex Mono
+
+plugins:
+ - search
+ - mkdocs-jupyter:
+ kernel_name: python3
+ execute: false
+ include_source: True
+ include_requirejs: true
+ - mkdocstrings:
+ default_handler: python
+ handlers:
+ python:
+ paths: [supervision]
+ load_external_modules: true
+ options:
+ parameter_headings: true
+ allow_inspection: true
+ show_bases: true
+ group_by_category: true
+ docstring_style: google
+ show_symbol_type_heading: true
+ show_root_heading: True
+ show_symbol_type_toc: true
+ show_category_heading: true
+ show_signature_annotations: true
+ show_docstring_examples: true
+ inventories:
+ - url: https://docs.python-requests.org/en/master/objects.inv
+ domains: [std, py]
+ - git-committers:
+ repository: roboflow/supervision
+ branch: develop
+ token: !ENV ["GITHUB_TOKEN"]
+ - git-revision-date-localized:
+ enable_creation_date: true
+
+markdown_extensions:
+ - admonition
+ - pymdownx.details
+
+ # uses Pygments + prompt stripping
+ - pymdownx.superfences:
+ custom_fences:
+ - name: pycon
+ class: pycon
+ lexer: pycon
+ format: !!python/name:pymdownx.superfences.fence_code_format
+
+ - pymdownx.inlinehilite
+ - attr_list
+ - md_in_html
+ - pymdownx.tabbed:
+ alternate_style: true
+ - toc:
+ permalink: true
+ - pymdownx.emoji:
+ emoji_index: !!python/name:material.extensions.emoji.twemoji
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg
+ - pymdownx.snippets:
+ check_paths: true
+
+ # Pygments pycon lexer with prompt stripping enabled
+ - pymdownx.highlight:
+ line_spans: __span
+ anchor_linenums: true
+ pygments_lang_class: true
+ linenums: false
+ auto_title: false
+ extend_pygments_lang:
+ - name: pycon
+ lang: pycon
+ options:
+ strip_prompt: true
+ strip_continuation_prompt: true
+
+ - pymdownx.arithmatex:
+ generic: true
+
+extra_javascript:
+ - "javascripts/init_kapa_widget.js"
+ - "javascripts/cookbooks-card.js"
+ - "javascripts/pycon_copy.js"
+ - "javascripts/segment.js"
+ - "javascripts/mathjax.js"
+ - "https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.8/purify.min.js"
+ - "https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js"
+
+validation:
+ nav:
+ absolute_links: ignore
+ links:
+ absolute_links: ignore
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..4972264
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,211 @@
+[build-system]
+build-backend = "setuptools.build_meta"
+requires = [ "setuptools>=61" ]
+
+[project]
+name = "supervision"
+version = "0.30.0.dev"
+description = "A set of easy-to-use utils that will come in handy in any Computer Vision project"
+readme = "README.md"
+keywords = [
+ "AI",
+ "deep-learning",
+ "DL",
+ "machine-learning",
+ "ML",
+ "Roboflow",
+ "vision",
+]
+license = "MIT"
+maintainers = [
+ { name = "Piotr Skalski", email = "piotr@roboflow.com" },
+]
+authors = [
+ { name = "Roboflow et al.", email = "develop@roboflow.com" },
+]
+requires-python = ">=3.10"
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Education",
+ "Intended Audience :: Science/Research",
+ "Operating System :: MacOS",
+ "Operating System :: Microsoft :: Windows",
+ "Operating System :: POSIX :: Linux",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
+ "Topic :: Multimedia :: Graphics",
+ "Topic :: Multimedia :: Video",
+ "Topic :: Scientific/Engineering",
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
+ "Topic :: Scientific/Engineering :: Image Recognition",
+ "Topic :: Software Development",
+ "Typing :: Typed",
+]
+dependencies = [
+ "defusedxml>=0.7.1",
+ "matplotlib>=3.6",
+ "numpy>=1.21.2",
+ "opencv-python>=4.5.5.64",
+ "pillow>=9.4",
+ "pydeprecate>=0.9,<0.11",
+ "pyyaml>=5.3",
+ "requests>=2.26",
+ "scipy>=1.10",
+ "tqdm>=4.62.3"
+]
+optional-dependencies.geotiff = [
+ "rasterio>=1.3", # 1.3 introduced stable window-read API and CRS.is_projected
+]
+optional-dependencies.metrics = [
+ "pandas>=2",
+]
+urls.Documentation = "https://supervision.roboflow.com/latest/"
+urls.Homepage = "https://github.com/roboflow/supervision"
+urls.Repository = "https://github.com/roboflow/supervision"
+
+[dependency-groups]
+dev = [
+ "docutils!=0.21",
+ "ipywidgets>=8.1.1",
+ "jupytext>=1.16.1",
+ "nbconvert>=7.14.2",
+ "notebook>=6.5.3,<8",
+ "pre-commit>=3.8",
+ "pytest>=7.2.2,<10",
+ "pytest-cov>=4,<8",
+ "tox>=4.11.4",
+ "types-tqdm",
+]
+docs = [
+ "mike>=2",
+ "mkdocs-git-committers-plugin-2>=2.4.1; python_version>='3.10' and python_version<'4'",
+ "mkdocs-git-revision-date-localized-plugin>=1.2.4",
+ "mkdocs-jupyter>=0.24.3",
+ "mkdocs-material[imaging]>=9.7",
+ "mkdocstrings>=1,<1.1",
+ "mkdocstrings-python>=2,<3",
+]
+build = [
+ "build>=1,<1.6",
+ "twine>=5.1.1,<7",
+ "wheel>=0.40,<0.48",
+]
+
+[tool.setuptools]
+packages.find.where = [ "src" ]
+packages.find.include = [ "supervision*" ]
+include-package-data = false
+package-data.supervision = [ "py.typed" ]
+# exclude = [ "docs*", "tests*", "examples*" ]
+
+[tool.ruff]
+target-version = "py310"
+line-length = 88
+indent-width = 4
+# Exclude a variety of commonly ignored directories.
+exclude = [
+ ".bzr",
+ ".direnv",
+ ".eggs",
+ ".git",
+ ".git-rewrite",
+ ".hg",
+ ".mypy_cache",
+ ".nox",
+ ".pants.d",
+ ".pytype",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ "__pypackages__",
+ "_build",
+ "buck-out",
+ "build",
+ "dist",
+ "docs",
+ "node_modules",
+ "venv",
+ "yarn-error.log",
+ "yarn.lock",
+]
+# Like Black, indent with spaces, rather than tabs.
+format.indent-style = "space"
+# Like Black, use double quotes for strings.
+format.quote-style = "double"
+# Like Black, automatically detect the appropriate line ending.
+format.line-ending = "auto"
+# Like Black, respect magic trailing commas.
+format.skip-magic-trailing-comma = false
+# Enable linting rules for code style, imports, and best practices.
+lint.select = [
+ "A", # flake8-builtins - https://docs.astral.sh/ruff/rules/#flake8-builtins-a
+ "E", # pycodestyle errors - https://docs.astral.sh/ruff/rules/#error-e
+ "F", # Pyflakes - https://docs.astral.sh/ruff/rules/#pyflakes-f
+ "I", # isort - https://docs.astral.sh/ruff/rules/#isort-i
+ "PT", # pytest - https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt
+ "Q", # flake8-quotes - https://docs.astral.sh/ruff/rules/#flake8-quotes-q
+ "RUF", # Ruff-specific rules - https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf
+ "S", # bandit - https://docs.astral.sh/ruff/rules/#flake8-bandit-s
+ "UP", # pyupgrade - https://docs.astral.sh/ruff/rules/#pyupgrade-up
+ "W", # pycodestyle warnings - https://docs.astral.sh/ruff/rules/#pycodestyle-w
+]
+lint.ignore = []
+lint.per-file-ignores."__init__.py" = [ "E402", "F401" ]
+lint.per-file-ignores."notebooks/**" = [
+ "PT018", # Assertion should be broken down into multiple parts
+ "S101", # Use of `assert` detected
+]
+lint.per-file-ignores."src/**" = [
+ "S101", # TODO: Replace asserts with proper error handling
+]
+lint.per-file-ignores."tests/**" = [
+ "S101", # Use of `assert` detected
+]
+lint.unfixable = []
+# Allow unused variables when underscore-prefixed.
+lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
+lint.flake8-quotes.docstring-quotes = "double"
+lint.flake8-quotes.inline-quotes = "double"
+lint.flake8-quotes.multiline-quotes = "double"
+lint.isort.no-sections = false
+lint.isort.order-by-type = true
+# Flag errors (`C901`) whenever the complexity level exceeds 5.
+lint.mccabe.max-complexity = 20
+lint.pydocstyle.convention = "google"
+lint.pylint.max-args = 20
+
+[tool.codespell]
+ignore-words-list = "STrack,sTrack,strack"
+skip = "*.ipynb"
+count = true
+quiet-level = 3
+
+[tool.mypy]
+mypy_path = "src"
+explicit_package_bases = true
+ignore_missing_imports = false
+python_version = "3.10"
+strict = true
+overrides = [ { module = [ "examples.*", "tests.*" ], ignore_errors = true } ]
+
+[tool.pytest]
+ini_options.testpaths = [ "src", "tests" ]
+ini_options.norecursedirs = [ ".git", ".venv", "build", "dist", "docs", "examples", "notebooks" ]
+ini_options.addopts = [
+ "--doctest-modules",
+ "--color=yes",
+]
+ini_options.filterwarnings = [
+ "error::DeprecationWarning",
+]
+ini_options.doctest_optionflags = "ELLIPSIS NORMALIZE_WHITESPACE"
+
+[tool.autoflake]
+check = true
+imports = [ "cv2", "supervision" ]
diff --git a/src/supervision/__init__.py b/src/supervision/__init__.py
new file mode 100644
index 0000000..a3f2999
--- /dev/null
+++ b/src/supervision/__init__.py
@@ -0,0 +1,309 @@
+import importlib.metadata as importlib_metadata
+from typing import TYPE_CHECKING, Any
+
+try:
+ # This will read version from pyproject.toml
+ __version__ = importlib_metadata.version(__package__ or __name__)
+except importlib_metadata.PackageNotFoundError:
+ __version__ = "development"
+
+from supervision.annotators.core import (
+ BackgroundOverlayAnnotator,
+ BlurAnnotator,
+ BoxAnnotator,
+ BoxCornerAnnotator,
+ CircleAnnotator,
+ ColorAnnotator,
+ ComparisonAnnotator,
+ CropAnnotator,
+ DotAnnotator,
+ EllipseAnnotator,
+ HaloAnnotator,
+ HeatMapAnnotator,
+ IconAnnotator,
+ LabelAnnotator,
+ MaskAnnotator,
+ OrientedBoxAnnotator,
+ PercentageBarAnnotator,
+ PixelateAnnotator,
+ PolygonAnnotator,
+ RichLabelAnnotator,
+ RoundBoxAnnotator,
+ TraceAnnotator,
+ TriangleAnnotator,
+)
+from supervision.annotators.utils import (
+ ColorLookup,
+ hex_to_rgba,
+ is_valid_hex,
+ rgba_to_hex,
+)
+from supervision.classification.core import Classifications
+from supervision.dataset.core import (
+ BaseDataset,
+ ClassificationDataset,
+ DetectionDataset,
+)
+from supervision.dataset.formats.coco import get_coco_class_index_mapping
+from supervision.detection.compact_mask import CompactMask
+from supervision.detection.core import Detections
+from supervision.detection.line_zone import (
+ LineZone,
+ LineZoneAnnotator,
+ LineZoneAnnotatorMulticlass,
+)
+from supervision.detection.tools.csv_sink import CSVSink
+from supervision.detection.tools.inference_slicer import (
+ InferenceSlicer,
+ WindowedRasterDataset,
+)
+from supervision.detection.tools.json_sink import JSONSink
+from supervision.detection.tools.polygon_zone import PolygonZone, PolygonZoneAnnotator
+from supervision.detection.tools.smoother import DetectionsSmoother
+from supervision.detection.utils.boxes import (
+ clip_boxes,
+ denormalize_boxes,
+ move_boxes,
+ pad_boxes,
+ scale_boxes,
+ xyxyxyxy_to_xyxy,
+)
+from supervision.detection.utils.converters import (
+ is_compressed_rle,
+ mask_to_polygons,
+ mask_to_rle,
+ mask_to_xyxy,
+ polygon_to_mask,
+ polygon_to_xyxy,
+ rle_to_mask,
+ xcycwh_to_xyxy,
+ xywh_to_xyxy,
+ xyxy_to_mask,
+ xyxy_to_polygons,
+ xyxy_to_xcycarh,
+ xyxy_to_xywh,
+)
+from supervision.detection.utils.iou_and_nms import (
+ OverlapFilter,
+ OverlapMetric,
+ box_iou,
+ box_iou_batch,
+ box_iou_batch_with_jaccard,
+ box_non_max_merge,
+ box_non_max_suppression,
+ mask_iou_batch,
+ mask_non_max_merge,
+ mask_non_max_suppression,
+ oriented_box_iou_batch,
+ oriented_box_non_max_merge,
+ oriented_box_non_max_suppression,
+)
+from supervision.detection.utils.masks import (
+ calculate_masks_centroids,
+ contains_holes,
+ contains_multiple_segments,
+ filter_segments_by_distance,
+ mask_to_roi,
+ move_masks,
+)
+from supervision.detection.utils.polygons import (
+ approximate_polygon,
+ filter_polygons_by_area,
+)
+from supervision.detection.utils.vlms import edit_distance, fuzzy_match_index
+from supervision.detection.vlm import LMM, VLM
+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,
+ draw_polygon,
+ draw_rectangle,
+ draw_text,
+)
+from supervision.geometry.core import Point, Position, Rect
+from supervision.geometry.utils import get_polygon_center
+from supervision.key_points.annotators import (
+ EdgeAnnotator,
+ VertexAnnotator,
+ VertexEllipseAnnotator,
+ VertexEllipseAreaAnnotator,
+ VertexEllipseHaloAnnotator,
+ VertexEllipseOutlineAnnotator,
+ VertexLabelAnnotator,
+)
+from supervision.key_points.core import KeyPoints
+from supervision.metrics.detection import ConfusionMatrix, MeanAveragePrecision
+from supervision.utils.conversion import cv2_to_pillow, pillow_to_cv2
+from supervision.utils.file import list_files_with_extensions
+from supervision.utils.image import (
+ ImageSink,
+ crop_image,
+ get_image_resolution_wh,
+ grayscale_image,
+ letterbox_image,
+ overlay_image,
+ resize_image,
+ scale_image,
+ tint_image,
+)
+from supervision.utils.notebook import plot_image, plot_images_grid
+from supervision.utils.video import (
+ FPSMonitor,
+ VideoInfo,
+ VideoSink,
+ get_video_frames_generator,
+ process_video,
+)
+
+if TYPE_CHECKING:
+ from supervision.tracker.byte_tracker.core import ByteTrack
+
+__all__ = [
+ "LMM",
+ "VLM",
+ "BackgroundOverlayAnnotator",
+ "BaseDataset",
+ "BlurAnnotator",
+ "BoxAnnotator",
+ "BoxCornerAnnotator",
+ "ByteTrack",
+ "CSVSink",
+ "CircleAnnotator",
+ "ClassificationDataset",
+ "Classifications",
+ "Color",
+ "ColorAnnotator",
+ "ColorLookup",
+ "ColorPalette",
+ "CompactMask",
+ "ComparisonAnnotator",
+ "ConfusionMatrix",
+ "CropAnnotator",
+ "DetectionDataset",
+ "Detections",
+ "DetectionsSmoother",
+ "DotAnnotator",
+ "EdgeAnnotator",
+ "EllipseAnnotator",
+ "FPSMonitor",
+ "HaloAnnotator",
+ "HeatMapAnnotator",
+ "IconAnnotator",
+ "ImageSink",
+ "InferenceSlicer",
+ "JSONSink",
+ "KeyPoints",
+ "LabelAnnotator",
+ "LineZone",
+ "LineZoneAnnotator",
+ "LineZoneAnnotatorMulticlass",
+ "MaskAnnotator",
+ "MeanAveragePrecision",
+ "OrientedBoxAnnotator",
+ "OverlapFilter",
+ "OverlapMetric",
+ "PercentageBarAnnotator",
+ "PixelateAnnotator",
+ "Point",
+ "PolygonAnnotator",
+ "PolygonZone",
+ "PolygonZoneAnnotator",
+ "Position",
+ "Rect",
+ "RichLabelAnnotator",
+ "RoundBoxAnnotator",
+ "TraceAnnotator",
+ "TriangleAnnotator",
+ "VertexAnnotator",
+ "VertexEllipseAnnotator",
+ "VertexEllipseAreaAnnotator",
+ "VertexEllipseHaloAnnotator",
+ "VertexEllipseOutlineAnnotator",
+ "VertexLabelAnnotator",
+ "VideoInfo",
+ "VideoSink",
+ "WindowedRasterDataset",
+ "approximate_polygon",
+ "box_iou",
+ "box_iou_batch",
+ "box_iou_batch_with_jaccard",
+ "box_non_max_merge",
+ "box_non_max_suppression",
+ "calculate_masks_centroids",
+ "calculate_optimal_line_thickness",
+ "calculate_optimal_text_scale",
+ "clip_boxes",
+ "contains_holes",
+ "contains_multiple_segments",
+ "crop_image",
+ "cv2_to_pillow",
+ "denormalize_boxes",
+ "draw_filled_polygon",
+ "draw_filled_rectangle",
+ "draw_image",
+ "draw_line",
+ "draw_polygon",
+ "draw_rectangle",
+ "draw_text",
+ "edit_distance",
+ "filter_polygons_by_area",
+ "filter_segments_by_distance",
+ "fuzzy_match_index",
+ "get_coco_class_index_mapping",
+ "get_image_resolution_wh",
+ "get_polygon_center",
+ "get_video_frames_generator",
+ "grayscale_image",
+ "hex_to_rgba",
+ "is_compressed_rle",
+ "is_valid_hex",
+ "letterbox_image",
+ "list_files_with_extensions",
+ "mask_iou_batch",
+ "mask_non_max_merge",
+ "mask_non_max_suppression",
+ "mask_to_polygons",
+ "mask_to_rle",
+ "mask_to_roi",
+ "mask_to_xyxy",
+ "move_boxes",
+ "move_masks",
+ "oriented_box_iou_batch",
+ "oriented_box_non_max_merge",
+ "oriented_box_non_max_suppression",
+ "overlay_image",
+ "pad_boxes",
+ "pillow_to_cv2",
+ "plot_image",
+ "plot_images_grid",
+ "polygon_to_mask",
+ "polygon_to_xyxy",
+ "process_video",
+ "resize_image",
+ "rgba_to_hex",
+ "rle_to_mask",
+ "scale_boxes",
+ "scale_image",
+ "tint_image",
+ "xcycwh_to_xyxy",
+ "xywh_to_xyxy",
+ "xyxy_to_mask",
+ "xyxy_to_polygons",
+ "xyxy_to_xcycarh",
+ "xyxy_to_xywh",
+ "xyxyxyxy_to_xyxy",
+]
+
+
+def __getattr__(name: str) -> Any:
+ """Lazily resolve deprecated compatibility exports."""
+ if name == "ByteTrack":
+ from supervision.tracker.byte_tracker.core import ByteTrack as byte_track
+
+ globals()[name] = byte_track
+ return byte_track
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/src/supervision/annotators/__init__.py b/src/supervision/annotators/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/supervision/annotators/base.py b/src/supervision/annotators/base.py
new file mode 100644
index 0000000..d8f6f2c
--- /dev/null
+++ b/src/supervision/annotators/base.py
@@ -0,0 +1,21 @@
+from abc import ABC, abstractmethod
+from typing import Any
+
+from supervision.detection.core import Detections
+
+
+class BaseAnnotator(ABC):
+ """Base class for annotators that consume :class:`Detections`.
+
+ Attributes:
+ requires_mask: Whether integrations must provide ``Detections.mask`` for
+ this annotator. Check this before materializing expensive mask payloads.
+ """
+
+ requires_mask: bool = False
+
+ @abstractmethod
+ def annotate(
+ self, scene: Any, detections: Detections, *args: Any, **kwargs: Any
+ ) -> Any:
+ pass
diff --git a/src/supervision/annotators/core.py b/src/supervision/annotators/core.py
new file mode 100644
index 0000000..14fb9ea
--- /dev/null
+++ b/src/supervision/annotators/core.py
@@ -0,0 +1,3536 @@
+from collections.abc import Iterator
+from functools import lru_cache
+from math import sqrt
+from typing import Any, ClassVar, cast
+
+import cv2
+import numpy as np
+import numpy.typing as npt
+from deprecate import deprecated, void # type: ignore[import-untyped,unused-ignore]
+from PIL import Image, ImageDraw, ImageFont
+from scipy.interpolate import splev, splprep
+
+from supervision.annotators.base import BaseAnnotator
+from supervision.annotators.utils import (
+ PENDING_TRACK_ID,
+ ColorLookup,
+ Trace,
+ _validate_labels,
+ calculate_dynamic_kernel_size,
+ calculate_dynamic_pixel_size,
+ get_labels_text,
+ hex_to_rgba,
+ resolve_color,
+ resolve_text_background_xyxy,
+ snap_boxes,
+ wrap_text,
+)
+from supervision.config import ORIENTED_BOX_COORDINATES
+from supervision.detection.compact_mask import CompactMask
+from supervision.detection.core import Detections
+from supervision.detection.utils.boxes import clip_boxes, spread_out_boxes
+from supervision.detection.utils.converters import (
+ mask_to_polygons,
+ polygon_to_mask,
+ xyxy_to_polygons,
+)
+from supervision.detection.utils.masks import _masks_to_roi
+from supervision.draw.base import ImageType
+from supervision.draw.color import Color, ColorPalette
+from supervision.draw.utils import draw_polygon, draw_rounded_rectangle, draw_text
+from supervision.geometry.core import Point, Position, Rect
+from supervision.utils.conversion import (
+ ensure_cv2_image_for_class_method,
+ ensure_pil_image_for_class_method,
+)
+from supervision.utils.image import (
+ _overlay_image,
+ crop_image,
+ letterbox_image,
+ scale_image,
+)
+from supervision.utils.logger import _get_logger
+
+logger = _get_logger(__name__)
+
+
+@lru_cache
+def _load_icon_from_path(
+ icon_path: str, icon_resolution_wh: tuple[int, int]
+) -> npt.NDArray[np.uint8]:
+ """Load and resize an icon image through a cache shared by annotators."""
+ icon = cv2.imread(icon_path, cv2.IMREAD_UNCHANGED)
+ if icon is None:
+ raise FileNotFoundError(f"Error: Couldn't load the icon image from {icon_path}")
+ icon_array = cast(npt.NDArray[np.uint8], icon)
+ result: npt.NDArray[np.uint8] = letterbox_image(
+ image=icon_array, resolution_wh=icon_resolution_wh
+ )
+ return result
+
+
+def _normalize_color_input(color: Color | ColorPalette | str) -> Color | ColorPalette:
+ """Normalize accepted color inputs to internal color objects.
+
+ Accepts `Color`, `ColorPalette`, or hex string input. Hex strings are parsed via
+ `hex_to_rgba` and converted to `Color` (alpha channel is ignored because annotator
+ drawing uses RGB/BGR colors).
+ """
+ if isinstance(color, str):
+ r, g, b, _ = hex_to_rgba(color)
+ return Color.from_rgb_tuple((r, g, b))
+ return color
+
+
+CV2_FONT = cv2.FONT_HERSHEY_SIMPLEX
+
+
+class _BaseLabelAnnotator(BaseAnnotator):
+ """
+ Base class for annotators that add labels to detections.
+
+ Attributes:
+ color: The color to use for the label background.
+ color_lookup: The method used to determine the color of the label.
+ text_color: The color to use for the label text.
+ text_padding: The padding around the label text, in pixels.
+ text_anchor: The position of the text relative to the detection
+ bounding box.
+ text_offset: A tuple of 2D coordinates `(x, y)` to
+ offset the text position from the anchor point, in pixels.
+ border_radius: The radius of the label background corners, in pixels.
+ smart_position: Whether to intelligently adjust the label position to
+ avoid overlapping with other elements.
+ max_line_length: Maximum number of characters per line before
+ wrapping the text. None means no wrapping.
+ """
+
+ def __init__(
+ self,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ text_color: Color | ColorPalette | str = Color.WHITE,
+ text_padding: int = 10,
+ text_position: Position = Position.TOP_LEFT,
+ text_offset: tuple[int, int] = (0, 0),
+ border_radius: int = 0,
+ smart_position: bool = False,
+ max_line_length: int | None = None,
+ ):
+ """
+ Initializes the _BaseLabelAnnotator.
+
+ Args:
+ color: The color to use for the label
+ background.
+ color_lookup: The method used to determine the color
+ of the label
+ text_color: The color to use for the
+ label text.
+ text_padding: The padding around the label text, in pixels.
+ text_position: The position of the text relative to the
+ detection bounding box.
+ text_offset: A tuple of 2D coordinates
+ `(x, y)` to offset the text position from the anchor point, in pixels.
+ border_radius: The radius of the label background corners,
+ in pixels.
+ smart_position: Whether to intelligently adjust the label
+ position to avoid overlapping with other elements.
+ max_line_length: Maximum number of characters per
+ line before wrapping the text. None means no wrapping.
+ """
+ self.color: Color | ColorPalette = _normalize_color_input(color)
+ self.color_lookup: ColorLookup = color_lookup
+ self.text_color: Color | ColorPalette = _normalize_color_input(text_color)
+ self.text_padding: int = text_padding
+ self.text_anchor: Position = text_position
+ self.text_offset: tuple[int, int] = text_offset
+ self.border_radius: int = border_radius
+ self.smart_position = smart_position
+ self.max_line_length: int | None = max_line_length
+
+ def _adjust_labels_in_frame(
+ self,
+ resolution_wh: tuple[int, int],
+ labels: list[str],
+ label_properties: npt.NDArray[np.float32],
+ ) -> npt.NDArray[np.float32]:
+ """
+ Adjusts the position of labels to ensure they stay within the frame boundaries.
+
+ Args:
+ resolution_wh: The width and height of the frame.
+ labels: The list of text labels.
+ label_properties: An array of label properties, where each row
+ contains [x1, y1, x2, y2, text_height, ...].
+
+ Returns:
+ The adjusted label properties.
+ """
+ adjusted_properties = label_properties.copy()
+
+ # First, make sure the boxes don't go outside the frame
+ adjusted_properties[:, :4] = snap_boxes(
+ adjusted_properties[:, :4],
+ resolution_wh,
+ )
+
+ # Apply the spread out algorithm to avoid box overlaps
+ if len(labels) > 1:
+ # Extract the box coordinates
+ boxes = adjusted_properties[:, :4]
+ # Use the spread_out_boxes function to adjust overlapping boxes
+ spread_boxes = spread_out_boxes(boxes)
+ # Update the properties with the spread out boxes
+ adjusted_properties[:, :4] = spread_boxes
+
+ # Additional check to ensure boxes are still within frame after spreading
+ adjusted_properties[:, :4] = snap_boxes(
+ adjusted_properties[:, :4], resolution_wh
+ )
+
+ return cast(
+ npt.NDArray[np.float32],
+ np.asarray(adjusted_properties, dtype=np.float32),
+ )
+
+
+class BoxAnnotator(BaseAnnotator):
+ """
+ A class for drawing bounding boxes on an image using provided detections.
+ """
+
+ def __init__(
+ self,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ thickness: int = 2,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ ):
+ """
+ Args:
+ color: The color or color palette to use for
+ annotating detections.
+ thickness: Thickness of the bounding box lines.
+ color_lookup: Strategy for mapping colors to annotations.
+ Options are `INDEX`, `CLASS`, `TRACK`.
+ """
+ self.color: Color | ColorPalette = _normalize_color_input(color)
+ self.thickness: int = thickness
+ self.color_lookup: ColorLookup = color_lookup
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ ) -> ImageType:
+ """
+ Annotates the given scene with bounding boxes based on the provided detections.
+
+ Args:
+ scene: The image where bounding boxes will be drawn. `ImageType`
+ is a flexible type, accepting either `numpy.ndarray` or
+ `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((100, 100, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... class_id=np.array([0])
+ ... )
+ >>> box_annotator = sv.BoxAnnotator()
+ >>> annotated_frame = box_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections
+ ... )
+
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ for detection_idx in range(len(detections)):
+ x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
+ color = resolve_color(
+ color=self.color,
+ detections=detections,
+ detection_idx=detection_idx,
+ color_lookup=self.color_lookup
+ if custom_color_lookup is None
+ else custom_color_lookup,
+ )
+ cv2.rectangle(
+ img=scene,
+ pt1=(x1, y1),
+ pt2=(x2, y2),
+ color=color.as_bgr(),
+ thickness=self.thickness,
+ )
+ return scene
+
+
+class OrientedBoxAnnotator(BaseAnnotator):
+ """
+ A class for drawing oriented bounding boxes on an image using provided detections.
+ """
+
+ def __init__(
+ self,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ thickness: int = 2,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ ):
+ """
+ Args:
+ color: The color or color palette to use for
+ annotating detections.
+ thickness: Thickness of the bounding box lines.
+ color_lookup: Strategy for mapping colors to annotations.
+ Options are `INDEX`, `CLASS`, `TRACK`.
+ """
+ self.color: Color | ColorPalette = _normalize_color_input(color)
+ self.thickness: int = thickness
+ self.color_lookup: ColorLookup = color_lookup
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ ) -> ImageType:
+ """
+ Annotates the given scene with oriented bounding boxes based on the
+ provided detections.
+
+ Args:
+ scene: The image where bounding boxes will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Example:
+ ```python
+ import cv2
+ import supervision as sv
+ from ultralytics import YOLO
+
+ image = cv2.imread("")
+ model = YOLO("yolov8n-obb.pt")
+
+ result = model(image)[0]
+ detections = sv.Detections.from_ultralytics(result)
+
+ oriented_box_annotator = sv.OrientedBoxAnnotator()
+ annotated_frame = oriented_box_annotator.annotate(
+ scene=image.copy(),
+ detections=detections
+ )
+ ```
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ if detections.data is None or ORIENTED_BOX_COORDINATES not in detections.data:
+ return scene
+ obb_boxes = np.array(detections.data[ORIENTED_BOX_COORDINATES]).astype(int)
+
+ for detection_idx in range(len(detections)):
+ obb = obb_boxes[detection_idx]
+ color = resolve_color(
+ color=self.color,
+ detections=detections,
+ detection_idx=detection_idx,
+ color_lookup=self.color_lookup
+ if custom_color_lookup is None
+ else custom_color_lookup,
+ )
+
+ cv2.drawContours(scene, [obb], 0, color.as_bgr(), self.thickness)
+
+ return scene
+
+
+# --- Shared mask-painting utilities ---
+def _iter_mask_crops(
+ detections: Detections,
+) -> Iterator[tuple[int, npt.NDArray[np.bool_], npt.NDArray[np.int32] | None]]:
+ """Yield ``(detection_idx, mask_or_crop, offset_or_None)`` for each mask.
+
+ Encapsulates the ``CompactMask`` vs dense dispatch so individual annotators
+ do not need inline ``isinstance`` checks. For ``CompactMask`` inputs yields
+ the bbox crop and its ``(x1, y1)`` image-space origin; for dense masks
+ yields the full-frame boolean slice with ``offset=None``.
+
+ Args:
+ detections: Object detections whose masks to iterate.
+
+ Yields:
+ Tuple of ``(detection_idx, mask_or_crop, offset_or_None)``.
+ ``mask_or_crop`` is boolean (crop-sized for ``CompactMask``, full-frame
+ for dense). ``offset_or_None`` is an int32 ``(x1, y1)`` array for
+ cropโimage translation, or ``None`` for dense masks.
+ """
+ masks = detections.mask
+ if masks is None:
+ return
+ # TODO: replace isinstance dispatch with a MaskLike Protocol (separate PR)
+ compact_mask = masks if isinstance(masks, CompactMask) else None
+ for detection_idx in range(len(detections)):
+ if compact_mask is None:
+ yield (
+ detection_idx,
+ cast(npt.NDArray[np.bool_], masks[detection_idx]),
+ None,
+ )
+ else:
+ yield (
+ detection_idx,
+ compact_mask.crop(detection_idx),
+ compact_mask.offsets[detection_idx],
+ )
+
+
+def _paint_masks_by_area(
+ canvas: npt.NDArray[np.uint8],
+ detections: Detections,
+ color: Color | ColorPalette,
+ color_lookup: ColorLookup | npt.NDArray[np.int_],
+ collect_union: bool = False,
+ canvas_origin: tuple[int, int] = (0, 0),
+) -> npt.NDArray[np.bool_] | None:
+ """Paint each detection's mask into `canvas` in descending-area order.
+
+ Smaller masks are drawn on top of larger ones. `CompactMask` detections
+ are painted into their bounding-box crop only, avoiding a full `(H, W)`
+ allocation per mask; dense masks fall back to full-frame boolean indexing.
+
+ Args:
+ canvas: BGR image array painted in place. Shape ``(H, W, 3)``.
+ detections: Detections whose masks to paint. Returns immediately
+ without modifying `canvas` when ``detections.mask`` is ``None``.
+ color: Single color or palette used to resolve each detection's color.
+ color_lookup: Strategy for mapping colors to detection indices.
+ collect_union: When ``True``, allocate and return a ``(H, W)``
+ boolean array that accumulates the union of all painted masks
+ (useful for callers like `HaloAnnotator` that need the combined
+ mask footprint). When ``False`` (default), returns ``None``.
+ canvas_origin: Absolute ``(x, y)`` origin of `canvas` within the source
+ image. Use the default for full-frame painting.
+
+ Returns:
+ A boolean array matching the canvas dimensions when
+ ``collect_union=True``, otherwise ``None``. When called with an
+ ROI sub-canvas, dimensions are the ROI size, not the full image.
+ """
+ masks = detections.mask
+ if masks is None:
+ return None
+ union: npt.NDArray[np.bool_] | None = (
+ np.zeros(canvas.shape[:2], dtype=bool) if collect_union else None
+ )
+ compact_mask = masks if isinstance(masks, CompactMask) else None
+ origin_x, origin_y = canvas_origin
+ canvas_h, canvas_w = canvas.shape[:2]
+ for detection_idx in np.flip(np.argsort(detections.area)):
+ color_bgr = resolve_color(
+ color=color,
+ detections=detections,
+ detection_idx=detection_idx,
+ color_lookup=color_lookup,
+ ).as_bgr()
+ if compact_mask is not None:
+ x1 = int(compact_mask.offsets[detection_idx, 0])
+ y1 = int(compact_mask.offsets[detection_idx, 1])
+ crop_m = compact_mask.crop(detection_idx)
+ crop_h, crop_w = crop_m.shape
+ crop_x1 = max(0, origin_x - x1)
+ crop_y1 = max(0, origin_y - y1)
+ canvas_x1 = max(0, x1 - origin_x)
+ canvas_y1 = max(0, y1 - origin_y)
+ paint_w = min(crop_w - crop_x1, canvas_w - canvas_x1)
+ paint_h = min(crop_h - crop_y1, canvas_h - canvas_y1)
+ if paint_w <= 0 or paint_h <= 0:
+ continue
+ crop_slice = crop_m[
+ crop_y1 : crop_y1 + paint_h, crop_x1 : crop_x1 + paint_w
+ ]
+ canvas_slice = canvas[
+ canvas_y1 : canvas_y1 + paint_h,
+ canvas_x1 : canvas_x1 + paint_w,
+ ]
+ canvas_slice[crop_slice] = color_bgr
+ if union is not None:
+ union[
+ canvas_y1 : canvas_y1 + paint_h,
+ canvas_x1 : canvas_x1 + paint_w,
+ ] |= crop_slice
+ else:
+ mask = np.asarray(masks[detection_idx], dtype=bool)
+ mask = mask[origin_y : origin_y + canvas_h, origin_x : origin_x + canvas_w]
+ canvas[mask] = color_bgr
+ if union is not None:
+ union |= mask
+ return union
+
+
+class MaskAnnotator(BaseAnnotator):
+ """
+ A class for drawing masks on an image using provided detections.
+
+ !!! warning
+
+ This annotator uses `sv.Detections.mask`.
+ """
+
+ requires_mask = True
+
+ def __init__(
+ self,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ opacity: float = 0.5,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ ):
+ """
+ Args:
+ color: The color or color palette to use for
+ annotating detections.
+ opacity: Opacity of the overlay mask. Must be between `0` and `1`.
+ color_lookup: Strategy for mapping colors to annotations.
+ Options are `INDEX`, `CLASS`, `TRACK`.
+ """
+ self.color: Color | ColorPalette = _normalize_color_input(color)
+ self.opacity = opacity
+ self.color_lookup: ColorLookup = color_lookup
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ ) -> ImageType:
+ """
+ Annotates the given scene with masks based on the provided detections.
+
+ Args:
+ scene: The image where masks will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((100, 100, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... mask=np.zeros((1, 100, 100), dtype=bool),
+ ... class_id=np.array([0])
+ ... )
+ >>> mask_annotator = sv.MaskAnnotator()
+ >>> annotated_frame = mask_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections
+ ... )
+
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ if detections.mask is None:
+ return scene
+
+ image_shape = (int(scene.shape[0]), int(scene.shape[1]))
+ effective_lookup = (
+ self.color_lookup if custom_color_lookup is None else custom_color_lookup
+ )
+ if len(detections) > 0:
+ resolve_color(
+ color=self.color,
+ detections=detections,
+ detection_idx=0,
+ color_lookup=effective_lookup,
+ )
+ roi = _masks_to_roi(detections.mask, image_shape, detections.xyxy)
+ if roi is None:
+ return scene
+
+ x1, y1, x2, y2 = roi
+ scene_roi = scene[y1:y2, x1:x2]
+ colored_mask = np.array(scene_roi, copy=True, dtype=np.uint8)
+ _paint_masks_by_area(
+ colored_mask,
+ detections,
+ self.color,
+ effective_lookup,
+ canvas_origin=(x1, y1),
+ )
+ tmp = cv2.addWeighted(
+ colored_mask, self.opacity, scene_roi.copy(), 1 - self.opacity, 0
+ )
+ scene_roi[:] = tmp
+ return scene
+
+
+class PolygonAnnotator(BaseAnnotator):
+ """
+ A class for drawing polygons on an image using provided detections.
+
+ !!! warning
+
+ This annotator uses `sv.Detections.mask`.
+ """
+
+ requires_mask = True
+
+ def __init__(
+ self,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ thickness: int = 2,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ ):
+ """
+ Args:
+ color: The color or color palette to use for
+ annotating detections.
+ thickness: Thickness of the polygon lines.
+ color_lookup: Strategy for mapping colors to annotations.
+ Options are `INDEX`, `CLASS`, `TRACK`.
+ """
+ self.color: Color | ColorPalette = _normalize_color_input(color)
+ self.thickness: int = thickness
+ self.color_lookup: ColorLookup = color_lookup
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ ) -> ImageType:
+ """
+ Annotates the given scene with polygons based on the provided detections.
+
+ Args:
+ scene: The image where polygons will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((100, 100, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... class_id=np.array([0])
+ ... )
+ >>> polygon_annotator = sv.PolygonAnnotator()
+ >>> annotated_frame = polygon_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections
+ ... )
+
+ ```
+
+ Note:
+ When `detections.mask` is a `CompactMask`, each detection's polygon
+ is decoded from a bbox-sized crop (O(crop_area)) rather than a
+ full-frame ``(H, W)`` allocation (O(HยทW)). Polygon coordinates are
+ shifted from crop-local space to image space via the stored
+ ``(x1, y1)`` bbox origin. Pixels outside the declared ``xyxy`` box
+ are not represented in compact storage and will not be drawn.
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ if detections.mask is None:
+ return scene
+
+ for detection_idx, mask, offset in _iter_mask_crops(detections):
+ color = resolve_color(
+ color=self.color,
+ detections=detections,
+ detection_idx=detection_idx,
+ color_lookup=self.color_lookup
+ if custom_color_lookup is None
+ else custom_color_lookup,
+ )
+ for polygon in mask_to_polygons(mask=mask):
+ if offset is not None:
+ # translate crop-local polygon to image space via (x1, y1) origin
+ polygon = polygon + offset
+ scene = draw_polygon(
+ scene=scene,
+ polygon=cast(npt.NDArray[np.int_], polygon),
+ color=color,
+ thickness=self.thickness,
+ )
+
+ return scene
+
+
+class ColorAnnotator(BaseAnnotator):
+ """
+ A class for drawing box masks on an image using provided detections.
+ """
+
+ def __init__(
+ self,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ opacity: float = 0.5,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ ):
+ """
+ Args:
+ color: The color or color palette to use for
+ annotating detections.
+ opacity: Opacity of the overlay mask. Must be between `0` and `1`.
+ color_lookup: Strategy for mapping colors to annotations.
+ Options are `INDEX`, `CLASS`, `TRACK`.
+ """
+ self.color: Color | ColorPalette = _normalize_color_input(color)
+ self.color_lookup: ColorLookup = color_lookup
+ self.opacity = opacity
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ ) -> ImageType:
+ """
+ Annotates the given scene with box masks based on the provided detections.
+
+ Args:
+ scene: The image where bounding boxes will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((100, 100, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... class_id=np.array([0])
+ ... )
+ >>> color_annotator = sv.ColorAnnotator()
+ >>> annotated_frame = color_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections
+ ... )
+
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ scene_with_boxes = scene.copy()
+ for detection_idx in range(len(detections)):
+ x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
+ color = resolve_color(
+ color=self.color,
+ detections=detections,
+ detection_idx=detection_idx,
+ color_lookup=self.color_lookup
+ if custom_color_lookup is None
+ else custom_color_lookup,
+ )
+ cv2.rectangle(
+ img=scene_with_boxes,
+ pt1=(x1, y1),
+ pt2=(x2, y2),
+ color=color.as_bgr(),
+ thickness=-1,
+ )
+
+ cv2.addWeighted(
+ scene_with_boxes, self.opacity, scene, 1 - self.opacity, gamma=0, dst=scene
+ )
+ return scene
+
+
+class HaloAnnotator(BaseAnnotator):
+ """
+ A class for drawing Halos on an image using provided detections.
+
+ !!! warning
+
+ This annotator uses `sv.Detections.mask`.
+ """
+
+ requires_mask = True
+
+ def __init__(
+ self,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ opacity: float = 0.8,
+ kernel_size: int = 40,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ ):
+ """
+ Args:
+ color: The color or color palette to use for
+ annotating detections.
+ opacity: Opacity of the overlay mask. Must be between `0` and `1`.
+ kernel_size: The size of the average pooling kernel used for creating
+ the halo.
+ color_lookup: Strategy for mapping colors to annotations.
+ Options are `INDEX`, `CLASS`, `TRACK`.
+ """
+ self.color: Color | ColorPalette = _normalize_color_input(color)
+ self.opacity = opacity
+ self.color_lookup: ColorLookup = color_lookup
+ self.kernel_size: int = kernel_size
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ ) -> ImageType:
+ """
+ Annotates the given scene with halos based on the provided detections.
+
+ Args:
+ scene: The image where the halo effect will be applied.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((100, 100, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... mask=np.zeros((1, 100, 100), dtype=bool),
+ ... class_id=np.array([0])
+ ... )
+ >>> halo_annotator = sv.HaloAnnotator()
+ >>> annotated_frame = halo_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections
+ ... )
+
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ if detections.mask is None:
+ return scene
+ colored_mask = np.zeros_like(scene, dtype=np.uint8)
+ fmask = _paint_masks_by_area(
+ colored_mask,
+ detections,
+ self.color,
+ self.color_lookup if custom_color_lookup is None else custom_color_lookup,
+ collect_union=True,
+ )
+ assert fmask is not None # collect_union=True always returns an array
+
+ colored_mask = cast(
+ npt.NDArray[np.uint8],
+ cv2.blur(colored_mask, (self.kernel_size, self.kernel_size)),
+ )
+ colored_mask[fmask] = [0, 0, 0]
+ gray = cv2.cvtColor(colored_mask, cv2.COLOR_BGR2GRAY)
+ gray_max = gray.max()
+ if gray_max == 0:
+ # no halo to draw (e.g. empty masks); leave the scene untouched
+ return scene
+ alpha = self.opacity * gray / gray_max
+ alpha_mask = alpha[:, :, np.newaxis]
+ # Blend in float space so halo opacity cannot wrap around uint8 boundaries.
+ blended_scene = np.clip(
+ scene.astype(np.float32) * (1 - alpha_mask)
+ + colored_mask.astype(np.float32) * alpha_mask,
+ 0,
+ 255,
+ ).astype(np.uint8)
+ np.copyto(scene, blended_scene)
+ return scene
+
+
+class EllipseAnnotator(BaseAnnotator):
+ """
+ A class for drawing ellipses on an image using provided detections.
+ """
+
+ def __init__(
+ self,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ thickness: int = 2,
+ start_angle: int = -45,
+ end_angle: int = 235,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ ):
+ """
+ Args:
+ color: The color or color palette to use for
+ annotating detections.
+ thickness: Thickness of the ellipse lines.
+ start_angle: Starting angle of the ellipse.
+ end_angle: Ending angle of the ellipse.
+ color_lookup: Strategy for mapping colors to annotations.
+ Options are `INDEX`, `CLASS`, `TRACK`.
+ """
+ self.color: Color | ColorPalette = _normalize_color_input(color)
+ self.thickness: int = thickness
+ self.start_angle: int = start_angle
+ self.end_angle: int = end_angle
+ self.color_lookup: ColorLookup = color_lookup
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ ) -> ImageType:
+ """
+ Annotates the given scene with ellipses based on the provided detections.
+
+ Args:
+ scene: The image where ellipses will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((100, 100, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... class_id=np.array([0])
+ ... )
+ >>> ellipse_annotator = sv.EllipseAnnotator()
+ >>> annotated_frame = ellipse_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections
+ ... )
+
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ for detection_idx in range(len(detections)):
+ x1, _y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
+ color = resolve_color(
+ color=self.color,
+ detections=detections,
+ detection_idx=detection_idx,
+ color_lookup=self.color_lookup
+ if custom_color_lookup is None
+ else custom_color_lookup,
+ )
+ center = (int((x1 + x2) / 2), y2)
+ width = x2 - x1
+ cv2.ellipse(
+ scene,
+ center=center,
+ axes=(int(width), int(0.35 * width)),
+ angle=0.0,
+ startAngle=self.start_angle,
+ endAngle=self.end_angle,
+ color=color.as_bgr(),
+ thickness=self.thickness,
+ lineType=cv2.LINE_4,
+ )
+ return scene
+
+
+class BoxCornerAnnotator(BaseAnnotator):
+ """
+ A class for drawing box corners on an image using provided detections.
+ """
+
+ def __init__(
+ self,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ thickness: int = 4,
+ corner_length: int = 15,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ ):
+ """
+ Args:
+ color: The color or color palette to use for
+ annotating detections.
+ thickness: Thickness of the corner lines.
+ corner_length: Length of each corner line.
+ color_lookup: Strategy for mapping colors to annotations.
+ Options are `INDEX`, `CLASS`, `TRACK`.
+ """
+ self.color: Color | ColorPalette = _normalize_color_input(color)
+ self.thickness: int = thickness
+ self.corner_length: int = corner_length
+ self.color_lookup: ColorLookup = color_lookup
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ ) -> ImageType:
+ """
+ Annotates the given scene with box corners based on the provided detections.
+
+ Args:
+ scene: The image where box corners will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((100, 100, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... class_id=np.array([0])
+ ... )
+ >>> corner_annotator = sv.BoxCornerAnnotator()
+ >>> annotated_frame = corner_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections
+ ... )
+
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ for detection_idx in range(len(detections)):
+ x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
+ color = resolve_color(
+ color=self.color,
+ detections=detections,
+ detection_idx=detection_idx,
+ color_lookup=self.color_lookup
+ if custom_color_lookup is None
+ else custom_color_lookup,
+ )
+ corners = [(x1, y1), (x2, y1), (x1, y2), (x2, y2)]
+
+ for x, y in corners:
+ x_end = x + self.corner_length if x == x1 else x - self.corner_length
+ cv2.line(
+ scene, (x, y), (x_end, y), color.as_bgr(), thickness=self.thickness
+ )
+
+ y_end = y + self.corner_length if y == y1 else y - self.corner_length
+ cv2.line(
+ scene, (x, y), (x, y_end), color.as_bgr(), thickness=self.thickness
+ )
+ return scene
+
+
+class CircleAnnotator(BaseAnnotator):
+ """
+ A class for drawing circle on an image using provided detections.
+ """
+
+ def __init__(
+ self,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ thickness: int = 2,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ ):
+ """
+ Args:
+ color: The color or color palette to use for
+ annotating detections.
+ thickness: Thickness of the circle line.
+ color_lookup: Strategy for mapping colors to annotations.
+ Options are `INDEX`, `CLASS`, `TRACK`.
+ """
+
+ self.color: Color | ColorPalette = _normalize_color_input(color)
+ self.thickness: int = thickness
+ self.color_lookup: ColorLookup = color_lookup
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ ) -> ImageType:
+ """
+ Annotates the given scene with circles based on the provided detections.
+
+ Args:
+ scene: The image where box corners will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((100, 100, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... class_id=np.array([0])
+ ... )
+ >>> circle_annotator = sv.CircleAnnotator()
+ >>> annotated_frame = circle_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections
+ ... )
+
+ ```
+
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ for detection_idx in range(len(detections)):
+ x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
+ center = ((x1 + x2) // 2, (y1 + y2) // 2)
+ distance = sqrt((x1 - center[0]) ** 2 + (y1 - center[1]) ** 2)
+ color = resolve_color(
+ color=self.color,
+ detections=detections,
+ detection_idx=detection_idx,
+ color_lookup=self.color_lookup
+ if custom_color_lookup is None
+ else custom_color_lookup,
+ )
+ cv2.circle(
+ img=scene,
+ center=center,
+ radius=int(distance),
+ color=color.as_bgr(),
+ thickness=self.thickness,
+ )
+
+ return scene
+
+
+class DotAnnotator(BaseAnnotator):
+ """
+ A class for drawing dots on an image at specific coordinates based on provided
+ detections.
+ """
+
+ def __init__(
+ self,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ radius: int = 4,
+ position: Position = Position.CENTER,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ outline_thickness: int = 0,
+ outline_color: Color | ColorPalette | str = Color.BLACK,
+ ):
+ """
+ Args:
+ color: The color or color palette to use for
+ annotating detections.
+ radius: Radius of the drawn dots.
+ position: The anchor position for placing the dot.
+ color_lookup: Strategy for mapping colors to annotations.
+ Options are `INDEX`, `CLASS`, `TRACK`.
+ outline_thickness: Thickness of the outline of the dot.
+ outline_color: The color or color palette to
+ use for outline. It is activated by setting outline_thickness to a value
+ greater than 0.
+ """
+ self.color: Color | ColorPalette = _normalize_color_input(color)
+ self.radius: int = radius
+ self.position: Position = position
+ self.color_lookup: ColorLookup = color_lookup
+ self.outline_thickness = outline_thickness
+ self.outline_color: Color | ColorPalette = _normalize_color_input(outline_color)
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ ) -> ImageType:
+ """
+ Annotates the given scene with dots based on the provided detections.
+
+ Args:
+ scene: The image where dots will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((100, 100, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... class_id=np.array([0])
+ ... )
+ >>> dot_annotator = sv.DotAnnotator()
+ >>> annotated_frame = dot_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections
+ ... )
+
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ xy = detections.get_anchors_coordinates(anchor=self.position)
+ for detection_idx in range(len(detections)):
+ color = resolve_color(
+ color=self.color,
+ detections=detections,
+ detection_idx=detection_idx,
+ color_lookup=self.color_lookup
+ if custom_color_lookup is None
+ else custom_color_lookup,
+ )
+ center = (int(xy[detection_idx, 0]), int(xy[detection_idx, 1]))
+
+ cv2.circle(scene, center, self.radius, color.as_bgr(), -1)
+ if self.outline_thickness:
+ outline_color = resolve_color(
+ color=self.outline_color,
+ detections=detections,
+ detection_idx=detection_idx,
+ color_lookup=self.color_lookup
+ if custom_color_lookup is None
+ else custom_color_lookup,
+ )
+ cv2.circle(
+ scene,
+ center,
+ self.radius,
+ outline_color.as_bgr(),
+ self.outline_thickness,
+ )
+ return scene
+
+
+class LabelAnnotator(_BaseLabelAnnotator):
+ """
+ A class for annotating labels on an image using provided detections.
+ """
+
+ def __init__(
+ self,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ text_color: Color | ColorPalette | str = Color.WHITE,
+ text_scale: float = 0.5,
+ text_thickness: int = 1,
+ text_padding: int = 10,
+ text_position: Position = Position.TOP_LEFT,
+ text_offset: tuple[int, int] = (0, 0),
+ border_radius: int = 0,
+ smart_position: bool = False,
+ max_line_length: int | None = None,
+ ):
+ """
+ Args:
+ color: The color or color palette to use for
+ annotating the text background.
+ color_lookup: Strategy for mapping colors to annotations.
+ Options are `INDEX`, `CLASS`, `TRACK`.
+ text_color: The color or color palette to use
+ for the text.
+ text_scale: Font scale for the text.
+ text_thickness: Thickness of the text characters.
+ text_padding: Padding around the text within its background box.
+ text_position: Position of the text relative to the detection.
+ Possible values are defined in the `Position` enum.
+ text_offset: A tuple of 2D coordinates `(x, y)` to
+ offset the text position from the anchor point, in pixels.
+ border_radius: The radius to apply round edges. If the selected
+ value is higher than the lower dimension, width or height, is clipped.
+ smart_position: Spread out the labels to avoid overlapping.
+ max_line_length: Maximum number of characters per line
+ before wrapping the text. None means no wrapping.
+ """
+ self.text_scale: float = text_scale
+ self.text_thickness: int = text_thickness
+ super().__init__(
+ color=color,
+ color_lookup=color_lookup,
+ text_color=text_color,
+ text_padding=text_padding,
+ text_position=text_position,
+ text_offset=text_offset,
+ border_radius=border_radius,
+ smart_position=smart_position,
+ max_line_length=max_line_length,
+ )
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ labels: list[str] | None = None,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ ) -> ImageType:
+ """
+ Annotates the given scene with labels based on the provided detections.
+
+ Args:
+ scene: The image where labels will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ labels: Custom labels for each detection.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((100, 100, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... confidence=np.array([0.9]),
+ ... class_id=np.array([0]),
+ ... data={'class_name': np.array(['person'])}
+ ... )
+ >>> labels = [
+ ... f"{class_name} {confidence:.2f}"
+ ... for class_name, confidence
+ ... in zip(detections['class_name'], detections.confidence)
+ ... ]
+ >>> label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER)
+ >>> annotated_frame = label_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections,
+ ... labels=labels
+ ... )
+
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ _validate_labels(labels, detections)
+
+ labels = get_labels_text(detections, labels)
+ label_properties: npt.NDArray[np.float32] = self._get_label_properties(
+ detections, labels
+ )
+
+ if self.smart_position:
+ label_properties = self._adjust_labels_in_frame(
+ (scene.shape[1], scene.shape[0]),
+ labels,
+ label_properties,
+ )
+
+ self._draw_labels(
+ scene=scene,
+ labels=labels,
+ label_properties=label_properties,
+ detections=detections,
+ custom_color_lookup=custom_color_lookup,
+ )
+
+ return scene
+
+ def _get_label_properties(
+ self,
+ detections: Detections,
+ labels: list[str],
+ ) -> Any:
+ label_properties = []
+ anchors_coordinates: npt.NDArray[np.int32] = detections.get_anchors_coordinates(
+ anchor=self.text_anchor
+ ).astype(int)
+
+ for label, center_coordinates in zip(labels, anchors_coordinates):
+ center_coordinates = (
+ center_coordinates[0] + self.text_offset[0],
+ center_coordinates[1] + self.text_offset[1],
+ )
+
+ wrapped_lines = wrap_text(label, self.max_line_length)
+ line_heights = []
+ line_widths = []
+
+ for line in wrapped_lines:
+ (text_w, text_h) = cv2.getTextSize(
+ text=line,
+ fontFace=CV2_FONT,
+ fontScale=self.text_scale,
+ thickness=self.text_thickness,
+ )[0]
+ line_heights.append(text_h)
+ line_widths.append(text_w)
+
+ # Get the maximum width and total height
+ max_width = max(line_widths) if line_widths else 0
+ total_height = (
+ sum(line_heights) + (len(line_heights) - 1) * self.text_padding
+ )
+
+ # Add padding around all sides
+ width_padded = max_width + 2 * self.text_padding
+ height_padded = total_height + 2 * self.text_padding
+
+ text_background_xyxy = resolve_text_background_xyxy(
+ center_coordinates=center_coordinates,
+ text_wh=(width_padded, height_padded),
+ position=self.text_anchor,
+ )
+
+ label_properties.append(
+ [
+ *text_background_xyxy,
+ total_height,
+ ]
+ )
+ return np.array(label_properties, dtype=np.float32).reshape(-1, 5)
+
+ def _draw_labels(
+ self,
+ scene: npt.NDArray[np.uint8],
+ labels: list[str],
+ label_properties: npt.NDArray[np.float32],
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None,
+ ) -> None:
+ assert len(labels) == len(label_properties) == len(detections), (
+ f"Number of label properties ({len(label_properties)}), "
+ f"labels ({len(labels)}) and detections ({len(detections)}) "
+ "do not match."
+ )
+
+ color_lookup = (
+ custom_color_lookup
+ if custom_color_lookup is not None
+ else self.color_lookup
+ )
+
+ for idx, label_property in enumerate(label_properties):
+ background_color = resolve_color(
+ color=self.color,
+ detections=detections,
+ detection_idx=idx,
+ color_lookup=color_lookup,
+ )
+ text_color = resolve_color(
+ color=self.text_color,
+ detections=detections,
+ detection_idx=idx,
+ color_lookup=color_lookup,
+ )
+
+ box_xyxy = label_property[:4].astype(int)
+
+ self.draw_rounded_rectangle(
+ scene=scene,
+ xyxy=box_xyxy,
+ color=background_color.as_bgr(),
+ border_radius=self.border_radius,
+ )
+
+ # Handle multiline text
+ wrapped_lines = wrap_text(labels[idx], self.max_line_length)
+ current_y = box_xyxy[1] + self.text_padding # Start y position
+
+ for line in wrapped_lines:
+ if not line:
+ # Use a character with ascenders and descenders as height reference
+ (_, text_h) = cv2.getTextSize(
+ text="Tg",
+ fontFace=CV2_FONT,
+ fontScale=self.text_scale,
+ thickness=self.text_thickness,
+ )[0]
+ current_y += text_h + self.text_padding
+ continue
+
+ (_, text_h) = cv2.getTextSize(
+ text=line,
+ fontFace=CV2_FONT,
+ fontScale=self.text_scale,
+ thickness=self.text_thickness,
+ )[0]
+
+ text_x = box_xyxy[0] + self.text_padding
+ text_y = current_y + text_h # Add height to get to text baseline
+
+ cv2.putText(
+ img=scene,
+ text=line,
+ org=(text_x, text_y),
+ fontFace=CV2_FONT,
+ fontScale=self.text_scale,
+ color=text_color.as_bgr(),
+ thickness=self.text_thickness,
+ lineType=cv2.LINE_AA,
+ )
+
+ current_y += text_h + self.text_padding # Move to next line position
+
+ @staticmethod
+ def draw_rounded_rectangle(
+ scene: npt.NDArray[np.uint8],
+ xyxy: tuple[int, int, int, int],
+ color: tuple[int, int, int],
+ border_radius: int,
+ ) -> npt.NDArray[np.uint8]:
+ """Draw a filled rectangle with optional rounded corners on an image.
+
+ Args:
+ scene: BGR image array to draw on; modified in-place and returned.
+ xyxy: Bounding box as (x1, y1, x2, y2) pixel coordinates.
+ color: Fill color as a BGR tuple (e.g. ``(0, 0, 255)`` for red).
+ border_radius: Corner rounding radius in pixels. Values <= 0
+ (including values clamped to 0 by a degenerate box) draw a
+ plain filled rectangle with square corners.
+
+ Returns:
+ The annotated ``scene`` array.
+
+ Example:
+ ```python
+ import numpy as np
+ import supervision as sv
+
+ scene = np.zeros((200, 200, 3), dtype=np.uint8)
+ scene = sv.LabelAnnotator.draw_rounded_rectangle(
+ scene=scene,
+ xyxy=(10, 10, 100, 50),
+ color=(0, 255, 0),
+ border_radius=0,
+ )
+ ```
+ """
+ x1, y1, x2, y2 = xyxy
+ width = x2 - x1
+ height = y2 - y1
+
+ border_radius = min(border_radius, min(width, height) // 2)
+
+ if border_radius <= 0:
+ # square corners: a single fill rectangle (the common default), rather
+ # than two rectangles plus four zero-radius corner circles
+ cv2.rectangle(
+ img=scene,
+ pt1=(x1, y1),
+ pt2=(x2, y2),
+ color=color,
+ thickness=-1,
+ )
+ return scene
+
+ rectangle_coordinates = [
+ ((x1 + border_radius, y1), (x2 - border_radius, y2)),
+ ((x1, y1 + border_radius), (x2, y2 - border_radius)),
+ ]
+ circle_centers = [
+ (x1 + border_radius, y1 + border_radius),
+ (x2 - border_radius, y1 + border_radius),
+ (x1 + border_radius, y2 - border_radius),
+ (x2 - border_radius, y2 - border_radius),
+ ]
+
+ for coordinates in rectangle_coordinates:
+ cv2.rectangle(
+ img=scene,
+ pt1=coordinates[0],
+ pt2=coordinates[1],
+ color=color,
+ thickness=-1,
+ )
+ for center in circle_centers:
+ cv2.circle(
+ img=scene,
+ center=center,
+ radius=border_radius,
+ color=color,
+ thickness=-1,
+ )
+ return scene
+
+
+class RichLabelAnnotator(_BaseLabelAnnotator):
+ """
+ A class for annotating labels on an image using provided detections,
+ with support for Unicode characters by using a custom font.
+ """
+
+ def __init__(
+ self,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ text_color: Color | ColorPalette | str = Color.WHITE,
+ font_path: str | None = None,
+ font_size: int = 10,
+ text_padding: int = 10,
+ text_position: Position = Position.TOP_LEFT,
+ text_offset: tuple[int, int] = (0, 0),
+ border_radius: int = 0,
+ smart_position: bool = False,
+ max_line_length: int | None = None,
+ ):
+ """
+ Args:
+ color: The color or color palette to use for
+ annotating the text background.
+ color_lookup: Strategy for mapping colors to annotations.
+ Options are `INDEX`, `CLASS`, `TRACK`.
+ text_color: The color to use for the text.
+ font_path: Path to the font file (e.g., ".ttf" or ".otf")
+ to use for rendering text. If `None`, the default PIL font will be used.
+ font_size: Font size for the text.
+ text_padding: Padding around the text within its background box.
+ text_position: Position of the text relative to the detection.
+ Possible values are defined in the `Position` enum.
+ text_offset: A tuple of 2D coordinates `(x, y)` to
+ offset the text position from the anchor point, in pixels.
+ border_radius: The radius to apply round edges. If the selected
+ value is higher than the lower dimension, width or height, is clipped.
+ smart_position: Spread out the labels to avoid overlapping.
+ max_line_length: Maximum number of characters per line
+ before wrapping the text. None means no wrapping.
+ """
+ self.font_path = font_path
+ self.font_size = font_size
+ self.font = self._load_font(font_size, font_path)
+ super().__init__(
+ color=color,
+ color_lookup=color_lookup,
+ text_color=text_color,
+ text_padding=text_padding,
+ text_position=text_position,
+ text_offset=text_offset,
+ border_radius=border_radius,
+ smart_position=smart_position,
+ max_line_length=max_line_length,
+ )
+
+ @ensure_pil_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ labels: list[str] | None = None,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ ) -> ImageType:
+ """
+ Annotates the given scene with labels based on the provided
+ detections, with support for Unicode characters.
+
+ Args:
+ scene: The image where labels will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ labels: Custom labels for each detection.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> from PIL import Image
+ >>> image = Image.fromarray(np.zeros((100, 100, 3), dtype=np.uint8))
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... confidence=np.array([0.9]),
+ ... class_id=np.array([0]),
+ ... data={'class_name': np.array(['person'])}
+ ... )
+ >>> labels = [
+ ... f"{class_name} {confidence:.2f}"
+ ... for class_name, confidence
+ ... in zip(detections['class_name'], detections.confidence)
+ ... ]
+ >>> rich_label_annotator = sv.RichLabelAnnotator()
+ >>> annotated_frame = rich_label_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections,
+ ... labels=labels
+ ... )
+
+ ```
+ """
+ _validate_labels(labels, detections)
+
+ draw = ImageDraw.Draw(scene)
+ labels = get_labels_text(detections, labels)
+ label_properties: npt.NDArray[np.float32] = self._get_label_properties(
+ draw, detections, labels
+ )
+
+ if self.smart_position:
+ scene_pil = cast(Image.Image, scene)
+ label_properties = self._adjust_labels_in_frame(
+ (scene_pil.width, scene_pil.height),
+ labels,
+ label_properties,
+ )
+
+ self._draw_labels(
+ draw=draw,
+ labels=labels,
+ label_properties=label_properties,
+ detections=detections,
+ custom_color_lookup=custom_color_lookup,
+ )
+
+ return scene
+
+ def _get_label_properties(
+ self, draw: ImageDraw.ImageDraw, detections: Detections, labels: list[str]
+ ) -> Any:
+ label_properties = []
+
+ anchor_coordinates: npt.NDArray[np.int32] = detections.get_anchors_coordinates(
+ anchor=self.text_anchor
+ ).astype(int)
+
+ for label, center_coordinates in zip(labels, anchor_coordinates):
+ center_coordinates = (
+ center_coordinates[0] + self.text_offset[0],
+ center_coordinates[1] + self.text_offset[1],
+ )
+
+ wrapped_lines = wrap_text(label, self.max_line_length)
+
+ # Calculate the total text height and maximum width
+ max_width = 0.0
+ total_height = 0.0
+
+ for line in wrapped_lines:
+ left, top, right, bottom = draw.textbbox((0, 0), line, font=self.font)
+ line_width = right - left
+ line_height = bottom - top
+
+ max_width = max(max_width, line_width)
+ total_height += line_height
+
+ # Add inter-line spacing
+ if len(wrapped_lines) > 1:
+ total_height += (len(wrapped_lines) - 1) * self.text_padding
+
+ width_padded = int(max_width + 2 * self.text_padding)
+ height_padded = int(total_height + 2 * self.text_padding)
+
+ text_background_xyxy = resolve_text_background_xyxy(
+ center_coordinates=center_coordinates,
+ text_wh=(width_padded, height_padded),
+ position=self.text_anchor,
+ )
+
+ # Get the text origin offsets
+ text_left, text_top, _, _ = draw.textbbox((0, 0), "Tg", font=self.font)
+
+ label_properties.append([*text_background_xyxy, text_left, text_top])
+
+ result: npt.NDArray[np.float32] = np.array(
+ label_properties, dtype=np.float32
+ ).reshape(-1, 6)
+ return result
+
+ def _draw_labels(
+ self,
+ draw: ImageDraw.ImageDraw,
+ labels: list[str],
+ label_properties: npt.NDArray[np.float32],
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None,
+ ) -> None:
+ assert len(labels) == len(label_properties) == len(detections), (
+ f"Number of label properties ({len(label_properties)}), "
+ f"labels ({len(labels)}) and detections ({len(detections)}) "
+ "do not match."
+ )
+ color_lookup = (
+ custom_color_lookup
+ if custom_color_lookup is not None
+ else self.color_lookup
+ )
+
+ for idx, label_property in enumerate(label_properties):
+ background_color = resolve_color(
+ color=self.color,
+ detections=detections,
+ detection_idx=idx,
+ color_lookup=color_lookup,
+ )
+ text_color = resolve_color(
+ color=self.text_color,
+ detections=detections,
+ detection_idx=idx,
+ color_lookup=color_lookup,
+ )
+
+ box_xyxy = label_property[:4].astype(int)
+ text_left = label_property[4]
+ text_top = label_property[5]
+
+ # Draw the rounded rectangle background
+ draw.rounded_rectangle(
+ tuple(box_xyxy),
+ radius=self.border_radius,
+ fill=background_color.as_rgb(),
+ outline=None,
+ )
+
+ # Draw each line of text
+ wrapped_lines = wrap_text(labels[idx], self.max_line_length)
+ x_position = box_xyxy[0] + self.text_padding - text_left
+ y_position = box_xyxy[1] + self.text_padding - text_top
+
+ for line in wrapped_lines:
+ draw.text(
+ xy=(x_position, y_position),
+ text=line,
+ font=self.font,
+ fill=text_color.as_rgb(),
+ )
+
+ # Move to the next line position
+ _left, top, _right, bottom = draw.textbbox((0, 0), line, font=self.font)
+ line_height = bottom - top
+ y_position += line_height + self.text_padding
+
+ @staticmethod
+ def _load_font(
+ font_size: int, font_path: str | None
+ ) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
+ def load_default_font(
+ size: int,
+ ) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
+ try:
+ return ImageFont.load_default(size)
+ except TypeError:
+ return ImageFont.load_default()
+
+ if font_path is None:
+ return load_default_font(font_size)
+
+ try:
+ return ImageFont.truetype(font_path, font_size)
+ except OSError:
+ logger.warning(
+ "Font path '%s' not found. Using PIL's default font.", font_path
+ )
+ return load_default_font(font_size)
+
+
+class IconAnnotator(BaseAnnotator):
+ """
+ A class for drawing an icon on an image, using provided detections.
+ """
+
+ def __init__(
+ self,
+ icon_resolution_wh: tuple[int, int] = (64, 64),
+ icon_position: Position = Position.TOP_CENTER,
+ offset_xy: tuple[int, int] = (0, 0),
+ ):
+ """
+ Args:
+ icon_resolution_wh: The size of drawn icons.
+ All icons will be resized to this resolution, keeping the aspect ratio.
+ icon_position: The position of the icon.
+ offset_xy: The offset to apply to the icon position,
+ in pixels. Can be both positive and negative.
+ """
+ self.icon_resolution_wh = icon_resolution_wh
+ self.position = icon_position
+ self.offset_xy = offset_xy
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ icon_path: str | list[str] = "",
+ ) -> ImageType:
+ """
+ Annotates the given scene with given icons.
+
+ Args:
+ scene: The image where labels will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ icon_path: The path to the PNG image to use as an
+ icon. Must be a single path or a list of paths, one for each detection.
+ Pass an empty string `""` to draw nothing.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Example:
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ available_icons = ["roboflow.png", "lenny.png"]
+ icon_paths = [np.random.choice(available_icons) for _ in detections]
+
+ icon_annotator = sv.IconAnnotator()
+ annotated_frame = icon_annotator.annotate(
+ scene=image.copy(),
+ detections=detections,
+ icon_path=icon_paths
+ )
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ if isinstance(icon_path, list) and len(icon_path) != len(detections):
+ raise ValueError(
+ f"The number of icon paths provided ({len(icon_path)}) does not match "
+ f"the number of detections ({len(detections)}). Either provide a single"
+ f" icon path or one for each detection."
+ )
+
+ xy: npt.NDArray[np.int32] = detections.get_anchors_coordinates(
+ anchor=self.position
+ ).astype(int)
+
+ for detection_idx in range(len(detections)):
+ current_path = (
+ icon_path if isinstance(icon_path, str) else icon_path[detection_idx]
+ )
+ if current_path == "":
+ continue
+ icon = self._load_icon(current_path)
+ icon_h, icon_w = icon.shape[:2]
+
+ x = int(xy[detection_idx, 0] - icon_w / 2 + self.offset_xy[0])
+ y = int(xy[detection_idx, 1] - icon_h / 2 + self.offset_xy[1])
+
+ scene[:] = _overlay_image(scene, icon, (x, y))
+ return scene
+
+ def _load_icon(self, icon_path: str) -> npt.NDArray[np.uint8]:
+ """Load an icon through the module-level cache shared by annotators."""
+ return _load_icon_from_path(
+ icon_path=icon_path, icon_resolution_wh=self.icon_resolution_wh
+ )
+
+
+class BlurAnnotator(BaseAnnotator):
+ """
+ A class for blurring regions in an image using provided detections.
+ """
+
+ def __init__(self, kernel_size: int | None = None):
+ """
+ Args:
+ kernel_size: The size of the average pooling kernel used for blurring.
+ If not set, a dynamic size is computed as one-third of the shorter
+ bounding-box dimension. Must be >= 1 when provided.
+ """
+ if kernel_size is not None and kernel_size < 1:
+ raise ValueError(f"kernel_size must be >= 1, got {kernel_size}.")
+ self.kernel_size: int | None = kernel_size
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ ) -> ImageType:
+ """
+ Annotates the given scene by blurring regions based on the provided detections.
+
+ Args:
+ scene: The image where blurring will be applied.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((100, 100, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... class_id=np.array([0])
+ ... )
+ >>> blur_annotator = sv.BlurAnnotator()
+ >>> annotated_frame = blur_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections
+ ... )
+
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ image_height, image_width = scene.shape[:2]
+ clipped_xyxy: npt.NDArray[np.int32] = clip_boxes(
+ xyxy=detections.xyxy,
+ resolution_wh=(image_width, image_height),
+ ).astype(int)
+
+ for x1, y1, x2, y2 in clipped_xyxy:
+ if x2 <= x1 or y2 <= y1:
+ continue
+ roi = scene[y1:y2, x1:x2]
+ kernel_size = (
+ self.kernel_size
+ if self.kernel_size is not None
+ else calculate_dynamic_kernel_size(x1, y1, x2, y2)
+ )
+ roi = cast(npt.NDArray[np.uint8], cv2.blur(roi, (kernel_size, kernel_size)))
+ scene[y1:y2, x1:x2] = roi
+
+ return scene
+
+
+class TraceAnnotator(BaseAnnotator):
+ """
+ A class for drawing trace paths on an image based on detection coordinates.
+
+ !!! warning
+
+ This annotator uses the `sv.Detections.tracker_id`. Read
+ [here](/latest/trackers/) to learn how to plug
+ tracking into your inference pipeline.
+ """
+
+ def __init__(
+ self,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ position: Position = Position.CENTER,
+ trace_length: int = 30,
+ thickness: int = 2,
+ smooth: bool = False,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ ):
+ """
+ Args:
+ color: The color to draw the trace, can be
+ a single color or a color palette.
+ position: The position of the trace.
+ Defaults to `CENTER`.
+ trace_length: The maximum length of the trace in terms of historical
+ points. Defaults to `30`.
+ thickness: The thickness of the trace lines. Defaults to `2`.
+ smooth: If `True`, applies spline smoothing to trace lines using
+ consecutive unique anchor points. Falls back to a raw polyline
+ when fewer than 4 unique points are available (e.g. when a
+ tracker is stationary).
+ color_lookup: Strategy for mapping colors to annotations.
+ Options are `INDEX`, `CLASS`, `TRACK`.
+ """
+ self.color: Color | ColorPalette = _normalize_color_input(color)
+ self.trace = Trace(max_size=trace_length, anchor=position)
+ self.thickness = thickness
+ self.smooth = smooth
+ self.color_lookup: ColorLookup = color_lookup
+
+ def reset(self) -> None:
+ """
+ Clears the accumulated trace history so the annotator can be reused
+ across independent streams without carrying over points from a
+ previous stream.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((20, 20, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[1, 1, 10, 10]]),
+ ... class_id=np.array([0]),
+ ... tracker_id=np.array([1])
+ ... )
+ >>> trace_annotator = sv.TraceAnnotator()
+ >>> _ = trace_annotator.annotate(scene=image.copy(), detections=detections)
+ >>> trace_annotator.trace.xy.shape
+ (1, 2)
+ >>> trace_annotator.reset()
+ >>> trace_annotator.trace.current_frame_id
+ 0
+ >>> trace_annotator.trace.xy.shape
+ (0, 2)
+
+ ```
+ """
+ self.trace.reset()
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ ) -> ImageType:
+ """
+ Draws trace paths on the frame based on the detection coordinates provided.
+
+ Args:
+ scene: The image on which the traces will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: The detections which include coordinates for
+ which the traces will be drawn.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Example:
+ ```python
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO('yolov8x.pt')
+ trace_annotator = sv.TraceAnnotator()
+
+ video_info = sv.VideoInfo.from_video_path(video_path='...')
+ frames_generator = sv.get_video_frames_generator(source_path='...')
+ tracker = sv.ByteTrack()
+
+ with sv.VideoSink(target_path='...', video_info=video_info) as sink:
+ for frame in frames_generator:
+ result = model(frame)[0]
+ detections = sv.Detections.from_ultralytics(result)
+ detections = tracker.update_with_detections(detections)
+ annotated_frame = trace_annotator.annotate(
+ scene=frame.copy(),
+ detections=detections)
+ sink.write_frame(frame=annotated_frame)
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ if detections.tracker_id is None:
+ raise ValueError(
+ "The `tracker_id` field is missing in the provided detections."
+ " See more: https://supervision.roboflow.com/latest/how_to/track_objects"
+ )
+ filtered_detections: Detections = detections[
+ detections.tracker_id != PENDING_TRACK_ID
+ ] # type: ignore
+
+ self.trace.put(filtered_detections)
+ for detection_idx in range(len(filtered_detections)):
+ tracker_id_val = filtered_detections.tracker_id[detection_idx] # type: ignore
+ if tracker_id_val is None:
+ continue
+ tracker_id = int(tracker_id_val)
+ color = resolve_color(
+ color=self.color,
+ detections=filtered_detections,
+ detection_idx=detection_idx,
+ color_lookup=self.color_lookup
+ if custom_color_lookup is None
+ else custom_color_lookup,
+ )
+ xy = self.trace.get(tracker_id=tracker_id)
+ spline_points: npt.NDArray[np.int32] = xy.astype(np.int32)
+
+ if self.smooth:
+ unique_xy = xy[
+ np.concatenate(([True], np.any(np.diff(xy, axis=0) != 0, axis=1)))
+ ]
+ if len(unique_xy) > 3:
+ try:
+ x, y = unique_xy[:, 0], unique_xy[:, 1]
+ tck, _u = splprep([x, y], s=20)
+ x_new, y_new = splev(
+ np.linspace(0, 1, 100),
+ cast(
+ tuple[
+ npt.NDArray[np.float64],
+ npt.NDArray[np.float64],
+ int,
+ ],
+ tck,
+ ),
+ )
+ spline_points = np.stack((x_new, y_new), axis=1).astype(
+ np.int32
+ )
+ except ValueError:
+ spline_points = unique_xy.astype(np.int32)
+ else:
+ spline_points = unique_xy.astype(np.int32)
+
+ if len(xy) > 1:
+ cv2.polylines(
+ scene,
+ [spline_points],
+ False,
+ color=color.as_bgr(),
+ thickness=self.thickness,
+ )
+ return scene
+
+
+class HeatMapAnnotator(BaseAnnotator):
+ """
+ A class for drawing heatmaps on an image based on provided detections.
+ Heat accumulates over time and is drawn as a semi-transparent overlay
+ of blurred circles.
+ """
+
+ def __init__(
+ self,
+ position: Position = Position.BOTTOM_CENTER,
+ opacity: float = 0.2,
+ radius: int = 40,
+ kernel_size: int | None = 25,
+ top_hue: int = 0,
+ low_hue: int = 125,
+ ):
+ """
+ Args:
+ position: The position of the heatmap. Defaults to
+ `BOTTOM_CENTER`.
+ opacity: Opacity of the overlay mask. Must be between `0` and `1`.
+ radius: Radius of the heat circle.
+ kernel_size: Kernel size for blurring the heatmap. Pass `None`
+ to disable blurring entirely.
+ top_hue: Hue at the top of the heatmap. Defaults to 0 (red).
+ low_hue: Hue at the bottom of the heatmap. Defaults to 125 (blue).
+ """
+ self.position = position
+ self.opacity = opacity
+ self.radius = radius
+ self.kernel_size = kernel_size
+ self.top_hue = top_hue
+ self.low_hue = low_hue
+ self.heat_mask: npt.NDArray[np.float32] | None = None
+
+ def reset(self) -> None:
+ """
+ Clears the accumulated heat so the annotator can be reused across
+ independent streams. `annotate` already reinitializes the heat mask
+ when the scene resolution changes; call this to discard heat from a
+ previous stream that shares the same resolution.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((40, 40, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(xyxy=np.array([[10, 10, 20, 20]]))
+ >>> heat_map_annotator = sv.HeatMapAnnotator()
+ >>> _ = heat_map_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections
+ ... )
+ >>> bool(heat_map_annotator.heat_mask.sum() > 0)
+ True
+ >>> heat_map_annotator.reset()
+ >>> heat_map_annotator.heat_mask is None
+ True
+
+ ```
+ """
+ self.heat_mask = None
+
+ @ensure_cv2_image_for_class_method
+ def annotate(self, scene: ImageType, detections: Detections) -> ImageType:
+ """
+ Annotates the scene with a heatmap based on the provided detections.
+
+ Args:
+ scene: The image where the heatmap will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Note:
+ When `detections` is empty or no heat has accumulated yet, the
+ scene is returned unchanged without raising a ``RuntimeWarning``.
+
+ Example:
+ ```python
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO('yolov8x.pt')
+
+ heat_map_annotator = sv.HeatMapAnnotator()
+
+ video_info = sv.VideoInfo.from_video_path(video_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:
+ result = model(frame)[0]
+ detections = sv.Detections.from_ultralytics(result)
+ annotated_frame = heat_map_annotator.annotate(
+ scene=frame.copy(),
+ detections=detections)
+ sink.write_frame(frame=annotated_frame)
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ if self.heat_mask is None or self.heat_mask.shape != scene.shape[:2]:
+ self.heat_mask = np.zeros(scene.shape[:2], dtype=np.float32)
+
+ mask: npt.NDArray[np.float32] = np.zeros(scene.shape[:2], dtype=np.float32)
+ for xy in detections.get_anchors_coordinates(self.position):
+ x, y = int(xy[0]), int(xy[1])
+ cv2.circle(
+ img=mask,
+ center=(x, y),
+ radius=self.radius,
+ color=(1,),
+ thickness=-1, # fill
+ )
+ self.heat_mask = mask + self.heat_mask
+ heat_mask = self.heat_mask
+ heat_values = heat_mask.copy()
+ max_val = heat_values.max()
+ if max_val > 0:
+ heat_values = self.low_hue - heat_values / max_val * (
+ self.low_hue - self.top_hue
+ )
+ heat_hue = heat_values.astype(np.uint8)
+ if self.kernel_size is not None:
+ heat_hue = cast(
+ npt.NDArray[np.uint8],
+ cv2.blur(heat_hue, (self.kernel_size, self.kernel_size)),
+ )
+ hsv = np.full(scene.shape, 255, dtype=np.uint8)
+ hsv[..., 0] = heat_hue
+ heat_bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
+ mask2d = heat_mask > 0
+ blended = cv2.addWeighted(heat_bgr, self.opacity, scene, 1 - self.opacity, 0)
+ scene[mask2d] = blended[mask2d]
+ return scene
+
+
+class PixelateAnnotator(BaseAnnotator):
+ """
+ A class for pixelating regions in an image using provided detections.
+ """
+
+ def __init__(self, pixel_size: int | None = None):
+ """
+ Args:
+ pixel_size: The size of the pixelation. If not set, a dynamic size is
+ computed as one-half of the shorter bounding-box dimension. When set
+ and the detection area is smaller than `pixel_size`, the region is
+ filled with its average colour instead to avoid an OpenCV crash.
+ Must be >= 1 when provided.
+ """
+ if pixel_size is not None and pixel_size < 1:
+ raise ValueError(f"pixel_size must be >= 1, got {pixel_size}.")
+ self.pixel_size: int | None = pixel_size
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ ) -> ImageType:
+ """
+ Annotates the given scene by pixelating regions based on the provided
+ detections.
+
+ Args:
+ scene: The image where pixelating will be applied.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Example:
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections = sv.Detections(...)
+
+ pixelate_annotator = sv.PixelateAnnotator()
+ annotated_frame = pixelate_annotator.annotate(
+ scene=image.copy(),
+ detections=detections
+ )
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ image_height, image_width = scene.shape[:2]
+ clipped_xyxy: npt.NDArray[np.int32] = clip_boxes(
+ xyxy=detections.xyxy,
+ resolution_wh=(image_width, image_height),
+ ).astype(int)
+
+ for x1, y1, x2, y2 in clipped_xyxy:
+ if x2 <= x1 or y2 <= y1:
+ continue
+ roi = scene[y1:y2, x1:x2]
+
+ pixel_size = (
+ self.pixel_size
+ if self.pixel_size is not None
+ else calculate_dynamic_pixel_size(x1, y1, x2, y2)
+ )
+ if min(y2 - y1, x2 - x1) < pixel_size:
+ if roi.ndim == 2 or (roi.ndim == 3 and roi.shape[2] == 1):
+ scene[y1:y2, x1:x2] = cv2.mean(roi)[0]
+ else:
+ num_channels = scene.shape[2]
+ scene[y1:y2, x1:x2] = cv2.mean(roi)[:num_channels]
+ continue
+
+ scaled_up_roi = cv2.resize(
+ src=roi, dsize=None, fx=1 / pixel_size, fy=1 / pixel_size
+ )
+ scaled_down_roi = cv2.resize(
+ src=scaled_up_roi,
+ dsize=(roi.shape[1], roi.shape[0]),
+ interpolation=cv2.INTER_NEAREST,
+ )
+
+ scene[y1:y2, x1:x2] = scaled_down_roi
+
+ return scene
+
+
+class TriangleAnnotator(BaseAnnotator):
+ """
+ A class for drawing triangle markers on an image at specific coordinates based on
+ provided detections.
+ """
+
+ def __init__(
+ self,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ base: int = 10,
+ height: int = 10,
+ position: Position = Position.TOP_CENTER,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ outline_thickness: int = 0,
+ outline_color: Color | ColorPalette | str = Color.BLACK,
+ ):
+ """
+ Args:
+ color: The color or color palette to use for
+ annotating detections.
+ base: The base width of the triangle.
+ height: The height of the triangle.
+ position: The anchor position for placing the triangle.
+ color_lookup: Strategy for mapping colors to annotations.
+ Options are `INDEX`, `CLASS`, `TRACK`.
+ outline_thickness: Thickness of the outline of the triangle.
+ outline_color: The color or color palette to
+ use for outline. It is activated by setting outline_thickness to a value
+ greater than 0.
+ """
+ self.color: Color | ColorPalette = _normalize_color_input(color)
+ self.base: int = base
+ self.height: int = height
+ self.position: Position = position
+ self.color_lookup: ColorLookup = color_lookup
+ self.outline_thickness: int = outline_thickness
+ self.outline_color: Color | ColorPalette = _normalize_color_input(outline_color)
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ ) -> ImageType:
+ """
+ Annotates the given scene with triangles based on the provided detections.
+
+ Args:
+ scene: The image where triangles will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((100, 100, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... class_id=np.array([0])
+ ... )
+ >>> triangle_annotator = sv.TriangleAnnotator()
+ >>> annotated_frame = triangle_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections
+ ... )
+
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ xy = detections.get_anchors_coordinates(anchor=self.position)
+ for detection_idx in range(len(detections)):
+ color = resolve_color(
+ color=self.color,
+ detections=detections,
+ detection_idx=detection_idx,
+ color_lookup=self.color_lookup
+ if custom_color_lookup is None
+ else custom_color_lookup,
+ )
+ tip_x, tip_y = int(xy[detection_idx, 0]), int(xy[detection_idx, 1])
+ vertices = np.array(
+ [
+ [tip_x - self.base // 2, tip_y - self.height],
+ [tip_x + self.base // 2, tip_y - self.height],
+ [tip_x, tip_y],
+ ],
+ np.int32,
+ )
+
+ cv2.fillPoly(scene, [vertices], color.as_bgr())
+ if self.outline_thickness:
+ outline_color = resolve_color(
+ color=self.outline_color,
+ detections=detections,
+ detection_idx=detection_idx,
+ color_lookup=self.color_lookup
+ if custom_color_lookup is None
+ else custom_color_lookup,
+ )
+ cv2.polylines(
+ scene,
+ [vertices],
+ True,
+ outline_color.as_bgr(),
+ thickness=self.outline_thickness,
+ )
+ return scene
+
+
+class RoundBoxAnnotator(BaseAnnotator):
+ """
+ A class for drawing bounding boxes with round edges on an image
+ using provided detections.
+ """
+
+ def __init__(
+ self,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ thickness: int = 2,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ roundness: float = 0.6,
+ ):
+ """
+ Args:
+ color: The color or color palette to use for
+ annotating detections.
+ thickness: Thickness of the bounding box lines.
+ color_lookup: Strategy for mapping colors to annotations.
+ Options are `INDEX`, `CLASS`, `TRACK`.
+ roundness: Percent of roundness for edges of bounding box.
+ Value must be float 0 < roundness <= 1.0
+ By default roundness percent is calculated based on smaller side
+ length (width or height).
+ """
+ self.color: Color | ColorPalette = _normalize_color_input(color)
+ self.thickness: int = thickness
+ self.color_lookup: ColorLookup = color_lookup
+ if not 0 < roundness <= 1.0:
+ raise ValueError("roundness attribute must be float between (0, 1.0]")
+ self.roundness: float = roundness
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ ) -> ImageType:
+ """
+ Annotates the given scene with bounding boxes with rounded edges
+ based on the provided detections.
+
+ Args:
+ scene: The image where rounded bounding boxes will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((100, 100, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... class_id=np.array([0])
+ ... )
+ >>> round_box_annotator = sv.RoundBoxAnnotator()
+ >>> annotated_frame = round_box_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections
+ ... )
+
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ for detection_idx in range(len(detections)):
+ x1, y1, x2, y2 = detections.xyxy[detection_idx].astype(int)
+ color = resolve_color(
+ color=self.color,
+ detections=detections,
+ detection_idx=detection_idx,
+ color_lookup=self.color_lookup
+ if custom_color_lookup is None
+ else custom_color_lookup,
+ )
+
+ radius = (
+ int((x2 - x1) // 2 * self.roundness)
+ if abs(x1 - x2) < abs(y1 - y2)
+ else int((y2 - y1) // 2 * self.roundness)
+ )
+
+ circle_coordinates = [
+ ((x1 + radius), (y1 + radius)),
+ ((x2 - radius), (y1 + radius)),
+ ((x2 - radius), (y2 - radius)),
+ ((x1 + radius), (y2 - radius)),
+ ]
+
+ line_coordinates = [
+ ((x1 + radius, y1), (x2 - radius, y1)),
+ ((x2, y1 + radius), (x2, y2 - radius)),
+ ((x1 + radius, y2), (x2 - radius, y2)),
+ ((x1, y1 + radius), (x1, y2 - radius)),
+ ]
+
+ start_angles = (180, 270, 0, 90)
+ end_angles = (270, 360, 90, 180)
+
+ for center_coordinates, line, start_angle, end_angle in zip(
+ circle_coordinates, line_coordinates, start_angles, end_angles
+ ):
+ cv2.ellipse(
+ img=scene,
+ center=center_coordinates,
+ axes=(radius, radius),
+ angle=0,
+ startAngle=start_angle,
+ endAngle=end_angle,
+ color=color.as_bgr(),
+ thickness=self.thickness,
+ )
+
+ cv2.line(
+ img=scene,
+ pt1=line[0],
+ pt2=line[1],
+ color=color.as_bgr(),
+ thickness=self.thickness,
+ )
+
+ return scene
+
+
+class PercentageBarAnnotator(BaseAnnotator):
+ """
+ A class for drawing percentage bars on an image using provided detections.
+ """
+
+ def __init__(
+ self,
+ height: int = 16,
+ width: int = 80,
+ color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ border_color: Color | str = Color.BLACK,
+ position: Position = Position.TOP_CENTER,
+ color_lookup: ColorLookup = ColorLookup.CLASS,
+ border_thickness: int | None = None,
+ ):
+ """
+ Args:
+ height: The height in pixels of the percentage bar.
+ width: The width in pixels of the percentage bar.
+ color: The color or color palette to use for
+ annotating detections.
+ border_color: The color of the border lines.
+ position: The anchor position of drawing the percentage bar.
+ color_lookup: Strategy for mapping colors to annotations.
+ Options are `INDEX`, `CLASS`, `TRACK`.
+ border_thickness: The thickness of the border lines.
+ """
+ self.height: int = height
+ self.width: int = width
+ self.color: Color | ColorPalette = _normalize_color_input(color)
+ self.border_color = cast(Color, _normalize_color_input(border_color))
+ self.position: Position = position
+ self.color_lookup: ColorLookup = color_lookup
+
+ self.border_thickness = (
+ border_thickness
+ if border_thickness is not None
+ else int(0.15 * self.height)
+ )
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ custom_values: npt.NDArray[np.float64] | None = None,
+ ) -> ImageType:
+ """
+ Annotates the given scene with percentage bars based on the provided
+ detections. The percentage bars visually represent the confidence or custom
+ values associated with each detection.
+
+ Args:
+ scene: The image where percentage bars will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+ custom_values: Custom values array to use instead
+ of the default detection confidences. This array should have the
+ same length as the number of detections and contain a value between
+ 0 and 1 (inclusive) for each detection, representing the percentage
+ to be displayed.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((100, 100, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... confidence=np.array([0.9]),
+ ... class_id=np.array([0])
+ ... )
+ >>> percentage_bar_annotator = sv.PercentageBarAnnotator()
+ >>> annotated_frame = percentage_bar_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections
+ ... )
+
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ self._validate_custom_values(custom_values=custom_values, detections=detections)
+
+ anchors = detections.get_anchors_coordinates(anchor=self.position)
+ for detection_idx in range(len(detections)):
+ anchor = anchors[detection_idx]
+ border_coordinates = self.calculate_border_coordinates(
+ anchor_xy=(int(anchor[0]), int(anchor[1])),
+ border_wh=(self.width, self.height),
+ position=self.position,
+ )
+ border_width = border_coordinates[1][0] - border_coordinates[0][0]
+
+ if custom_values is not None:
+ value = custom_values[detection_idx]
+ else:
+ assert detections.confidence is not None # MyPy type hint
+ value = detections.confidence[detection_idx]
+
+ color = resolve_color(
+ color=self.color,
+ detections=detections,
+ detection_idx=detection_idx,
+ color_lookup=self.color_lookup
+ if custom_color_lookup is None
+ else custom_color_lookup,
+ )
+ cv2.rectangle(
+ img=scene,
+ pt1=border_coordinates[0],
+ pt2=(
+ border_coordinates[0][0] + int(border_width * value),
+ border_coordinates[1][1],
+ ),
+ color=color.as_bgr(),
+ thickness=-1,
+ )
+ cv2.rectangle(
+ img=scene,
+ pt1=border_coordinates[0],
+ pt2=border_coordinates[1],
+ color=self.border_color.as_bgr(),
+ thickness=self.border_thickness,
+ )
+ return scene
+
+ @staticmethod
+ def calculate_border_coordinates(
+ anchor_xy: tuple[int, int], border_wh: tuple[int, int], position: Position
+ ) -> tuple[tuple[int, int], tuple[int, int]]:
+ """Compute the border corner coordinates for a given anchor position."""
+ cx, cy = anchor_xy
+ width, height = border_wh
+
+ if position == Position.TOP_LEFT:
+ return (cx - width, cy - height), (cx, cy)
+ elif position == Position.TOP_CENTER:
+ return (cx - width // 2, cy), (cx + width // 2, cy - height)
+ elif position == Position.TOP_RIGHT:
+ return (cx, cy), (cx + width, cy - height)
+ elif position == Position.CENTER_LEFT:
+ return (cx - width, cy - height // 2), (cx, cy + height // 2)
+ elif position == Position.CENTER or position == Position.CENTER_OF_MASS:
+ return (
+ (cx - width // 2, cy - height // 2),
+ (cx + width // 2, cy + height // 2),
+ )
+ elif position == Position.CENTER_RIGHT:
+ return (cx, cy - height // 2), (cx + width, cy + height // 2)
+ elif position == Position.BOTTOM_LEFT:
+ return (cx - width, cy), (cx, cy + height)
+ elif position == Position.BOTTOM_CENTER:
+ return (cx - width // 2, cy), (cx + width // 2, cy + height)
+ elif position == Position.BOTTOM_RIGHT:
+ return (cx, cy), (cx + width, cy + height)
+ raise ValueError(f"Unsupported position: {position}")
+
+ @staticmethod
+ def _validate_custom_values(
+ custom_values: npt.NDArray[np.float64] | list[float] | None,
+ detections: Detections,
+ ) -> None:
+ if custom_values is None:
+ if detections.confidence is None:
+ raise ValueError(
+ "The provided detections do not contain confidence values. "
+ "Please provide `custom_values` or ensure that the detections "
+ "contain confidence values (e.g. by using a different model)."
+ )
+
+ else:
+ if not isinstance(custom_values, (np.ndarray, list)):
+ raise TypeError(
+ "custom_values must be either a numpy array or a list of floats."
+ )
+
+ if len(custom_values) != len(detections):
+ raise ValueError(
+ "The length of custom_values must match the number of detections."
+ )
+
+ if not all(0 <= value <= 1 for value in custom_values):
+ raise ValueError("All values in custom_values must be between 0 and 1.")
+
+ @staticmethod
+ @deprecated( # type: ignore[untyped-decorator]
+ target=_validate_custom_values.__func__, # type: ignore[attr-defined]
+ deprecated_in="0.29.0",
+ remove_in="0.32.0",
+ )
+ def validate_custom_values(
+ custom_values: npt.NDArray[np.float64] | list[float] | None,
+ detections: Detections,
+ ) -> None:
+ void(custom_values, detections)
+
+
+class CropAnnotator(BaseAnnotator):
+ """
+ A class for drawing scaled up crops of detections on the scene.
+ """
+
+ def __init__(
+ self,
+ position: Position = Position.TOP_CENTER,
+ scale_factor: float = 2.0,
+ border_color: Color | ColorPalette | str = ColorPalette.DEFAULT,
+ border_thickness: int = 2,
+ border_color_lookup: ColorLookup = ColorLookup.CLASS,
+ ):
+ """
+ Args:
+ position: The anchor position for placing the cropped and scaled
+ part of the detection in the scene.
+ scale_factor: The factor by which to scale the cropped image part. A
+ factor of 2, for example, would double the size of the cropped area,
+ allowing for a closer view of the detection.
+ border_color: The color or color palette to
+ use for annotating border around the cropped area.
+ border_thickness: The thickness of the border around the cropped area.
+ border_color_lookup: Strategy for mapping colors to
+ annotations. Options are `INDEX`, `CLASS`, `TRACK`.
+ """
+ self.position: Position = position
+ self.scale_factor: float = scale_factor
+ self.border_color: Color | ColorPalette = _normalize_color_input(border_color)
+ self.border_thickness: int = border_thickness
+ self.border_color_lookup: ColorLookup = border_color_lookup
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self,
+ scene: ImageType,
+ detections: Detections,
+ custom_color_lookup: npt.NDArray[np.int_] | None = None,
+ ) -> ImageType:
+ """
+ Annotates the provided scene with scaled and cropped parts of the image based
+ on the provided detections. Each detection is cropped from the original scene
+ and scaled according to the annotator's scale factor before being placed back
+ onto the scene at the specified position.
+
+
+ Args:
+ scene: The image where cropped detection will be placed.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+ custom_color_lookup: Custom color lookup array.
+ Allows to override the default color mapping strategy.
+
+ Returns:
+ The annotated image.
+
+ Note:
+ Detections whose bounding boxes extend partially outside `scene` are
+ clipped to scene bounds before cropping. Detections fully outside the
+ scene collapse to zero area after clipping and are skipped without
+ raising an error.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((100, 100, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... class_id=np.array([0])
+ ... )
+ >>> crop_annotator = sv.CropAnnotator()
+ >>> annotated_frame = crop_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections
+ ... )
+
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ image_height, image_width = scene.shape[:2]
+ clipped_xyxy: npt.NDArray[np.int32] = clip_boxes(
+ xyxy=detections.xyxy,
+ resolution_wh=(image_width, image_height),
+ ).astype(np.int32)
+ anchors: npt.NDArray[np.int32] = detections.get_anchors_coordinates(
+ anchor=self.position
+ ).astype(np.int32)
+ # Snapshot before the loop so later crops are taken from the original image,
+ # not a scene already annotated by earlier iterations (overlapping-box case).
+ source_scene = scene.copy()
+
+ for idx, (xyxy, anchor) in enumerate(zip(clipped_xyxy, anchors)):
+ crop_x1, crop_y1, crop_x2, crop_y2 = xyxy
+ if crop_x2 <= crop_x1 or crop_y2 <= crop_y1:
+ continue
+ crop = crop_image(image=source_scene, xyxy=xyxy)
+ resized_crop = scale_image(image=crop, scale_factor=self.scale_factor)
+ crop_wh = resized_crop.shape[1], resized_crop.shape[0]
+ (x1, y1), (x2, y2) = self.calculate_crop_coordinates(
+ anchor=anchor, crop_wh=crop_wh, position=self.position
+ )
+ scene = _overlay_image(image=scene, overlay=resized_crop, anchor=(x1, y1))
+ color = resolve_color(
+ color=self.border_color,
+ detections=detections,
+ detection_idx=idx,
+ color_lookup=self.border_color_lookup
+ if custom_color_lookup is None
+ else custom_color_lookup,
+ )
+ cv2.rectangle(
+ img=scene,
+ pt1=(x1, y1),
+ pt2=(x2, y2),
+ color=color.as_bgr(),
+ thickness=self.border_thickness,
+ )
+
+ return scene
+
+ @staticmethod
+ def calculate_crop_coordinates(
+ anchor: tuple[int, int], crop_wh: tuple[int, int], position: Position
+ ) -> tuple[tuple[int, int], tuple[int, int]]:
+ """Compute the crop coordinates for a given anchor position."""
+ anchor_x, anchor_y = anchor
+ width, height = crop_wh
+
+ if position == Position.TOP_LEFT:
+ return (anchor_x - width, anchor_y - height), (anchor_x, anchor_y)
+ elif position == Position.TOP_CENTER:
+ return (
+ (anchor_x - width // 2, anchor_y - height),
+ (anchor_x + width // 2, anchor_y),
+ )
+ elif position == Position.TOP_RIGHT:
+ return (anchor_x, anchor_y - height), (anchor_x + width, anchor_y)
+ elif position == Position.CENTER_LEFT:
+ return (
+ (anchor_x - width, anchor_y - height // 2),
+ (anchor_x, anchor_y + height // 2),
+ )
+ elif position == Position.CENTER or position == Position.CENTER_OF_MASS:
+ return (
+ (anchor_x - width // 2, anchor_y - height // 2),
+ (anchor_x + width // 2, anchor_y + height // 2),
+ )
+ elif position == Position.CENTER_RIGHT:
+ return (
+ (anchor_x, anchor_y - height // 2),
+ (anchor_x + width, anchor_y + height // 2),
+ )
+ elif position == Position.BOTTOM_LEFT:
+ return (anchor_x - width, anchor_y), (anchor_x, anchor_y + height)
+ elif position == Position.BOTTOM_CENTER:
+ return (
+ (anchor_x - width // 2, anchor_y),
+ (anchor_x + width // 2, anchor_y + height),
+ )
+ elif position == Position.BOTTOM_RIGHT:
+ return (anchor_x, anchor_y), (anchor_x + width, anchor_y + height)
+ raise ValueError(f"Unsupported position: {position}")
+
+
+class BackgroundOverlayAnnotator(BaseAnnotator):
+ """
+ A class for drawing a colored overlay on the background of an image outside
+ the region of detections.
+
+ If masks are provided, the background is colored outside the masks.
+ If masks are not provided, the background is colored outside the bounding boxes.
+
+ You can use the `force_box` parameter to force the annotator to use bounding boxes.
+
+ !!! warning
+
+ This annotator uses `sv.Detections.mask`.
+ """
+
+ def __init__(
+ self,
+ color: Color = Color.BLACK,
+ opacity: float = 0.5,
+ force_box: bool = False,
+ ):
+ """
+ Args:
+ color: The color to use for annotating detections.
+ opacity: Opacity of the overlay mask. Must be between `0` and `1`.
+ force_box: If `True`, forces the annotator to use bounding boxes when
+ masks are provided in the supplied sv.Detections.
+ """
+ self.color: Color = color
+ self.opacity = opacity
+ self.force_box = force_box
+
+ @ensure_cv2_image_for_class_method
+ def annotate(self, scene: ImageType, detections: Detections) -> ImageType:
+ """
+ Applies a colored overlay to the scene outside of the detected regions.
+
+ Args:
+ scene: The image where masks will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections: Object detections to annotate.
+
+ Returns:
+ The annotated image, matching the type of `scene` (`numpy.ndarray`
+ or `PIL.Image.Image`)
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> image = np.zeros((100, 100, 3), dtype=np.uint8)
+ >>> detections = sv.Detections(
+ ... xyxy=np.array([[20, 20, 80, 80]]),
+ ... class_id=np.array([0])
+ ... )
+ >>> background_overlay_annotator = sv.BackgroundOverlayAnnotator()
+ >>> annotated_frame = background_overlay_annotator.annotate(
+ ... scene=image.copy(),
+ ... detections=detections
+ ... )
+
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ colored_mask = np.full_like(scene, self.color.as_bgr(), dtype=np.uint8)
+
+ cv2.addWeighted(
+ scene, 1 - self.opacity, colored_mask, self.opacity, 0, dst=colored_mask
+ )
+
+ if detections.mask is None or self.force_box:
+ image_height, image_width = scene.shape[:2]
+ clipped_xyxy: npt.NDArray[np.int32] = clip_boxes(
+ xyxy=detections.xyxy,
+ resolution_wh=(image_width, image_height),
+ ).astype(np.int32)
+ for x1, y1, x2, y2 in clipped_xyxy:
+ colored_mask[y1:y2, x1:x2] = scene[y1:y2, x1:x2]
+ else:
+ for mask in detections.mask:
+ mask_bool = np.asarray(mask, dtype=bool)
+ colored_mask[mask_bool] = scene[mask_bool]
+
+ np.copyto(scene, colored_mask)
+ return scene
+
+
+class ComparisonAnnotator:
+ """
+ Highlights the differences between two sets of detections.
+ Useful for comparing results from two different models, or the difference
+ between a ground truth and a prediction.
+
+ If present, uses the oriented bounding box data.
+ Otherwise, if present, uses a mask.
+ Otherwise, uses the bounding box data.
+ """
+
+ # Not a BaseAnnotator subclass โ duck-typing callers can still check requires_mask
+ requires_mask: ClassVar[bool] = False
+
+ def __init__(
+ self,
+ color_1: Color = Color.RED,
+ color_2: Color = Color.GREEN,
+ color_overlap: Color = Color.BLUE,
+ *,
+ opacity: float = 0.75,
+ label_1: str = "",
+ label_2: str = "",
+ label_overlap: str = "",
+ label_scale: float = 1.0,
+ ):
+ """
+ Args:
+ color_1: Color of areas only present in the first set of
+ detections.
+ color_2: Color of areas only present in the second set of
+ detections.
+ color_overlap: Color of areas present in both sets of detections.
+ opacity: Opacity of the overlay mask. Must be between `0` and `1`.
+ label_1: Label for the first set of detections.
+ label_2: Label for the second set of detections.
+ label_overlap: Label for areas present in both sets of detections.
+ label_scale: Controls how large the labels are.
+ """
+
+ self.color_1 = color_1
+ self.color_2 = color_2
+ self.color_overlap = color_overlap
+
+ self.opacity = opacity
+ self.label_1 = label_1
+ self.label_2 = label_2
+ self.label_overlap = label_overlap
+ self.label_scale = label_scale
+ self.text_thickness = int(self.label_scale + 1.2)
+
+ @ensure_cv2_image_for_class_method
+ def annotate(
+ self, scene: ImageType, detections_1: Detections, detections_2: Detections
+ ) -> ImageType:
+ """
+ Highlights the differences between two sets of detections.
+
+ Args:
+ scene: The image where detections will be drawn.
+ `ImageType` is a flexible type, accepting either `numpy.ndarray`
+ or `PIL.Image.Image`.
+ detections_1: The first set of detections or predictions.
+ detections_2: The second set of detections to compare or
+ ground truth.
+
+ Returns:
+ The annotated image.
+
+ Example:
+ ```python
+ import supervision as sv
+
+ image = ...
+ detections_1 = sv.Detections(...)
+ detections_2 = sv.Detections(...)
+
+ comparison_annotator = sv.ComparisonAnnotator()
+ annotated_frame = comparison_annotator.annotate(
+ scene=image.copy(),
+ detections_1=detections_1,
+ detections_2=detections_2
+ )
+ ```
+
+ 
+ """
+ if not isinstance(scene, np.ndarray):
+ return scene
+ if detections_1.is_empty() and detections_2.is_empty():
+ return scene
+
+ use_obb = self._use_obb(detections_1, detections_2)
+ use_mask = self._use_mask(detections_1, detections_2)
+
+ if use_obb:
+ mask_1 = self._mask_from_obb(scene, detections_1)
+ mask_2 = self._mask_from_obb(scene, detections_2)
+
+ elif use_mask:
+ mask_1 = self._mask_from_mask(scene, detections_1)
+ mask_2 = self._mask_from_mask(scene, detections_2)
+
+ else:
+ mask_1 = self._mask_from_xyxy(scene, detections_1)
+ mask_2 = self._mask_from_xyxy(scene, detections_2)
+
+ mask_overlap = mask_1 & mask_2
+ mask_1 = mask_1 & ~mask_overlap
+ mask_2 = mask_2 & ~mask_overlap
+
+ color_layer = np.zeros_like(scene, dtype=np.uint8)
+ color_layer[mask_overlap] = self.color_overlap.as_bgr()
+ color_layer[mask_1] = self.color_1.as_bgr()
+ color_layer[mask_2] = self.color_2.as_bgr()
+
+ scene[mask_overlap] = (1 - self.opacity) * scene[
+ mask_overlap
+ ] + self.opacity * color_layer[mask_overlap]
+ scene[mask_1] = (1 - self.opacity) * scene[mask_1] + self.opacity * color_layer[
+ mask_1
+ ]
+ scene[mask_2] = (1 - self.opacity) * scene[mask_2] + self.opacity * color_layer[
+ mask_2
+ ]
+
+ self._draw_labels(scene)
+
+ return scene
+
+ @staticmethod
+ def _use_obb(detections_1: Detections, detections_2: Detections) -> bool:
+ assert not detections_1.is_empty() or not detections_2.is_empty()
+ is_obb_1 = ORIENTED_BOX_COORDINATES in detections_1.data
+ is_obb_2 = ORIENTED_BOX_COORDINATES in detections_2.data
+ return (
+ (is_obb_1 and is_obb_2)
+ or (is_obb_1 and detections_2.is_empty())
+ or (detections_1.is_empty() and is_obb_2)
+ )
+
+ @staticmethod
+ def _use_mask(detections_1: Detections, detections_2: Detections) -> bool:
+ assert not detections_1.is_empty() or not detections_2.is_empty()
+ is_mask_1 = detections_1.mask is not None
+ is_mask_2 = detections_2.mask is not None
+ return (
+ (is_mask_1 and is_mask_2)
+ or (is_mask_1 and detections_2.is_empty())
+ or (detections_1.is_empty() and is_mask_2)
+ )
+
+ @staticmethod
+ def _mask_from_xyxy(
+ scene: npt.NDArray[np.uint8], detections: Detections
+ ) -> npt.NDArray[np.bool_]:
+ mask = np.zeros(scene.shape[:2], dtype=np.bool_)
+ if detections.is_empty():
+ return mask
+
+ resolution_wh = scene.shape[1], scene.shape[0]
+ polygons = xyxy_to_polygons(detections.xyxy)
+
+ for polygon in polygons:
+ polygon_mask = polygon_to_mask(polygon, resolution_wh=resolution_wh)
+ mask |= polygon_mask.astype(np.bool_)
+ return mask
+
+ @staticmethod
+ def _mask_from_obb(
+ scene: npt.NDArray[np.uint8], detections: Detections
+ ) -> npt.NDArray[np.bool_]:
+ mask = np.zeros(scene.shape[:2], dtype=np.bool_)
+ if detections.is_empty():
+ return mask
+
+ resolution_wh = scene.shape[1], scene.shape[0]
+
+ for polygon in detections.data[ORIENTED_BOX_COORDINATES]:
+ polygon_mask = polygon_to_mask(polygon, resolution_wh=resolution_wh)
+ mask |= polygon_mask.astype(np.bool_)
+ return mask
+
+ @staticmethod
+ def _mask_from_mask(
+ scene: npt.NDArray[np.uint8], detections: Detections
+ ) -> npt.NDArray[np.bool_]:
+ mask = np.zeros(scene.shape[:2], dtype=np.bool_)
+ if detections.is_empty():
+ return mask
+ assert detections.mask is not None
+
+ for detections_mask in detections.mask:
+ mask |= detections_mask.astype(np.bool_)
+ return mask
+
+ def _draw_labels(self, scene: npt.NDArray[np.uint8]) -> None:
+ """
+ Draw the labels, explaining what each color represents, with automatically
+ computed positions.
+
+ Args:
+ scene: The image where the labels will be drawn.
+ """
+ margin = int(50 * self.label_scale)
+ gap = int(40 * self.label_scale)
+ y0 = int(50 * self.label_scale)
+ height = int(50 * self.label_scale)
+
+ marker_size = int(20 * self.label_scale)
+ padding = int(10 * self.label_scale)
+ text_box_corner_radius = int(10 * self.label_scale)
+ marker_corner_radius = int(4 * self.label_scale)
+ text_scale = self.label_scale
+
+ label_color_pairs = [
+ (self.label_1, self.color_1),
+ (self.label_2, self.color_2),
+ (self.label_overlap, self.color_overlap),
+ ]
+
+ x0 = margin
+ for text, color in label_color_pairs:
+ if not text:
+ continue
+
+ (text_w, _) = cv2.getTextSize(
+ text=text,
+ fontFace=CV2_FONT,
+ fontScale=self.label_scale,
+ thickness=self.text_thickness,
+ )[0]
+
+ width = text_w + marker_size + padding * 4
+ center_x = x0 + width // 2
+ center_y = y0 + height // 2
+
+ draw_rounded_rectangle(
+ scene=scene,
+ rect=Rect(x=x0, y=y0, width=width, height=height),
+ color=Color.WHITE,
+ border_radius=text_box_corner_radius,
+ )
+
+ draw_rounded_rectangle(
+ scene=scene,
+ rect=Rect(
+ x=x0 + padding,
+ y=center_y - marker_size / 2,
+ width=marker_size,
+ height=marker_size,
+ ),
+ color=color,
+ border_radius=marker_corner_radius,
+ )
+
+ draw_text(
+ scene,
+ text,
+ text_anchor=Point(x=center_x + marker_size, y=center_y),
+ text_scale=text_scale,
+ text_thickness=self.text_thickness,
+ )
+
+ x0 += width + gap
diff --git a/src/supervision/annotators/utils.py b/src/supervision/annotators/utils.py
new file mode 100644
index 0000000..afb72d0
--- /dev/null
+++ b/src/supervision/annotators/utils.py
@@ -0,0 +1,538 @@
+import re
+import textwrap
+from enum import Enum
+from typing import Any, cast
+
+import numpy as np
+import numpy.typing as npt
+from deprecate import deprecated, void
+
+from supervision.config import CLASS_NAME_DATA_FIELD
+from supervision.detection.core import Detections
+from supervision.draw.color import Color, ColorPalette
+from supervision.geometry.core import Position
+
+PENDING_TRACK_COLOR = Color.GREY
+PENDING_TRACK_ID = -1
+
+
+class ColorLookup(Enum):
+ """
+ Enumeration class to define strategies for mapping colors to annotations.
+
+ This enum supports three different lookup strategies:
+ - `INDEX`: Colors are determined by the index of the detection within the scene.
+ - `CLASS`: Colors are determined by the class label of the detected object.
+ - `TRACK`: Colors are determined by the tracking identifier of the object.
+ """
+
+ INDEX = "index"
+ CLASS = "class"
+ TRACK = "track"
+
+ @classmethod
+ def list(cls) -> list[str]:
+ return list(map(lambda c: c.value, cls))
+
+
+def resolve_color_idx(
+ detections: Detections,
+ detection_idx: int,
+ color_lookup: ColorLookup | npt.NDArray[np.int_] = ColorLookup.CLASS,
+) -> int:
+ if detection_idx >= len(detections):
+ raise ValueError(
+ f"Detection index {detection_idx} "
+ f"is out of bounds for detections of length {len(detections)}"
+ )
+
+ if isinstance(color_lookup, np.ndarray):
+ if len(color_lookup) != len(detections):
+ raise ValueError(
+ f"Length of color lookup {len(color_lookup)} "
+ f"does not match length of detections {len(detections)}"
+ )
+ return int(color_lookup[detection_idx])
+ elif color_lookup == ColorLookup.INDEX:
+ return detection_idx
+ elif color_lookup == ColorLookup.CLASS:
+ if detections.class_id is None:
+ raise ValueError(
+ "Could not resolve color by class because "
+ "Detections do not have class_id. If using an annotator, "
+ "try setting color_lookup to sv.ColorLookup.INDEX or "
+ "sv.ColorLookup.TRACK."
+ )
+ return int(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. Did you call "
+ "tracker.update_with_detections(...) before annotating?"
+ )
+ return int(detections.tracker_id[detection_idx])
+ raise ValueError(f"Unsupported color lookup strategy: {color_lookup}")
+
+
+def resolve_text_background_xyxy(
+ center_coordinates: tuple[int, int],
+ text_wh: tuple[int, int],
+ position: Position,
+) -> tuple[int, int, int, int]:
+ """Compute the background box for text anchored at `position`."""
+ center_x, center_y = center_coordinates
+ text_w, text_h = text_wh
+
+ if position == Position.TOP_LEFT:
+ return center_x, center_y - text_h, center_x + text_w, center_y
+ elif position == Position.TOP_RIGHT:
+ return center_x - text_w, center_y - text_h, center_x, center_y
+ elif position == Position.TOP_CENTER:
+ return (
+ center_x - text_w // 2,
+ center_y - text_h,
+ center_x + text_w // 2,
+ center_y,
+ )
+ elif position == Position.CENTER or position == Position.CENTER_OF_MASS:
+ return (
+ center_x - text_w // 2,
+ center_y - text_h // 2,
+ center_x + text_w // 2,
+ center_y + text_h // 2,
+ )
+ elif position == Position.BOTTOM_LEFT:
+ return center_x, center_y, center_x + text_w, center_y + text_h
+ elif position == Position.BOTTOM_RIGHT:
+ return center_x - text_w, center_y, center_x, center_y + text_h
+ elif position == Position.BOTTOM_CENTER:
+ return (
+ center_x - text_w // 2,
+ center_y,
+ center_x + text_w // 2,
+ center_y + text_h,
+ )
+ elif position == Position.CENTER_LEFT:
+ return (
+ center_x - text_w,
+ center_y - text_h // 2,
+ center_x,
+ center_y + text_h // 2,
+ )
+ elif position == Position.CENTER_RIGHT:
+ return (
+ center_x,
+ center_y - text_h // 2,
+ center_x + text_w,
+ center_y + text_h // 2,
+ )
+ raise ValueError(f"Unsupported position: {position}")
+
+
+def get_color_by_index(color: Color | ColorPalette, idx: int) -> Color:
+ """Resolve a color-like object to a concrete `Color` for an index."""
+ color_like = cast(Any, color)
+ # Accept ColorPalette-like objects without depending on their exact concrete class.
+ if callable(getattr(color_like, "by_idx", None)):
+ color_like = color_like.by_idx(idx)
+ if isinstance(color_like, Color):
+ return color_like
+ return Color(
+ r=int(color_like.r),
+ g=int(color_like.g),
+ b=int(color_like.b),
+ a=int(getattr(color_like, "a", 255)),
+ )
+
+
+def resolve_color(
+ color: Color | ColorPalette,
+ detections: Detections,
+ detection_idx: int,
+ color_lookup: ColorLookup | npt.NDArray[np.int_] = ColorLookup.CLASS,
+) -> Color:
+ idx = resolve_color_idx(
+ detections=detections,
+ detection_idx=detection_idx,
+ color_lookup=color_lookup,
+ )
+ if (
+ isinstance(color_lookup, ColorLookup)
+ and color_lookup == ColorLookup.TRACK
+ and idx == PENDING_TRACK_ID
+ ):
+ return PENDING_TRACK_COLOR
+ return get_color_by_index(color=color, idx=idx)
+
+
+def wrap_text(text: object, max_line_length: int | None = None) -> list[str]:
+ """
+ Wrap `text` to the specified maximum line length, respecting existing
+ newlines. Falls back to str() if `text` is not already a string.
+
+ Args:
+ text: The text (or object) to wrap.
+ max_line_length: Maximum width for each wrapped line.
+
+ Returns:
+ Wrapped lines.
+ """
+
+ if not text:
+ return [""]
+
+ if not isinstance(text, str):
+ text = str(text)
+
+ if max_line_length is None:
+ return text.splitlines() or [""]
+
+ if max_line_length <= 0:
+ raise ValueError("max_line_length must be a positive integer")
+
+ paragraphs = text.split("\n")
+ all_lines: list[str] = []
+
+ for paragraph in paragraphs:
+ if paragraph == "":
+ all_lines.append("")
+ continue
+
+ wrapped = textwrap.wrap(
+ paragraph,
+ width=max_line_length,
+ break_long_words=True,
+ replace_whitespace=False,
+ drop_whitespace=True,
+ )
+
+ all_lines.extend(wrapped or [""])
+
+ return all_lines or [""]
+
+
+def _validate_labels(labels: list[str] | None, detections: Detections) -> None:
+ """
+ Validates that the number of provided labels matches the number of detections.
+
+ Args:
+ labels: A list of labels, one for each detection. Can
+ be None.
+ detections: The detections to be labeled.
+
+ Raises:
+ ValueError: If `labels` is not None and its length does not match the number
+ of detections.
+ """
+ if labels is not None and len(labels) != len(detections):
+ raise ValueError(
+ f"The number of labels ({len(labels)}) does not match the "
+ f"number of detections ({len(detections)}). Each detection "
+ f"should have exactly 1 label."
+ )
+
+
+@deprecated( # type: ignore[untyped-decorator]
+ target=_validate_labels,
+ deprecated_in="0.29.0",
+ remove_in="0.32.0",
+)
+def validate_labels(labels: list[str] | None, detections: Detections) -> None:
+ void(labels, detections)
+
+
+def get_labels_text(
+ detections: Detections, custom_labels: list[str] | None
+) -> list[str]:
+ """
+ Retrieves the text labels for the detections.
+
+ If `custom_labels` are provided, they are used. Otherwise, the labels are
+ extracted from the `detections` object, prioritizing the 'class_name' field,
+ then the `class_id`, and finally using the detection index as a string.
+
+ Args:
+ detections: The detections to get labels for.
+ custom_labels: An optional list of custom labels.
+
+ Returns:
+ A list of text labels for each detection.
+ """
+ if custom_labels is not None:
+ return custom_labels
+
+ labels = []
+ for idx in range(len(detections)):
+ if CLASS_NAME_DATA_FIELD in detections.data:
+ labels.append(str(detections.data[CLASS_NAME_DATA_FIELD][idx]))
+ elif detections.class_id is not None:
+ labels.append(str(detections.class_id[idx]))
+ else:
+ labels.append(str(idx))
+ return labels
+
+
+def snap_boxes(
+ xyxy: npt.NDArray[np.float32],
+ resolution_wh: tuple[int, int],
+) -> npt.NDArray[np.float32]:
+ """
+ Shifts `label` bounding boxes into the frame so that they are fully contained
+ within the given resolution, prioritizing the top/left edge.
+ Unlike `clip_boxes`, this function does not crop boxes.
+ It moves them entirely if they exceed the frame boundaries.
+
+ Args:
+ xyxy: 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)`.
+ resolution_wh: A tuple `(width, height)`
+ representing the resolution of the frame.
+
+ Returns:
+ A numpy array of shape `(N, 4)` with boxes shifted into frame.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.annotators.utils import snap_boxes
+ >>> xyxy = np.array([
+ ... [-10, 10, 30, 50], # Off left edge
+ ... [310, 200, 350, 250], # Off right edge
+ ... [100, -20, 150, 30], # Off top edge
+ ... [200, 220, 250, 270], # Off bottom edge
+ ... [-20, 10, 350, 50], # Wider than frame (370 vs 320)
+ ... [10, -20, 30, 260] # Taller than frame (280 vs 240)
+ ... ])
+ >>> resolution_wh = (320, 240)
+ >>> snapped_boxes = snap_boxes(xyxy=xyxy, resolution_wh=resolution_wh)
+ >>> snapped_boxes
+ array([[ 0., 10., 40., 50.],
+ [280., 190., 320., 240.],
+ [100., 0., 150., 50.],
+ [200., 190., 250., 240.],
+ [ 0., 10., 370., 50.],
+ [ 10., 0., 30., 280.]], dtype=float32)
+
+ ```
+ """
+ result: npt.NDArray[np.float32] = np.array(xyxy, dtype=np.float32, copy=True)
+ width, height = resolution_wh
+
+ # X-axis (prioritize left edge)
+ left_overflow = result[:, 0] < 0
+ result[left_overflow, 0:3:2] -= result[left_overflow, 0:1]
+
+ right_overflow = (~left_overflow) & (result[:, 2] > width)
+ right_shift = width - result[right_overflow, 2]
+ result[right_overflow, 0:3:2] += right_shift[:, np.newaxis]
+
+ # Y-axis (prioritize top edge)
+ top_overflow = result[:, 1] < 0
+ result[top_overflow, 1:4:2] -= result[top_overflow, 1:2]
+
+ bottom_overflow = (~top_overflow) & (result[:, 3] > height)
+ bottom_shift = height - result[bottom_overflow, 3]
+ result[bottom_overflow, 1:4:2] += bottom_shift[:, np.newaxis]
+
+ return cast(npt.NDArray[np.float32], result.astype(np.float32, copy=False))
+
+
+class Trace:
+ def __init__(
+ self,
+ max_size: int | None = None,
+ start_frame_id: int = 0,
+ anchor: Position = Position.CENTER,
+ ) -> None:
+ self.current_frame_id = start_frame_id
+ self.max_size = max_size
+ self.anchor = anchor
+
+ self.frame_id: npt.NDArray[np.int_] = np.array([], dtype=int)
+ self.xy: npt.NDArray[np.float32] = np.empty((0, 2), dtype=np.float32)
+ self.tracker_id: npt.NDArray[np.int_] = np.array([], dtype=int)
+
+ def put(self, detections: Detections) -> None:
+ """Append a frame of detections to the trace history."""
+ if detections.tracker_id is None:
+ raise ValueError(
+ "Could not put detections into Trace because "
+ "Detections do not have tracker_id."
+ )
+
+ frame_id: npt.NDArray[np.int_] = np.full(
+ len(detections), self.current_frame_id, dtype=int
+ )
+ self.frame_id = np.concatenate([self.frame_id, frame_id])
+ self.xy = np.concatenate(
+ [
+ self.xy,
+ detections.get_anchors_coordinates(self.anchor),
+ ]
+ )
+ self.tracker_id = np.concatenate([self.tracker_id, detections.tracker_id])
+
+ unique_frame_id = np.unique(self.frame_id)
+
+ if self.max_size is not None and 0 < self.max_size < len(unique_frame_id):
+ max_allowed_frame_id = self.current_frame_id - self.max_size + 1
+ filtering_mask = self.frame_id >= max_allowed_frame_id
+ self.frame_id = self.frame_id[filtering_mask]
+ self.xy = self.xy[filtering_mask]
+ self.tracker_id = self.tracker_id[filtering_mask]
+
+ self.current_frame_id += 1
+
+ def get(self, tracker_id: int) -> npt.NDArray[np.float32]:
+ xy: npt.NDArray[np.float32] = np.asarray(
+ self.xy[self.tracker_id == tracker_id], dtype=np.float32
+ )
+ return xy
+
+ def reset(self) -> None:
+ """Restore the trace buffers to their initial empty state.
+
+ Clears the accumulated `frame_id`, `xy`, and `tracker_id` history and
+ rewinds `current_frame_id` to `0`, so the trace can be reused across
+ independent streams without carrying over points from a previous run.
+ """
+ self.current_frame_id = 0
+ self.frame_id = np.array([], dtype=int)
+ self.xy = np.empty((0, 2), dtype=np.float32)
+ self.tracker_id = np.array([], dtype=int)
+
+
+def hex_to_rgba(hex_color: str) -> tuple[int, int, int, int]:
+ """
+ Converts a hex color string (e.g. "#FF00FF" or "#FF00FF80") to an RGBA tuple.
+
+ Args:
+ hex_color: A hex color string.
+
+ Returns:
+ RGBA values in range 0-255.
+
+ Raises:
+ ValueError: If the format is invalid.
+
+ Examples:
+ ```pycon
+ >>> from supervision.annotators.utils import hex_to_rgba
+ >>> hex_to_rgba("#FF00FF")
+ (255, 0, 255, 255)
+ >>> hex_to_rgba("#FF00FF80")
+ (255, 0, 255, 128)
+
+ ```
+ """
+ hex_color = hex_color.strip().removeprefix("#")
+ if len(hex_color) == 6:
+ hex_color += "FF" # default full opacity
+ if len(hex_color) != 8:
+ raise ValueError(f"Invalid hex color format: {hex_color}")
+ try:
+ r = int(hex_color[0:2], 16)
+ g = int(hex_color[2:4], 16)
+ b = int(hex_color[4:6], 16)
+ a = int(hex_color[6:8], 16)
+ except ValueError as exc:
+ raise ValueError(f"Invalid hex digits in {hex_color}") from exc
+ return (r, g, b, a)
+
+
+def rgba_to_hex(rgba: tuple[int, int, int, int]) -> str:
+ """
+ Converts an RGBA tuple (0-255 each) to a hex color string.
+
+ Args:
+ rgba: RGBA values in range 0-255.
+
+ Returns:
+ Hex color string in the format "#RRGGBBAA".
+
+ Raises:
+ ValueError: If `rgba` is not a 4-tuple or contains values outside 0-255.
+
+ Examples:
+ ```pycon
+ >>> from supervision.annotators.utils import rgba_to_hex
+ >>> rgba_to_hex((255, 0, 255, 128))
+ '#FF00FF80'
+
+ ```
+ """
+ if len(rgba) != 4 or not all(0 <= c <= 255 for c in rgba):
+ raise ValueError("RGBA must be a 4-tuple with values between 0-255.")
+ return "#{:02X}{:02X}{:02X}{:02X}".format(*rgba)
+
+
+def is_valid_hex(hex_color: str) -> bool:
+ """
+ Checks if a given string is a valid hex color.
+
+ Args:
+ hex_color: A hex color string with an optional leading "#". Supports
+ 6-digit (RGB) or 8-digit (RGBA) formats.
+
+ Returns:
+ True if the string is a valid 6- or 8-digit hex color, otherwise False.
+
+ Examples:
+ ```pycon
+ >>> from supervision.annotators.utils import is_valid_hex
+ >>> is_valid_hex("#FF00FF")
+ True
+ >>> is_valid_hex("not-a-color")
+ False
+
+ ```
+ """
+ return bool(re.fullmatch(r"#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?", hex_color.strip()))
+
+
+def calculate_dynamic_kernel_size(x1: int, y1: int, x2: int, y2: int) -> int:
+ """
+ Computes a blur kernel size proportional to the shorter side of a bounding box.
+
+ Args:
+ x1: Left edge of the bounding box.
+ y1: Top edge of the bounding box.
+ x2: Right edge of the bounding box.
+ y2: Bottom edge of the bounding box.
+
+ Returns:
+ Kernel size as one-third of the shorter dimension, minimum 1.
+
+ Examples:
+ ```pycon
+ >>> calculate_dynamic_kernel_size(0, 0, 90, 60)
+ 20
+
+ ```
+ """
+ return max(1, min(y2 - y1, x2 - x1) // 3)
+
+
+def calculate_dynamic_pixel_size(x1: int, y1: int, x2: int, y2: int) -> int:
+ """
+ Computes a pixelation size proportional to the shorter side of a bounding box.
+
+ Args:
+ x1: Left edge of the bounding box.
+ y1: Top edge of the bounding box.
+ x2: Right edge of the bounding box.
+ y2: Bottom edge of the bounding box.
+
+ Returns:
+ Pixel size as one-half of the shorter dimension, minimum 1.
+
+ Examples:
+ ```pycon
+ >>> calculate_dynamic_pixel_size(0, 0, 90, 60)
+ 30
+
+ ```
+ """
+ return max(1, min(y2 - y1, x2 - x1) // 2)
diff --git a/src/supervision/assets/__init__.py b/src/supervision/assets/__init__.py
new file mode 100644
index 0000000..3ed213f
--- /dev/null
+++ b/src/supervision/assets/__init__.py
@@ -0,0 +1,4 @@
+from supervision.assets.downloader import download_assets
+from supervision.assets.list import ImageAssets, VideoAssets
+
+__all__ = ["ImageAssets", "VideoAssets", "download_assets"]
diff --git a/src/supervision/assets/downloader.py b/src/supervision/assets/downloader.py
new file mode 100644
index 0000000..99387df
--- /dev/null
+++ b/src/supervision/assets/downloader.py
@@ -0,0 +1,166 @@
+import os
+from hashlib import md5
+from pathlib import Path
+from shutil import copyfileobj
+
+from requests import get
+from tqdm.auto import tqdm
+
+from supervision.assets.list import MEDIA_ASSETS, Assets
+from supervision.utils.logger import _get_logger
+
+logger = _get_logger(__name__)
+
+
+def is_md5_hash_matching(filename: str | Path, original_md5_hash: str) -> bool:
+ """
+ Check if the MD5 hash of a file matches the original hash.
+
+ Note: MD5 is used here for file integrity checking (detecting corruption),
+ not for cryptographic security purposes.
+
+ Args:
+ filename: The path to the file to be checked.
+ original_md5_hash: The original MD5 hash to compare against.
+
+ Returns:
+ True if the hashes match, False otherwise.
+ """
+ if not os.path.exists(filename):
+ return False
+
+ with open(filename, "rb") as file:
+ file_contents = file.read()
+ computed_md5_hash = md5(file_contents, usedforsecurity=False)
+
+ return computed_md5_hash.hexdigest() == original_md5_hash
+
+
+def _download_asset(filename: str, destination: Path) -> None:
+ """
+ Download asset bytes to the target destination via a temporary file.
+ """
+ response = get(
+ MEDIA_ASSETS[filename][0], stream=True, allow_redirects=True, timeout=30
+ )
+ response.raise_for_status()
+
+ file_size = int(response.headers.get("Content-Length", 0))
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ temp_path = destination.with_name(f"{destination.name}.part")
+
+ try:
+ with tqdm.wrapattr(
+ response.raw, "read", total=file_size, desc="", colour="#a351fb"
+ ) as raw_resp:
+ with temp_path.open("wb") as file:
+ copyfileobj(raw_resp, file)
+ except Exception:
+ temp_path.unlink(missing_ok=True)
+ raise
+
+ try:
+ os.replace(temp_path, destination)
+ finally:
+ temp_path.unlink(missing_ok=True)
+
+
+def _download_verified_asset(
+ filename: str,
+ original_md5_hash: str,
+ destination: Path,
+ check_target: str | Path,
+ retry_on_mismatch: bool = True,
+) -> None:
+ """
+ Download an asset and reject payloads whose MD5 does not match the catalog.
+ """
+ _download_asset(filename, destination)
+
+ if is_md5_hash_matching(check_target, original_md5_hash):
+ return
+
+ logger.warning("File corrupted. Re-downloading...")
+ os.remove(check_target)
+
+ if retry_on_mismatch:
+ _download_verified_asset(
+ filename=filename,
+ original_md5_hash=original_md5_hash,
+ destination=destination,
+ check_target=check_target,
+ retry_on_mismatch=False,
+ )
+ return
+
+ raise ValueError(f"Downloaded asset {filename!r} failed MD5 verification.")
+
+
+def download_assets(
+ asset_name: Assets | str,
+ directory: str | Path | None = None,
+) -> str:
+ """
+ Download a specified asset if it doesn't already exist or is corrupted.
+
+ Args:
+ asset_name: The name or type of the asset to be downloaded.
+ directory: Optional output directory. Defaults to the current working
+ directory for backward compatibility.
+
+ Returns:
+ The downloaded asset path. When `directory` is omitted, this preserves
+ the historical filename-only return value.
+
+ Example:
+ ```pycon
+ >>> from supervision.assets import download_assets, ImageAssets, VideoAssets
+ >>> download_assets(VideoAssets.VEHICLES) # doctest: +SKIP
+ 'vehicles.mp4'
+
+ >>> download_assets(ImageAssets.PEOPLE_WALKING) # doctest: +SKIP
+ 'people-walking.jpg'
+
+ ```
+ """
+
+ filename = asset_name.filename if isinstance(asset_name, Assets) else asset_name
+ if directory is None:
+ destination = Path.cwd() / filename
+ check_target: str | Path = filename
+ return_value = filename
+ else:
+ destination_directory = Path(directory).expanduser().resolve()
+ destination = destination_directory / filename
+ check_target = str(destination)
+ return_value = str(destination)
+
+ if filename in MEDIA_ASSETS:
+ original_md5_hash = MEDIA_ASSETS[filename][1]
+ if not Path(check_target).exists():
+ logger.info("Downloading %s assets", filename)
+ _download_verified_asset(
+ filename=filename,
+ original_md5_hash=original_md5_hash,
+ destination=destination,
+ check_target=check_target,
+ )
+ else:
+ if not is_md5_hash_matching(check_target, original_md5_hash):
+ logger.warning("File corrupted. Re-downloading...")
+ os.remove(check_target)
+ _download_verified_asset(
+ filename=filename,
+ original_md5_hash=original_md5_hash,
+ destination=destination,
+ check_target=check_target,
+ )
+
+ logger.info("%s asset download complete.", filename)
+ else:
+ valid_assets = ", ".join(filename for filename in MEDIA_ASSETS.keys())
+ raise ValueError(
+ f"Invalid asset. It should be one of the following: {valid_assets}."
+ )
+
+ return return_value
diff --git a/src/supervision/assets/list.py b/src/supervision/assets/list.py
new file mode 100644
index 0000000..9b2a2ba
--- /dev/null
+++ b/src/supervision/assets/list.py
@@ -0,0 +1,84 @@
+from __future__ import annotations
+
+from enum import Enum
+
+BASE_VIDEO_URL = "https://media.roboflow.com/supervision/video-examples/"
+BASE_IMAGE_URL = "https://media.roboflow.com/supervision/image-examples/"
+
+
+class Assets(Enum):
+ filename: str
+ md5_hash: str
+
+ def __new__(cls, filename: str, md5_hash: str) -> Assets:
+ obj = object.__new__(cls)
+ obj._value_ = filename
+ obj.filename = filename
+ obj.md5_hash = md5_hash
+ return obj
+
+ @classmethod
+ def list(cls) -> list[str]:
+ return [asset.filename for asset in cls]
+
+
+class VideoAssets(Assets):
+ """
+ Each member of this class represents a video asset. The value associated with each
+ member has a filename and hash of the video. File names and links can be seen below.
+
+ | Asset | Video Filename | Video URL |
+ |------------------------|----------------------------|---------------------------------------------------------------------------------------|
+ | `VEHICLES` | `vehicles.mp4` | [Link](https://media.roboflow.com/supervision/video-examples/vehicles.mp4) |
+ | `MILK_BOTTLING_PLANT` | `milk-bottling-plant.mp4` | [Link](https://media.roboflow.com/supervision/video-examples/milk-bottling-plant.mp4) |
+ | `VEHICLES_2` | `vehicles-2.mp4` | [Link](https://media.roboflow.com/supervision/video-examples/vehicles-2.mp4) |
+ | `GROCERY_STORE` | `grocery-store.mp4` | [Link](https://media.roboflow.com/supervision/video-examples/grocery-store.mp4) |
+ | `SUBWAY` | `subway.mp4` | [Link](https://media.roboflow.com/supervision/video-examples/subway.mp4) |
+ | `MARKET_SQUARE` | `market-square.mp4` | [Link](https://media.roboflow.com/supervision/video-examples/market-square.mp4) |
+ | `PEOPLE_WALKING` | `people-walking.mp4` | [Link](https://media.roboflow.com/supervision/video-examples/people-walking.mp4) |
+ | `BEACH` | `beach-1.mp4` | [Link](https://media.roboflow.com/supervision/video-examples/beach-1.mp4) |
+ | `BASKETBALL` | `basketball-1.mp4` | [Link](https://media.roboflow.com/supervision/video-examples/basketball-1.mp4) |
+ | `SKIING` | `skiing.mp4` | [Link](https://media.roboflow.com/supervision/video-examples/skiing.mp4) |
+ """ # noqa: E501 // docs
+
+ VEHICLES = ("vehicles.mp4", "8155ff4e4de08cfa25f39de96483f918")
+ MILK_BOTTLING_PLANT = (
+ "milk-bottling-plant.mp4",
+ "9e8fb6e883f842a38b3d34267290bdc7",
+ )
+ VEHICLES_2 = ("vehicles-2.mp4", "830af6fba21ffbf14867a7fea595937b")
+ GROCERY_STORE = ("grocery-store.mp4", "48608fb4a8981f1c2469fa492adeec9c")
+ SUBWAY = ("subway.mp4", "453475750691fb23c56a0cffef089194")
+ MARKET_SQUARE = ("market-square.mp4", "859179bf4a21f80a8baabfdb2ed716dc")
+ PEOPLE_WALKING = ("people-walking.mp4", "0574c053c8686c3f1dc0aa3743e45cb9")
+ BEACH = ("beach-1.mp4", "4175d42fec4d450ed081523fd39e0cf8")
+ BASKETBALL = ("basketball-1.mp4", "60d94a3c7c47d16f09d342b088012ecc")
+ SKIING = ("skiing.mp4", "d30987cbab1bbc5934199cdd1b293119")
+
+
+class ImageAssets(Assets):
+ """
+ Each member of this enum represents a image asset. The value associated with each
+ member is the filename of the image.
+
+ | Asset | Image Filename | Video URL |
+ |--------------------|------------------------|---------------------------------------------------------------------------------------|
+ | `PEOPLE_WALKING` | `people-walking.jpg` | [Link](https://media.roboflow.com/supervision/image-examples/people-walking.jpg) |
+ | `SOCCER` | `soccer.jpg` | [Link](https://media.roboflow.com/supervision/image-examples/soccer.jpg) |
+
+ """ # noqa: E501 // docs
+
+ PEOPLE_WALKING = ("people-walking.jpg", "e6bda00b47f2908eeae7df86ef995dcd")
+ SOCCER = ("soccer.jpg", "0f5a4b98abf3e3973faf9e9260a7d876")
+
+
+MEDIA_ASSETS: dict[str, tuple[str, str]] = {
+ **{
+ asset.filename: (f"{BASE_VIDEO_URL}{asset.filename}", asset.md5_hash)
+ for asset in VideoAssets
+ },
+ **{
+ asset.filename: (f"{BASE_IMAGE_URL}{asset.filename}", asset.md5_hash)
+ for asset in ImageAssets
+ },
+}
diff --git a/src/supervision/classification/__init__.py b/src/supervision/classification/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/supervision/classification/core.py b/src/supervision/classification/core.py
new file mode 100644
index 0000000..9b7a15b
--- /dev/null
+++ b/src/supervision/classification/core.py
@@ -0,0 +1,216 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any
+
+import numpy as np
+import numpy.typing as npt
+
+if TYPE_CHECKING:
+ import torch # type: ignore[import-not-found, unused-ignore]
+
+
+def _validate_class_ids(class_id: Any, n: int) -> None:
+ """
+ Ensure that class_id is a 1d np.ndarray with (n, ) shape.
+ """
+ is_valid = isinstance(class_id, np.ndarray) and class_id.shape == (n,)
+ if not is_valid:
+ raise ValueError("class_id must be 1d np.ndarray with (n, ) shape")
+
+
+def _validate_confidence(confidence: Any, n: int) -> None:
+ """
+ Ensure that confidence is a 1d np.ndarray with (n, ) shape.
+ """
+ if confidence is not None:
+ is_valid = isinstance(confidence, np.ndarray) and confidence.shape == (n,)
+ if not is_valid:
+ raise ValueError("confidence must be 1d np.ndarray with (n, ) shape")
+
+
+@dataclass
+class Classifications:
+ class_id: npt.NDArray[np.int_]
+ confidence: npt.NDArray[np.floating] | None = None
+
+ def __post_init__(self) -> None:
+ """
+ Validate the classification inputs.
+ """
+ n = len(self.class_id)
+
+ _validate_class_ids(self.class_id, n)
+ _validate_confidence(self.confidence, n)
+
+ def __eq__(self, other: object) -> bool:
+ """
+ Compare classifications by value across numpy-backed fields.
+ """
+ if not isinstance(other, Classifications):
+ return NotImplemented
+ if not np.array_equal(self.class_id, other.class_id):
+ return False
+ if self.confidence is None or other.confidence is None:
+ return self.confidence is other.confidence
+ return bool(np.array_equal(self.confidence, other.confidence))
+
+ def __len__(self) -> int:
+ """
+ Returns the number of classifications.
+ """
+ return len(self.class_id)
+
+ @classmethod
+ def from_clip(cls, clip_results: torch.Tensor) -> Classifications:
+ """
+ Creates a Classifications instance from a
+ [clip](https://github.com/openai/clip) inference result.
+
+ Args:
+ clip_results: The inference result from clip model.
+
+ Returns:
+ A new Classifications object.
+
+ Example:
+ ```python
+ from PIL import Image
+ import clip
+ import supervision as sv
+
+ model, preprocess = clip.load('ViT-B/32')
+
+ image = cv2.imread(SOURCE_IMAGE_PATH)
+ image = preprocess(image).unsqueeze(0)
+
+ text = clip.tokenize(["a diagram", "a dog", "a cat"])
+ output, _ = model(image, text)
+ classifications = sv.Classifications.from_clip(output)
+ ```
+ """
+
+ confidence = clip_results.softmax(dim=-1).cpu().detach().numpy()[0]
+
+ if len(confidence) == 0:
+ return cls(
+ class_id=np.array([], dtype=np.int_),
+ confidence=np.array([], dtype=np.float32),
+ )
+
+ class_ids = np.arange(len(confidence))
+ return cls(class_id=class_ids, confidence=confidence)
+
+ @classmethod
+ def from_ultralytics(cls, ultralytics_results: Any) -> Classifications:
+ """
+ Creates a Classifications instance from a
+ [ultralytics](https://github.com/ultralytics/ultralytics) inference result.
+
+ Args:
+ ultralytics_results: The inference result from ultralytics model.
+
+ Returns:
+ A new Classifications object.
+
+ Example:
+ ```python
+ import cv2
+ from ultralytics import YOLO
+ import supervision as sv
+
+ image = cv2.imread(SOURCE_IMAGE_PATH)
+ model = YOLO('yolov8n-cls.pt')
+
+ output = model(image)[0]
+ classifications = sv.Classifications.from_ultralytics(output)
+ ```
+ """
+ confidence = ultralytics_results.probs.data.cpu().numpy()
+ return cls(class_id=np.arange(confidence.shape[0]), confidence=confidence)
+
+ @classmethod
+ def from_timm(cls, timm_results: Any) -> Classifications:
+ """
+ Creates a Classifications instance from a
+ [timm](https://huggingface.co/docs/hub/timm) inference result.
+
+ Note:
+ Returned confidences are softmax-normalized probabilities, so
+ thresholds calibrated against raw logits may need recalibration.
+
+ Args:
+ timm_results: The inference result from timm model.
+
+ Returns:
+ A new Classifications object.
+
+ Example:
+ ```python
+ from PIL import Image
+ import timm
+ from timm.data import resolve_data_config, create_transform
+ import supervision as sv
+
+ model = timm.create_model(
+ model_name='hf-hub:nateraw/resnet50-oxford-iiit-pet',
+ pretrained=True
+ ).eval()
+
+ config = resolve_data_config({}, model=model)
+ transform = create_transform(**config)
+
+ image = Image.open(SOURCE_IMAGE_PATH).convert('RGB')
+ x = transform(image).unsqueeze(0)
+
+ output = model(x)
+
+ classifications = sv.Classifications.from_timm(output)
+ ```
+ """
+ confidence = timm_results.softmax(dim=-1).cpu().detach().numpy()[0]
+
+ if len(confidence) == 0:
+ return cls(
+ class_id=np.array([], dtype=np.int_),
+ confidence=np.array([], dtype=np.float32),
+ )
+
+ class_id = np.arange(len(confidence))
+ return cls(class_id=class_id, confidence=confidence)
+
+ def get_top_k(
+ self, k: int
+ ) -> tuple[npt.NDArray[np.int_], npt.NDArray[np.floating]]:
+ """
+ Retrieve the top k class IDs and confidences,
+ ordered in descending order by confidence.
+
+ Args:
+ k: The number of top class IDs and confidences to retrieve.
+
+ Returns:
+ A tuple containing the top k class IDs and confidences.
+
+ Example:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> classifications = sv.Classifications(
+ ... class_id=np.array([0, 1, 2]),
+ ... confidence=np.array([0.3, 0.9, 0.5])
+ ... )
+ >>> classifications.get_top_k(1)
+ (array([1]), array([0.9]))
+
+ ```
+ """
+ if self.confidence is None:
+ raise ValueError("top_k could not be calculated, confidence is None")
+
+ order = np.argsort(self.confidence)[::-1]
+ top_k_order = order[:k]
+ top_k_class_id = self.class_id[top_k_order]
+ top_k_confidence = self.confidence[top_k_order]
+
+ return top_k_class_id, top_k_confidence
diff --git a/src/supervision/config.py b/src/supervision/config.py
new file mode 100644
index 0000000..18600b4
--- /dev/null
+++ b/src/supervision/config.py
@@ -0,0 +1,13 @@
+CLASS_NAME_DATA_FIELD: str = "class_name"
+COCO_RAW_SEGMENTATION: str = "coco_raw_segmentation"
+#: Key for oriented bounding-box corner coordinates in ``Detections.data``.
+#:
+#: Value layout: ``np.ndarray`` of shape ``(N, 4, 2)``, dtype ``float32``, pixel
+#: coordinates ordered as ``[[x1, y1], [x2, y2], [x3, y3], [x4, y4]]`` per
+#: detection where the four points are the corners of the oriented box.
+#: Used by :func:`~supervision.dataset.formats.yolo.detections_to_yolo_annotations`
+#: (``is_obb=True``) and
+#: :func:`~supervision.dataset.formats.yolo.yolo_annotations_to_detections`
+#: (``is_obb=True``).
+#: Also triggers sequential mode in ``InferenceSlicer`` when present.
+ORIENTED_BOX_COORDINATES: str = "xyxyxyxy"
diff --git a/src/supervision/dataset/__init__.py b/src/supervision/dataset/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/supervision/dataset/core.py b/src/supervision/dataset/core.py
new file mode 100644
index 0000000..5ae23a5
--- /dev/null
+++ b/src/supervision/dataset/core.py
@@ -0,0 +1,1313 @@
+from __future__ import annotations
+
+import os
+from abc import ABC, abstractmethod
+from collections.abc import Iterator
+from copy import deepcopy
+from dataclasses import dataclass
+from itertools import chain
+from pathlib import Path
+from typing import cast
+
+import cv2
+import numpy as np
+import numpy.typing as npt
+from tqdm.auto import tqdm
+
+from supervision.classification.core import Classifications
+from supervision.config import CLASS_NAME_DATA_FIELD
+from supervision.dataset.formats.coco import (
+ load_coco_annotations,
+ save_coco_annotations,
+)
+from supervision.dataset.formats.createml import (
+ load_createml_annotations,
+ save_createml_annotations,
+)
+from supervision.dataset.formats.labelme import (
+ load_labelme_annotations,
+ save_labelme_annotations,
+)
+from supervision.dataset.formats.pascal_voc import (
+ load_pascal_voc_annotations,
+ save_pascal_voc_annotations,
+)
+from supervision.dataset.formats.yolo import (
+ load_yolo_annotations,
+ save_data_yaml,
+ save_yolo_annotations,
+)
+from supervision.dataset.utils import (
+ build_class_index_mapping,
+ check_no_basename_collisions,
+ map_detections_class_id,
+ merge_class_lists,
+ save_dataset_images,
+ train_test_split,
+)
+from supervision.detection.core import Detections
+from supervision.utils.internal import warn_deprecated
+from supervision.utils.iterables import find_duplicates
+
+_IMAGE_FILE_EXTENSIONS = frozenset(
+ {".bmp", ".jpeg", ".jpg", ".png", ".tif", ".tiff", ".webp"}
+)
+
+
+class BaseDataset(ABC):
+ @abstractmethod
+ def __len__(self) -> int:
+ pass
+
+ @abstractmethod
+ def split(
+ self,
+ split_ratio: float = 0.8,
+ random_state: int | None = None,
+ shuffle: bool = True,
+ ) -> tuple[BaseDataset, BaseDataset]:
+ pass
+
+
+class DetectionDataset(BaseDataset):
+ """
+ Contains information about a detection dataset. Handles lazy image loading
+ and annotation retrieval, dataset splitting, conversions into multiple
+ formats.
+
+ Attributes:
+ classes: List containing dataset class names.
+ images:
+ Accepts a list of image paths. Passing a dict
+ (``Dict[str, np.ndarray]``) is deprecated in ``0.30.0`` and will
+ be removed in ``0.33.0``; use a list of paths instead.
+ When a list of paths is provided, images are loaded lazily on
+ demand, which is more memory-efficient.
+ annotations: Dictionary mapping
+ image path to annotations. The dictionary keys match
+ match the keys in `images` or entries in the list of
+ image paths.
+ """
+
+ def __init__(
+ self,
+ classes: list[str],
+ images: list[str] | dict[str, npt.NDArray[np.uint8]],
+ 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 = {
+ image_path: deepcopy(annotation)
+ for image_path, annotation in annotations.items()
+ }
+
+ np_classes = np.array(self.classes)
+ for image_path, annotation in self.annotations.items():
+ class_ids = annotation.class_id
+ if class_ids is None:
+ continue
+ if not np.issubdtype(class_ids.dtype, np.integer):
+ raise ValueError(
+ f"Detection annotation for image {image_path!r} contains "
+ f"non-integer class_id values with dtype {class_ids.dtype}."
+ )
+
+ invalid_class_ids = class_ids[
+ (class_ids < 0) | (class_ids >= len(self.classes))
+ ]
+ if len(invalid_class_ids) > 0:
+ valid_range = (
+ "empty"
+ if len(self.classes) == 0
+ else f"[0, {len(self.classes) - 1}]"
+ )
+ raise ValueError(
+ f"Detection annotation for image {image_path!r} contains "
+ f"class_id {int(invalid_class_ids[0])}, outside the valid "
+ f"range {valid_range} for {len(self.classes)} classes."
+ )
+
+ annotation.data[CLASS_NAME_DATA_FIELD] = np_classes[class_ids]
+
+ # Eliminate duplicates while preserving order
+ self.image_paths = list(dict.fromkeys(images))
+
+ self._images_in_memory: dict[str, npt.NDArray[np.uint8]] = {}
+ if isinstance(images, dict):
+ self._images_in_memory = images
+ warn_deprecated(
+ "Passing a `Dict[str, np.ndarray]` into `DetectionDataset` is "
+ "deprecated in `0.30.0` and will be removed in `0.33.0`. Use "
+ "a list of paths `List[str]` instead."
+ )
+
+ def _get_image(self, image_path: str) -> npt.NDArray[np.uint8]:
+ """Assumes that image is in dataset."""
+ if self._images_in_memory:
+ return self._images_in_memory[image_path]
+ image = cv2.imread(image_path)
+ if image is None:
+ raise ValueError(f"Could not read image from path: {image_path}")
+ return cast(npt.NDArray[np.uint8], image)
+
+ def __len__(self) -> int:
+ return len(self._images_in_memory) or len(self.image_paths)
+
+ def __getitem__(self, i: int) -> tuple[str, npt.NDArray[np.uint8], Detections]:
+ """
+ Returns:
+ 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, npt.NDArray[np.uint8], Detections]]:
+ """
+ Iterate over the images and annotations in the dataset.
+
+ Yields:
+ Tuples containing the image path, image data, and its annotation.
+ """
+ for i in range(len(self)):
+ image_path, image, annotation = self[i]
+ yield image_path, image, annotation
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, DetectionDataset):
+ return False
+
+ if self.classes != 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: float = 0.8,
+ random_state: int | None = None,
+ shuffle: bool = True,
+ ) -> tuple[DetectionDataset, DetectionDataset]:
+ """
+ Splits the dataset into two parts (training and testing)
+ using the provided split_ratio. The input dataset is not mutated.
+
+ Args:
+ split_ratio: The ratio of the training
+ set to the entire dataset.
+ random_state: The seed for the random number generator.
+ This is used for reproducibility.
+ shuffle: Whether to shuffle the data before splitting.
+
+ Returns:
+ A tuple containing
+ the training and testing datasets.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> ds = sv.DetectionDataset(
+ ... classes=['dog', 'person'],
+ ... images={
+ ... 'img1.jpg': np.zeros((100, 100, 3), dtype=np.uint8),
+ ... 'img2.jpg': np.zeros((100, 100, 3), dtype=np.uint8),
+ ... },
+ ... annotations={
+ ... 'img1.jpg': sv.Detections(xyxy=np.array([[10, 10, 20, 20]])),
+ ... 'img2.jpg': sv.Detections(xyxy=np.array([[30, 30, 40, 40]])),
+ ... }
+ ... )
+ >>> train_ds, test_ds = ds.split(split_ratio=0.5, random_state=42)
+ >>> len(train_ds), len(test_ds)
+ (1, 1)
+
+ ```
+ """
+
+ train_paths, test_paths = train_test_split(
+ data=self.image_paths,
+ train_ratio=split_ratio,
+ random_state=random_state,
+ shuffle=shuffle,
+ )
+
+ 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=train_paths,
+ annotations=train_annotations,
+ )
+ test_dataset = DetectionDataset(
+ classes=self.classes,
+ images=test_paths,
+ annotations=test_annotations,
+ )
+ if self._images_in_memory:
+ train_dataset._images_in_memory = {
+ path: self._images_in_memory[path] for path in train_paths
+ }
+ test_dataset._images_in_memory = {
+ path: self._images_in_memory[path] for path in test_paths
+ }
+ 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: A list of `DetectionDataset`
+ objects to merge.
+
+ Returns:
+ A single `DetectionDataset` object containing
+ the merged data from the input list.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> ds_1 = sv.DetectionDataset(
+ ... classes=['dog', 'person'],
+ ... images={'img1.jpg': np.zeros((100, 100, 3), dtype=np.uint8)},
+ ... annotations={'img1.jpg': sv.Detections.empty()}
+ ... )
+ >>> len(ds_1)
+ 1
+ >>> ds_1.classes
+ ['dog', 'person']
+ >>> ds_2 = sv.DetectionDataset(
+ ... classes=['cat'],
+ ... images={'img2.jpg': np.zeros((100, 100, 3), dtype=np.uint8)},
+ ... annotations={'img2.jpg': sv.Detections.empty()}
+ ... )
+ >>> len(ds_2)
+ 1
+ >>> ds_2.classes
+ ['cat']
+ >>> ds_merged = sv.DetectionDataset.merge([ds_1, ds_2])
+ >>> len(ds_merged)
+ 2
+ >>> 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],
+ )
+
+ merged_dataset = cls(
+ classes=classes,
+ images=image_paths,
+ annotations=annotations,
+ )
+ if all_in_memory:
+ merged_dataset._images_in_memory = images_in_memory
+ return merged_dataset
+
+ def as_pascal_voc(
+ self,
+ images_directory_path: str | None = None,
+ annotations_directory_path: str | None = None,
+ min_image_area_percentage: float = 0.0,
+ max_image_area_percentage: float = 1.0,
+ approximation_percentage: float = 0.0,
+ show_progress: bool = False,
+ ) -> None:
+ """
+ Exports the dataset to PASCAL VOC format. This method saves the images
+ and their corresponding annotations in PASCAL VOC format. Both output
+ layouts are preflighted before any files are written so a collision in
+ either target fails without partial output.
+
+ Args:
+ images_directory_path: The path to the directory
+ where the images should be saved.
+ If not provided, images will not be saved.
+ annotations_directory_path: The path to
+ the directory where the annotations in PASCAL VOC format should be
+ saved. If not provided, annotations will not be saved.
+ min_image_area_percentage: The minimum percentage of
+ detection area relative to
+ the image area for a detection to be included.
+ Argument is used only for segmentation datasets.
+ max_image_area_percentage: The maximum percentage
+ of detection area relative to
+ the image area for a detection to be included.
+ Argument is used only for segmentation datasets.
+ approximation_percentage: The percentage of
+ polygon points to be removed from the input polygon,
+ in the range [0, 1). Argument is used only for segmentation datasets.
+ show_progress: If True, display a progress bar during saving.
+
+ Raises:
+ ValueError: If two image paths share the same basename (when
+ images_directory_path is set) or the same stem (when
+ annotations_directory_path is set), which would cause one
+ output file to overwrite another. Rename images to ensure
+ unique basenames before exporting a merged dataset.
+ """
+ if images_directory_path:
+ check_no_basename_collisions(
+ image_paths=self.image_paths,
+ key=lambda image_path: Path(image_path).name,
+ output_kind="image",
+ )
+ if annotations_directory_path:
+ check_no_basename_collisions(
+ image_paths=self.image_paths,
+ key=lambda image_path: f"{Path(image_path).stem}.xml",
+ output_kind="Pascal VOC annotation",
+ )
+
+ if images_directory_path:
+ save_dataset_images(
+ dataset=self,
+ images_directory_path=images_directory_path,
+ show_progress=show_progress,
+ )
+ if annotations_directory_path:
+ save_pascal_voc_annotations(
+ dataset=self,
+ annotations_directory_path=annotations_directory_path,
+ min_image_area_percentage=min_image_area_percentage,
+ max_image_area_percentage=max_image_area_percentage,
+ approximation_percentage=approximation_percentage,
+ show_progress=show_progress,
+ )
+
+ @classmethod
+ def from_pascal_voc(
+ cls,
+ images_directory_path: str,
+ annotations_directory_path: str,
+ force_masks: bool = False,
+ show_progress: bool = False,
+ ) -> DetectionDataset:
+ """
+ Creates a Dataset instance from PASCAL VOC formatted data.
+
+ Args:
+ images_directory_path: Path to the directory containing the images.
+ annotations_directory_path: Path to the directory
+ containing the PASCAL VOC XML annotations.
+ force_masks: If True, forces masks to
+ be loaded for all annotations, regardless of whether they are present.
+ show_progress: If True, display a progress bar during loading.
+
+ Returns:
+ A DetectionDataset instance containing
+ the loaded images and annotations.
+
+ Examples:
+ ```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("voc")
+
+ ds = sv.DetectionDataset.from_pascal_voc(
+ images_directory_path=f"{dataset.location}/train/images",
+ annotations_directory_path=f"{dataset.location}/train/labels",
+ # pass show_progress=True to enable a tqdm progress bar
+ )
+
+ ds.classes
+ # ['dog', 'person']
+ ```
+ """
+
+ classes, image_paths, annotations = load_pascal_voc_annotations(
+ images_directory_path=images_directory_path,
+ annotations_directory_path=annotations_directory_path,
+ force_masks=force_masks,
+ show_progress=show_progress,
+ )
+
+ return DetectionDataset(
+ classes=classes, images=image_paths, annotations=annotations
+ )
+
+ @classmethod
+ def from_yolo(
+ cls,
+ images_directory_path: str,
+ annotations_directory_path: str,
+ data_yaml_path: str,
+ force_masks: bool = False,
+ is_obb: bool = False,
+ show_progress: bool = False,
+ ) -> DetectionDataset:
+ """
+ Creates a Dataset instance from YOLO formatted data.
+
+ Args:
+ images_directory_path: The path to the
+ directory containing the images.
+ annotations_directory_path: The path to the directory
+ containing the YOLO annotation files.
+ data_yaml_path: The path to the data
+ YAML file containing class information.
+ force_masks: If True, forces
+ masks to be loaded for all annotations,
+ regardless of whether they are present.
+ is_obb: 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.
+ show_progress: If True, display a progress bar during loading.
+
+ Returns:
+ A DetectionDataset instance
+ containing the loaded images and annotations.
+
+ Examples:
+ ```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("yolov5")
+
+ ds = 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",
+ # pass show_progress=True to enable a tqdm progress bar
+ )
+
+ ds.classes
+ # ['dog', 'person']
+ ```
+ """
+ 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,
+ show_progress=show_progress,
+ )
+ return DetectionDataset(
+ classes=classes, images=image_paths, annotations=annotations
+ )
+
+ def as_yolo(
+ self,
+ images_directory_path: str | None = None,
+ annotations_directory_path: str | None = None,
+ data_yaml_path: str | None = None,
+ min_image_area_percentage: float = 0.0,
+ max_image_area_percentage: float = 1.0,
+ approximation_percentage: float = 0.0,
+ is_obb: bool = False,
+ show_progress: bool = False,
+ ) -> None:
+ """
+ Exports the dataset to YOLO format. This method saves the
+ images and their corresponding annotations in YOLO format.
+
+ Args:
+ images_directory_path: The path to the
+ directory where the images should be saved.
+ If not provided, images will not be saved.
+ annotations_directory_path: The path to the
+ directory where the annotations in
+ YOLO format should be saved. If not provided,
+ annotations will not be saved.
+ data_yaml_path: The path where the data.yaml
+ file should be saved.
+ If not provided, the file will not be saved.
+ min_image_area_percentage: The minimum percentage of
+ detection area relative to
+ the image area for a detection to be included.
+ Argument is used only for segmentation datasets.
+ max_image_area_percentage: The maximum percentage
+ of detection area relative to
+ the image area for a detection to be included.
+ Argument is used only for segmentation datasets.
+ approximation_percentage: The percentage of polygon points to
+ be removed from the input polygon, in the range [0, 1).
+ This is useful for simplifying the annotations.
+ Argument is used only for segmentation datasets.
+ is_obb: If True, exports annotations in OBB format
+ (`class_id x1 y1 x2 y2 x3 y3 x4 y4`) using the oriented
+ corners stored in `detections.data["xyxyxyxy"]`. Mirrors
+ `from_yolo(..., is_obb=True)`. Masks are ignored when
+ `is_obb=True`.
+ show_progress: If True, display a progress bar during saving.
+
+ Raises:
+ ValueError: If two image paths share the same basename (when
+ images_directory_path is set) or the same annotation
+ file name (when annotations_directory_path is set),
+ which would cause one output file to overwrite another.
+ """
+ if is_obb and (
+ min_image_area_percentage != 0.0
+ or max_image_area_percentage != 1.0
+ or approximation_percentage != 0.0
+ ):
+ import warnings
+
+ warnings.warn(
+ "`min_image_area_percentage`, `max_image_area_percentage`, and "
+ "`approximation_percentage` have no effect when `is_obb=True`; "
+ "OBB annotations use corner coordinates directly.",
+ UserWarning,
+ stacklevel=2,
+ )
+ # Pre-flight: validate output uniqueness before writing any file
+ if images_directory_path:
+ check_no_basename_collisions(
+ image_paths=self.image_paths,
+ key=lambda image_path: Path(image_path).name,
+ output_kind="image",
+ )
+ if annotations_directory_path:
+ check_no_basename_collisions(
+ image_paths=self.image_paths,
+ key=lambda image_path: Path(image_path).stem + ".txt",
+ output_kind="YOLO annotation",
+ )
+
+ if images_directory_path is not None:
+ save_dataset_images(
+ dataset=self,
+ images_directory_path=images_directory_path,
+ show_progress=show_progress,
+ )
+ if annotations_directory_path is not None:
+ save_yolo_annotations(
+ dataset=self,
+ annotations_directory_path=annotations_directory_path,
+ min_image_area_percentage=min_image_area_percentage,
+ max_image_area_percentage=max_image_area_percentage,
+ approximation_percentage=approximation_percentage,
+ show_progress=show_progress,
+ is_obb=is_obb,
+ )
+ if data_yaml_path is not None:
+ save_data_yaml(data_yaml_path=data_yaml_path, classes=self.classes)
+
+ @classmethod
+ def from_labelme(
+ cls,
+ images_directory_path: str,
+ annotations_directory_path: str,
+ force_masks: bool = False,
+ ) -> DetectionDataset:
+ """
+ Creates a Dataset instance from LabelMe formatted data.
+
+ LabelMe stores one JSON file per image, each containing a list of
+ ``shapes``. ``rectangle`` shapes are loaded as bounding boxes and
+ ``polygon`` shapes as masks (with their bounding boxes); other shape
+ types are skipped. Class names are inferred from the labels present in
+ the files. When an image file contains a ``polygon`` shape, or when
+ ``force_masks=True`` is set, both ``rectangle`` and ``polygon`` shapes
+ produce masks: rectangles via a four-corner polygon fill.
+
+ Args:
+ images_directory_path: The path to the
+ directory containing the images.
+ annotations_directory_path: The path to the directory
+ containing the LabelMe ``.json`` annotation files.
+ force_masks: If True, forces masks to be loaded for all
+ annotations, regardless of whether polygon shapes are present.
+ Requires ``imageWidth`` and ``imageHeight`` in every JSON file.
+
+ Returns:
+ A DetectionDataset instance containing
+ the loaded images and annotations.
+
+ Raises:
+ ValueError: If an annotation is malformed - for example
+ ``imagePath`` is empty or resolves to ``..``, a shape is
+ missing its ``label`` or ``points``, or a mask is required but
+ ``imageWidth`` / ``imageHeight`` are missing or zero.
+
+ Examples:
+ ```python
+ import supervision as sv
+
+ ds = sv.DetectionDataset.from_labelme(
+ images_directory_path="",
+ annotations_directory_path="",
+ )
+
+ ds.classes
+ # ['dog', 'person']
+ ```
+ """
+ classes, image_paths, annotations = load_labelme_annotations(
+ images_directory_path=images_directory_path,
+ annotations_directory_path=annotations_directory_path,
+ force_masks=force_masks,
+ )
+ return DetectionDataset(
+ classes=classes, images=image_paths, annotations=annotations
+ )
+
+ def as_labelme(
+ self,
+ images_directory_path: str | None = None,
+ annotations_directory_path: str | None = None,
+ ) -> None:
+ """
+ Exports the dataset to LabelMe format. This method saves the images and
+ their corresponding annotations as per-image LabelMe ``.json`` files.
+ Masked detections are written as ``polygon`` shapes whose vertices
+ approximate the mask contour, so masks are not bit-exact on round-trip.
+ Because the bounding box is recomputed from the quantized polygon contour
+ on re-import, bounding boxes for masked detections may also shift by
+ approximately one pixel after a save-load cycle.
+
+ Args:
+ images_directory_path: The path to the directory
+ where the images should be saved.
+ If not provided, images will not be saved.
+ annotations_directory_path: The path to the directory where the
+ LabelMe ``.json`` files should be saved.
+ If not provided, annotations will not be saved.
+
+ Examples:
+ ```python
+ import supervision as sv
+
+ ds = sv.DetectionDataset(...)
+
+ ds.as_labelme(
+ images_directory_path="",
+ annotations_directory_path="",
+ )
+ ```
+ """
+ if images_directory_path is not None:
+ save_dataset_images(
+ dataset=self, images_directory_path=images_directory_path
+ )
+ if annotations_directory_path is not None:
+ save_labelme_annotations(
+ dataset=self,
+ annotations_directory_path=annotations_directory_path,
+ )
+
+ @classmethod
+ def from_createml(
+ cls,
+ images_directory_path: str,
+ annotations_path: str,
+ show_progress: bool = False,
+ ) -> DetectionDataset:
+ """
+ Creates a Dataset instance from CreateML formatted data.
+
+ CreateML stores object-detection annotations in a single JSON file as a
+ list of per-image entries, with each box expressed as a pixel-space
+ centre point plus width and height. Class names are inferred from the
+ labels present in the file.
+
+ Args:
+ images_directory_path: The path to the directory containing the
+ images.
+ annotations_path: The path to the CreateML json annotation file.
+ show_progress: If True, display a progress bar during loading.
+
+ Returns:
+ A DetectionDataset instance containing the loaded images and
+ annotations.
+
+ Examples:
+ ```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("createml")
+
+ ds = sv.DetectionDataset.from_createml(
+ images_directory_path=f"{dataset.location}/train",
+ annotations_path=f"{dataset.location}/train/_annotations.createml.json",
+ )
+
+ ds.classes
+ # ['dog', 'person']
+ ```
+ """
+ classes, image_paths, annotations = load_createml_annotations(
+ images_directory_path=images_directory_path,
+ annotations_path=annotations_path,
+ show_progress=show_progress,
+ )
+ return DetectionDataset(
+ classes=classes, images=image_paths, annotations=annotations
+ )
+
+ def as_createml(
+ self,
+ images_directory_path: str | None = None,
+ annotations_path: str | None = None,
+ show_progress: bool = False,
+ ) -> None:
+ """
+ Exports the dataset to CreateML format. This method saves the
+ images and their corresponding annotations in CreateML format.
+
+ Args:
+ images_directory_path: The path to the directory where the images
+ should be saved. If not provided, images will not be saved.
+ annotations_path: The path to the CreateML json annotation file.
+ If not provided, the annotations will not be saved.
+ show_progress: If True, display a progress bar while saving images.
+
+ Returns:
+ None. Side-effects only: writes images and/or annotation file.
+
+ Examples:
+ ```python
+ import supervision as sv
+
+ ds = sv.DetectionDataset(classes=["dog"], images=[], annotations={})
+ ds.as_createml(
+ images_directory_path="/tmp/images",
+ annotations_path="/tmp/annotations.json",
+ )
+ ```
+ """
+ if images_directory_path is not None:
+ save_dataset_images(
+ dataset=self,
+ images_directory_path=images_directory_path,
+ show_progress=show_progress,
+ )
+ if annotations_path is not None:
+ save_createml_annotations(
+ dataset=self,
+ annotations_path=annotations_path,
+ )
+
+ @classmethod
+ def from_coco(
+ cls,
+ images_directory_path: str,
+ annotations_path: str,
+ force_masks: bool = False,
+ show_progress: bool = False,
+ *,
+ use_iscrowd: bool = True,
+ ) -> DetectionDataset:
+ """
+ Creates a Dataset instance from COCO formatted data.
+
+ Args:
+ images_directory_path: The path to the
+ directory containing the images.
+ annotations_path: The path to the json annotation files.
+ force_masks: If True,
+ forces masks to be loaded for all annotations,
+ regardless of whether they are present.
+ show_progress: If True, display a progress bar during loading.
+ use_iscrowd: If True, includes COCO ``iscrowd`` and ``area``
+ annotation fields in ``Detections.data``.
+ Returns:
+ A DetectionDataset instance containing
+ the loaded images and annotations.
+
+ Examples:
+ ```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 = sv.DetectionDataset.from_coco(
+ images_directory_path=f"{dataset.location}/train",
+ annotations_path=f"{dataset.location}/train/_annotations.coco.json",
+ # pass show_progress=True to enable a tqdm progress bar
+ )
+
+ ds.classes
+ # ['dog', 'person']
+ ```
+ """
+ classes, images, annotations = load_coco_annotations(
+ images_directory_path=images_directory_path,
+ annotations_path=annotations_path,
+ force_masks=force_masks,
+ use_iscrowd=use_iscrowd,
+ show_progress=show_progress,
+ )
+ return DetectionDataset(classes=classes, images=images, annotations=annotations)
+
+ def as_coco(
+ self,
+ images_directory_path: str | None = None,
+ annotations_path: str | None = None,
+ min_image_area_percentage: float = 0.0,
+ max_image_area_percentage: float = 1.0,
+ approximation_percentage: float = 0.0,
+ starting_image_id: int = 1,
+ starting_annotation_id: int = 1,
+ show_progress: bool = False,
+ ) -> tuple[int, int]:
+ """
+ Exports the dataset to COCO format. This method saves the
+ images and their corresponding annotations in COCO format.
+
+ !!! tip
+
+ The format of the mask is determined automatically based on its structure:
+
+ - If a mask contains multiple disconnected components or holes, it will be
+ saved using the Run-Length Encoding (RLE) format for efficient storage and
+ processing.
+ - If a mask consists of a single, contiguous region without any holes, it
+ will be encoded as a polygon, preserving the outline of the object.
+
+ This automatic selection ensures that the masks are stored in the most
+ appropriate and space-efficient format, complying with COCO dataset
+ standards.
+
+ Args:
+ images_directory_path: The path to the directory
+ where the images should be saved.
+ If not provided, images will not be saved.
+ annotations_path: The path to COCO annotation file.
+ min_image_area_percentage: The minimum percentage of
+ detection area relative to
+ the image area for a detection to be included.
+ Argument is used only for segmentation datasets.
+ max_image_area_percentage: The maximum percentage of
+ detection area relative to
+ the image area for a detection to be included.
+ Argument is used only for segmentation datasets.
+ approximation_percentage: The percentage of polygon points
+ to be removed from the input polygon,
+ in the range [0, 1). This is useful for simplifying the annotations.
+ Argument is used only for segmentation datasets.
+ starting_image_id: First image id to assign in the exported file.
+ Defaults to ``1``. Override when exporting multiple splits into
+ a coordinated COCO collection so ids remain unique across the
+ set (see example below).
+ starting_annotation_id: First annotation id to assign in the
+ exported file. Defaults to ``1``. Override for the same
+ multi-split reason as ``starting_image_id``.
+ show_progress: If True, display a progress bar during saving.
+
+ Returns:
+ A ``(next_image_id, next_annotation_id)`` tuple containing the
+ first unused ids after this export. Feed them straight back into
+ ``starting_image_id`` and ``starting_annotation_id`` on the next
+ split so ids stay globally unique. When ``annotations_path`` is
+ ``None`` (images-only export) the starting ids are returned
+ unchanged so chaining still composes.
+
+ Example:
+ ```python
+ # Exporting train, valid, and test splits with non-colliding ids
+ # so the three annotation files can later be merged into one COCO.
+ next_image_id, next_annotation_id = train_ds.as_coco(
+ images_directory_path="out/train/images",
+ annotations_path="out/train/annotations.json",
+ )
+ next_image_id, next_annotation_id = valid_ds.as_coco(
+ images_directory_path="out/valid/images",
+ annotations_path="out/valid/annotations.json",
+ starting_image_id=next_image_id,
+ starting_annotation_id=next_annotation_id,
+ )
+ _, _ = test_ds.as_coco(
+ images_directory_path="out/test/images",
+ annotations_path="out/test/annotations.json",
+ starting_image_id=next_image_id,
+ starting_annotation_id=next_annotation_id,
+ ) # return value not needed โ no further split
+ ```
+ """
+ if images_directory_path is not None:
+ save_dataset_images(
+ dataset=self,
+ images_directory_path=images_directory_path,
+ show_progress=show_progress,
+ )
+ if annotations_path is not None:
+ return save_coco_annotations(
+ dataset=self,
+ annotation_path=annotations_path,
+ min_image_area_percentage=min_image_area_percentage,
+ max_image_area_percentage=max_image_area_percentage,
+ approximation_percentage=approximation_percentage,
+ starting_image_id=starting_image_id,
+ starting_annotation_id=starting_annotation_id,
+ show_progress=show_progress,
+ )
+ return starting_image_id, starting_annotation_id
+
+
+@dataclass
+class ClassificationDataset(BaseDataset):
+ """
+ Contains information about a classification dataset, handles lazy image
+ loading, dataset splitting.
+
+ Attributes:
+ classes: List containing dataset class names.
+ images:
+ List of image paths or dictionary mapping image name to image data.
+ annotations: Dictionary mapping
+ image name to annotations.
+ """
+
+ def __init__(
+ self,
+ classes: list[str],
+ images: list[str] | dict[str, npt.NDArray[np.uint8]],
+ 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, npt.NDArray[np.uint8]] = {}
+ 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."
+ )
+
+ def _get_image(self, image_path: str) -> npt.NDArray[np.uint8]:
+ """Assumes that image is in dataset."""
+ if self._images_in_memory:
+ return self._images_in_memory[image_path]
+ image = cv2.imread(image_path)
+ if image is None:
+ raise ValueError(f"Could not read image from path: {image_path}")
+ return cast(npt.NDArray[np.uint8], image)
+
+ def __len__(self) -> int:
+ return len(self._images_in_memory) or len(self.image_paths)
+
+ def __getitem__(self, i: int) -> tuple[str, npt.NDArray[np.uint8], Classifications]:
+ """
+ Returns:
+ 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, npt.NDArray[np.uint8], Classifications]]:
+ """
+ Iterate over the images and annotations in the dataset.
+
+ Yields:
+ Tuples containing the image path, image data, and its annotation.
+ """
+ for i in range(len(self)):
+ image_path, image, annotation = self[i]
+ yield image_path, image, annotation
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, ClassificationDataset):
+ return False
+
+ if self.classes != 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: float = 0.8,
+ random_state: int | None = None,
+ shuffle: bool = True,
+ ) -> tuple[ClassificationDataset, ClassificationDataset]:
+ """
+ Splits the dataset into two parts (training and testing)
+ using the provided split_ratio.
+
+ Args:
+ split_ratio: The ratio of the training
+ set to the entire dataset.
+ random_state: The seed for the
+ random number generator. This is used for reproducibility.
+ shuffle: Whether to shuffle the data before splitting.
+
+ Returns:
+ A tuple containing
+ the training and testing datasets.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> import supervision as sv
+ >>> cd = sv.ClassificationDataset(
+ ... classes=['cat', 'dog'],
+ ... images={
+ ... 'img1.jpg': np.zeros((100, 100, 3), dtype=np.uint8),
+ ... 'img2.jpg': np.zeros((100, 100, 3), dtype=np.uint8),
+ ... },
+ ... annotations={
+ ... 'img1.jpg': sv.Classifications(class_id=np.array([0])),
+ ... 'img2.jpg': sv.Classifications(class_id=np.array([1])),
+ ... }
+ ... )
+ >>> train_cd, test_cd = cd.split(split_ratio=0.5, random_state=42)
+ >>> len(train_cd), len(test_cd)
+ (1, 1)
+
+ ```
+ """
+ train_paths, test_paths = train_test_split(
+ data=self.image_paths,
+ train_ratio=split_ratio,
+ random_state=random_state,
+ shuffle=shuffle,
+ )
+
+ train_input: list[str] | dict[str, npt.NDArray[np.uint8]]
+ test_input: list[str] | dict[str, npt.NDArray[np.uint8]]
+ 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=train_input,
+ annotations=train_annotations,
+ )
+ test_dataset = ClassificationDataset(
+ classes=self.classes,
+ images=test_input,
+ annotations=test_annotations,
+ )
+
+ return train_dataset, test_dataset
+
+ def as_folder_structure(
+ self, root_directory_path: str, show_progress: bool = False
+ ) -> None:
+ """
+ Saves the dataset as a multi-class folder structure.
+
+ Args:
+ root_directory_path: The path to the directory
+ where the dataset will be saved.
+ show_progress: If True, display a progress bar during saving.
+ """
+ os.makedirs(root_directory_path, exist_ok=True)
+
+ for class_name in self.classes:
+ os.makedirs(os.path.join(root_directory_path, class_name), exist_ok=True)
+
+ for image_save_path, image, annotation in tqdm(
+ self,
+ total=len(self),
+ desc="Saving classification images",
+ disable=not show_progress,
+ ):
+ image_name = Path(image_save_path).name
+ class_id = (
+ annotation.class_id[0]
+ if annotation.confidence is None
+ else annotation.get_top_k(1)[0][0]
+ )
+ class_name = self.classes[class_id]
+ 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, show_progress: bool = False
+ ) -> ClassificationDataset:
+ """
+ Load data from a multiclass folder structure into a ClassificationDataset.
+
+ Args:
+ root_directory_path: The path to the dataset directory. Hidden
+ entries, root-level files, nested directories, and files whose
+ suffix is not a supported image extension are ignored.
+ show_progress: If True, display a progress bar during loading.
+
+ Returns:
+ The dataset.
+
+ Examples:
+ ```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("folder")
+
+ cd = sv.ClassificationDataset.from_folder_structure(
+ root_directory_path=f"{dataset.location}/train"
+ # pass show_progress=True to enable a tqdm progress bar
+ )
+ ```
+ """
+ root_directory = Path(root_directory_path)
+ classes = sorted(
+ {
+ entry.name
+ for entry in root_directory.iterdir()
+ if entry.is_dir() and not entry.name.startswith(".")
+ }
+ )
+
+ image_paths = []
+ annotations = {}
+
+ for class_id, class_name in enumerate(
+ tqdm(
+ classes,
+ total=len(classes),
+ desc="Loading classification dataset",
+ disable=not show_progress,
+ )
+ ):
+ class_directory = root_directory / class_name
+
+ for image_path in sorted(
+ class_directory.iterdir(), key=lambda path: path.name
+ ):
+ if image_path.name.startswith(".") or not image_path.is_file():
+ continue
+ if image_path.suffix.lower() not in _IMAGE_FILE_EXTENSIONS:
+ continue
+ image_paths.append(str(image_path))
+ annotations[str(image_path)] = Classifications(
+ class_id=np.array([class_id]),
+ )
+
+ return cls(
+ classes=classes,
+ images=image_paths,
+ annotations=annotations,
+ )
diff --git a/src/supervision/dataset/formats/__init__.py b/src/supervision/dataset/formats/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/supervision/dataset/formats/coco.py b/src/supervision/dataset/formats/coco.py
new file mode 100644
index 0000000..7cd0be0
--- /dev/null
+++ b/src/supervision/dataset/formats/coco.py
@@ -0,0 +1,708 @@
+from __future__ import annotations
+
+import warnings
+from datetime import datetime
+from pathlib import Path
+from typing import TYPE_CHECKING, Any, cast
+
+import numpy as np
+import numpy.typing as npt
+from tqdm.auto import tqdm
+
+from supervision.config import COCO_RAW_SEGMENTATION
+from supervision.dataset.utils import (
+ approximate_mask_with_polygons,
+ check_no_basename_collisions,
+ map_detections_class_id,
+)
+from supervision.detection.core import Detections
+from supervision.detection.utils.converters import (
+ mask_to_rle,
+ polygon_to_mask,
+ rle_to_mask,
+)
+from supervision.detection.utils.masks import contains_holes, contains_multiple_segments
+from supervision.utils.file import read_json_file, save_json_file
+
+if TYPE_CHECKING:
+ from supervision.dataset.core import DetectionDataset
+
+CocoDict = dict[str, Any]
+
+
+def coco_categories_to_classes(coco_categories: list[CocoDict]) -> list[str]:
+ return [
+ category["name"]
+ for category in sorted(coco_categories, key=lambda category: category["id"])
+ ]
+
+
+def build_coco_class_index_mapping(
+ coco_categories: list[CocoDict], target_classes: list[str]
+) -> dict[int, int]:
+ source_class_to_index = {
+ category["name"]: category["id"] for category in coco_categories
+ }
+ return {
+ source_class_to_index[target_class_name]: target_class_index
+ for target_class_index, target_class_name in enumerate(target_classes)
+ }
+
+
+def classes_to_coco_categories(classes: list[str]) -> list[CocoDict]:
+ """Convert a list of class names to COCO ``categories`` entries.
+
+ Category ids are emitted 1-indexed to comply with the COCO specification
+ and tools such as CVAT, which require ``category_id`` values to start at
+ ``1``. The id assigned to the class at position ``class_index`` is
+ ``class_index + 1``, keeping it consistent with the ``category_id`` written
+ by [`detections_to_coco_annotations`](#detections_to_coco_annotations).
+
+ Args:
+ classes: Class names ordered by their internal (0-indexed) class id.
+
+ Returns:
+ A list of COCO category dictionaries with 1-indexed ``id`` values.
+
+ Examples:
+ ```python
+ from supervision.dataset.formats.coco import classes_to_coco_categories
+
+ classes_to_coco_categories(classes=["cat", "dog"])
+ # [
+ # {"id": 1, "name": "cat", "supercategory": "common-objects"},
+ # {"id": 2, "name": "dog", "supercategory": "common-objects"},
+ # ]
+ ```
+ """
+ return [
+ {
+ "id": class_index + 1,
+ "name": class_name,
+ "supercategory": "common-objects",
+ }
+ for class_index, class_name in enumerate(classes)
+ ]
+
+
+def group_coco_annotations_by_image_id(
+ coco_annotations: list[CocoDict],
+) -> dict[int, list[CocoDict]]:
+ annotations: dict[int, list[CocoDict]] = {}
+ for annotation in coco_annotations:
+ image_id = annotation["image_id"]
+ if image_id not in annotations:
+ annotations[image_id] = []
+ annotations[image_id].append(annotation)
+ return annotations
+
+
+def coco_annotations_to_masks(
+ image_annotations: list[CocoDict], resolution_wh: tuple[int, int]
+) -> npt.NDArray[np.bool_]:
+ height, width = resolution_wh[1], resolution_wh[0]
+ empty_mask: npt.NDArray[np.bool_] = np.zeros((height, width), dtype=bool)
+ masks = []
+
+ for image_annotation in image_annotations:
+ segmentation = image_annotation.get("segmentation")
+ if not segmentation:
+ # `force_masks=True` may request masks even for bbox-only annotations.
+ # Keep detection count aligned by emitting an empty mask for that object.
+ masks.append(empty_mask.copy())
+ continue
+
+ if isinstance(segmentation, dict):
+ if "counts" not in segmentation:
+ warnings.warn(
+ "Skipping annotation "
+ f"{image_annotation.get('id', '?')}: segmentation is a dict but "
+ "missing 'counts' key (expected RLE format)",
+ stacklevel=2,
+ )
+ masks.append(empty_mask.copy())
+ continue
+ masks.append(
+ rle_to_mask(rle=segmentation["counts"], resolution_wh=resolution_wh)
+ )
+ continue
+
+ if not isinstance(segmentation, list):
+ masks.append(empty_mask.copy())
+ continue
+ polygons = segmentation if isinstance(segmentation[0], list) else [segmentation]
+
+ object_mask = empty_mask.copy()
+ for polygon in polygons:
+ polygon_array: npt.NDArray[np.int32] = np.reshape(
+ np.asarray(polygon, dtype=np.int32), (-1, 2)
+ )
+ if polygon_array.size == 0:
+ warnings.warn(
+ "Skipping empty polygon while loading COCO segmentation for "
+ f"annotation id={image_annotation.get('id')}.",
+ stacklevel=2,
+ )
+ continue
+ # COCO polygon segmentation can contain multiple disjoint parts.
+ # Merge all parts into a single per-object mask.
+ object_mask |= polygon_to_mask(
+ polygon=polygon_array, resolution_wh=resolution_wh
+ ).astype(bool)
+
+ masks.append(object_mask)
+
+ return np.asarray(masks, dtype=bool)
+
+
+def coco_annotations_to_detections(
+ image_annotations: list[CocoDict],
+ resolution_wh: tuple[int, int],
+ with_masks: bool,
+ use_iscrowd: bool = True,
+) -> Detections:
+ """Convert COCO annotation dicts for a single image into a `Detections` object.
+
+ .. warning::
+ The returned ``Detections.class_id`` contains **raw COCO** ``category_id``
+ values, not the final 0-indexed internal class ids. Callers **must** pass
+ the result through :func:`map_detections_class_id` with the appropriate
+ ``source_to_target_mapping`` (built by
+ :func:`build_coco_class_index_mapping`) before the ``class_id`` values are
+ meaningful. Skipping the remap step yields 1-based ids in a field that the
+ rest of supervision treats as 0-based.
+
+ Args:
+ image_annotations: List of COCO annotation dicts for one image.
+ resolution_wh: ``(width, height)`` of the image, used for mask decoding.
+ with_masks: Whether to decode segmentation fields into binary masks.
+ use_iscrowd: When ``True``, store ``iscrowd`` and ``area`` in
+ ``Detections.data``.
+
+ Returns:
+ Detections with ``class_id`` set to raw COCO ``category_id`` values.
+ Call :func:`map_detections_class_id` on the result before use.
+ When ``with_masks=False``, ``detections.data[COCO_RAW_SEGMENTATION]`` is
+ populated as an object array (shape ``(N,)``) holding the raw polygon list or
+ RLE dict per annotation; consumed by :func:`detections_to_coco_annotations`
+ for a coordinate-preserving round-trip.
+ """
+ if not image_annotations:
+ return Detections.empty()
+
+ class_ids = [
+ image_annotation["category_id"] for image_annotation in image_annotations
+ ]
+ xyxy_list = [image_annotation["bbox"] for image_annotation in image_annotations]
+ xyxy: npt.NDArray[np.float32] = np.asarray(xyxy_list, dtype=np.float32)
+ xyxy[:, 2:4] += xyxy[:, 0:2]
+
+ data: dict[str, npt.NDArray[np.generic] | list[Any]] = {}
+ if use_iscrowd:
+ iscrowd = [
+ image_annotation.get("iscrowd", 0) for image_annotation in image_annotations
+ ]
+ area = []
+ for image_annotation in image_annotations:
+ if "area" in image_annotation:
+ area.append(image_annotation["area"])
+ elif with_masks and _with_seg_mask(image_annotation):
+ area.append(np.nan)
+ else:
+ area.append(image_annotation["bbox"][2] * image_annotation["bbox"][3])
+ data = dict(
+ iscrowd=np.asarray(iscrowd, dtype=int), area=np.asarray(area, dtype=float)
+ )
+
+ if with_masks:
+ mask = coco_annotations_to_masks(
+ image_annotations=image_annotations, resolution_wh=resolution_wh
+ )
+ else:
+ mask = None
+ # Preserve raw polygon/RLE data so as_coco() can round-trip without
+ # binary-mask encoding. Stored as an object array (one entry per detection).
+ raw_segs: npt.NDArray[np.object_] = np.empty(
+ len(image_annotations), dtype=object
+ )
+ for k, _ann in enumerate(image_annotations):
+ raw_segs[k] = _ann.get("segmentation", [])
+ data[COCO_RAW_SEGMENTATION] = raw_segs
+
+ return Detections(
+ class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=mask, data=data
+ )
+
+
+def detections_to_coco_annotations(
+ detections: Detections,
+ image_id: int,
+ annotation_id: int,
+ min_image_area_percentage: float = 0.0,
+ max_image_area_percentage: float = 1.0,
+ approximation_percentage: float = 0.75,
+) -> tuple[list[CocoDict], int]:
+ """Convert `Detections` to COCO ``annotations`` entries.
+
+ The internal 0-indexed ``Detections.class_id`` is serialized as a 1-indexed
+ COCO ``category_id`` (``category_id = class_id + 1``). This complies with the
+ COCO specification and tools such as CVAT, and stays consistent with the ids
+ emitted by [`classes_to_coco_categories`](#classes_to_coco_categories), so a
+ detection with internal ``class_id=k`` maps to ``category_id=k + 1``.
+
+ Args:
+ detections: The detections to convert. ``class_id`` must not be ``None``.
+ image_id: COCO ``image_id`` shared by every produced annotation.
+ annotation_id: First annotation id to assign; incremented per detection.
+ min_image_area_percentage: Lower bound on detection area / image area,
+ used only when approximating masks with polygons.
+ max_image_area_percentage: Upper bound on detection area / image area,
+ used only when approximating masks with polygons.
+ approximation_percentage: Polygon-simplification ratio in ``[0, 1)``.
+
+ Returns:
+ A ``(coco_annotations, next_annotation_id)`` tuple, where
+ ``next_annotation_id`` is one greater than the last id assigned.
+
+ Raises:
+ ValueError: If any detection has ``class_id`` equal to ``None``.
+
+ Note:
+ For ``iscrowd=0`` annotations, ``segmentation`` is a
+ ``list[list[float]]`` where each inner list encodes one polygon
+ part as flat ``[x1, y1, x2, y2, ...]`` coordinates. A single
+ object with *N* disjoint parts produces *N* inner lists.
+
+ When ``iscrowd`` is not in ``detections.data``, masks with holes
+ or multiple disjoint segments are auto-encoded as RLE
+ (``iscrowd=1``); simple single-region masks use polygon format
+ (``iscrowd=0``). Supply ``data={"iscrowd": np.array([0])}`` to
+ force polygon output regardless of mask topology.
+
+ Examples:
+ ```python
+ import numpy as np
+ from supervision import Detections
+ from supervision.dataset.formats.coco import (
+ detections_to_coco_annotations,
+ )
+
+ detections = Detections(
+ xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
+ class_id=np.array([0], dtype=int),
+ )
+ annotations, next_id = detections_to_coco_annotations(
+ detections=detections, image_id=1, annotation_id=1
+ )
+ annotations[0]["category_id"]
+ # 1
+ ```
+ """
+ coco_annotations: list[CocoDict] = []
+ for xyxy, mask, _, class_id, _, data in detections:
+ if class_id is None:
+ raise ValueError("Detections must include class_id for COCO export.")
+ box_width, box_height = xyxy[2] - xyxy[0], xyxy[3] - xyxy[1]
+ segmentation: list[list[float]] | dict[str, list[int]] = []
+ if mask is not None:
+ mask_bool = mask
+ if "iscrowd" in data:
+ iscrowd = int(np.asarray(data["iscrowd"]).item())
+ else:
+ iscrowd = int(
+ contains_holes(mask=mask_bool)
+ or contains_multiple_segments(mask=mask_bool)
+ )
+
+ if iscrowd:
+ segmentation = {
+ "counts": cast(
+ list[int], mask_to_rle(mask=mask_bool, compressed=False)
+ ),
+ "size": list(mask.shape[:2]),
+ }
+ else:
+ polygons = approximate_mask_with_polygons(
+ mask=mask_bool,
+ min_image_area_percentage=min_image_area_percentage,
+ max_image_area_percentage=max_image_area_percentage,
+ approximation_percentage=approximation_percentage,
+ )
+ # Small/noisy masks can be filtered out by approximation settings.
+ # Guard against empty output and keep a valid COCO annotation record.
+ if polygons:
+ # Export ALL polygons so disjoint mask components are preserved.
+ segmentation = [list(p.flatten()) for p in polygons]
+ else:
+ warnings.warn(
+ "Skipping COCO polygon segmentation for annotation "
+ f"id={annotation_id} because mask approximation "
+ "returned no polygons.",
+ stacklevel=2,
+ )
+ else:
+ iscrowd = int(np.asarray(data.get("iscrowd", 0)).item())
+ # When masks were not decoded during loading, fall back to the raw
+ # polygon/RLE stored in data["segmentation"] for a lossless round-trip.
+ raw_seg = data.get(COCO_RAW_SEGMENTATION)
+ if raw_seg is not None and bool(raw_seg):
+ if isinstance(raw_seg, dict):
+ # RLE format โ pass through unchanged
+ segmentation = raw_seg
+ elif (
+ isinstance(raw_seg, list)
+ and raw_seg
+ and not isinstance(raw_seg[0], (list, tuple))
+ ):
+ # Flat list shorthand [x1,y1,...] โ wrap to list-of-lists
+ segmentation = [list(raw_seg)]
+ else:
+ segmentation = list(raw_seg)
+
+ stored_area = None
+ if "area" in data:
+ stored_area = float(np.asarray(data["area"]).item())
+
+ if stored_area is not None and np.isfinite(stored_area):
+ area = stored_area
+ elif mask is not None:
+ area = float(np.count_nonzero(mask))
+ else:
+ area = float(box_width * box_height)
+ coco_annotation = {
+ "id": annotation_id,
+ "image_id": image_id,
+ "category_id": int(class_id) + 1,
+ "bbox": [xyxy[0], xyxy[1], box_width, box_height],
+ "area": area,
+ "segmentation": segmentation,
+ "iscrowd": iscrowd,
+ }
+ coco_annotations.append(coco_annotation)
+ annotation_id += 1
+ return coco_annotations, annotation_id
+
+
+def get_coco_class_index_mapping(annotations_path: str) -> dict[int, int]:
+ """
+ Generates a mapping from sequential class indices to original COCO class ids.
+
+ This function is essential when working with models that expect class ids to be
+ zero-indexed and sequential (0 to 79), as opposed to the original COCO
+ dataset where category ids are non-contiguous ranging from 1 to 90 but skipping some
+ ids.
+
+ Use Cases:
+ - Evaluating models trained with COCO-style annotations where class ids
+ are sequential ranging from 0 to 79.
+ - Ensuring consistent class indexing across training, inference and evaluation,
+ when using different tools or datasets with COCO format.
+ - Reproducing results from models that assume sequential class ids (0 to 79).
+
+ How it Works:
+ - Reads the COCO annotation file in its original format (`annotations_path`).
+ - Extracts and sorts all class names by their original COCO id (1 to 90).
+ - Builds a mapping from COCO class ids (not sequential with skipped ids) to
+ new class ids (sequential ranging from 0 to 79).
+ - Returns a dictionary mapping: `{new_class_id: original_COCO_class_id}`.
+
+ Args:
+ annotations_path: Path to COCO JSON annotations file
+ (e.g., `instances_val2017.json`).
+
+ Returns:
+ A mapping from new class id (sequential ranging from 0 to 79)
+ to original COCO class id (1 to 90 with skipped ids).
+
+ Examples:
+ >>> import json
+ >>> import os
+ >>> import tempfile
+ >>> from supervision.dataset.formats.coco import get_coco_class_index_mapping
+ >>> coco_data = {
+ ... "categories": [
+ ... {"id": 1, "name": "person"},
+ ... {"id": 3, "name": "car"},
+ ... ],
+ ... "images": [],
+ ... "annotations": [],
+ ... }
+ >>> annotations_path = None
+ >>> try:
+ ... with tempfile.NamedTemporaryFile(
+ ... mode="w", suffix=".json", delete=False
+ ... ) as f:
+ ... annotations_path = f.name
+ ... json.dump(coco_data, f)
+ ... mapping = get_coco_class_index_mapping(annotations_path)
+ ... print(mapping)
+ ... finally:
+ ... if annotations_path is not None:
+ ... os.remove(annotations_path)
+ {0: 1, 1: 3}
+ >>> os.path.exists(annotations_path)
+ False
+ """
+ coco_data = read_json_file(annotations_path)
+ classes = coco_categories_to_classes(coco_categories=coco_data["categories"])
+ class_mapping = build_coco_class_index_mapping(
+ coco_categories=coco_data["categories"], target_classes=classes
+ )
+ return {v: k for k, v in class_mapping.items()}
+
+
+def load_coco_annotations(
+ images_directory_path: str,
+ annotations_path: str,
+ force_masks: bool = False,
+ use_iscrowd: bool = True,
+ show_progress: bool = False,
+) -> tuple[list[str], list[str], dict[str, Detections]]:
+ """
+ Load COCO annotations and convert them to `Detections`.
+
+ If `force_masks` is `False`, masks are still loaded for images whose annotations
+ include a `segmentation` field. This keeps mask handling consistent with other
+ dataset loaders that infer masks from annotation content.
+
+ Args:
+ images_directory_path: Path to the image directory.
+ annotations_path: Path to COCO JSON annotations.
+ force_masks: If `True`, always attempt to load masks.
+ use_iscrowd: If `True`, include `iscrowd` and `area` in detection data.
+ show_progress: If `True`, display a progress bar during loading.
+
+ Returns:
+ A tuple of `(classes, image_paths, annotations)` where image paths are
+ canonicalized resolved paths inside ``images_directory_path``.
+
+ Raises:
+ ValueError: If any annotation's ``file_name`` resolves to the images
+ directory itself, to a path outside the images directory (e.g. via
+ ``../`` traversal or an absolute path), or to a subdirectory instead
+ of a regular image file.
+ ValueError: If two image entries resolve to the same canonical path.
+
+ Note:
+ Each annotation's ``file_name`` is validated against
+ ``images_directory_path`` before loading. Annotations that reference
+ paths outside the directory are rejected to prevent path-traversal
+ attacks when loading user-supplied annotation files. Symlinked images
+ pointing outside the resolved images directory are also rejected.
+ """
+ coco_data = read_json_file(file_path=annotations_path)
+ classes = coco_categories_to_classes(coco_categories=coco_data["categories"])
+
+ class_index_mapping = build_coco_class_index_mapping(
+ coco_categories=coco_data["categories"], target_classes=classes
+ )
+
+ coco_images = coco_data["images"]
+ coco_annotations_groups = group_coco_annotations_by_image_id(
+ coco_annotations=coco_data["annotations"]
+ )
+
+ images = []
+ annotations = {}
+ images_directory_resolved = Path(images_directory_path).resolve()
+
+ for coco_image in tqdm(
+ coco_images,
+ total=len(coco_images),
+ desc="Loading COCO annotations",
+ disable=not show_progress,
+ ):
+ image_name, image_width, image_height = (
+ coco_image["file_name"],
+ coco_image["width"],
+ coco_image["height"],
+ )
+ image_annotations = coco_annotations_groups.get(coco_image["id"], [])
+ image_path = str(Path(images_directory_path) / Path(image_name))
+ try:
+ resolved_image_path = Path(image_path).resolve()
+ except (OSError, ValueError) as exc:
+ raise ValueError(
+ f"COCO annotation refers to image {image_name!r}, which "
+ f"produces an invalid path: {exc}"
+ ) from exc
+ if resolved_image_path == images_directory_resolved:
+ raise ValueError(
+ f"COCO annotation refers to image {image_name!r}, which "
+ f"resolves to the images directory itself "
+ f"({images_directory_resolved}). Expected a path to an "
+ "image file."
+ )
+ if images_directory_resolved not in resolved_image_path.parents:
+ raise ValueError(
+ f"COCO annotation refers to image {image_name!r}, which "
+ f"resolves to {resolved_image_path} โ outside the images "
+ f"directory {images_directory_resolved}."
+ )
+ if resolved_image_path.is_dir():
+ raise ValueError(
+ f"COCO annotation refers to image {image_name!r}, which "
+ f"resolves to directory {resolved_image_path}. Expected a "
+ "path to an image file."
+ )
+ image_path = str(resolved_image_path)
+ if image_path in annotations:
+ raise ValueError(
+ f"COCO annotation file contains duplicate entries for image "
+ f"{image_name!r}. Each image must appear at most once."
+ )
+
+ with_masks = force_masks or any(
+ _with_seg_mask(annotation) for annotation in image_annotations
+ )
+ annotation = coco_annotations_to_detections(
+ image_annotations=image_annotations,
+ resolution_wh=(image_width, image_height),
+ with_masks=with_masks,
+ use_iscrowd=use_iscrowd,
+ )
+
+ annotation = map_detections_class_id(
+ source_to_target_mapping=class_index_mapping,
+ detections=annotation,
+ )
+
+ images.append(image_path)
+ annotations[image_path] = annotation
+
+ return classes, images, annotations
+
+
+def _with_seg_mask(annotation: dict[str, Any]) -> bool:
+ return bool(annotation.get("segmentation"))
+
+
+def save_coco_annotations(
+ dataset: DetectionDataset,
+ annotation_path: str,
+ min_image_area_percentage: float = 0.0,
+ max_image_area_percentage: float = 1.0,
+ approximation_percentage: float = 0.0,
+ starting_image_id: int = 1,
+ starting_annotation_id: int = 1,
+ show_progress: bool = False,
+) -> tuple[int, int]:
+ """Save a DetectionDataset to a COCO-format ``annotations.json`` file.
+
+ Args:
+ dataset: The DetectionDataset to write.
+ annotation_path: Output path for the COCO ``annotations.json``.
+ min_image_area_percentage: Lower bound on detection area / image area;
+ used only for segmentation datasets.
+ max_image_area_percentage: Upper bound on detection area / image area;
+ used only for segmentation datasets.
+ approximation_percentage: Polygon-simplification ratio in ``[0, 1)``;
+ used only for segmentation datasets.
+ starting_image_id: First image id to assign in the exported file.
+ Defaults to ``1``. Override when exporting multiple splits into
+ a coordinated COCO collection so ids remain unique across the set.
+ starting_annotation_id: First annotation id to assign in the exported
+ file. Defaults to ``1``. Override for the same multi-split reason
+ as ``starting_image_id``.
+ show_progress: If ``True``, display a progress bar during saving.
+
+ Returns:
+ A ``(next_image_id, next_annotation_id)`` tuple. The returned values
+ are one greater than the highest ids written, so they can be fed
+ directly back into ``starting_image_id`` and ``starting_annotation_id``
+ when exporting another split into a coordinated COCO collection
+ (see ``DetectionDataset.as_coco`` for the chaining pattern). When the
+ dataset is empty the starting ids are returned unchanged.
+
+ .. note::
+ This function ensures globally unique integer ``id`` values across
+ splits. It rejects duplicate image basenames before writing because
+ ``file_name`` is set to the bare image basename, so two input paths
+ that differ only by directory would otherwise collapse to the same
+ COCO image record.
+
+ Raises:
+ ValueError: If two image paths share the same basename and would map to
+ the same COCO ``file_name``.
+
+ Example:
+ ```python
+ import supervision as sv
+ from supervision.dataset.formats.coco import save_coco_annotations
+
+ ds = sv.DetectionDataset.from_yolo(
+ images_directory_path="train/images",
+ annotations_directory_path="train/labels",
+ data_yaml_path="data.yaml",
+ )
+ next_img_id, next_ann_id = save_coco_annotations(
+ dataset=ds, annotation_path="out/train/annotations.json"
+ )
+ # next_img_id and next_ann_id are the first unused ids โ pass them
+ # to the next split to keep ids globally unique across files.
+ ```
+ """
+ if starting_image_id < 1 or starting_annotation_id < 1:
+ raise ValueError(
+ "starting_image_id and starting_annotation_id must be >= 1 "
+ "(COCO spec requires 1-indexed ids); "
+ f"got {starting_image_id=}, {starting_annotation_id=}"
+ )
+ check_no_basename_collisions(
+ image_paths=dataset.image_paths,
+ key=lambda image_path: Path(image_path).name,
+ output_kind="COCO image",
+ )
+ Path(annotation_path).parent.mkdir(parents=True, exist_ok=True)
+ licenses = [
+ {
+ "id": 1,
+ "url": "https://creativecommons.org/licenses/by/4.0/",
+ "name": "CC BY 4.0",
+ }
+ ]
+
+ coco_annotations = []
+ coco_images = []
+ coco_categories = classes_to_coco_categories(classes=dataset.classes)
+
+ image_id, annotation_id = starting_image_id, starting_annotation_id
+ for image_path, image, annotation in tqdm(
+ dataset,
+ total=len(dataset),
+ desc="Saving COCO annotations",
+ disable=not show_progress,
+ ):
+ image_height, image_width, _ = image.shape
+ image_name = f"{Path(image_path).stem}{Path(image_path).suffix}"
+ coco_image = {
+ "id": image_id,
+ "license": 1,
+ "file_name": image_name,
+ "height": image_height,
+ "width": image_width,
+ "date_captured": datetime.now().strftime("%m/%d/%Y,%H:%M:%S"),
+ }
+
+ coco_images.append(coco_image)
+ coco_annotation, annotation_id = detections_to_coco_annotations(
+ detections=annotation,
+ image_id=image_id,
+ annotation_id=annotation_id,
+ min_image_area_percentage=min_image_area_percentage,
+ max_image_area_percentage=max_image_area_percentage,
+ approximation_percentage=approximation_percentage,
+ )
+
+ coco_annotations.extend(coco_annotation)
+ image_id += 1
+
+ annotation_dict = {
+ "info": {},
+ "licenses": licenses,
+ "categories": coco_categories,
+ "images": coco_images,
+ "annotations": coco_annotations,
+ }
+ save_json_file(annotation_dict, file_path=annotation_path)
+ return image_id, annotation_id
diff --git a/src/supervision/dataset/formats/createml.py b/src/supervision/dataset/formats/createml.py
new file mode 100644
index 0000000..84b939e
--- /dev/null
+++ b/src/supervision/dataset/formats/createml.py
@@ -0,0 +1,331 @@
+from __future__ import annotations
+
+from pathlib import Path
+from typing import TYPE_CHECKING, Any, cast
+
+import numpy as np
+from tqdm.auto import tqdm
+
+from supervision.dataset.utils import check_no_basename_collisions
+from supervision.detection.core import Detections
+from supervision.utils.file import read_json_file, save_json_file
+
+if TYPE_CHECKING:
+ from supervision.dataset.core import DetectionDataset
+
+CreateMLDict = dict[str, Any]
+
+
+def _resolve_image_path(images_directory_path: str, image_name: str) -> str:
+ """Resolve and validate an image path against the images directory.
+
+ Rejects annotations whose ``image`` field escapes ``images_directory_path``
+ (via ``..`` traversal, an absolute path, or a symlink pointing outside),
+ mirroring the protection used by the COCO loader. Returns the canonical
+ resolved path so aliases collapse to a single dataset entry.
+ """
+ images_directory_resolved = Path(images_directory_path).resolve()
+ image_path = Path(images_directory_path) / Path(image_name)
+ try:
+ resolved_image_path = image_path.resolve()
+ except (OSError, ValueError) as exc:
+ raise ValueError(
+ f"CreateML annotation refers to image {image_name!r}, which "
+ f"produces an invalid path: {exc}"
+ ) from exc
+ if resolved_image_path == images_directory_resolved:
+ raise ValueError(
+ f"CreateML annotation refers to image {image_name!r}, which "
+ f"resolves to the images directory itself "
+ f"({images_directory_resolved}). Expected a path to an image file."
+ )
+ if images_directory_resolved not in resolved_image_path.parents:
+ raise ValueError(
+ f"CreateML annotation refers to image {image_name!r}, which "
+ f"resolves to {resolved_image_path} โ outside the images "
+ f"directory {images_directory_resolved}."
+ )
+ if resolved_image_path.is_dir():
+ raise ValueError(
+ f"CreateML annotation refers to image {image_name!r}, which "
+ f"resolves to directory {resolved_image_path}. Expected a path "
+ "to an image file."
+ )
+ return str(resolved_image_path)
+
+
+def createml_annotations_to_detections(
+ image_annotations: list[CreateMLDict], class_to_index: dict[str, int]
+) -> Detections:
+ """Convert a single image's CreateML annotations into ``Detections``.
+
+ CreateML stores each box as a pixel-space centre point plus width/height
+ (``{"x", "y", "width", "height"}``); they are converted to ``xyxy`` corners.
+
+ Args:
+ image_annotations: List of annotation dicts for one image, each containing
+ a ``"label"`` key and a ``"coordinates"`` dict with ``"x"``, ``"y"``,
+ ``"width"``, and ``"height"`` keys.
+ class_to_index: Mapping from class name to zero-based integer id.
+
+ Returns:
+ A ``Detections`` instance with ``xyxy`` boxes and ``class_id`` set.
+ Returns ``Detections.empty()`` when ``image_annotations`` is empty.
+
+ Raises:
+ ValueError: If an annotation is missing required keys (``"coordinates"``,
+ ``"label"``, or any coordinate sub-key), or if a coordinate value
+ cannot be converted to float.
+
+ Examples:
+ ```python
+ import supervision as sv
+ from supervision.dataset.formats.createml import (
+ createml_annotations_to_detections,
+ )
+
+ annotations = [
+ {
+ "label": "dog",
+ "coordinates": {"x": 50, "y": 50, "width": 20, "height": 20},
+ }
+ ]
+ detections = createml_annotations_to_detections(annotations, {"dog": 0})
+ # detections.xyxy โ [[40, 40, 60, 60]]
+ ```
+ """
+ if not image_annotations:
+ return Detections.empty()
+
+ xyxy = []
+ class_ids = []
+ for annotation in image_annotations:
+ try:
+ coordinates = annotation["coordinates"]
+ x_center = float(coordinates["x"])
+ y_center = float(coordinates["y"])
+ width = float(coordinates["width"])
+ height = float(coordinates["height"])
+ label = annotation["label"]
+ except (KeyError, TypeError) as exc:
+ raise ValueError(
+ f"Malformed CreateML annotation entry {annotation!r}: {exc}"
+ ) from exc
+ xyxy.append(
+ [
+ x_center - width / 2,
+ y_center - height / 2,
+ x_center + width / 2,
+ y_center + height / 2,
+ ]
+ )
+ class_ids.append(class_to_index[label])
+
+ return Detections(
+ xyxy=np.array(xyxy, dtype=np.float32),
+ class_id=np.array(class_ids, dtype=int),
+ )
+
+
+def load_createml_annotations(
+ images_directory_path: str,
+ annotations_path: str,
+ show_progress: bool = False,
+) -> tuple[list[str], list[str], dict[str, Detections]]:
+ """Load CreateML object-detection annotations and convert them to ``Detections``.
+
+ CreateML uses a single JSON file containing a list of per-image entries, each
+ holding axis-aligned bounding boxes. Class names are inferred from the labels
+ present in the file and assigned stable, sorted, zero-based ids. Because the
+ format has no explicit category list, a class with no boxes anywhere in the
+ file will not appear in the returned ``classes``.
+
+ Args:
+ images_directory_path: Path to the directory containing the images.
+ annotations_path: Path to the CreateML JSON annotation file.
+ show_progress: If ``True``, display a tqdm progress bar while loading
+ annotations.
+
+ Returns:
+ A tuple of three elements:
+
+ - ``classes`` (``list[str]``): globally sorted class names inferred from
+ all labels present in the file.
+ - ``image_paths`` (``list[str]``): canonical resolved path for every
+ entry in the JSON, in file order.
+ - ``annotations`` (``dict[str, Detections]``): mapping from canonical
+ resolved image path to its ``Detections``.
+
+ Raises:
+ ValueError: If the JSON root is not a list.
+ ValueError: If an entry is missing the required ``"image"`` key.
+ ValueError: If an annotation is missing required coordinate or label keys.
+ ValueError: If two entries resolve to the same image path.
+ ValueError: If an annotation's ``image`` field resolves to the images
+ directory itself or to a path outside it (e.g. via ``..`` traversal
+ or an absolute path).
+ """
+ createml_data = cast(
+ "list[CreateMLDict]", read_json_file(file_path=annotations_path)
+ )
+ if not isinstance(createml_data, list):
+ raise ValueError(
+ f"CreateML annotation file must contain a JSON list at the root, "
+ f"got {type(createml_data).__name__}."
+ )
+
+ try:
+ classes = sorted(
+ {
+ annotation["label"]
+ for entry in createml_data
+ for annotation in (entry.get("annotations") or [])
+ }
+ )
+ except (KeyError, TypeError) as exc:
+ raise ValueError(
+ f"Malformed CreateML annotation entry "
+ f"(missing or non-string 'label'): {exc}"
+ ) from exc
+ class_to_index = {class_name: index for index, class_name in enumerate(classes)}
+
+ image_paths: list[str] = []
+ annotations: dict[str, Detections] = {}
+ for entry in tqdm(
+ createml_data,
+ desc="Loading CreateML annotations",
+ disable=not show_progress,
+ ):
+ image_name = entry.get("image")
+ if image_name is None:
+ raise ValueError(
+ f"CreateML annotation entry is missing the required 'image' key: "
+ f"{entry!r}"
+ )
+ image_path = _resolve_image_path(
+ images_directory_path=images_directory_path, image_name=image_name
+ )
+ if image_path in annotations:
+ raise ValueError(
+ f"CreateML annotation file contains duplicate entries for image "
+ f"{image_name!r}. Each image must appear at most once."
+ )
+ annotations[image_path] = createml_annotations_to_detections(
+ image_annotations=entry.get("annotations") or [],
+ class_to_index=class_to_index,
+ )
+ image_paths.append(image_path)
+
+ return classes, image_paths, annotations
+
+
+def detections_to_createml_annotations(
+ detections: Detections, classes: list[str]
+) -> list[CreateMLDict]:
+ """Convert ``Detections`` into a list of CreateML annotation dicts.
+
+ Each bounding box is stored as a pixel-space centre point plus width and
+ height, which is the CreateML object-detection convention.
+
+ Args:
+ detections: The detections to convert. ``class_id`` must not be ``None``.
+ classes: Ordered list of class names; ``detections.class_id`` values are
+ used as indices into this list.
+
+ Returns:
+ A list of dicts, each with a ``"label"`` key (class name) and a
+ ``"coordinates"`` dict containing ``"x"``, ``"y"``, ``"width"``, and
+ ``"height"`` in pixel space.
+
+ Raises:
+ ValueError: If ``detections.class_id`` is ``None``.
+
+ Examples:
+ ```python
+ import numpy as np
+ import supervision as sv
+ from supervision.dataset.formats.createml import (
+ detections_to_createml_annotations,
+ )
+
+ detections = sv.Detections(
+ xyxy=np.array([[40, 40, 60, 60]], dtype=np.float32),
+ class_id=np.array([0], dtype=int),
+ )
+ detections_to_createml_annotations(detections, classes=["dog"])
+ # [{"label": "dog", "coordinates": {"x": 50.0, "y": 50.0, ...}}]
+ ```
+ """
+ class_ids = detections.class_id
+ if class_ids is None:
+ raise ValueError(
+ "class_id is required for CreateML export, but the provided "
+ "Detections has class_id=None."
+ )
+ annotations: list[CreateMLDict] = []
+ for xyxy, class_id in zip(detections.xyxy, class_ids):
+ x_min, y_min, x_max, y_max = (float(value) for value in xyxy)
+ annotations.append(
+ {
+ "label": classes[int(class_id)],
+ "coordinates": {
+ "x": (x_min + x_max) / 2,
+ "y": (y_min + y_max) / 2,
+ "width": x_max - x_min,
+ "height": y_max - y_min,
+ },
+ }
+ )
+ return annotations
+
+
+def save_createml_annotations(
+ dataset: DetectionDataset,
+ annotations_path: str,
+) -> None:
+ """Export a ``DetectionDataset`` to a CreateML object-detection JSON file.
+
+ Only the filename component of each image path is stored in the JSON (e.g.
+ ``"img.jpg"`` rather than ``"/data/train/img.jpg"``). This matches CreateML
+ convention and means the loader reconstructs paths relative to
+ ``images_directory_path``. As a consequence, two images with the same
+ basename from different directories would collapse to the same ``"image"``
+ key, so the exporter rejects that case before writing.
+
+ Args:
+ dataset: The ``DetectionDataset`` to write.
+ annotations_path: Output path for the CreateML JSON file. Parent
+ directories are created if they do not already exist.
+
+ Raises:
+ ValueError: If two image paths share the same basename and would map to
+ the same CreateML ``image`` entry.
+
+ Examples:
+ ```python
+ import supervision as sv
+ from supervision.dataset.formats.createml import save_createml_annotations
+
+ dataset = sv.DetectionDataset(classes=["dog"], images=[], annotations={})
+ save_createml_annotations(dataset, "/tmp/annotations.json")
+ ```
+ """
+ check_no_basename_collisions(
+ image_paths=dataset.image_paths,
+ key=lambda image_path: Path(image_path).name,
+ output_kind="CreateML image",
+ )
+ Path(annotations_path).parent.mkdir(parents=True, exist_ok=True)
+ createml_data: list[CreateMLDict] = [
+ {
+ "image": Path(image_path).name,
+ "annotations": detections_to_createml_annotations(
+ detections=dataset.annotations[image_path], classes=dataset.classes
+ ),
+ }
+ for image_path in dataset.image_paths
+ ]
+ save_json_file(
+ data=createml_data, # type: ignore[arg-type] # save_json_file accepts list at runtime
+ file_path=annotations_path,
+ )
diff --git a/src/supervision/dataset/formats/labelme.py b/src/supervision/dataset/formats/labelme.py
new file mode 100644
index 0000000..b34570c
--- /dev/null
+++ b/src/supervision/dataset/formats/labelme.py
@@ -0,0 +1,405 @@
+from __future__ import annotations
+
+import warnings
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+import numpy as np
+import numpy.typing as npt
+
+from supervision.dataset.utils import check_no_basename_collisions
+from supervision.detection.core import Detections
+from supervision.detection.utils.converters import (
+ mask_to_polygons,
+ polygon_to_mask,
+ polygon_to_xyxy,
+)
+from supervision.utils.file import (
+ list_files_with_extensions,
+ read_json_file,
+ save_json_file,
+)
+
+if TYPE_CHECKING:
+ from supervision.dataset.core import DetectionDataset
+
+LabelMeDict = dict[str, Any]
+# Written to every exported JSON; never read back on import.
+_LABELME_EXPORT_VERSION = "5.5.0"
+SUPPORTED_SHAPE_TYPES = ("rectangle", "polygon")
+
+__all__ = [
+ "detections_to_labelme_shapes",
+ "labelme_shapes_to_detections",
+ "load_labelme_annotations",
+ "save_labelme_annotations",
+]
+
+
+def _rectangle_to_xyxy(points: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
+ x_coordinates = points[:, 0]
+ y_coordinates = points[:, 1]
+ return np.array(
+ [
+ float(x_coordinates.min()),
+ float(y_coordinates.min()),
+ float(x_coordinates.max()),
+ float(y_coordinates.max()),
+ ],
+ dtype=np.float32,
+ )
+
+
+def _xyxy_to_polygon(xyxy: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
+ x_min, y_min, x_max, y_max = (float(value) for value in xyxy)
+ return np.array(
+ [[x_min, y_min], [x_max, y_min], [x_max, y_max], [x_min, y_max]],
+ dtype=np.float32,
+ )
+
+
+def labelme_shapes_to_detections(
+ shapes: list[LabelMeDict],
+ class_to_index: dict[str, int],
+ resolution_wh: tuple[int, int],
+ with_masks: bool,
+) -> Detections:
+ """Convert a single image's LabelMe shapes into ``Detections``.
+
+ Only ``rectangle`` and ``polygon`` shapes are imported; other shape types
+ (``circle``, ``line``, ``point``, ``linestrip``) are skipped with a warning.
+ When ``with_masks`` is ``True``, both ``rectangle`` and ``polygon`` shapes
+ produce masks: rectangles via a four-corner polygon fill.
+
+ Args:
+ shapes: List of LabelMe shape dicts for one image.
+ class_to_index: Mapping from class name to integer class ID.
+ resolution_wh: Image ``(width, height)`` used to rasterise masks.
+ with_masks: If ``True``, produce a binary mask for every detection.
+
+ Returns:
+ A :class:`Detections` instance with ``xyxy``, ``class_id``, and
+ optionally ``mask`` populated.
+
+ Raises:
+ ValueError: If a shape is missing its ``label`` or ``points`` field,
+ if a ``polygon`` has fewer than 3 points, or a ``rectangle`` has
+ fewer than 2 points.
+
+ Warns:
+ UserWarning: When unsupported shape types are encountered and skipped.
+ """
+ xyxy_list: list[npt.NDArray[np.float32]] = []
+ class_ids: list[int] = []
+ polygons: list[npt.NDArray[np.float32]] = []
+ skipped_types: set[str] = set()
+
+ for shape in shapes:
+ shape_type = shape.get("shape_type")
+ if shape_type not in SUPPORTED_SHAPE_TYPES:
+ skipped_types.add(str(shape_type))
+ continue
+ label = shape.get("label")
+ points_raw = shape.get("points")
+ if label is None or points_raw is None:
+ missing = "label" if label is None else "points"
+ raise ValueError(
+ f"LabelMe shape of type {shape_type!r} is missing the "
+ f"required {missing!r} field."
+ )
+ points = np.array(points_raw, dtype=np.float32)
+ if points.ndim != 2 or points.shape[1] != 2:
+ raise ValueError(
+ f"LabelMe shape of type {shape_type!r} (label={label!r}) has "
+ f"malformed points: expected an (N, 2) array, got shape "
+ f"{points.shape}."
+ )
+ if shape_type == "rectangle":
+ if len(points) < 2:
+ raise ValueError(
+ f"LabelMe rectangle shape (label={label!r}) has "
+ f"{len(points)} point(s); expected at least 2."
+ )
+ xyxy = _rectangle_to_xyxy(points)
+ polygon = _xyxy_to_polygon(xyxy)
+ else:
+ if len(points) < 3:
+ raise ValueError(
+ f"LabelMe polygon shape (label={label!r}) has "
+ f"{len(points)} point(s); expected at least 3."
+ )
+ xyxy = polygon_to_xyxy(polygon=points).astype(np.float32)
+ polygon = points
+ xyxy_list.append(xyxy)
+ class_ids.append(class_to_index[label])
+ if with_masks:
+ polygons.append(polygon)
+
+ if skipped_types:
+ warnings.warn(
+ f"Skipped unsupported LabelMe shape type(s) {sorted(skipped_types)}; "
+ f"only {list(SUPPORTED_SHAPE_TYPES)} are imported.",
+ UserWarning,
+ stacklevel=2,
+ )
+
+ if not xyxy_list:
+ return Detections.empty()
+
+ xyxy = np.array(xyxy_list, dtype=np.float32)
+ class_id = np.array(class_ids, dtype=int)
+ if not with_masks:
+ return Detections(xyxy=xyxy, class_id=class_id)
+
+ mask = np.array(
+ [
+ polygon_to_mask(
+ polygon=np.round(polygon).astype(np.int32),
+ resolution_wh=resolution_wh,
+ )
+ for polygon in polygons
+ ],
+ dtype=bool,
+ )
+ return Detections(xyxy=xyxy, class_id=class_id, mask=mask)
+
+
+def load_labelme_annotations(
+ images_directory_path: str,
+ annotations_directory_path: str,
+ force_masks: bool = False,
+) -> tuple[list[str], list[str], dict[str, Detections]]:
+ """Load LabelMe annotations and convert them to ``Detections``.
+
+ LabelMe stores one JSON file per image, each containing a list of ``shapes``.
+ ``rectangle`` shapes become bounding boxes; ``polygon`` shapes become masks
+ (and their bounding boxes); other shape types are skipped with a warning.
+ When any polygon is present in a file or when ``force_masks`` is ``True``,
+ both ``rectangle`` and ``polygon`` shapes produce masks: rectangles via a
+ four-corner polygon fill. Class names are inferred from the labels present
+ across all files and assigned sorted, zero-based ids.
+
+ Each image is located by the basename of the JSON's ``imagePath`` joined to
+ ``images_directory_path``; the directory portion of ``imagePath`` (which
+ LabelMe stores relative to the JSON file) is ignored, so annotation-supplied
+ path traversal cannot escape ``images_directory_path``.
+
+ Args:
+ images_directory_path: Path to the directory containing the images.
+ annotations_directory_path: Path to the directory containing the LabelMe
+ ``.json`` files.
+ force_masks: If ``True``, load masks for every image regardless of
+ whether it contains polygon shapes.
+
+ Returns:
+ A tuple of ``(classes, image_paths, annotations)``.
+
+ Raises:
+ ValueError: If an annotation's ``imagePath`` is missing, empty, or
+ resolves to ``..`` or ``.``; if two annotation files reference the
+ same image basename; or if a polygon mask is requested for a file
+ missing ``imageWidth`` / ``imageHeight``.
+
+ Examples:
+ ```python
+ from supervision.dataset.formats.labelme import load_labelme_annotations
+
+ classes, image_paths, annotations = load_labelme_annotations(
+ images_directory_path="",
+ annotations_directory_path="",
+ )
+
+ classes
+ # ['dog', 'person']
+ ```
+ """
+ annotation_paths = sorted(
+ str(path)
+ for path in list_files_with_extensions(
+ directory=annotations_directory_path, extensions=["json"]
+ )
+ )
+
+ # Two-pass design (collect all class labels first, then assign IDs) requires
+ # materialising all annotation dicts upfront.
+ entries: list[LabelMeDict] = [
+ read_json_file(file_path=annotation_path)
+ for annotation_path in annotation_paths
+ ]
+
+ classes = sorted(
+ {
+ shape.get("label")
+ for entry in entries
+ for shape in entry.get("shapes", [])
+ if shape.get("shape_type") in SUPPORTED_SHAPE_TYPES
+ }
+ - {None}
+ )
+ class_to_index = {class_name: index for index, class_name in enumerate(classes)}
+
+ image_paths: list[str] = []
+ annotations: dict[str, Detections] = {}
+ for entry in entries:
+ shapes = entry.get("shapes", [])
+ raw_image_path = entry.get("imagePath")
+ if not raw_image_path:
+ raise ValueError(
+ "A LabelMe annotation file is missing the required "
+ "'imagePath' field or it is empty."
+ )
+ # ponytail: basename-only, no symlink resolution โ images_directory_path
+ # is trusted; annotation-driven traversal is neutralised by .name.
+ # See createml._resolve_image_path for the full .resolve()+parents pattern.
+ image_name = Path(raw_image_path).name
+ if not image_name or image_name in ("..", "."):
+ raise ValueError(
+ f"LabelMe annotation has an invalid 'imagePath' {raw_image_path!r}."
+ )
+ image_path = str(Path(images_directory_path) / image_name)
+ if image_path in annotations:
+ raise ValueError(
+ f"Duplicate image basename {image_name!r} resolved from multiple "
+ "annotation files. All annotation files must reference unique "
+ "image basenames."
+ )
+ with_masks = force_masks or any(
+ shape.get("shape_type") == "polygon" for shape in shapes
+ )
+ if with_masks and not (entry.get("imageWidth") and entry.get("imageHeight")):
+ raise ValueError(
+ f"LabelMe annotation for {image_name!r} requires "
+ "'imageWidth' and 'imageHeight' to build masks, but they are "
+ "missing or zero."
+ )
+ resolution_wh = (
+ int(entry.get("imageWidth", 0)),
+ int(entry.get("imageHeight", 0)),
+ )
+ annotations[image_path] = labelme_shapes_to_detections(
+ shapes=shapes,
+ class_to_index=class_to_index,
+ resolution_wh=resolution_wh,
+ with_masks=with_masks,
+ )
+ image_paths.append(image_path)
+
+ return classes, image_paths, annotations
+
+
+def _build_shape(label: str, points: list[list[float]], shape_type: str) -> LabelMeDict:
+ return {
+ "label": label,
+ "points": points,
+ "group_id": None,
+ "description": "",
+ "shape_type": shape_type,
+ "flags": {},
+ }
+
+
+def detections_to_labelme_shapes(
+ detections: Detections, classes: list[str]
+) -> list[LabelMeDict]:
+ """Convert ``Detections`` into a list of LabelMe shape dicts.
+
+ Masked detections are exported as ``polygon`` shapes (one per connected
+ component); box-only detections โ and masked detections whose mask yields no
+ polygon contour (e.g. an empty or sub-pixel mask) โ are exported as
+ ``rectangle`` shapes, so no detection is silently dropped.
+
+ Args:
+ detections: The detections to export.
+ classes: List of class names indexed by ``class_id``.
+
+ Returns:
+ A list of LabelMe shape dicts ready to embed in a ``.json`` annotation.
+
+ Raises:
+ ValueError: If ``detections.class_id`` is ``None`` or if any
+ ``class_id`` value is out of range for ``classes``.
+ """
+ class_ids = detections.class_id
+ if class_ids is None:
+ raise ValueError(
+ "class_id is required for LabelMe export, but the provided "
+ "Detections has class_id=None."
+ )
+ masks = detections.mask
+ shapes: list[LabelMeDict] = []
+ for index in range(len(detections)):
+ class_index = int(class_ids[index])
+ if class_index < 0 or class_index >= len(classes):
+ raise ValueError(
+ f"class_id {class_index} at detection index {index} is out of "
+ f"range for classes list of length {len(classes)}."
+ )
+ label = classes[class_index]
+ if masks is not None:
+ mask_arr = np.asarray(masks[index], dtype=np.bool_)
+ polygons = mask_to_polygons(mask_arr)
+ else:
+ polygons = []
+ if polygons:
+ for polygon in polygons:
+ points = [[float(x), float(y)] for x, y in polygon]
+ shapes.append(_build_shape(label, points, "polygon"))
+ else:
+ x_min, y_min, x_max, y_max = (
+ float(value) for value in detections.xyxy[index]
+ )
+ points = [[x_min, y_min], [x_max, y_max]]
+ shapes.append(_build_shape(label, points, "rectangle"))
+ return shapes
+
+
+def save_labelme_annotations(
+ dataset: DetectionDataset,
+ annotations_directory_path: str,
+) -> None:
+ """Export a ``DetectionDataset`` to per-image LabelMe ``.json`` files.
+
+ Args:
+ dataset: The ``DetectionDataset`` to write.
+ annotations_directory_path: Directory where the LabelMe ``.json`` files
+ are written (created if it does not exist).
+
+ Raises:
+ ValueError: If two image paths map to the same output .json stem,
+ which would cause one annotation file to overwrite another.
+
+ Examples:
+ ```python
+ import supervision as sv
+ from supervision.dataset.formats.labelme import save_labelme_annotations
+
+ dataset = sv.DetectionDataset(classes=["dog"], images=[], annotations={})
+ save_labelme_annotations(
+ dataset=dataset,
+ annotations_directory_path="",
+ )
+ ```
+ """
+ check_no_basename_collisions(
+ image_paths=dataset.image_paths,
+ key=lambda image_path: f"{Path(image_path).stem}.json",
+ output_kind="LabelMe annotation",
+ )
+ Path(annotations_directory_path).mkdir(parents=True, exist_ok=True)
+ for image_path, image, detections in dataset:
+ image_height, image_width, _ = image.shape
+ labelme_dict: LabelMeDict = {
+ "version": _LABELME_EXPORT_VERSION,
+ "flags": {},
+ "shapes": detections_to_labelme_shapes(
+ detections=detections, classes=dataset.classes
+ ),
+ "imagePath": Path(image_path).name,
+ "imageData": None,
+ "imageHeight": int(image_height),
+ "imageWidth": int(image_width),
+ }
+ annotation_path = (
+ Path(annotations_directory_path) / f"{Path(image_path).stem}.json"
+ )
+ save_json_file(data=labelme_dict, file_path=str(annotation_path))
diff --git a/src/supervision/dataset/formats/pascal_voc.py b/src/supervision/dataset/formats/pascal_voc.py
new file mode 100644
index 0000000..771a97f
--- /dev/null
+++ b/src/supervision/dataset/formats/pascal_voc.py
@@ -0,0 +1,449 @@
+import os
+from pathlib import Path
+from typing import TYPE_CHECKING
+from xml.etree.ElementTree import Element, SubElement
+
+if TYPE_CHECKING:
+ from supervision.dataset.core import DetectionDataset
+
+import cv2
+import numpy as np
+import numpy.typing as npt
+from defusedxml.ElementTree import parse, tostring
+from defusedxml.minidom import parseString
+from tqdm.auto import tqdm
+
+from supervision.dataset.utils import (
+ approximate_mask_with_polygons,
+ check_no_basename_collisions,
+)
+from supervision.detection.core import Detections
+from supervision.detection.utils.converters import polygon_to_mask, polygon_to_xyxy
+from supervision.utils.file import list_files_with_extensions
+
+
+def object_to_pascal_voc(
+ xyxy: npt.NDArray[np.number],
+ name: str,
+ polygon: npt.NDArray[np.number] | None = None,
+) -> Element:
+ """Build a Pascal VOC ```` XML element for one detection.
+
+ Coordinates are converted to 1-indexed Pascal VOC convention before writing.
+ The input arrays are never mutated; new arrays are allocated for the offset.
+
+ Args:
+ xyxy: Bounding box in zero-indexed pixel coordinates ``[x1, y1, x2, y2]``.
+ Shape ``(4,)``.
+ name: Class label string written to the ```` child element.
+ polygon: Optional segmentation polygon in zero-indexed pixel coordinates.
+ Shape ``(N, 2)``.
+
+ Returns:
+ An XML ``Element`` rooted at ```` containing ````,
+ ````, and optionally ```` children.
+
+ Examples:
+ >>> import numpy as np
+ >>> from supervision.dataset.formats.pascal_voc import object_to_pascal_voc
+ >>> elem = object_to_pascal_voc(np.array([0, 0, 9, 9]), name="cat")
+ >>> elem.find("bndbox/xmin").text
+ '1'
+ >>> elem.find("bndbox/xmax").text
+ '10'
+ """
+ root = Element("object")
+
+ object_name = SubElement(root, "name")
+ object_name.text = name
+
+ # Pascal VOC coordinates are 1-indexed (https://github.com/roboflow/supervision/issues/144).
+ # Rebind to a new array instead of `+= 1`: `xyxy` is a view into the source
+ # `Detections.xyxy` (yielded by `Detections.__iter__`), so an in-place add
+ # would corrupt the caller's detections by +1 on every export.
+ xyxy = xyxy + 1
+
+ bndbox = SubElement(root, "bndbox")
+ xmin = SubElement(bndbox, "xmin")
+ xmin.text = str(int(xyxy[0]))
+ ymin = SubElement(bndbox, "ymin")
+ ymin.text = str(int(xyxy[1]))
+ xmax = SubElement(bndbox, "xmax")
+ xmax.text = str(int(xyxy[2]))
+ ymax = SubElement(bndbox, "ymax")
+ ymax.text = str(int(xyxy[3]))
+
+ if polygon is not None:
+ # 1-indexed, rebound to avoid mutating the caller's array (see above).
+ polygon = polygon + 1
+ object_polygon = SubElement(root, "polygon")
+ for index, point in enumerate(polygon, start=1):
+ x_coordinate, y_coordinate = point
+ x = SubElement(object_polygon, f"x{index}")
+ x.text = str(x_coordinate)
+ y = SubElement(object_polygon, f"y{index}")
+ y.text = str(y_coordinate)
+
+ return root
+
+
+def detections_to_pascal_voc(
+ detections: Detections,
+ classes: list[str],
+ filename: str,
+ image_shape: tuple[int, int, int],
+ min_image_area_percentage: float = 0.0,
+ max_image_area_percentage: float = 1.0,
+ approximation_percentage: float = 0.75,
+) -> str:
+ """
+ Converts Detections object to Pascal VOC XML format.
+
+ Args:
+ detections: A Detections object containing bounding boxes,
+ class ids, and other relevant information.
+ classes: A list of class names corresponding to the
+ class ids in the Detections object.
+ filename: The name of the image file associated with the detections.
+ image_shape: The shape of the image
+ file associated with the detections.
+ min_image_area_percentage: Minimum detection area
+ relative to area of image associated with it.
+ max_image_area_percentage: Maximum detection area
+ relative to area of image associated with it.
+ approximation_percentage: The percentage of
+ polygon points to be removed from the input polygon, in the range [0, 1).
+ Returns:
+ An XML string in Pascal VOC format representing the detections.
+
+ Note:
+ ``detections`` is never mutated by this function; the source ``xyxy``
+ array is unchanged after the call. The function is therefore safe to
+ call multiple times on the same ``Detections`` object.
+ """
+ height, width, depth = image_shape
+
+ # Create root element
+ annotation = Element("annotation")
+
+ # Add folder element
+ folder = SubElement(annotation, "folder")
+ folder.text = "VOC"
+
+ # Add filename element
+ file_name = SubElement(annotation, "filename")
+ file_name.text = filename
+
+ # Add source element
+ source = SubElement(annotation, "source")
+ database = SubElement(source, "database")
+ database.text = "roboflow.ai"
+
+ # Add size element
+ size = SubElement(annotation, "size")
+ w = SubElement(size, "width")
+ w.text = str(width)
+ h = SubElement(size, "height")
+ h.text = str(height)
+ d = SubElement(size, "depth")
+ d.text = str(depth)
+
+ # Add segmented element
+ segmented = SubElement(annotation, "segmented")
+ segmented.text = "0"
+
+ # Add object elements
+ for xyxy, mask, _, class_id, _, _ in detections:
+ if class_id is None:
+ raise ValueError("Detections must include class_id for Pascal VOC export.")
+ if not isinstance(class_id, (int, np.integer)):
+ raise ValueError(
+ f"Detections class_id must be an integer for Pascal VOC export, "
+ f"got {type(class_id)!r}."
+ )
+ name = classes[class_id]
+ if mask is not None:
+ polygons = approximate_mask_with_polygons(
+ mask=mask,
+ min_image_area_percentage=min_image_area_percentage,
+ max_image_area_percentage=max_image_area_percentage,
+ approximation_percentage=approximation_percentage,
+ )
+ for polygon in polygons:
+ xyxy = polygon_to_xyxy(polygon=polygon)
+ next_object = object_to_pascal_voc(
+ xyxy=xyxy, name=name, polygon=polygon
+ )
+ annotation.append(next_object)
+ else:
+ next_object = object_to_pascal_voc(xyxy=xyxy, name=name)
+ annotation.append(next_object)
+
+ # Generate XML string
+ xml_string = str(
+ parseString(tostring(annotation).decode("utf-8")).toprettyxml(indent=" ")
+ )
+ return xml_string
+
+
+def load_pascal_voc_annotations(
+ images_directory_path: str,
+ annotations_directory_path: str,
+ force_masks: bool = False,
+ show_progress: bool = False,
+) -> tuple[list[str], list[str], dict[str, Detections]]:
+ """
+ Load Pascal VOC XML annotations in sorted image-path order.
+
+ Args:
+ images_directory_path: The path to the directory containing the images.
+ annotations_directory_path: The path to the directory containing the
+ PASCAL VOC annotation files.
+ force_masks: If True, forces masks to be loaded for all
+ annotations, regardless of whether they are present.
+ show_progress: If True, display a progress bar during loading.
+
+ Returns:
+ A tuple with a list of class names, a sorted list of paths to images,
+ and a dictionary with image paths as keys and corresponding
+ Detections instances as values.
+ """
+
+ image_paths = sorted(
+ str(path)
+ for path in list_files_with_extensions(
+ directory=images_directory_path, extensions=["jpg", "jpeg", "png"]
+ )
+ )
+
+ classes: list[str] = []
+ annotations = {}
+
+ for image_path in tqdm(
+ image_paths,
+ total=len(image_paths),
+ desc="Loading Pascal VOC annotations",
+ disable=not show_progress,
+ ):
+ 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):
+ annotations[image_path] = Detections.empty()
+ continue
+
+ tree = parse(annotation_path)
+ root = tree.getroot()
+ if root is None:
+ raise ValueError(f"Failed to parse XML root from {annotation_path}")
+
+ image = cv2.imread(image_path)
+ if image is None:
+ raise ValueError(f"Could not read image from path: {image_path}")
+ resolution_wh = (image.shape[1], image.shape[0])
+ annotation, classes = detections_from_xml_obj(
+ root, classes, resolution_wh, force_masks
+ )
+ annotations[image_path] = annotation
+
+ return classes, image_paths, annotations
+
+
+def detections_from_xml_obj(
+ root: Element,
+ classes: list[str],
+ resolution_wh: tuple[int, int],
+ force_masks: bool = False,
+) -> tuple[Detections, list[str]]:
+ """
+ Converts an XML object in Pascal VOC format to a Detections object.
+ Expected XML format:
+
+ ...
+
+ dog
+
+ 48
+ 240
+ 195
+ 371
+
+
+ 48
+ 240
+ 195
+ 240
+ 195
+ 371
+ 48
+ 371
+
+
+
+
+ Args:
+ root: Parsed Pascal VOC ```` XML element.
+ classes: Existing class names used to assign stable class ids.
+ resolution_wh: Image resolution as ``(width, height)`` for mask
+ rasterization.
+ force_masks: If True, returns a mask array for every object even when
+ no ```` element is present.
+
+ Returns:
+ A tuple containing a Detections object and an
+ updated list of class names, extended with the class names
+ from the XML object.
+ """
+ xyxy: list[list[int]] = []
+ class_names: list[str] = []
+ masks: list[npt.NDArray[np.bool_]] = []
+ with_masks = force_masks or any(
+ _with_poly_mask(obj) for obj in root.findall("object")
+ )
+ extended_classes = classes[:]
+ for obj in root.findall("object"):
+ class_name = _get_required_text(obj, "name")
+ class_names.append(class_name)
+
+ bbox = obj.find("bndbox")
+ if bbox is None:
+ raise ValueError("Missing bndbox in Pascal VOC annotation.")
+ x1 = int(_get_required_text(bbox, "xmin"))
+ y1 = int(_get_required_text(bbox, "ymin"))
+ x2 = int(_get_required_text(bbox, "xmax"))
+ y2 = int(_get_required_text(bbox, "ymax"))
+
+ xyxy.append([x1, y1, x2, y2])
+
+ object_mask: npt.NDArray[np.bool_] = np.zeros(
+ (resolution_wh[1], resolution_wh[0]), dtype=bool
+ )
+ for polygon_element in obj.findall("polygon"):
+ polygon = parse_polygon_points(polygon_element)
+ # https://github.com/roboflow/supervision/issues/144
+ polygon -= 1
+
+ mask_from_polygon = polygon_to_mask(
+ polygon=polygon,
+ resolution_wh=resolution_wh,
+ )
+ object_mask |= mask_from_polygon.astype(bool)
+
+ if with_masks:
+ masks.append(object_mask)
+
+ xyxy_arr: npt.NDArray[np.float32]
+ if xyxy:
+ xyxy_arr = np.array(xyxy, dtype=np.float32)
+ else:
+ xyxy_arr = np.empty((0, 4), dtype=np.float32)
+
+ # https://github.com/roboflow/supervision/issues/144
+ xyxy_arr -= 1
+
+ for k in sorted(set(class_names)):
+ if k not in extended_classes:
+ extended_classes.append(k)
+ class_id = np.array(
+ [extended_classes.index(class_name) for class_name in class_names]
+ )
+
+ annotation = Detections(
+ xyxy=xyxy_arr,
+ mask=np.array(masks, dtype=bool) if with_masks else None,
+ class_id=class_id,
+ )
+
+ return annotation, extended_classes
+
+
+def _with_poly_mask(obj: Element) -> bool:
+ return obj.find("polygon") is not None
+
+
+def parse_polygon_points(polygon: Element) -> npt.NDArray[np.int_]:
+ coordinates: list[int] = []
+ for coord in polygon.findall(".//*"):
+ if coord.text is None:
+ raise ValueError("Missing polygon coordinate value in Pascal VOC.")
+ coordinates.append(int(coord.text))
+ return np.array(
+ [(coordinates[i], coordinates[i + 1]) for i in range(0, len(coordinates), 2)],
+ dtype=int,
+ )
+
+
+def _get_required_text(element: Element, tag: str) -> str:
+ child = element.find(tag)
+ if child is None or child.text is None:
+ raise ValueError(f"Missing '{tag}' in Pascal VOC annotation.")
+ return child.text
+
+
+def save_pascal_voc_annotations(
+ dataset: "DetectionDataset",
+ annotations_directory_path: str,
+ min_image_area_percentage: float = 0.0,
+ max_image_area_percentage: float = 1.0,
+ approximation_percentage: float = 0.75,
+ show_progress: bool = False,
+) -> None:
+ """Write Pascal VOC XML annotation files for every image in *dataset*.
+
+ Args:
+ dataset: Dataset whose annotations are saved.
+ annotations_directory_path: Destination directory for ``.xml`` files;
+ created automatically if it does not exist.
+ min_image_area_percentage: Minimum detection area as a fraction of the
+ image area. Detections below this threshold are omitted. Must be in
+ ``[0, 1]``. Default ``0.0`` keeps all detections.
+ max_image_area_percentage: Maximum detection area as a fraction of the
+ image area. Detections above this threshold are omitted. Must be in
+ ``[0, 1]``. Default ``1.0`` keeps all detections.
+ approximation_percentage: Fraction of polygon vertices to remove when
+ approximating instance masks as polygons. Range ``[0, 1)``. Default
+ ``0.75`` applies aggressive simplification.
+ show_progress: If ``True``, display a tqdm progress bar while writing
+ annotation files. Default ``False``.
+
+ Raises:
+ ValueError: If two image paths map to the same ``.xml`` output name.
+
+ Examples:
+ >>> import tempfile
+ >>> from supervision.dataset.core import DetectionDataset
+ >>> from supervision.dataset.formats.pascal_voc import (
+ ... save_pascal_voc_annotations,
+ ... )
+ >>> dataset = DetectionDataset(classes=[], images={}, annotations={})
+ >>> with tempfile.TemporaryDirectory() as tmpdir:
+ ... save_pascal_voc_annotations(dataset, tmpdir)
+ """
+
+ check_no_basename_collisions(
+ image_paths=dataset.image_paths,
+ key=lambda image_path: f"{Path(image_path).stem}.xml",
+ output_kind="Pascal VOC annotation",
+ )
+ Path(annotations_directory_path).mkdir(parents=True, exist_ok=True)
+ for image_path, image, annotations in tqdm(
+ dataset,
+ total=len(dataset),
+ desc="Saving Pascal VOC annotations",
+ disable=not show_progress,
+ ):
+ 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=annotations,
+ classes=dataset.classes,
+ filename=image_name,
+ image_shape=(image.shape[0], image.shape[1], image.shape[2]),
+ min_image_area_percentage=min_image_area_percentage,
+ max_image_area_percentage=max_image_area_percentage,
+ approximation_percentage=approximation_percentage,
+ )
+ with open(annotations_path, "w") as f:
+ f.write(pascal_voc_xml)
diff --git a/src/supervision/dataset/formats/yolo.py b/src/supervision/dataset/formats/yolo.py
new file mode 100644
index 0000000..476ebc7
--- /dev/null
+++ b/src/supervision/dataset/formats/yolo.py
@@ -0,0 +1,502 @@
+from __future__ import annotations
+
+import os
+import warnings
+from collections.abc import Sequence
+from pathlib import Path
+from typing import TYPE_CHECKING, Any, cast
+
+import numpy as np
+import numpy.typing as npt
+from PIL import Image
+from tqdm.auto import tqdm
+
+from supervision.config import ORIENTED_BOX_COORDINATES
+from supervision.dataset.utils import (
+ approximate_mask_with_polygons,
+ check_no_basename_collisions,
+)
+from supervision.detection.core import Detections
+from supervision.detection.utils._typing import _DetectionDataType
+from supervision.detection.utils.converters import polygon_to_mask, polygon_to_xyxy
+from supervision.utils.file import (
+ list_files_with_extensions,
+ read_txt_file,
+ read_yaml_file,
+ save_text_file,
+ save_yaml_file,
+)
+
+if TYPE_CHECKING:
+ from supervision.dataset.core import DetectionDataset
+
+
+def _parse_box(values: list[str]) -> npt.NDArray[np.float32]:
+ x_center, y_center, width, height = values
+ return np.array(
+ [
+ float(x_center) - float(width) / 2,
+ float(y_center) - float(height) / 2,
+ float(x_center) + float(width) / 2,
+ float(y_center) + float(height) / 2,
+ ],
+ dtype=np.float32,
+ )
+
+
+def _box_to_polygon(box: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
+ return np.array(
+ [[box[0], box[1]], [box[2], box[1]], [box[2], box[3]], [box[0], box[3]]],
+ dtype=np.float32,
+ )
+
+
+def _parse_polygon(values: list[str]) -> npt.NDArray[np.float32]:
+ return np.array(values, dtype=np.float32).reshape(-1, 2)
+
+
+def _polygons_to_masks(
+ polygons: Sequence[npt.NDArray[np.number]], resolution_wh: tuple[int, int]
+) -> npt.NDArray[np.bool_]:
+ return np.array(
+ [
+ polygon_to_mask(
+ polygon=np.round(polygon).astype(np.int32),
+ resolution_wh=resolution_wh,
+ )
+ for polygon in polygons
+ ],
+ dtype=bool,
+ )
+
+
+def _with_seg_mask(lines: list[str]) -> bool:
+ return any([len(line.split()) > 5 for line in lines])
+
+
+def _extract_class_names(file_path: str) -> list[str]:
+ """Return class names from a YOLO data.yaml file ordered by class index.
+
+ Supports list and dict forms of the ``names`` field. Dict keys that are
+ all int-like (plain ints or digit strings) are sorted numerically so
+ class index 10 follows index 9. All-non-numeric keys are sorted
+ lexicographically. Mixed numeric/non-numeric keys raise ``ValueError``.
+ Boolean YAML keys (``true``/``false``) are excluded from numeric sorting
+ because ``bool`` is a subclass of ``int`` in Python.
+
+ Args:
+ file_path: Path to the data.yaml file.
+
+ Returns:
+ Class names in class-index order.
+
+ Raises:
+ ValueError: If the YAML root is not a mapping, if ``names`` is
+ neither a list nor a dict, or if the dict has mixed key types.
+ """
+ data: dict[str, Any] = read_yaml_file(file_path=file_path)
+ if not isinstance(data, dict):
+ raise ValueError(
+ f"Expected mapping in data.yaml at '{file_path}',"
+ f" got {type(data).__name__}."
+ )
+ names = data.get("names")
+ if isinstance(names, dict):
+ keys = list(names.keys())
+
+ def _is_int_like(key: Any) -> bool:
+ # bool subclasses int; YAML `true`/`false` must not become class indices
+ if isinstance(key, bool):
+ return False
+ if isinstance(key, int):
+ return True
+ if isinstance(key, str):
+ stripped = key.strip()
+ return stripped.isdigit()
+ return False
+
+ int_like = [_is_int_like(k) for k in keys]
+ if any(int_like) and not all(int_like):
+ mixed_numeric = [k for k, il in zip(keys, int_like) if il][:3]
+ mixed_other = [k for k, il in zip(keys, int_like) if not il][:3]
+ raise ValueError(
+ f"Expected 'names' dict in data.yaml at '{file_path}' to have either "
+ f"all numeric or all non-numeric keys, got a mix: "
+ f"numeric {mixed_numeric} and non-numeric {mixed_other} keys."
+ )
+ if all(int_like):
+ sorted_keys = sorted(keys, key=lambda k: int(k))
+ else:
+ sorted_keys = sorted(keys, key=str)
+ return [str(names[key]) for key in sorted_keys]
+ if isinstance(names, list):
+ return [str(name) for name in names]
+ raise ValueError(
+ "Expected 'names' to be a list or dict in data.yaml at "
+ f"'{file_path}', got {type(names).__name__}."
+ )
+
+
+def _image_name_to_annotation_name(image_name: str) -> str:
+ base_name, _ = os.path.splitext(image_name)
+ return base_name + ".txt"
+
+
+def yolo_annotations_to_detections(
+ 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_list: list[int] = []
+ relative_xyxy_list: list[npt.NDArray[np.number]] = []
+ relative_polygon_list: list[npt.NDArray[np.float32]] = []
+ relative_xyxyxyxy_list: list[npt.NDArray[np.float32]] = []
+ w, h = resolution_wh
+ for line in lines:
+ values = line.split()
+ class_id_list.append(int(values[0]))
+ if len(values) == 5:
+ box = _parse_box(values=values[1:])
+ relative_xyxy_list.append(box)
+ if with_masks:
+ relative_polygon_list.append(_box_to_polygon(box=box))
+ elif len(values) > 5:
+ polygon = _parse_polygon(values=values[1:])
+ relative_xyxy_list.append(polygon_to_xyxy(polygon=polygon))
+ if is_obb:
+ relative_xyxyxyxy_list.append(np.array(values[1:], dtype=np.float32))
+ if with_masks:
+ relative_polygon_list.append(polygon)
+
+ class_id = np.array(class_id_list, dtype=int)
+ relative_xyxy = np.array(relative_xyxy_list, dtype=np.float32)
+ xyxy = relative_xyxy * np.array([w, h, w, h], dtype=np.float32)
+ data: _DetectionDataType = {}
+
+ if is_obb:
+ relative_xyxyxyxy = np.array(relative_xyxyxyxy_list, dtype=np.float32)
+ xyxyxyxy = relative_xyxyxyxy.reshape(-1, 4, 2)
+ xyxyxyxy *= np.array([w, h], dtype=np.float32)
+ data[ORIENTED_BOX_COORDINATES] = cast(npt.NDArray[np.generic], xyxyxyxy)
+
+ if not with_masks:
+ return Detections(class_id=class_id, xyxy=xyxy, data=data)
+
+ polygons = [
+ polygon * np.array(resolution_wh, dtype=np.float32)
+ for polygon in relative_polygon_list
+ ]
+ mask = _polygons_to_masks(polygons=polygons, resolution_wh=resolution_wh)
+ return Detections(class_id=class_id, xyxy=xyxy, data=data, mask=mask)
+
+
+def load_yolo_annotations(
+ images_directory_path: str,
+ annotations_directory_path: str,
+ data_yaml_path: str,
+ force_masks: bool = False,
+ is_obb: bool = False,
+ show_progress: bool = False,
+) -> tuple[list[str], list[str], dict[str, Detections]]:
+ """
+ Loads YOLO annotations and returns class names, images,
+ and their corresponding detections.
+
+ Args:
+ images_directory_path: The path to the directory containing the images.
+ annotations_directory_path: The path to the directory
+ containing the YOLO annotation files.
+ data_yaml_path: The path to the data
+ YAML file containing class information.
+ force_masks: If True, forces masks to be loaded
+ for all annotations, regardless of whether they are present.
+ This parameter has no effect when `is_obb=True`; mask generation
+ is always disabled for OBB annotations.
+ is_obb: 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.
+ show_progress: If True, display a progress bar during loading.
+
+ Returns:
+ 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.
+ """
+ if is_obb and force_masks:
+ warnings.warn(
+ "`force_masks=True` has no effect when `is_obb=True`; "
+ "mask generation is always disabled for OBB annotations.",
+ UserWarning,
+ stacklevel=2,
+ )
+ image_paths = [
+ str(path)
+ for path in list_files_with_extensions(
+ directory=images_directory_path,
+ extensions=[
+ "bmp",
+ "dng",
+ "jpg",
+ "jpeg",
+ "mpo",
+ "png",
+ "tif",
+ "tiff",
+ "webp",
+ ],
+ )
+ ]
+
+ classes = _extract_class_names(file_path=data_yaml_path)
+ annotations = {}
+
+ for image_path in tqdm(
+ image_paths,
+ total=len(image_paths),
+ desc="Loading YOLO annotations",
+ disable=not show_progress,
+ ):
+ image_stem = Path(image_path).stem
+ annotation_path = os.path.join(annotations_directory_path, f"{image_stem}.txt")
+ if not os.path.exists(annotation_path):
+ annotations[image_path] = Detections.empty()
+ continue
+
+ # PIL is much faster than cv2 for checking image shape: https://github.com/roboflow/supervision/issues/1554
+ with Image.open(image_path) as image:
+ w, h = image.size
+ lines = read_txt_file(file_path=annotation_path, skip_empty=True)
+ resolution_wh = (w, h)
+
+ with_masks = not is_obb and (force_masks or _with_seg_mask(lines=lines))
+ annotation = yolo_annotations_to_detections(
+ lines=lines,
+ resolution_wh=resolution_wh,
+ with_masks=with_masks,
+ is_obb=is_obb,
+ )
+ annotations[image_path] = annotation
+ return classes, image_paths, annotations
+
+
+def object_to_yolo(
+ xyxy: npt.NDArray[np.number],
+ class_id: int,
+ image_shape: tuple[int, int, int],
+ polygon: npt.NDArray[np.number] | None = None,
+) -> str:
+ h, w, _ = image_shape
+ if polygon is None:
+ xyxy_relative = xyxy / np.array([w, h, w, h], dtype=np.float32)
+ x_min, y_min, x_max, y_max = xyxy_relative
+ x_center = (x_min + x_max) / 2
+ y_center = (y_min + y_max) / 2
+ width = x_max - x_min
+ height = y_max - y_min
+ return f"{int(class_id)} {x_center:.5f} {y_center:.5f} {width:.5f} {height:.5f}"
+ else:
+ polygon_relative = polygon / np.array([w, h], dtype=np.float32)
+ polygon_relative = polygon_relative.reshape(-1)
+ polygon_parsed = " ".join([f"{value:.5f}" for value in polygon_relative])
+ return f"{int(class_id)} {polygon_parsed}"
+
+
+def detections_to_yolo_annotations(
+ detections: Detections,
+ image_shape: tuple[int, int, int],
+ min_image_area_percentage: float = 0.0,
+ max_image_area_percentage: float = 1.0,
+ approximation_percentage: float = 0.75,
+ is_obb: bool = False,
+) -> list[str]:
+ """Convert detections to YOLO annotation lines.
+
+ Args:
+ detections: The detections to serialize. Each detection must have a
+ valid integer ``class_id``. When ``is_obb=True``, each non-empty
+ detection must also carry ``detections.data['xyxyxyxy']`` with
+ shape ``(N, 4, 2)``.
+ image_shape: The ``(height, width, channels)`` shape of the source
+ image, used to normalize coordinates to ``[0, 1]``.
+ min_image_area_percentage: Minimum detection area as a fraction of the
+ image area; smaller detections are omitted. Ignored when
+ ``is_obb=True``.
+ max_image_area_percentage: Maximum detection area as a fraction of the
+ image area; larger detections are omitted. Ignored when
+ ``is_obb=True``.
+ approximation_percentage: Fraction of polygon points removed during
+ contour approximation when saving mask annotations. Ignored when
+ ``is_obb=True``.
+ is_obb: If ``True``, serializes oriented bounding-box corners from
+ ``detections.data['xyxyxyxy']`` as a 9-token YOLO OBB line
+ ``class_id x1 y1 x2 y2 x3 y3 x4 y4``. Mask data is ignored.
+
+ Returns:
+ A list of YOLO annotation strings, one per detection (or one per
+ polygon for instance-segmentation annotations).
+
+ Raises:
+ ValueError: If any detection has ``class_id=None`` or a non-integer
+ ``class_id``.
+ ValueError: If ``is_obb=True`` and any non-empty detection is missing
+ ``'xyxyxyxy'`` in ``detections.data``.
+
+ Examples:
+ >>> import numpy as np
+ >>> from supervision.detection.core import Detections
+ >>> from supervision.dataset.formats.yolo import detections_to_yolo_annotations
+ >>> detections = Detections(
+ ... xyxy=np.array([[10, 10, 90, 90]], dtype=np.float32),
+ ... class_id=np.array([0]),
+ ... )
+ >>> detections_to_yolo_annotations(detections, image_shape=(100, 100, 3))
+ ['0 0.50000 0.50000 0.80000 0.80000']
+ """
+ if (
+ is_obb
+ and len(detections) > 0
+ and ORIENTED_BOX_COORDINATES not in detections.data
+ ):
+ raise ValueError(
+ f"`is_obb=True` requires `'{ORIENTED_BOX_COORDINATES}'` in "
+ "`detections.data` with shape (N, 4, 2). Load OBB datasets via "
+ "`DetectionDataset.from_yolo(..., is_obb=True)` or set "
+ f"`detections.data['{ORIENTED_BOX_COORDINATES}']` "
+ "(shape (N, 4, 2)) before exporting."
+ )
+
+ if is_obb and detections.mask is not None:
+ warnings.warn(
+ "`detections.mask` is ignored when `is_obb=True`; "
+ "OBB annotations use corner coordinates from "
+ f"`detections.data['{ORIENTED_BOX_COORDINATES}']`.",
+ UserWarning,
+ stacklevel=2,
+ )
+
+ annotation: list[str] = []
+ for xyxy, mask, _, class_id, _, data in detections:
+ if class_id is None:
+ raise ValueError("Class ID is required for YOLO annotations.")
+ if not isinstance(class_id, (int, np.integer)):
+ raise ValueError(
+ f"Detections class_id must be an integer for YOLO export, "
+ f"got {type(class_id)!r}."
+ )
+ class_id_int = int(class_id)
+
+ if is_obb:
+ corners = np.asarray(data[ORIENTED_BOX_COORDINATES], dtype=np.float32)
+ if corners.shape != (4, 2):
+ raise ValueError(
+ f"OBB data for each detection must have shape (4, 2), "
+ f"got {corners.shape}. Ensure "
+ f"`detections.data['{ORIENTED_BOX_COORDINATES}']` has "
+ "shape (N, 4, 2) before exporting."
+ )
+ next_object = object_to_yolo(
+ xyxy=xyxy,
+ class_id=class_id_int,
+ image_shape=image_shape,
+ polygon=corners,
+ )
+ annotation.append(next_object)
+ continue
+
+ if mask is not None:
+ polygons = approximate_mask_with_polygons(
+ mask=mask,
+ min_image_area_percentage=min_image_area_percentage,
+ max_image_area_percentage=max_image_area_percentage,
+ approximation_percentage=approximation_percentage,
+ )
+ for polygon in polygons:
+ xyxy = polygon_to_xyxy(polygon=polygon)
+ next_object = object_to_yolo(
+ xyxy=xyxy,
+ class_id=class_id_int,
+ image_shape=image_shape,
+ polygon=polygon,
+ )
+ annotation.append(next_object)
+ else:
+ next_object = object_to_yolo(
+ xyxy=xyxy, class_id=class_id_int, image_shape=image_shape
+ )
+ annotation.append(next_object)
+ return annotation
+
+
+def save_yolo_annotations(
+ dataset: DetectionDataset,
+ annotations_directory_path: str,
+ min_image_area_percentage: float = 0.0,
+ max_image_area_percentage: float = 1.0,
+ approximation_percentage: float = 0.75,
+ is_obb: bool = False,
+ show_progress: bool = False,
+) -> None:
+ """Save dataset annotations in YOLO format.
+
+ Args:
+ dataset: The dataset whose annotations are saved.
+ annotations_directory_path: Path to the directory where annotation
+ ``.txt`` files are written; created automatically if absent.
+ min_image_area_percentage: Minimum detection area as a fraction of the
+ image area; smaller detections are omitted. Ignored when
+ ``is_obb=True``.
+ max_image_area_percentage: Maximum detection area as a fraction of the
+ image area; larger detections are omitted. Ignored when
+ ``is_obb=True``.
+ approximation_percentage: Fraction of polygon points removed during
+ contour approximation when saving mask annotations. Ignored when
+ ``is_obb=True``.
+ is_obb: If ``True``, writes oriented bounding-box annotations using
+ the 9-token format ``class_id x1 y1 x2 y2 x3 y3 x4 y4``. Each
+ non-empty detection must carry ``detections.data['xyxyxyxy']``
+ with shape ``(N, 4, 2)``.
+ show_progress: If ``True``, display a tqdm progress bar while
+ saving annotations.
+
+ Examples:
+ >>> from supervision.dataset.core import DetectionDataset
+ >>> from supervision.dataset.formats.yolo import save_yolo_annotations
+ >>> dataset = DetectionDataset(classes=["cat"], images={}, annotations={})
+ >>> save_yolo_annotations(dataset, "/tmp/labels")
+ """
+ check_no_basename_collisions(
+ image_paths=dataset.image_paths,
+ key=lambda image_path: _image_name_to_annotation_name(Path(image_path).name),
+ output_kind="YOLO annotation",
+ )
+ Path(annotations_directory_path).mkdir(parents=True, exist_ok=True)
+ for image_path, image, annotation in tqdm(
+ dataset,
+ total=len(dataset),
+ desc="Saving YOLO annotations",
+ disable=not show_progress,
+ ):
+ 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=annotation,
+ image_shape=image.shape,
+ min_image_area_percentage=min_image_area_percentage,
+ max_image_area_percentage=max_image_area_percentage,
+ approximation_percentage=approximation_percentage,
+ is_obb=is_obb,
+ )
+ save_text_file(lines=lines, file_path=yolo_annotations_path)
+
+
+def save_data_yaml(data_yaml_path: str, classes: list[str]) -> None:
+ data = {"nc": len(classes), "names": classes}
+ Path(data_yaml_path).parent.mkdir(parents=True, exist_ok=True)
+ save_yaml_file(data=data, file_path=data_yaml_path)
diff --git a/src/supervision/dataset/utils.py b/src/supervision/dataset/utils.py
new file mode 100644
index 0000000..45067de
--- /dev/null
+++ b/src/supervision/dataset/utils.py
@@ -0,0 +1,265 @@
+from __future__ import annotations
+
+__all__ = ["check_no_basename_collisions", "train_test_split"]
+
+import copy
+import os
+import random
+import shutil
+from collections.abc import Callable
+from pathlib import Path
+from typing import TYPE_CHECKING, TypeVar, cast
+
+import cv2
+import numpy as np
+import numpy.typing as npt
+from deprecate import deprecated, void # type: ignore[import-untyped,unused-ignore]
+from tqdm.auto import tqdm
+
+from supervision.detection.core import Detections
+from supervision.detection.utils.converters import mask_to_polygons
+from supervision.detection.utils.converters import (
+ mask_to_rle as _mask_to_rle,
+)
+from supervision.detection.utils.converters import (
+ rle_to_mask as _rle_to_mask,
+)
+from supervision.detection.utils.polygons import (
+ approximate_polygon,
+ filter_polygons_by_area,
+)
+
+
+@deprecated(target=_mask_to_rle, deprecated_in="0.28.0", remove_in="0.31.0") # type: ignore[untyped-decorator]
+def mask_to_rle(
+ mask: npt.NDArray[np.bool_], compressed: bool = False
+) -> list[int] | str:
+ """Deprecated since 0.28.0.
+
+ Use `supervision.detection.utils.converters.mask_to_rle`.
+ """
+ return cast(list[int] | str, void(mask, compressed))
+
+
+@deprecated(target=_rle_to_mask, deprecated_in="0.28.0", remove_in="0.31.0") # type: ignore[untyped-decorator]
+def rle_to_mask(
+ rle: npt.NDArray[np.integer] | list[int] | str | bytes,
+ resolution_wh: tuple[int, int],
+) -> npt.NDArray[np.bool_]:
+ """Deprecated since 0.28.0.
+
+ Use `supervision.detection.utils.converters.rle_to_mask`.
+ """
+ return cast(npt.NDArray[np.bool_], void(rle, resolution_wh))
+
+
+if TYPE_CHECKING:
+ from supervision.dataset.core import DetectionDataset
+
+T = TypeVar("T")
+
+
+def approximate_mask_with_polygons(
+ mask: npt.NDArray[np.bool_],
+ min_image_area_percentage: float = 0.0,
+ max_image_area_percentage: float = 1.0,
+ approximation_percentage: float = 0.0,
+) -> list[npt.NDArray[np.number]]:
+ """Filter mask polygons by area and optionally simplify them.
+
+ The default `approximation_percentage=0.0` preserves the original contour
+ unless callers explicitly ask for simplification.
+ """
+ height, width = mask.shape
+ image_area = height * width
+ minimum_detection_area = min_image_area_percentage * image_area
+ maximum_detection_area = max_image_area_percentage * image_area
+
+ polygons = cast(list[npt.NDArray[np.number]], mask_to_polygons(mask=mask))
+ if len(polygons) == 1:
+ polygons = filter_polygons_by_area(
+ polygons=polygons, min_area=None, max_area=maximum_detection_area
+ )
+ else:
+ polygons = filter_polygons_by_area(
+ polygons=polygons,
+ min_area=minimum_detection_area,
+ max_area=maximum_detection_area,
+ )
+ return [
+ approximate_polygon(polygon=polygon, percentage=approximation_percentage)
+ for polygon in polygons
+ ]
+
+
+def merge_class_lists(class_lists: list[list[str]]) -> list[str]:
+ unique_classes = set()
+
+ for class_list in class_lists:
+ for class_name in class_list:
+ unique_classes.add(class_name)
+
+ return sorted(list(unique_classes))
+
+
+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):
+ if class_name not in target_classes:
+ raise ValueError(
+ f"Class {class_name} not found in target classes. "
+ "source_classes must be a subset of target_classes."
+ )
+ corresponding_index = target_classes.index(class_name)
+ index_mapping[i] = corresponding_index
+
+ return index_mapping
+
+
+def map_detections_class_id(
+ source_to_target_mapping: dict[int, int], detections: Detections
+) -> Detections:
+ if detections.class_id is None:
+ raise ValueError("Detections must have class_id attribute.")
+ if set(np.unique(detections.class_id)) - set(source_to_target_mapping.keys()):
+ raise ValueError(
+ "Detections class_id must be a subset of source_to_target_mapping keys."
+ )
+
+ detections_copy = copy.deepcopy(detections)
+
+ if len(detections) > 0:
+ detections_copy.class_id = np.vectorize(source_to_target_mapping.get)(
+ detections_copy.class_id
+ )
+
+ return detections_copy
+
+
+def check_no_basename_collisions(
+ image_paths: list[str],
+ key: Callable[[str], str],
+ output_kind: str,
+) -> None:
+ """Raise if two image paths would be written to the same output file.
+
+ Dataset image paths may share a basename when they originate from different
+ directories (a legal, common state after :meth:`DetectionDataset.merge`).
+ Exporting them into a single flat output directory keyed on the basename or
+ stem would silently overwrite one file with another and mispair images with
+ their annotations. This guard detects such collisions before any file is
+ written and names the colliding source paths.
+
+ Args:
+ image_paths: The dataset image paths about to be written.
+ key: Maps an image path to the output file name it would be written to.
+ output_kind: Human-readable description of the output (e.g. ``"image"``
+ or ``"YOLO annotation"``) used in the error message.
+
+ Raises:
+ ValueError: If two image paths map to the same output file name.
+
+ Examples:
+ >>> from pathlib import Path
+ >>> from supervision.dataset.utils import check_no_basename_collisions
+ >>> check_no_basename_collisions(
+ ... ["a/img.jpg", "b/img.jpg"], lambda p: Path(p).name, "image"
+ ... )
+ Traceback (most recent call last):
+ ...
+ ValueError: Cannot export dataset: image paths 'a/img.jpg' and ...
+ """
+ seen: dict[str, tuple[str, str]] = {} # casefold(key) โ (original name, image_path)
+ for image_path in image_paths:
+ output_name = key(image_path)
+ case_key = output_name.casefold()
+ if case_key in seen:
+ first_name, first_path = seen[case_key]
+ raise ValueError(
+ f"Cannot export dataset: image paths {first_path!r} and "
+ f"{image_path!r} both map to {output_kind} file {first_name!r}. "
+ "Ensure all image basenames are unique before exporting."
+ )
+ seen[case_key] = (output_name, image_path)
+
+
+def save_dataset_images(
+ dataset: DetectionDataset,
+ images_directory_path: str,
+ show_progress: bool = False,
+) -> None:
+ """Save all images from a dataset to a directory.
+
+ Images already in memory are written with ``cv2.imwrite``; images stored
+ only as file paths are copied with ``shutil.copyfile``.
+
+ Args:
+ dataset: The dataset whose images are saved.
+ images_directory_path: Destination directory path; created
+ automatically if it does not exist.
+ show_progress: If ``True``, display a tqdm progress bar while
+ saving images.
+
+ Examples:
+ >>> from supervision.dataset.core import DetectionDataset
+ >>> from supervision.dataset.utils import save_dataset_images
+ >>> dataset = DetectionDataset(classes=["cat"], images={}, annotations={})
+ >>> save_dataset_images(dataset, "/tmp/images")
+ """
+ check_no_basename_collisions(
+ image_paths=dataset.image_paths,
+ key=lambda image_path: Path(image_path).name,
+ output_kind="image",
+ )
+ Path(images_directory_path).mkdir(parents=True, exist_ok=True)
+ for image_path in tqdm(
+ dataset.image_paths,
+ desc="Saving images",
+ disable=not show_progress,
+ ):
+ 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(
+ data: list[T],
+ train_ratio: float = 0.8,
+ random_state: int | None = None,
+ shuffle: bool = True,
+) -> tuple[list[T], list[T]]:
+ """
+ Splits the data into two parts using the provided train_ratio.
+
+ Args:
+ data: The data to split.
+ train_ratio: The ratio of the training set to the entire dataset.
+ random_state: The seed for the random number generator.
+ shuffle: Whether to shuffle the data before splitting.
+
+ Returns:
+ The split data. The input list is copied and never mutated.
+
+ Examples:
+ >>> train, test = train_test_split(
+ ... [1, 2, 3, 4, 5], train_ratio=0.6, random_state=0
+ ... )
+ >>> len(train), len(test)
+ (3, 2)
+ """
+ rng = random.Random(random_state) # noqa: S311 โ dataset split, not cryptographic
+ if shuffle:
+ data = list(data)
+ rng.shuffle(data)
+ else:
+ data = list(data) # copy to guarantee non-mutation
+
+ split_index = int(len(data) * train_ratio)
+ return data[:split_index], data[split_index:]
diff --git a/src/supervision/detection/__init__.py b/src/supervision/detection/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/supervision/detection/compact_mask.py b/src/supervision/detection/compact_mask.py
new file mode 100644
index 0000000..e425eab
--- /dev/null
+++ b/src/supervision/detection/compact_mask.py
@@ -0,0 +1,1706 @@
+"""Crop-RLE compact mask storage for memory-efficient instance segmentation.
+
+Dense ``(N, H, W)`` boolean masks use O(NยทHยทW) memory, which becomes
+prohibitive for aerial imagery (e.g. 1000 objects x 4K image ~ 8.3 GB).
+:class:`CompactMask` stores each mask as a run-length encoding of its
+bounding-box crop, reducing typical usage to tens of MB.
+
+The bounding boxes (``xyxy``) already present in ``Detections`` serve as the
+crop boundaries, so no extra metadata is required from the caller.
+
+CompactMask reduces memory footprint but does not improve computational
+speed. The ingestion path (base48 decode, column split, RLE trim) is
+Python-level and is typically slower than the dense NumPy path. The primary
+benefit is memory savings for large images with many sparse masks.
+"""
+
+from __future__ import annotations
+
+import os
+from collections.abc import Iterator, Mapping, Sequence
+from typing import Any, cast, overload
+
+import numpy as np
+import numpy.typing as npt
+
+# _base48_decode and _delta_decode are private to the COCO RLE codec. They
+# live in converters.py and are shared by public conversion helpers here and
+# in that module.
+from supervision.detection.utils.converters import (
+ _base48_decode,
+ _delta_decode,
+ _mask_to_rle_counts,
+ _rle_counts_to_mask,
+)
+
+
+def _rle_area(rle: npt.NDArray[np.int32]) -> int:
+ """Return the number of ``True`` pixels in a run-length encoded mask.
+
+ Args:
+ rle: int32 array of run lengths as produced by :func:`_mask_to_rle_counts`.
+
+ Returns:
+ Total number of ``True`` pixels.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import _rle_area
+ >>> rle = np.array([1, 2, 1, 1, 1], dtype=np.int32)
+ >>> _rle_area(rle)
+ 3
+
+ ```
+ """
+ return int(np.sum(rle[1::2]))
+
+
+def _rle_split_cols(
+ rle: npt.NDArray[np.int32],
+ crop_h: int,
+ crop_w: int,
+ x_start: int = 0,
+ x_stop: int | None = None,
+) -> list[list[int]]:
+ """Split a flat F-order RLE into per-column run lists.
+
+ With F-order (column-major) RLE the flat pixel sequence visits all rows
+ of column 0, then all rows of column 1, etc. Each column therefore
+ contains ``crop_h`` contiguous pixels.
+
+ Runs that cross column boundaries are split at the boundary. Each
+ returned list starts with a ``False``-run count (possibly 0), matching
+ the convention of :func:`_mask_to_rle_counts`.
+
+ When ``x_start`` and ``x_stop`` are provided, only columns in the closed
+ range ``[x_start, x_stop]`` are collected. Pixels in skipped columns
+ are consumed without being stored, which avoids O(W) allocation when
+ only a small crop of a wide image is needed.
+
+ Note:
+ ``x_start`` uses ``np.cumsum`` + ``np.searchsorted`` to jump directly
+ to the first relevant run in O(log R) time, avoiding the O(pixel_prefix)
+ walk that previously made right-edge crops on wide images expensive.
+
+ Args:
+ rle: int32 run-length array as produced by
+ :func:`~supervision.detection.utils.converters._mask_to_rle_counts`.
+ crop_h: Number of rows (pixels per column).
+ crop_w: Number of columns.
+ x_start: First column to collect (0-indexed, inclusive). Columns
+ before ``x_start`` are skipped. Defaults to ``0``.
+ x_stop: Last column to collect (0-indexed, inclusive). Processing
+ stops after column ``x_stop`` is complete. Defaults to
+ ``crop_w - 1`` (collect all columns).
+
+ Returns:
+ List of ``x_stop - x_start + 1`` run lists. Index ``i`` in the
+ returned list corresponds to column ``x_start + i``. Each list
+ sums to ``crop_h``.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import _rle_split_cols
+ >>> from supervision.detection.utils.converters import _mask_to_rle_counts
+ >>> mask = np.array([[True, False], [True, True]], dtype=bool)
+ >>> rle = _mask_to_rle_counts(mask)
+ >>> rle.tolist()
+ [0, 2, 1, 1]
+ >>> _rle_split_cols(rle, 2, 2)
+ [[0, 2], [1, 1]]
+ >>> _rle_split_cols(rle, 2, 2, x_start=1)
+ [[1, 1]]
+
+ ```
+ """
+ if x_stop is None:
+ x_stop = crop_w - 1
+
+ # Convert numpy int32 array to Python ints to avoid scalar boxing overhead
+ # in the inner loop (np.int32 boxing/unboxing slows pure-Python loops).
+ rle_list: list[int] = rle.tolist()
+
+ n_cols = x_stop - x_start + 1
+ per_col: list[list[int]] = [[] for _ in range(n_cols)]
+
+ # Fast-forward to the first run that overlaps column x_start using O(log R)
+ # searchsorted instead of an O(pixel_prefix) sequential walk.
+ start_pixel = x_start * crop_h
+ if start_pixel > 0 and len(rle_list) > 0:
+ cumsum_ends = np.cumsum(rle, dtype=np.int64)
+ first_run = int(np.searchsorted(cumsum_ends, start_pixel, side="right"))
+ if first_run >= len(rle_list):
+ for c in range(n_cols):
+ per_col[c] = [crop_h]
+ return per_col
+ prefix = int(cumsum_ends[first_run - 1]) if first_run > 0 else 0
+ else:
+ first_run = 0
+ prefix = 0
+
+ col = prefix // crop_h
+ row = prefix % crop_h
+
+ for run_idx in range(first_run, len(rle_list)):
+ run_len = rle_list[run_idx]
+ is_true = run_idx % 2 == 1
+ remaining = run_len
+ while remaining > 0:
+ # Past the requested range โ stop early.
+ if col > x_stop:
+ remaining = 0
+ break
+ space_in_col = crop_h - row
+ take = min(remaining, space_in_col)
+ if col >= x_start:
+ local_col = col - x_start
+ if len(per_col[local_col]) == 0:
+ if is_true:
+ per_col[local_col].append(0) # leading False count = 0
+ # Check if last run has same parity (True/False) as current chunk.
+ # Last element's parity: index (len-1) odd โ True, even โ False.
+ elif is_true == ((len(per_col[local_col]) - 1) % 2 == 1):
+ per_col[local_col][-1] += take
+ remaining -= take
+ row += take
+ if row >= crop_h:
+ row = 0
+ col += 1
+ continue
+ per_col[local_col].append(take)
+ remaining -= take
+ row += take
+ if row >= crop_h:
+ row = 0
+ col += 1
+ if col > x_stop:
+ break
+
+ # Fill any empty columns (all-False).
+ for c in range(n_cols):
+ if not per_col[c]:
+ per_col[c] = [crop_h]
+
+ return per_col
+
+
+def _rle_scale_col(
+ col_runs: list[int],
+ src_h: int,
+ row_map: npt.NDArray[np.int32],
+) -> list[int]:
+ """Scale one column's run list to a new height using a precomputed row map.
+
+ Each output row is mapped to a source row via ``row_map``, which
+ implements nearest-neighbour resampling in the vertical direction.
+
+ Args:
+ col_runs: Per-column run list starting with a ``False``-run count.
+ src_h: Height of the source column (sum of ``col_runs``).
+ row_map: int32 array of length ``new_crop_h``; ``row_map[r']`` is the
+ source row index for output row ``r'``. Use
+ ``(np.arange(new_crop_h) * src_h // new_crop_h)`` for
+ ``cv2.INTER_NEAREST``-compatible mapping.
+
+ Returns:
+ Scaled run list of total length ``len(row_map)``, always starting
+ with a ``False``-run count.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import _rle_scale_col
+ >>> col_runs = [0, 2, 2] # F=0, T=2, F=2 โ [T, T, F, F]
+ >>> row_map = np.array([0, 1, 2, 3, 0, 1, 2, 3], dtype=np.int32)
+ >>> _rle_scale_col(col_runs, 4, row_map)
+ [0, 2, 2, 2, 2]
+
+ ```
+ """
+ new_crop_h = len(row_map)
+ if new_crop_h == 0:
+ return [0]
+
+ # Reconstruct per-source-row boolean values from run list.
+ src_values: npt.NDArray[np.bool_] = np.empty(src_h, dtype=np.bool_)
+ pos = 0
+ for ri, rl in enumerate(col_runs):
+ src_values[pos : pos + rl] = ri % 2 == 1 # odd index โ True
+ pos += rl
+ if pos < src_h:
+ src_values[pos:] = False # pad truncated RLE
+
+ # Map output rows to source values.
+ out_values = src_values[row_map]
+
+ # RLE-encode the output column; vectorised via np.diff on bool view.
+ out_uint8 = out_values.view(np.uint8)
+ boundaries = np.flatnonzero(np.diff(out_uint8))
+ run_starts: npt.NDArray[np.int64] = np.empty(len(boundaries) + 1, dtype=np.int64)
+ run_ends: npt.NDArray[np.int64] = np.empty(len(boundaries) + 1, dtype=np.int64)
+ run_starts[0] = 0
+ run_starts[1:] = boundaries + 1
+ run_ends[:-1] = boundaries + 1
+ run_ends[-1] = new_crop_h
+ result_runs: list[int] = (run_ends - run_starts).tolist()
+ # RLE starts with a False count; prepend 0 if output begins with True.
+ if bool(out_values[0]):
+ result_runs.insert(0, 0)
+ return result_runs
+
+
+def _rle_join_cols(
+ scaled_cols: list[list[int]],
+ new_total: int,
+) -> npt.NDArray[np.int32]:
+ """Concatenate per-column run lists into a flat RLE, merging junctions.
+
+ Each column run list starts with a ``False``-run count. Two junction types
+ can be merged across column boundaries:
+
+ * ``False``/``False``: the trailing False run merges with the leading False
+ run of the next column (leading count may be zero).
+ * ``True``/``True``: when the accumulated output ends on a True run and the
+ next column's leading False count is zero (column starts with True), the
+ two True runs are merged to avoid inserting a zero-length False run that
+ would inflate ``len(rle)`` and skew the density metric in
+ :func:`_resize_crop`.
+
+ Args:
+ scaled_cols: List of per-column run lists, each starting with a
+ ``False``-run count.
+ new_total: Total pixel count of the output (fallback for empty input).
+
+ Returns:
+ Flat int32 RLE array starting with a ``False``-run count.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import _rle_join_cols
+ >>> cols = [[1, 2], [1, 2]] # each col: F=1, T=2
+ >>> _rle_join_cols(cols, 6).tolist()
+ [1, 2, 1, 2]
+
+ ```
+ """
+ output_runs: list[int] = []
+ for col_runs in scaled_cols:
+ if not output_runs:
+ output_runs.extend(col_runs)
+ else:
+ last_is_true = (len(output_runs) - 1) % 2 == 1
+ # col_runs always starts with a False count โ first_is_true=False
+ if not last_is_true: # last == False == first โ merge
+ output_runs[-1] += col_runs[0]
+ output_runs.extend(col_runs[1:])
+ elif col_runs[0] == 0 and len(col_runs) > 1:
+ # last run = True; column also starts True (leading False = 0)
+ # โ merge to avoid a zero-length False run at the junction.
+ output_runs[-1] += col_runs[1]
+ output_runs.extend(col_runs[2:])
+ else:
+ output_runs.extend(col_runs)
+
+ return np.array(output_runs if output_runs else [new_total], dtype=np.int32)
+
+
+def _rle_trim_col_runs(col_runs: Sequence[int], y1: int, y2: int) -> list[int]:
+ """Restrict one full-height column RLE to inclusive rows ``[y1, y2]``.
+
+ Args:
+ col_runs: Run lengths for one full-height column, starting with a
+ ``False`` run.
+ y1: Inclusive top row of the crop.
+ y2: Inclusive bottom row of the crop.
+
+ Returns:
+ Run lengths for the cropped column, also starting with a ``False`` run.
+
+ Examples:
+ ```pycon
+ >>> from supervision.detection.compact_mask import _rle_trim_col_runs
+ >>> # Full column F=2, T=2, F=2 (height 6); crop to rows 1..4 inclusive
+ >>> # yields rows [F, T, T, F] โ F=1, T=2, F=1.
+ >>> _rle_trim_col_runs([2, 2, 2], y1=1, y2=4)
+ [1, 2, 1]
+
+ ```
+ """
+ target_height = y2 - y1 + 1
+ # Sum invariant: returned list sums to target_height. Correctness depends
+ # on the caller (from_coco_rle) having already validated that counts sum
+ # equals img_h * img_w; no re-validation here.
+ collected: list[tuple[bool, int]] = []
+ row = 0
+ for run_idx, run_len in enumerate(col_runs):
+ is_true = run_idx % 2 == 1
+ start = row
+ end = row + int(run_len)
+ row = end
+
+ crop_start = max(start, y1)
+ crop_end = min(end, y2 + 1)
+ if crop_end > crop_start:
+ collected.append((is_true, crop_end - crop_start))
+ if row > y2:
+ break
+
+ if not collected:
+ return [target_height]
+
+ result: list[int] = []
+ if collected[0][0]:
+ result.append(0)
+ for is_true, length in collected:
+ last_is_true = bool(result) and (len(result) - 1) % 2 == 1
+ if result and last_is_true == is_true:
+ result[-1] += length
+ else:
+ result.append(length)
+ return result
+
+
+def _coco_rle_counts_to_array(counts: Any) -> npt.NDArray[np.int32]:
+ """Decode COCO RLE counts into absolute F-order run lengths.
+
+ Args:
+ counts: COCO compressed counts (``str`` or ``bytes``), or uncompressed
+ integer run lengths.
+
+ Returns:
+ One-dimensional ``int32`` run-length array.
+
+ Raises:
+ ValueError: If counts cannot be decoded into non-negative run lengths.
+
+ Examples:
+ ```pycon
+ >>> from supervision.detection.compact_mask import _coco_rle_counts_to_array
+ >>> _coco_rle_counts_to_array([0, 2, 2, 2, 10])
+ array([ 0, 2, 2, 2, 10], dtype=int32)
+
+ ```
+ """
+ try:
+ if isinstance(counts, bytes):
+ counts = counts.decode("utf-8")
+ if isinstance(counts, str):
+ decoded_counts = _delta_decode(_base48_decode(counts))
+ counts_arr = np.array(decoded_counts, dtype=np.int32)
+ else:
+ # Convert to int64 first, then range-check against int32 bounds before
+ # narrowing. A direct int32 cast wraps silently on some numpy versions
+ # and raises on others; this makes overflow detection deterministic.
+ counts_arr64 = np.asarray(counts, dtype=np.int64)
+ int32_info = np.iinfo(np.int32)
+ if counts_arr64.size and (
+ counts_arr64.max() > int32_info.max
+ or counts_arr64.min() < int32_info.min
+ ):
+ raise ValueError("COCO RLE counts exceed int32 range.")
+ counts_arr = counts_arr64.astype(np.int32)
+ except (TypeError, ValueError, OverflowError) as exc:
+ raise ValueError("Invalid COCO RLE counts.") from exc
+
+ if counts_arr.ndim != 1:
+ raise ValueError("COCO RLE counts must be one-dimensional.")
+ if counts_arr.size == 0:
+ raise ValueError("COCO RLE counts cannot be empty.")
+ if np.any(counts_arr < 0):
+ raise ValueError("COCO RLE counts must be non-negative.")
+ return counts_arr
+
+
+def _rle_resize(
+ rle: npt.NDArray[np.int32],
+ crop_h: int,
+ crop_w: int,
+ new_crop_h: int,
+ new_crop_w: int,
+) -> npt.NDArray[np.int32]:
+ """Resize an F-order RLE-encoded crop via nearest-neighbour resampling.
+
+ Manipulates run lengths directly without decoding to a full 2D boolean
+ array. Delegates to :func:`_rle_split_cols`, :func:`_rle_scale_col`,
+ and :func:`_rle_join_cols`.
+
+ The nearest-neighbour mapping ``src = floor(dst * src_size / dst_size)``
+ is bit-exact with ``cv2.INTER_NEAREST``.
+
+ Args:
+ rle: int32 array of F-order run lengths as produced by
+ :func:`~supervision.detection.utils.converters._mask_to_rle_counts`.
+ Starts with a ``False``-run count (may be 0).
+ crop_h: Height of the original crop.
+ crop_w: Width of the original crop.
+ new_crop_h: Height of the resized crop.
+ new_crop_w: Width of the resized crop.
+
+ Returns:
+ int32 array of F-order run lengths for the resized crop, starting
+ with the ``False``-run count.
+
+ Examples:
+ Upscale a 3x3 mask with a diagonal True stripe to 6x6:
+
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import _rle_resize
+ >>> from supervision.detection.utils.converters import (
+ ... _mask_to_rle_counts, _rle_counts_to_mask,
+ ... )
+ >>> mask = np.array([
+ ... [True, False, False],
+ ... [False, True, False],
+ ... [False, False, True ],
+ ... ], dtype=bool)
+ >>> rle = _mask_to_rle_counts(mask)
+ >>> resized_rle = _rle_resize(rle, 3, 3, 6, 6)
+ >>> result = _rle_counts_to_mask(resized_rle, 6, 6)
+ >>> result.astype(int)
+ array([[1, 1, 0, 0, 0, 0],
+ [1, 1, 0, 0, 0, 0],
+ [0, 0, 1, 1, 0, 0],
+ [0, 0, 1, 1, 0, 0],
+ [0, 0, 0, 0, 1, 1],
+ [0, 0, 0, 0, 1, 1]])
+
+ ```
+ """
+ new_total = new_crop_h * new_crop_w
+
+ if crop_h * crop_w == 0 or new_total == 0:
+ return np.array([0], dtype=np.int32)
+ if len(rle) == 1 or int(np.sum(rle[1::2])) == 0:
+ return np.array([new_total], dtype=np.int32)
+ if len(rle) == 2 and rle[0] == 0:
+ return np.array([0, new_total], dtype=np.int32)
+
+ per_col = _rle_split_cols(rle, crop_h, crop_w)
+
+ # cv2.INTER_NEAREST column mapping: src = floor(dst * src_w / dst_w)
+ col_map = (np.arange(new_crop_w) * crop_w // new_crop_w).astype(np.int32)
+
+ # cv2.INTER_NEAREST row mapping: src = floor(dst * src_h / dst_h)
+ row_map = (np.arange(new_crop_h) * crop_h // new_crop_h).astype(np.int32)
+
+ # Scale each unique source column once; reuse via cache for repeated cols.
+ col_cache: dict[int, list[int]] = {}
+ scaled_cols = []
+ for src_c in col_map:
+ src_col = int(src_c)
+ if src_col not in col_cache:
+ col_cache[src_col] = _rle_scale_col(per_col[src_col], crop_h, row_map)
+ scaled_cols.append(col_cache[src_col])
+
+ return _rle_join_cols(scaled_cols, new_total)
+
+
+# Fraction of (run_count / pixel_count) below which _rle_resize is used
+# instead of the decode โ cv2 โ re-encode path. Sparse masks have few long
+# runs; dense/complex masks approach 1 run per 2 pixels.
+_L3_DENSITY_THRESHOLD: float = 0.25
+# Thread overhead outweighs gains below this mask count.
+_PARALLEL_THRESHOLD: int = 8
+# Hard ceiling on each image dimension accepted by from_coco_rle, guarding
+# against crafted payloads that allocate O(H x W) column lists.
+_MAX_IMAGE_DIMENSION: int = 32768
+# Images at or below this pixel count use a fully-vectorised numpy dense-decode
+# path inside from_coco_rle instead of the pure-Python column-split loop.
+# Crossover measured at ~8-16 K px (128x128); threshold set at 128x128 = 16 384.
+_SMALL_IMAGE_DENSE_THRESHOLD: int = 128 * 128
+
+
+def _resize_crop(
+ rle: npt.NDArray[np.int32],
+ orig_h: int,
+ orig_w: int,
+ new_h: int,
+ new_w: int,
+) -> npt.NDArray[np.int32]:
+ """Resize one RLE crop to ``(new_h, new_w)``, choosing the fastest path.
+
+ Dispatch order:
+
+ 1. **All-False fast path** โ returns a single False run; no decode.
+ 2. **L3 direct RLE path** โ used when run density is below
+ :data:`_L3_DENSITY_THRESHOLD`; manipulates run lengths without
+ allocating a 2D array.
+ 3. **cv2 fallback** โ decodes to ``uint8``, calls
+ ``cv2.resize(INTER_NEAREST)``, re-encodes; used for dense masks.
+
+ Args:
+ rle: int32 run-length array for the source crop.
+ orig_h: Height of the source crop.
+ orig_w: Width of the source crop.
+ new_h: Target height.
+ new_w: Target width.
+
+ Returns:
+ int32 RLE array for the resized crop.
+ """
+ import cv2
+
+ # All-False: skip decode entirely.
+ if _rle_area(rle) == 0:
+ return np.array([new_h * new_w], dtype=np.int32)
+
+ # L3: direct RLE arithmetic for sparse masks.
+ if len(rle) / max(1, orig_h * orig_w) < _L3_DENSITY_THRESHOLD:
+ return _rle_resize(rle, orig_h, orig_w, new_h, new_w)
+
+ # cv2 fallback for dense masks.
+ crop = _rle_counts_to_mask(rle, orig_h, orig_w)
+ resized = cv2.resize(
+ crop.view(np.uint8),
+ (new_w, new_h),
+ interpolation=cv2.INTER_NEAREST,
+ ).astype(bool)
+ return _mask_to_rle_counts(resized)
+
+
+class CompactMask:
+ """Memory-efficient crop-RLE mask storage for instance segmentation.
+
+ Instead of storing N full ``(H, W)`` boolean arrays, :class:`CompactMask`
+ encodes each mask as a run-length sequence of its bounding-box crop. This
+ reduces memory from O(NยทHยทW) to roughly O(Nยทbbox_area), which is orders of
+ magnitude smaller for sparse masks on high-resolution images.
+
+ The class exposes a duck-typed interface compatible with ``np.ndarray``
+ masks used elsewhere in ``supervision``:
+
+ * ``mask[int]`` โ dense ``(H, W)`` bool array (annotators, converters).
+ * ``mask[slice | list | ndarray]`` โ new :class:`CompactMask` (filtering).
+ * ``np.asarray(mask)`` โ dense ``(N, H, W)`` bool array (numpy interop).
+ * ``mask.shape``, ``mask.dtype``, ``mask.area`` โ match the dense API.
+
+ :class:`CompactMask` is **not** a drop-in ``np.ndarray`` replacement.
+ When you need to call arbitrary ndarray methods (``astype``, ``reshape``,
+ ``ravel``, ``any``, ``all``, โฆ) call :meth:`to_dense` first:
+ ``cm.to_dense().astype(np.uint8)``. :meth:`to_dense` is the single
+ explicit materialisation boundary.
+
+ .. note:: **RLE encoding โ COCO / pycocotools pixel-scan order**
+
+ :class:`CompactMask` uses **column-major (Fortran-order, F-order)**
+ run-lengths scoped to each mask's bounding-box crop, matching the
+ pixel-scan order used by the COCO API (pycocotools). The crop scope
+ still differs from the full-image scope used by pycocotools, so a
+ :class:`CompactMask` RLE cannot be passed directly to
+ ``maskUtils.iou()`` or ``maskUtils.decode()`` without re-scoping to
+ the full canvas. Use :meth:`to_dense` to obtain a standard boolean
+ array for pycocotools interop.
+
+ This scan order is part of CompactMask's internal RLE representation.
+ Switching from row-major (C-order) to column-major (F-order) is a
+ backward-incompatible format change for any persisted or serialized
+ :class:`CompactMask` state, including pickled objects and any
+ external storage of ``._rles``. Older stored RLE arrays will decode
+ incorrectly under the new convention.
+
+ Migration note: load or decode legacy masks with the older version,
+ materialize them to dense boolean arrays, and then re-encode them
+ with the current version (for example via :meth:`to_dense` followed
+ by :meth:`from_dense`) before persisting them again.
+
+ Args:
+ rles: List of N int32 run-length arrays.
+ crop_shapes: Array of shape ``(N, 2)`` โ ``(crop_h, crop_w)`` per mask.
+ offsets: Array of shape ``(N, 2)`` โ ``(x1, y1)`` bounding-box origins.
+ image_shape: ``(H, W)`` of the full image.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> masks = np.zeros((2, 100, 100), dtype=bool)
+ >>> masks[0, 10:20, 10:20] = True
+ >>> masks[1, 50:70, 50:80] = True
+ >>> xyxy = np.array([[10, 10, 19, 19], [50, 50, 79, 69]], dtype=np.float32)
+ >>> cm = CompactMask.from_dense(masks, xyxy, image_shape=(100, 100))
+ >>> len(cm)
+ 2
+ >>> cm.shape
+ (2, 100, 100)
+
+ ```
+ """
+
+ __slots__ = ("_crop_shapes", "_image_shape", "_offsets", "_rles")
+
+ def __init__(
+ self,
+ rles: list[npt.NDArray[np.int32]],
+ crop_shapes: npt.NDArray[np.int32],
+ offsets: npt.NDArray[np.int32],
+ image_shape: tuple[int, int],
+ ) -> None:
+ self._rles: list[npt.NDArray[np.int32]] = rles
+ self._crop_shapes: npt.NDArray[np.int32] = crop_shapes # (N,2): (h,w)
+ self._offsets: npt.NDArray[np.int32] = offsets # (N,2): (x1,y1)
+ self._image_shape: tuple[int, int] = image_shape # (H, W)
+
+ # ------------------------------------------------------------------
+ # Construction
+ # ------------------------------------------------------------------
+
+ @classmethod
+ def from_dense(
+ cls,
+ masks: npt.NDArray[np.bool_],
+ xyxy: npt.NDArray[np.number],
+ image_shape: tuple[int, int],
+ ) -> CompactMask:
+ """Create a :class:`CompactMask` from a dense ``(N, H, W)`` bool array.
+
+ Bounding boxes are clipped to image bounds and interpreted in the
+ supervision ``xyxy`` convention (inclusive max coordinates). A
+ box with invalid ordering (``x2 < x1`` or ``y2 < y1``) is replaced by
+ a ``1x1`` all-False crop to avoid degenerate RLE.
+
+ Args:
+ masks: Dense boolean mask array of shape ``(N, H, W)``.
+ xyxy: Bounding boxes of shape ``(N, 4)`` in ``[x1, y1, x2, y2]``
+ format.
+ image_shape: ``(H, W)`` of the full image.
+
+ Returns:
+ A new :class:`CompactMask` instance.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> masks = np.zeros((1, 100, 100), dtype=bool)
+ >>> masks[0, 10:20, 10:20] = True
+ >>> xyxy = np.array([[10, 10, 19, 19]], dtype=np.float32)
+ >>> cm = CompactMask.from_dense(masks, xyxy, image_shape=(100, 100))
+ >>> cm.shape
+ (1, 100, 100)
+
+ ```
+ """
+ img_h, img_w = image_shape
+ num_masks = len(masks)
+
+ if num_masks == 0:
+ return cls(
+ [],
+ np.empty((0, 2), dtype=np.int32),
+ np.empty((0, 2), dtype=np.int32),
+ image_shape,
+ )
+
+ rles: list[npt.NDArray[np.int32]] = []
+ crop_shapes_list: list[tuple[int, int]] = []
+ offsets_list: list[tuple[int, int]] = []
+
+ for mask_idx in range(num_masks):
+ x1, y1, x2, y2 = xyxy[mask_idx]
+ x1i, y1i, x2i, y2i = int(x1), int(y1), int(x2), int(y2)
+ x1c = int(max(0, min(x1i, img_w - 1)))
+ y1c = int(max(0, min(y1i, img_h - 1)))
+ crop: npt.NDArray[np.bool_]
+
+ # supervision xyxy uses inclusive max coords, so slicing must add +1.
+ if (
+ x2i < x1i
+ or y2i < y1i
+ or x2i < 0
+ or y2i < 0
+ or x1i >= img_w
+ or y1i >= img_h
+ ):
+ crop = np.zeros((1, 1), dtype=bool)
+ crop_h = 1
+ crop_w = 1
+ else:
+ x2c = int(max(0, min(x2i, img_w - 1)))
+ y2c = int(max(0, min(y2i, img_h - 1)))
+ crop = masks[mask_idx, y1c : y2c + 1, x1c : x2c + 1]
+ crop_h = y2c - y1c + 1
+ crop_w = x2c - x1c + 1
+ rles.append(_mask_to_rle_counts(crop))
+ crop_shapes_list.append((crop_h, crop_w))
+ offsets_list.append((x1c, y1c))
+
+ crop_shapes = np.array(crop_shapes_list, dtype=np.int32)
+ offsets = np.array(offsets_list, dtype=np.int32)
+ return cls(rles, crop_shapes, offsets, image_shape)
+
+ @classmethod
+ def from_coco_rle(
+ cls,
+ rles: Sequence[Mapping[str, Any]],
+ xyxy: npt.NDArray[np.floating],
+ image_shape: tuple[int, int],
+ ) -> CompactMask:
+ """Create a :class:`CompactMask` from full-frame COCO RLE masks.
+
+ Transcodes full-image COCO RLE payloads into the crop-scoped RLE format
+ used by :class:`CompactMask`. The conversion uses run-length arithmetic
+ scoped by ``xyxy`` boxes and does not materialise a dense ``(N, H, W)``
+ mask stack.
+
+ Args:
+ rles: Sequence of COCO RLE dictionaries. Each dictionary must contain
+ ``"size"`` as ``[height, width]`` and ``"counts"`` as compressed
+ counts (``str`` or ``bytes``) or uncompressed integer run lengths.
+ xyxy: Bounding boxes of shape ``(N, 4)`` in ``[x1, y1, x2, y2]``
+ format. Max coordinates follow supervision's inclusive convention.
+ image_shape: ``(H, W)`` of the full image. This must match every RLE
+ ``"size"`` value.
+
+ Returns:
+ A new :class:`CompactMask` instance.
+
+ Raises:
+ ValueError: If the RLE payloads are malformed, are not aligned with
+ ``xyxy``, or their sizes/counts do not match ``image_shape``.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> # 4x4 image with a 2x2 True block at the top-left corner.
+ >>> # Uncompressed F-order COCO counts: F=0, T=2, F=2, T=2, F=10
+ >>> # (column-major: col0=[T,T,F,F], col1=[T,T,F,F], cols2-3 all F).
+ >>> rles = [{"size": [4, 4], "counts": [0, 2, 2, 2, 10]}]
+ >>> xyxy = np.array([[0, 0, 3, 3]], dtype=np.float32)
+ >>> cm = CompactMask.from_coco_rle(rles, xyxy, image_shape=(4, 4))
+ >>> cm.shape
+ (1, 4, 4)
+ >>> cm.area.tolist()
+ [4]
+
+ ```
+ """
+ img_h, img_w = (int(image_shape[0]), int(image_shape[1]))
+ if img_h <= 0 or img_w <= 0:
+ raise ValueError("image_shape must contain positive height and width.")
+ if img_h > _MAX_IMAGE_DIMENSION or img_w > _MAX_IMAGE_DIMENSION:
+ raise ValueError(
+ f"image_shape {(img_h, img_w)} exceeds the maximum allowed dimension "
+ f"of {_MAX_IMAGE_DIMENSION} pixels per side."
+ )
+
+ xyxy_arr = np.asarray(xyxy)
+ if xyxy_arr.shape != (len(rles), 4):
+ raise ValueError(
+ "xyxy must have shape (N, 4), where N matches the number of RLEs."
+ )
+
+ if len(rles) == 0:
+ return cls(
+ [],
+ np.empty((0, 2), dtype=np.int32),
+ np.empty((0, 2), dtype=np.int32),
+ (img_h, img_w),
+ )
+
+ crop_rles: list[npt.NDArray[np.int32]] = []
+ crop_shapes_list: list[tuple[int, int]] = []
+ offsets_list: list[tuple[int, int]] = []
+
+ for mask_idx, rle in enumerate(rles):
+ if not isinstance(rle, Mapping):
+ raise ValueError("Each RLE payload must be a mapping.")
+ if "size" not in rle or "counts" not in rle:
+ raise ValueError("Each RLE payload must contain 'size' and 'counts'.")
+
+ try:
+ # COCO standard: size=[height, width] (h,w order per pycocotools spec)
+ rle_h, rle_w = rle["size"]
+ rle_h = int(rle_h)
+ rle_w = int(rle_w)
+ except (TypeError, ValueError) as exc:
+ raise ValueError("RLE size must be [height, width].") from exc
+
+ if (rle_h, rle_w) != (img_h, img_w):
+ raise ValueError(
+ f"RLE size {(rle_h, rle_w)} must match image_shape "
+ f"{(img_h, img_w)}."
+ )
+
+ counts = _coco_rle_counts_to_array(rle["counts"])
+ if int(np.sum(counts, dtype=np.int64)) != img_h * img_w:
+ raise ValueError(
+ "The sum of COCO RLE counts must match the image area."
+ )
+
+ x1, y1, x2, y2 = xyxy_arr[mask_idx]
+ x1i, y1i, x2i, y2i = int(x1), int(y1), int(x2), int(y2)
+ x1c = max(0, min(x1i, img_w - 1))
+ y1c = max(0, min(y1i, img_h - 1))
+
+ if (
+ x2i < x1i
+ or y2i < y1i
+ or x2i < 0
+ or y2i < 0
+ or x1i >= img_w
+ or y1i >= img_h
+ ):
+ crop_rles.append(np.array([1], dtype=np.int32))
+ crop_shapes_list.append((1, 1))
+ offsets_list.append((x1c, y1c))
+ continue
+
+ x2c = max(0, min(x2i, img_w - 1))
+ y2c = max(0, min(y2i, img_h - 1))
+ crop_h = y2c - y1c + 1
+ crop_w = x2c - x1c + 1
+
+ if img_h * img_w <= _SMALL_IMAGE_DENSE_THRESHOLD:
+ # Small image: vectorised numpy decode avoids the O(img_w)-column
+ # Python loop. Decode RLE to flat F-order bool, extract crop, and
+ # re-encode directly.
+ ends = np.cumsum(counts, dtype=np.int64)
+ starts = ends - counts.astype(np.int64)
+ # Mark True runs (odd-indexed) via difference-array decode (O(R)).
+ true_starts = starts[1::2]
+ true_ends = ends[1::2]
+ if true_starts.size > 0:
+ indicator = np.zeros(img_h * img_w + 1, dtype=np.int32)
+ np.add.at(indicator, true_starts, 1)
+ np.add.at(indicator, true_ends, -1)
+ # cumsum in int32 avoids int8 overflow; cast to uint8 (0/1).
+ flat = np.cumsum(indicator[:-1], dtype=np.int32).astype(np.uint8)
+ else:
+ flat = np.zeros(img_h * img_w, dtype=np.uint8)
+ # Extract crop: reshape to (img_w, img_h) F-order view, slice.
+ flat_crop = flat.reshape(img_w, img_h)[
+ x1c : x2c + 1, y1c : y2c + 1
+ ].ravel()
+ # RLE-encode the flat crop: find value-change positions.
+ change_pos = np.where(np.diff(flat_crop.view(np.int8)))[0] + 1
+ boundaries = np.concatenate([[0], change_pos, [len(flat_crop)]])
+ run_lens = np.diff(boundaries)
+ if flat_crop[0]:
+ run_lens = np.concatenate([[0], run_lens])
+ crop_rle_arr = run_lens.astype(np.int32)
+ else:
+ cols = _rle_split_cols(counts, img_h, img_w, x_start=x1c, x_stop=x2c)
+ selected_columns = [_rle_trim_col_runs(col, y1c, y2c) for col in cols]
+ crop_rle_arr = _rle_join_cols(selected_columns, crop_h * crop_w)
+
+ crop_rles.append(crop_rle_arr)
+ crop_shapes_list.append((crop_h, crop_w))
+ offsets_list.append((x1c, y1c))
+
+ crop_shapes = np.array(crop_shapes_list, dtype=np.int32)
+ offsets = np.array(offsets_list, dtype=np.int32)
+ return cls(crop_rles, crop_shapes, offsets, (img_h, img_w))
+
+ # ------------------------------------------------------------------
+ # Materialisation
+ # ------------------------------------------------------------------
+
+ def to_dense(self) -> npt.NDArray[np.bool_]:
+ """Materialise all masks as a dense ``(N, H, W)`` boolean array.
+
+ Returns:
+ Boolean array of shape ``(N, H, W)``.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> masks = np.zeros((1, 50, 50), dtype=bool)
+ >>> masks[0, 10:20, 10:30] = True
+ >>> xyxy = np.array([[10, 10, 29, 19]], dtype=np.float32)
+ >>> cm = CompactMask.from_dense(masks, xyxy, image_shape=(50, 50))
+ >>> cm.to_dense().shape
+ (1, 50, 50)
+
+ ```
+ """
+ num_masks = len(self._rles)
+ img_h, img_w = self._image_shape
+ result: npt.NDArray[np.bool_] = np.zeros((num_masks, img_h, img_w), dtype=bool)
+ for mask_idx in range(num_masks):
+ crop_h, crop_w = (
+ int(self._crop_shapes[mask_idx, 0]),
+ int(self._crop_shapes[mask_idx, 1]),
+ )
+ x1, y1 = int(self._offsets[mask_idx, 0]), int(self._offsets[mask_idx, 1])
+ crop = _rle_counts_to_mask(self._rles[mask_idx], crop_h, crop_w)
+ result[mask_idx, y1 : y1 + crop_h, x1 : x1 + crop_w] = crop
+ return result
+
+ def crop(self, index: int) -> npt.NDArray[np.bool_]:
+ """Decode a single mask crop without allocating the full image array.
+
+ This is an O(crop_area) operation โ ideal for annotators that only
+ need the cropped region.
+
+ Args:
+ index: Index of the mask to decode.
+
+ Returns:
+ Boolean array of shape ``(crop_h, crop_w)``.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> masks = np.zeros((1, 100, 100), dtype=bool)
+ >>> masks[0, 20:30, 10:40] = True
+ >>> xyxy = np.array([[10, 20, 39, 29]], dtype=np.float32)
+ >>> cm = CompactMask.from_dense(masks, xyxy, image_shape=(100, 100))
+ >>> cm.crop(0).shape
+ (10, 30)
+
+ ```
+ """
+ crop_h = int(self._crop_shapes[index, 0])
+ crop_w = int(self._crop_shapes[index, 1])
+ return _rle_counts_to_mask(self._rles[index], crop_h, crop_w)
+
+ # ------------------------------------------------------------------
+ # Sequence / array protocol
+ # ------------------------------------------------------------------
+
+ def __len__(self) -> int:
+ """Return the number of masks.
+
+ Returns:
+ Number of masks N.
+
+ Examples:
+ ```pycon
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> import numpy as np
+ >>> cm = CompactMask(
+ ... [], np.empty((0, 2), dtype=np.int32),
+ ... np.empty((0, 2), dtype=np.int32), (100, 100))
+ >>> len(cm)
+ 0
+
+ ```
+ """
+ return len(self._rles)
+
+ def __iter__(self) -> Iterator[npt.NDArray[np.bool_]]:
+ """Iterate over masks as dense ``(H, W)`` boolean arrays."""
+ for mask_idx in range(len(self)):
+ yield self[mask_idx]
+
+ @property
+ def shape(self) -> tuple[int, int, int]:
+ """Return ``(N, H, W)`` matching the dense mask convention.
+
+ Returns:
+ Tuple ``(N, H, W)``.
+
+ Examples:
+ ```pycon
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> import numpy as np
+ >>> cm = CompactMask(
+ ... [], np.empty((0, 2), dtype=np.int32),
+ ... np.empty((0, 2), dtype=np.int32), (480, 640))
+ >>> cm.shape
+ (0, 480, 640)
+
+ ```
+ """
+ img_h, img_w = self._image_shape
+ return (len(self), img_h, img_w)
+
+ @property
+ def image_shape(self) -> tuple[int, int]:
+ """Return ``(H, W)`` of the full image this mask is scoped to.
+
+ Returns:
+ Tuple ``(H, W)``.
+
+ Examples:
+ ```pycon
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> import numpy as np
+ >>> cm = CompactMask(
+ ... [], np.empty((0, 2), dtype=np.int32),
+ ... np.empty((0, 2), dtype=np.int32), (480, 640))
+ >>> cm.image_shape
+ (480, 640)
+
+ ```
+ """
+ return self._image_shape
+
+ @property
+ def offsets(self) -> npt.NDArray[np.int32]:
+ """Return per-mask crop origins as ``(x1, y1)`` integer offsets.
+
+ Returns:
+ Array of shape ``(N, 2)`` with ``int32`` offsets.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> masks = np.zeros((1, 10, 10), dtype=bool)
+ >>> masks[0, 2:4, 3:5] = True
+ >>> xyxy = np.array([[3, 2, 4, 3]], dtype=np.float32)
+ >>> cm = CompactMask.from_dense(masks, xyxy, image_shape=(10, 10))
+ >>> cm.offsets.tolist()
+ [[3, 2]]
+
+ ```
+ """
+ return self._offsets
+
+ @property
+ def bbox_xyxy(self) -> npt.NDArray[np.int32]:
+ """Return per-mask inclusive bounding boxes in ``xyxy`` format.
+
+ Boxes are derived from crop metadata:
+ ``x2 = x1 + crop_w - 1``, ``y2 = y1 + crop_h - 1``.
+
+ Returns:
+ Array of shape ``(N, 4)`` with ``int32`` boxes
+ ``[x1, y1, x2, y2]``.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> masks = np.zeros((1, 10, 10), dtype=bool)
+ >>> masks[0, 2:5, 3:7] = True
+ >>> xyxy = np.array([[3, 2, 6, 4]], dtype=np.float32)
+ >>> cm = CompactMask.from_dense(masks, xyxy, image_shape=(10, 10))
+ >>> cm.bbox_xyxy.tolist()
+ [[3, 2, 6, 4]]
+
+ ```
+ """
+ if len(self) == 0:
+ return np.empty((0, 4), dtype=np.int32)
+
+ x1: npt.NDArray[np.int32] = self._offsets[:, 0]
+ y1: npt.NDArray[np.int32] = self._offsets[:, 1]
+ x2: npt.NDArray[np.int32] = x1 + self._crop_shapes[:, 1] - 1
+ y2: npt.NDArray[np.int32] = y1 + self._crop_shapes[:, 0] - 1
+ return np.column_stack((x1, y1, x2, y2)).astype(np.int32, copy=False)
+
+ @property
+ def dtype(self) -> np.dtype[np.bool_]:
+ """Return ``np.dtype(bool)`` โ always.
+
+ Returns:
+ ``np.dtype(bool)``.
+
+ Examples:
+ ```pycon
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> import numpy as np
+ >>> cm = CompactMask(
+ ... [], np.empty((0, 2), dtype=np.int32),
+ ... np.empty((0, 2), dtype=np.int32), (100, 100))
+ >>> cm.dtype
+ dtype('bool')
+
+ ```
+ """
+ return np.dtype(bool)
+
+ @property
+ def area(self) -> npt.NDArray[np.int64]:
+ """Compute the area (``True`` pixel count) of each mask.
+
+ Note:
+ The implementation iterates over the N individual RLE arrays in a
+ Python loop (one :func:`_rle_area` call per mask). This is negligible
+ for typical N, but callers processing thousands of detections per
+ frame should be aware of the per-mask Python-level overhead.
+
+ Returns:
+ int64 array of shape ``(N,)`` with per-mask pixel counts.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> masks = np.zeros((2, 100, 100), dtype=bool)
+ >>> masks[0, 0:10, 0:10] = True # 100 pixels
+ >>> masks[1, 0:5, 0:5] = True # 25 pixels
+ >>> xyxy = np.array([[0, 0, 9, 9], [0, 0, 4, 4]], dtype=np.float32)
+ >>> cm = CompactMask.from_dense(masks, xyxy, image_shape=(100, 100))
+ >>> cm.area.tolist()
+ [100, 25]
+
+ ```
+ """
+ return np.array([_rle_area(rle) for rle in self._rles], dtype=np.int64)
+
+ def sum(
+ self, axis: int | tuple[int, ...] | None = None
+ ) -> npt.NDArray[np.int64] | np.int64:
+ """NumPy-compatible sum with a fast path for per-mask area.
+
+ When ``axis=(1, 2)``, returns the per-mask True-pixel count via
+ :attr:`area` without materialising the full dense array.
+
+ Args:
+ axis: Axis or axes to sum over.
+
+ Returns:
+ Sum result matching NumPy semantics.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> masks = np.zeros((1, 10, 10), dtype=bool)
+ >>> masks[0, 0:3, 0:3] = True
+ >>> xyxy = np.array([[0, 0, 2, 2]], dtype=np.float32)
+ >>> cm = CompactMask.from_dense(masks, xyxy, image_shape=(10, 10))
+ >>> cm.sum(axis=(1, 2)).tolist()
+ [9]
+
+ ```
+ """
+ if axis == (1, 2):
+ return self.area
+ return cast(npt.NDArray[np.int64] | np.int64, self.to_dense().sum(axis=axis))
+
+ @overload
+ def __getitem__(self, index: int | np.integer) -> npt.NDArray[np.bool_]: ...
+
+ @overload
+ def __getitem__(
+ self,
+ index: slice
+ | list[int]
+ | list[bool]
+ | npt.NDArray[np.int_]
+ | npt.NDArray[np.bool_],
+ ) -> CompactMask: ...
+
+ def __getitem__(
+ self,
+ index: (
+ int
+ | np.integer
+ | slice
+ | list[int]
+ | list[bool]
+ | npt.NDArray[np.int_]
+ | npt.NDArray[np.bool_]
+ ),
+ ) -> npt.NDArray[np.bool_] | CompactMask:
+ """Index into the mask collection.
+
+ * ``int`` โ dense ``(H, W)`` bool array (for annotators, iterators).
+ * ``slice | list | ndarray`` โ new :class:`CompactMask` (for filtering).
+
+ Args:
+ index: An integer returns a dense ``(H, W)`` mask. Any other
+ supported index type returns a new :class:`CompactMask`.
+
+ Returns:
+ Dense ``(H, W)`` ``np.ndarray`` for integer index, or a new
+ :class:`CompactMask` for all other index types.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> masks = np.zeros((3, 20, 20), dtype=bool)
+ >>> xyxy = np.array(
+ ... [[0,0,5,5],[5,5,10,10],[10,10,15,15]], dtype=np.float32)
+ >>> cm = CompactMask.from_dense(masks, xyxy, image_shape=(20, 20))
+ >>> cm[0].shape # int โ dense (H, W)
+ (20, 20)
+ >>> len(cm[[0, 2]]) # list โ CompactMask
+ 2
+
+ ```
+ """
+ if isinstance(index, (int, np.integer)):
+ idx = int(index)
+ img_h, img_w = self._image_shape
+ result: npt.NDArray[np.bool_] = np.zeros((img_h, img_w), dtype=bool)
+ crop_h = int(self._crop_shapes[idx, 0])
+ crop_w = int(self._crop_shapes[idx, 1])
+ x1 = int(self._offsets[idx, 0])
+ y1 = int(self._offsets[idx, 1])
+ crop = _rle_counts_to_mask(self._rles[idx], crop_h, crop_w)
+ result[y1 : y1 + crop_h, x1 : x1 + crop_w] = crop
+ return result
+
+ if isinstance(index, slice):
+ selected_rles = [rle.copy() for rle in self._rles[index]]
+ return CompactMask(
+ selected_rles,
+ self._crop_shapes[index].copy(),
+ self._offsets[index].copy(),
+ self._image_shape,
+ )
+
+ # Boolean selectors and fancy index โ convert to integer positions first.
+ if isinstance(index, np.ndarray) and index.dtype == bool:
+ idx_arr = np.where(index)[0]
+ elif isinstance(index, list) and all(
+ isinstance(item, (bool, np.bool_)) for item in index
+ ):
+ idx_arr = np.flatnonzero(np.asarray(index, dtype=bool))
+ else:
+ idx_arr = np.asarray(list(index), dtype=np.intp)
+
+ new_rles = [self._rles[int(mask_idx)].copy() for mask_idx in idx_arr]
+ new_crop_shapes: npt.NDArray[np.int32] = self._crop_shapes[idx_arr].copy()
+ new_offsets: npt.NDArray[np.int32] = self._offsets[idx_arr].copy()
+ return CompactMask(new_rles, new_crop_shapes, new_offsets, self._image_shape)
+
+ def __array__(
+ self, dtype: np.dtype[np.generic] | None = None
+ ) -> npt.NDArray[np.generic]:
+ """NumPy interop: materialise as a dense ``(N, H, W)`` array.
+
+ Called by ``np.asarray(compact_mask)`` and similar NumPy functions.
+
+ Args:
+ dtype: Optional dtype to cast the result to.
+
+ Returns:
+ Dense boolean array of shape ``(N, H, W)``.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> masks = np.zeros((1, 10, 10), dtype=bool)
+ >>> xyxy = np.array([[0, 0, 5, 5]], dtype=np.float32)
+ >>> cm = CompactMask.from_dense(masks, xyxy, image_shape=(10, 10))
+ >>> np.asarray(cm).shape
+ (1, 10, 10)
+
+ ```
+ """
+ result = self.to_dense()
+ if dtype is not None:
+ return result.astype(dtype)
+ return result
+
+ def __eq__(self, other: object) -> bool:
+ """Element-wise equality with another :class:`CompactMask` or ndarray.
+
+ Args:
+ other: Another :class:`CompactMask` or ``np.ndarray``.
+
+ Returns:
+ ``True`` if all masks are pixel-identical.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> masks = np.zeros((1, 10, 10), dtype=bool)
+ >>> xyxy = np.array([[0, 0, 5, 5]], dtype=np.float32)
+ >>> cm1 = CompactMask.from_dense(masks, xyxy, image_shape=(10, 10))
+ >>> cm2 = CompactMask.from_dense(masks, xyxy, image_shape=(10, 10))
+ >>> cm1 == cm2
+ True
+
+ ```
+ """
+ if isinstance(other, CompactMask):
+ return bool(np.array_equal(self.to_dense(), other.to_dense()))
+ if isinstance(other, np.ndarray):
+ return bool(np.array_equal(self.to_dense(), other))
+ return NotImplemented
+
+ # ------------------------------------------------------------------
+ # Collection utilities
+ # ------------------------------------------------------------------
+
+ @staticmethod
+ def merge(masks_list: list[CompactMask]) -> CompactMask:
+ """Concatenate multiple :class:`CompactMask` objects into one.
+
+ All inputs must have the same ``image_shape``.
+
+ Args:
+ masks_list: Non-empty list of :class:`CompactMask` objects.
+
+ Returns:
+ A new :class:`CompactMask` containing every mask from the inputs,
+ in order.
+
+ Raises:
+ ValueError: If ``masks_list`` is empty or image shapes differ.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> masks1 = np.zeros((2, 50, 50), dtype=bool)
+ >>> masks2 = np.zeros((3, 50, 50), dtype=bool)
+ >>> xyxy1 = np.array([[0,0,10,10],[10,10,20,20]], dtype=np.float32)
+ >>> xyxy2 = np.array(
+ ... [[0,0,5,5],[5,5,10,10],[10,10,15,15]], dtype=np.float32)
+ >>> cm1 = CompactMask.from_dense(masks1, xyxy1, image_shape=(50, 50))
+ >>> cm2 = CompactMask.from_dense(masks2, xyxy2, image_shape=(50, 50))
+ >>> len(CompactMask.merge([cm1, cm2]))
+ 5
+
+ ```
+ """
+ if not masks_list:
+ raise ValueError("Cannot merge an empty list of CompactMask objects.")
+
+ image_shape = masks_list[0]._image_shape
+ for cm in masks_list[1:]:
+ if cm._image_shape != image_shape:
+ raise ValueError(
+ f"Cannot merge CompactMask objects with different image shapes: "
+ f"{image_shape} vs {cm._image_shape}"
+ )
+
+ # list.extend is a C-level call and avoids the per-element Python
+ # bytecode overhead of a flat list comprehension. This matters under
+ # GIL contention when multiple threads call merge concurrently.
+ new_rles: list[npt.NDArray[np.int32]] = []
+ for cm in masks_list:
+ new_rles.extend(cm._rles)
+
+ # np.concatenate handles (0, 2) arrays correctly.
+ # No .astype() needed โ _crop_shapes and _offsets are already int32.
+ new_crop_shapes: npt.NDArray[np.int32] = np.concatenate(
+ [cm._crop_shapes for cm in masks_list], axis=0
+ )
+ new_offsets: npt.NDArray[np.int32] = np.concatenate(
+ [cm._offsets for cm in masks_list], axis=0
+ )
+
+ return CompactMask(new_rles, new_crop_shapes, new_offsets, image_shape)
+
+ def repack(self) -> CompactMask:
+ """Re-encode all masks using tight bounding boxes.
+
+ When the original ``xyxy`` boxes are padded or loose โ common with
+ object-detector outputs and full-image boxes used in tests โ each RLE
+ crop encodes more background (``False``) pixels than necessary. This
+ method decodes every crop, trims it to the minimal rectangle that
+ contains all ``True`` pixels, and re-encodes. All-``False`` masks are
+ normalised to a ``1x1`` all-``False`` crop.
+
+ The call is O(sum of crop areas) โ suitable as a one-time cleanup
+ after accumulating many merges (e.g. after
+ :class:`~supervision.detection.tools.inference_slicer.InferenceSlicer`
+ tiles are merged).
+
+ Returns:
+ A new :class:`CompactMask` with minimal-area crops and updated
+ offsets.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> masks = np.zeros((1, 10, 10), dtype=bool)
+ >>> masks[0, 3:7, 3:7] = True
+ >>> # Deliberately loose bbox: covers the full image.
+ >>> xyxy = np.array([[0, 0, 9, 9]], dtype=np.float32)
+ >>> cm = CompactMask.from_dense(masks, xyxy, image_shape=(10, 10))
+ >>> repacked = cm.repack()
+ >>> repacked.offsets.tolist() # tight origin: x1=3, y1=3
+ [[3, 3]]
+
+ ```
+ """
+ num_masks = len(self._rles)
+ if num_masks == 0:
+ return CompactMask(
+ [],
+ np.empty((0, 2), dtype=np.int32),
+ np.empty((0, 2), dtype=np.int32),
+ self._image_shape,
+ )
+
+ new_rles: list[npt.NDArray[np.int32]] = []
+ new_crop_shapes_list: list[tuple[int, int]] = []
+ new_offsets_list: list[tuple[int, int]] = []
+
+ for mask_idx in range(num_masks):
+ crop = self.crop(mask_idx)
+ x1_off = int(self._offsets[mask_idx, 0])
+ y1_off = int(self._offsets[mask_idx, 1])
+
+ rows_any = np.any(crop, axis=1)
+ cols_any = np.any(crop, axis=0)
+
+ if not rows_any.any():
+ # All-False: normalise to 1x1 to avoid zero-sized arrays.
+ new_rles.append(_mask_to_rle_counts(np.zeros((1, 1), dtype=bool)))
+ new_crop_shapes_list.append((1, 1))
+ new_offsets_list.append((x1_off, y1_off))
+ continue
+
+ y_indices = np.where(rows_any)[0]
+ x_indices = np.where(cols_any)[0]
+ y_min, y_max = int(y_indices[0]), int(y_indices[-1])
+ x_min, x_max = int(x_indices[0]), int(x_indices[-1])
+
+ tight = crop[y_min : y_max + 1, x_min : x_max + 1]
+ new_rles.append(_mask_to_rle_counts(tight))
+ new_crop_shapes_list.append((y_max - y_min + 1, x_max - x_min + 1))
+ new_offsets_list.append((x1_off + x_min, y1_off + y_min))
+
+ return CompactMask(
+ new_rles,
+ np.array(new_crop_shapes_list, dtype=np.int32),
+ np.array(new_offsets_list, dtype=np.int32),
+ self._image_shape,
+ )
+
+ # ------------------------------------------------------------------
+ # Slicer support
+ # ------------------------------------------------------------------
+
+ def with_offset(
+ self,
+ dx: int,
+ dy: int,
+ new_image_shape: tuple[int, int],
+ ) -> CompactMask:
+ """Return a new :class:`CompactMask` with adjusted offsets and image shape.
+
+ Used by :class:`~supervision.detection.tools.inference_slicer.InferenceSlicer`
+ to relocate tile-local masks into full-image coordinates without
+ materialising the dense ``(N, H, W)`` array.
+
+ Args:
+ dx: Pixels to add to every mask's ``x1`` offset.
+ dy: Pixels to add to every mask's ``y1`` offset.
+ new_image_shape: ``(H, W)`` of the full (destination) image.
+
+ Returns:
+ New :class:`CompactMask` with updated offsets and image shape.
+ Crops are clipped to stay inside ``new_image_shape``; masks fully
+ outside are represented as ``1x1`` all-False crops.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> masks = np.zeros((1, 20, 20), dtype=bool)
+ >>> xyxy = np.array([[5, 5, 15, 15]], dtype=np.float32)
+ >>> cm = CompactMask.from_dense(masks, xyxy, image_shape=(20, 20))
+ >>> cm2 = cm.with_offset(100, 200, new_image_shape=(400, 400))
+ >>> cm2.offsets[0].tolist()
+ [105, 205]
+
+ ```
+ """
+ new_h, new_w = new_image_shape
+ if new_h <= 0 or new_w <= 0:
+ raise ValueError("new_image_shape must contain positive dimensions")
+
+ num_masks = len(self)
+ if num_masks == 0:
+ return CompactMask(
+ [],
+ np.empty((0, 2), dtype=np.int32),
+ np.empty((0, 2), dtype=np.int32),
+ new_image_shape,
+ )
+
+ # Vectorised bounds check: compute every new [x1,y1,x2,y2] at once.
+ # For the common case (InferenceSlicer tiles that fit fully inside the
+ # new canvas) this catches the "no clipping needed" path in O(N) numpy
+ # without touching any RLE data.
+ new_offsets: npt.NDArray[np.int32] = self._offsets + np.array(
+ [dx, dy], dtype=np.int32
+ )
+ x1s = new_offsets[:, 0]
+ y1s = new_offsets[:, 1]
+ x2s = x1s + self._crop_shapes[:, 1] - 1
+ y2s = y1s + self._crop_shapes[:, 0] - 1
+
+ needs_clip: npt.NDArray[np.bool_] = (
+ (x1s < 0) | (y1s < 0) | (x2s >= new_w) | (y2s >= new_h)
+ )
+
+ if not needs_clip.any():
+ # Fast path: pure offset arithmetic, no decode/re-encode needed.
+ return CompactMask(
+ list(self._rles),
+ self._crop_shapes.copy(),
+ new_offsets,
+ new_image_shape,
+ )
+
+ # Slow path: only decode+clip+re-encode the masks that actually overflow.
+ out_rles: list[npt.NDArray[np.int32]] = []
+ out_crop_shapes: list[tuple[int, int]] = []
+ out_offsets_list: list[tuple[int, int]] = []
+
+ for mask_idx in range(num_masks):
+ x1 = int(x1s[mask_idx])
+ y1 = int(y1s[mask_idx])
+ x2 = int(x2s[mask_idx])
+ y2 = int(y2s[mask_idx])
+
+ if not needs_clip[mask_idx]:
+ out_rles.append(self._rles[mask_idx])
+ out_crop_shapes.append(
+ (
+ int(self._crop_shapes[mask_idx, 0]),
+ int(self._crop_shapes[mask_idx, 1]),
+ )
+ )
+ out_offsets_list.append((x1, y1))
+ continue
+
+ ix1 = max(0, x1)
+ iy1 = max(0, y1)
+ ix2 = min(new_w - 1, x2)
+ iy2 = min(new_h - 1, y2)
+
+ if ix1 > ix2 or iy1 > iy2:
+ anchor_x = min(max(x1, 0), new_w - 1)
+ anchor_y = min(max(y1, 0), new_h - 1)
+ out_rles.append(_mask_to_rle_counts(np.zeros((1, 1), dtype=bool)))
+ out_crop_shapes.append((1, 1))
+ out_offsets_list.append((anchor_x, anchor_y))
+ continue
+
+ crop = self.crop(mask_idx)
+ clipped = crop[iy1 - y1 : iy2 - y1 + 1, ix1 - x1 : ix2 - x1 + 1]
+ out_rles.append(_mask_to_rle_counts(clipped))
+ out_crop_shapes.append((iy2 - iy1 + 1, ix2 - ix1 + 1))
+ out_offsets_list.append((ix1, iy1))
+
+ return CompactMask(
+ out_rles,
+ np.array(out_crop_shapes, dtype=np.int32),
+ np.array(out_offsets_list, dtype=np.int32),
+ new_image_shape,
+ )
+
+ # ------------------------------------------------------------------
+ # Resize
+ # ------------------------------------------------------------------
+
+ def resize(self, new_image_shape: tuple[int, int]) -> CompactMask:
+ """Return a new CompactMask scaled to a different image resolution.
+
+ Each crop mask is resized with nearest-neighbour interpolation.
+ Sparse masks use direct RLE arithmetic (:func:`_rle_resize`); dense
+ masks fall back to ``cv2.resize(INTER_NEAREST)``. Offsets and crop
+ dimensions are scaled proportionally to the new image size.
+
+ Performance notes:
+
+ * Coordinate arithmetic is fully vectorised (no Python loop over N).
+ * All-``False`` crops skip decode/resize entirely.
+ * For N >= 8, resize runs in a thread pool โ NumPy and OpenCV
+ release the GIL so crops execute in parallel on multi-core CPUs.
+
+ Args:
+ new_image_shape: ``(H, W)`` of the target image.
+
+ Returns:
+ New :class:`CompactMask` with updated ``image_shape``, scaled
+ offsets, scaled crop shapes, and re-encoded RLE crops.
+
+ Raises:
+ ValueError: If any dimension in *new_image_shape* is ``<= 0``.
+
+ Examples:
+ ```pycon
+ >>> import numpy as np
+ >>> from supervision.detection.compact_mask import CompactMask
+ >>> masks = np.zeros((1, 100, 100), dtype=bool)
+ >>> masks[0, 20:40, 30:60] = True
+ >>> xyxy = np.array([[30, 20, 59, 39]], dtype=np.float32)
+ >>> cm = CompactMask.from_dense(masks, xyxy, image_shape=(100, 100))
+ >>> small = cm.resize((50, 50))
+ >>> small.shape
+ (1, 50, 50)
+ >>> small.offsets[0].tolist()
+ [15, 10]
+
+ ```
+ """
+ from concurrent.futures import ThreadPoolExecutor
+
+ new_h, new_w = new_image_shape
+ if new_h <= 0 or new_w <= 0:
+ raise ValueError("new_image_shape must contain positive dimensions")
+
+ # fast path โ identity resize; list() creates a new container but the
+ # individual RLE numpy arrays are shared (shallow copy). Callers must
+ # not mutate returned RLE arrays in-place.
+ if (new_h, new_w) == self._image_shape:
+ return CompactMask(
+ list(self._rles),
+ self._crop_shapes.copy(),
+ self._offsets.copy(),
+ new_image_shape,
+ )
+
+ # empty guard
+ if len(self) == 0:
+ return CompactMask(
+ [],
+ np.empty((0, 2), dtype=np.int32),
+ np.empty((0, 2), dtype=np.int32),
+ new_image_shape,
+ )
+
+ img_h, img_w = self._image_shape
+ sx = new_w / img_w
+ sy = new_h / img_h
+
+ # L1 โ vectorised coordinate arithmetic; no Python loop over N masks.
+ x1s = self._offsets[:, 0].astype(np.float64)
+ y1s = self._offsets[:, 1].astype(np.float64)
+ x2s = x1s + self._crop_shapes[:, 1] - 1 # inclusive right edge
+ y2s = y1s + self._crop_shapes[:, 0] - 1 # inclusive bottom edge
+
+ new_x1s = np.clip(np.round(x1s * sx), 0, new_w - 1).astype(np.int32)
+ new_y1s = np.clip(np.round(y1s * sy), 0, new_h - 1).astype(np.int32)
+ new_x2s = np.clip(np.round(x2s * sx), 0, new_w - 1).astype(np.int32)
+ new_y2s = np.clip(np.round(y2s * sy), 0, new_h - 1).astype(np.int32)
+ new_crop_ws: npt.NDArray[np.int32] = np.maximum(
+ 1, new_x2s - new_x1s + 1
+ ).astype(np.int32)
+ new_crop_hs: npt.NDArray[np.int32] = np.maximum(
+ 1, new_y2s - new_y1s + 1
+ ).astype(np.int32)
+
+ # L2b โ parallel per-crop resize; NumPy and OpenCV release the GIL.
+ orig_crop_hs = self._crop_shapes[:, 0]
+ orig_crop_ws = self._crop_shapes[:, 1]
+
+ args = [
+ (
+ self._rles[i],
+ int(orig_crop_hs[i]),
+ int(orig_crop_ws[i]),
+ int(new_crop_hs[i]),
+ int(new_crop_ws[i]),
+ )
+ for i in range(len(self))
+ ]
+
+ n = len(self)
+ if n >= _PARALLEL_THRESHOLD:
+ with ThreadPoolExecutor(max_workers=min(n, os.cpu_count() or 4)) as pool:
+ new_rles: list[npt.NDArray[np.int32]] = list(
+ pool.map(lambda a: _resize_crop(*a), args)
+ )
+ else:
+ new_rles = [_resize_crop(*a) for a in args]
+
+ new_crop_shapes = np.column_stack((new_crop_hs, new_crop_ws)).astype(np.int32)
+ new_offsets = np.column_stack((new_x1s, new_y1s)).astype(np.int32)
+ return CompactMask(new_rles, new_crop_shapes, new_offsets, new_image_shape)
diff --git a/src/supervision/detection/core.py b/src/supervision/detection/core.py
new file mode 100644
index 0000000..50b6bf1
--- /dev/null
+++ b/src/supervision/detection/core.py
@@ -0,0 +1,3477 @@
+from __future__ import annotations
+
+import warnings
+from collections.abc import Iterator
+from dataclasses import dataclass, field
+from functools import reduce
+from typing import Any, cast
+
+import numpy as np
+import numpy.typing as npt
+from deprecate import deprecated, void
+
+from supervision.config import (
+ CLASS_NAME_DATA_FIELD,
+ ORIENTED_BOX_COORDINATES,
+)
+from supervision.detection.compact_mask import CompactMask
+from supervision.detection.tools.transformers import (
+ process_transformers_detection_result,
+ process_transformers_v4_segmentation_result,
+ process_transformers_v5_segmentation_result,
+)
+from supervision.detection.utils._typing import (
+ _DetectionDataType,
+ _DetectionDataValueType,
+ _MetadataType,
+)
+from supervision.detection.utils.boxes import (
+ _oriented_box_anchors,
+ obb_polygon_area,
+ xyxyxyxy_to_xyxy,
+)
+from supervision.detection.utils.converters import (
+ mask_to_xyxy,
+ polygon_to_mask,
+ rle_to_mask,
+ xywh_to_xyxy,
+)
+from supervision.detection.utils.internal import (
+ extract_ultralytics_masks,
+ get_data_item,
+ is_data_equal,
+ is_metadata_equal,
+ merge_data,
+ merge_metadata,
+ process_roboflow_result,
+)
+from supervision.detection.utils.iou_and_nms import (
+ OverlapMetric,
+ box_iou_batch,
+ box_non_max_merge,
+ box_non_max_suppression,
+ mask_iou_batch,
+ mask_non_max_merge,
+ mask_non_max_suppression,
+ oriented_box_non_max_merge,
+ oriented_box_non_max_suppression,
+)
+from supervision.detection.utils.masks import (
+ calculate_masks_centroids,
+ count_mask_pixels,
+)
+from supervision.detection.vlm import (
+ LMM,
+ VLM,
+ _validate_vlm_parameters,
+ from_deepseek_vl_2,
+ from_florence_2,
+ from_google_gemini_2_0,
+ from_google_gemini_2_5,
+ from_moondream,
+ from_paligemma,
+ from_qwen_2_5_vl,
+ from_qwen_3_vl,
+)
+from supervision.geometry.core import Position
+from supervision.utils.internal import (
+ SupervisionWarnings,
+ get_instance_variables,
+ warn_deprecated,
+)
+from supervision.validators import (
+ _validate_data,
+ _validate_detections_fields,
+ _validate_resolution,
+)
+
+
+@dataclass
+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](/latest/detection/annotators/), and [tools](/detection/tools/line_zone/).
+
+ === "Inference"
+
+ Use [`sv.Detections.from_inference`](/detection/core/#supervision.detection.core.Detections.from_inference)
+ method, which accepts model results from both detection and segmentation models.
+
+ ```python
+ import cv2
+ import supervision as sv
+ from inference import get_model
+
+ model = get_model(model_id="yolov8n-640")
+ image = cv2.imread("")
+ results = model.infer(image)[0]
+ detections = sv.Detections.from_inference(results)
+ ```
+
+ === "Ultralytics"
+
+ Use [`sv.Detections.from_ultralytics`](/detection/core/#supervision.detection.core.Detections.from_ultralytics)
+ method, which accepts model results from both detection and segmentation models.
+
+ ```python
+ import cv2
+ import supervision as sv
+ from ultralytics import YOLO
+
+ model = YOLO("yolov8n.pt")
+ image = cv2.imread("")
+ results = model(image)[0]
+ detections = sv.Detections.from_ultralytics(results)
+ ```
+
+ === "Transformers"
+
+ Use [`sv.Detections.from_transformers`](/detection/core/#supervision.detection.core.Detections.from_transformers)
+ method, which accepts model results from both detection and segmentation models.
+
+ ```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("")
+ 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)
+ ```
+
+ Attributes:
+ xyxy: An array of shape `(n, 4)` containing
+ the bounding boxes coordinates in format `[x1, y1, x2, y2]`
+ mask: An array of shape `(n, H, W)` containing the segmentation masks
+ (`bool` data type), or `None` when masks are not available, or as
+ :class:`~supervision.detection.compact_mask.CompactMask`.
+ confidence: An array of shape `(n,)` containing the confidence scores
+ of the detections, or `None` when confidence values are not available.
+ class_id: An array of shape `(n,)` containing the class ids of the
+ detections, or `None` when class ids are not available.
+ tracker_id: An array of shape `(n,)` containing the tracker ids of the
+ detections, or `None` when tracker ids are not available.
+ data: 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.
+ metadata: A dictionary containing collection-level metadata
+ that applies to the entire set of detections. This may include information such
+ as the video name, camera parameters, timestamp, or other global metadata.
+ """ # noqa: E501 // docs
+
+ xyxy: npt.NDArray[np.number]
+ mask: npt.NDArray[np.bool_] | CompactMask | None = None
+ confidence: npt.NDArray[np.floating] | None = None
+ class_id: npt.NDArray[np.integer] | None = None
+ tracker_id: npt.NDArray[np.integer] | None = None
+ data: _DetectionDataType = field(default_factory=dict)
+ metadata: _MetadataType = field(default_factory=dict)
+
+ def __post_init__(self) -> None:
+ _validate_detections_fields(
+ xyxy=self.xyxy,
+ mask=self.mask,
+ confidence=self.confidence,
+ class_id=self.class_id,
+ tracker_id=self.tracker_id,
+ data=self.data,
+ )
+
+ def __len__(self) -> int:
+ """
+ Returns the number of detections in the Detections object.
+ """
+ return len(self.xyxy)
+
+ def __iter__(
+ self,
+ ) -> Iterator[
+ tuple[
+ npt.NDArray[np.number],
+ npt.NDArray[np.bool_] | None,
+ np.generic | None,
+ np.generic | None,
+ np.generic | None,
+ _DetectionDataType,
+ ]
+ ]:
+ """
+ Iterates over the Detections object and yield a tuple of
+ `(xyxy, mask, confidence, class_id, tracker_id, data)` for each detection.
+ """
+ for i in range(len(self.xyxy)):
+ yield (
+ self.xyxy[i],
+ self.mask[i] if self.mask is not None else None,
+ self.confidence[i] if self.confidence is not None else None,
+ self.class_id[i] if self.class_id is not None else None,
+ self.tracker_id[i] if self.tracker_id is not None else None,
+ get_data_item(self.data, i),
+ )
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, Detections):
+ return NotImplemented
+
+ def array_equal_or_none(
+ a: npt.NDArray[np.generic] | None,
+ b: npt.NDArray[np.generic] | None,
+ ) -> bool:
+ if a is None or b is None:
+ return a is b
+ return bool(np.array_equal(a, b))
+
+ def mask_equal(
+ a: npt.NDArray[np.generic] | CompactMask | None,
+ b: npt.NDArray[np.generic] | CompactMask | None,
+ ) -> bool:
+ if a is None or b is None:
+ return a is b
+ if isinstance(a, CompactMask):
+ return bool(a == b)
+ if isinstance(b, CompactMask):
+ return bool(b == a)
+ return bool(np.array_equal(a, b))
+
+ return all(
+ [
+ np.array_equal(self.xyxy, other.xyxy),
+ mask_equal(self.mask, other.mask),
+ array_equal_or_none(self.class_id, other.class_id),
+ array_equal_or_none(self.confidence, other.confidence),
+ array_equal_or_none(self.tracker_id, other.tracker_id),
+ is_data_equal(self.data, other.data),
+ is_metadata_equal(self.metadata, other.metadata),
+ ]
+ )
+
+ @classmethod
+ def from_yolov5(cls, yolov5_results: Any) -> Detections:
+ """
+ Creates a Detections instance from a
+ [YOLOv5](https://github.com/ultralytics/yolov5) inference result.
+
+ Args:
+ yolov5_results: The output Detections instance from YOLOv5.
+
+ Returns:
+ A new Detections object.
+
+ Example:
+ ```python
+ import cv2
+ import torch
+ import supervision as sv
+
+ image = cv2.imread("")
+ model = torch.hub.load('ultralytics/yolov5', 'yolov5s')
+ result = model(image)
+ detections = sv.Detections.from_yolov5(result)
+ ```
+ """
+ yolov5_detections_predictions = yolov5_results.pred[0].cpu().cpu().numpy()
+
+ return cls(
+ xyxy=yolov5_detections_predictions[:, :4],
+ confidence=yolov5_detections_predictions[:, 4],
+ class_id=yolov5_detections_predictions[:, 5].astype(int),
+ )
+
+ @classmethod
+ def from_ultralytics(cls, ultralytics_results: Any) -> Detections:
+ """
+ Creates a `sv.Detections` instance from a
+ [YOLOv8](https://github.com/ultralytics/ultralytics) inference result.
+
+ !!! Note
+
+ `from_ultralytics` is compatible with
+ [detection](https://docs.ultralytics.com/tasks/detect/),
+ [segmentation](https://docs.ultralytics.com/tasks/segment/), and
+ [OBB](https://docs.ultralytics.com/tasks/obb/) models.
+
+ Args:
+ ultralytics_results: The output Results instance from Ultralytics.
+
+ Returns:
+ A new Detections object.
+
+ Example:
+ ```python
+ import cv2
+ import supervision as sv
+ from ultralytics import YOLO
+
+ image = cv2.imread("