From 1a9418619586b556e327c231d48b31fc1e96e41b Mon Sep 17 00:00:00 2001 From: Anatoly Baksheev Date: Thu, 13 Jan 2011 13:04:00 +0000 Subject: [PATCH] First version of CascadeClassifier_GPU. Only for VS2008 now. Sample for it. new NPP_staging for VS2008 only --- .../NPP_staging_static_Windows_32_v1.lib | Bin 573356 -> 607300 bytes 3rdparty/NPP_staging/npp_staging.h | 42 +- modules/gpu/CMakeLists.txt | 23 +- modules/gpu/include/opencv2/gpu/gpu.hpp | 92 +- modules/gpu/src/arithm.cpp | 6 +- modules/gpu/src/cascadeclassifier.cpp | 746 ++++- modules/gpu/src/cuda/internal_shared.hpp | 24 +- modules/gpu/src/cuda/surf_key_point.h | 24 +- modules/gpu/src/element_operations.cpp | 50 +- .../FaceDetectionFeed.cpp_NvidiaAPI_sample | 362 +++ modules/gpu/src/nvidia/NCV.cpp | 571 ++++ modules/gpu/src/nvidia/NCV.hpp | 837 ++++++ .../gpu/src/nvidia/NCVHaarObjectDetection.cu | 2603 +++++++++++++++++ .../gpu/src/nvidia/NCVHaarObjectDetection.hpp | 501 ++++ .../gpu/src/nvidia/NCVRuntimeTemplates.hpp | 174 ++ modules/gpu/src/precomp.hpp | 3 + samples/gpu/cascadeclassifier.cpp | 193 ++ 17 files changed, 6066 insertions(+), 185 deletions(-) create mode 100644 modules/gpu/src/nvidia/FaceDetectionFeed.cpp_NvidiaAPI_sample create mode 100644 modules/gpu/src/nvidia/NCV.cpp create mode 100644 modules/gpu/src/nvidia/NCV.hpp create mode 100644 modules/gpu/src/nvidia/NCVHaarObjectDetection.cu create mode 100644 modules/gpu/src/nvidia/NCVHaarObjectDetection.hpp create mode 100644 modules/gpu/src/nvidia/NCVRuntimeTemplates.hpp create mode 100644 samples/gpu/cascadeclassifier.cpp diff --git a/3rdparty/NPP_staging/NPP_staging_static_Windows_32_v1.lib b/3rdparty/NPP_staging/NPP_staging_static_Windows_32_v1.lib index 39d29b8b1a46d8b893b9aa2bad19d11d14dbdeb4..98d5c227a1a540dd1ccd14a34215268b7a5ba011 100644 GIT binary patch delta 124842 zcmeEv2Xqui)^3MU&nN%_gfhxx5K1VJG&9Os1e1*rLS#e|If!6vgiLIkasUiCV1ikK z*K#%(CmaCdoG{>o69(tC3GcgAJwXiKf9>73=e=|Og6_F>>)x-bZdKh1J(}KgHRidM z?bd~vlM@p%($doPn)5+*={7O-GA5I z9tEiTFSqx#1^D0g|G(1#)pqwu|H@VOuL%8*GOsR^)W3Gs{Z&N|J=3=l(x1QT{)*Cn zP^a!MmHIzdfVw|#vj1G>$h+<@wAp{M0ROMHzs3Bl^xwGZ{+&#`Bkj_^dHG)ppeJ~j z>s!4C>l5oD4vzCVqOtpAbDVyDO zR0fCJ?bbqjK~Z*5R;e{9B`qywbW+vWjHJ|rq|u`fCPdUIb;VYDVSagWetDKPGa<9b z*a?%XGvl&Lt1^=_ljcmFF)1@?T1rJyLPBb0(#$!TN#mxCn>H&m$%<-LWLYZ`a-d(F zU1%#TD*j9QbpR+V&MnHz$twCw0Hj}@m0M!B{U!Z6szFInd2w-8*8iRL0t+O9PqBuXhtT4O4MiSmm2{!VKDYuo;+lx!BrTK+f zc6(9jHB#HYR66Sqx3fFO<~c!JyeWt+p8Pd2j43# z*el;Ff%83Fp0*L+g8yN)e^hAeb2PSm1Bv+Qu zFzeqQAFY>n?CKJ6b1EEi!s}J~sg4Pba>J(tYAFKb%*kqfaHkYq>ztuK@a6)2V3$qo zz_&`8KJuNigTKF%E&EoI_p>^))SJx;79~Di8>n~e?ypyLAE=*77{%87prq)}Bu4m5 zuCAP6DA;BN3E`t3mBf0wQ}UW)s{b&D-KRPL-AC6x-rZ_kb**jkgmE=fs%vUprD3D? zTP*sL*?uk~8XS}U&}@s78*+m^C(|}qR2rUl>Wp~<=S{1&T8HR0$#*r+nmVhdc0$cK zH{cWdEZ+L8IW3$JjB^g14bZ!N60D#7q`O}6X=gpQ$1-;3MWyq>(l1uY2k%XLRt_#J z$S*C<%`UfAB`2pQRaT}Zj7}L_m6C99QO3Pma8YTItngoKo_ zY|Cj=zTW$xi3h*7jyJKyaC5M}eoeeS?K7|7>l-dJj!P*eCHnr=L25}s0-GLbZqxpc zq-R!D*7Tn`XQnl$ATA+6Uy{>SBg$q48EN9g)|?1IJd@M5m9?aVm{#jh+mLcwNkKt^ zzJ7IEm2}syNzo&71BI-6gdGnw=L>dSZm~xCqIhzNUY;AYA+J4%=6PA%s%%>ea|~PO zXV&~n#MElD4Kn6EXOezlgqOY~Pc$WIBn#a$t;}uo#p|QoV!rve0exG3kiI3qFQ?i< z+oYe)pM==@&24TEH~Z;#ZT8a_7ijwUf^fk_tY5a>URIu4T$p1W(_?IET94GEPygVM!%53($yi&4)ex=N=<&~7$O6)xPbU(Y9=8Qr+Pe$qT%d&E_$_q-lkt_xI zIc4_jyh0;ZnwwjcRp>}7%gre(vWb|vpsXk_JG;uq-z>D=Ui~?xH3N(-+zE?YV^oyzctq0XD55yR5u4 zhc^Z37Zv0c7um}4MB5hIu~sUv@$B_`j$~Pii^@yv`6UK?aY=T5X|6kbaY~yMItBtsT-{3(y|Q*k z{@t-OU|c*F%F4aXfqL)bUG#zzv0$AUR?P1K`sNdPdd={5h@BW72oKflc7J%OsFRxR zeX_0KB38(j%jQ;Wu52dWl8@T3HL|(2UNRy_Oi;It7(sYv`{-S64Aw($%wV;#=90MU z&IJ_}Gce~`6O5oY9~rDACym8_kns-EHnL3%!L7(j>|Zf{>dabJ`MVZ+FlXdcCAipT z%g@QrwT`Y#OG>Fs8l9R_)uVcJO4`BKMqTyNw{3~`$ttsEXP4Rv3*nM?^tNOd+X`$& zg$4TS6ME^VC*5o=&Mzs+wdd)TlX{y=Y+1Pl1zF6t%hKCYQk-8{Qf}vxma?LfqGAko zs3qwq=5!IYy*#IVz_e)t#ERgCX*5L@6krdw@sbwdRYNIk-(!~cg49drb}(_?6NBL-yBz^j! zB;D6em6pA#N)g|hq-V_*$sXw~g)5RSFY2a$HLrQ3EXk6W@z0$yNhV2^RDJ2fHu|Xr z7Cm8psD9UcfBotCJ}tn9miT9qy!8F^qj}n~dBOUsg_yzS`*){Qjmwa<7tn+}(TY=- zxRjGgZ?+&>AG9D{KQS**U$bC~etJQxXpv?}a2*GVp{K8(AEK{Z*qrCS2Bo7C{?TXi zMKSuJ`F{Gd^8(vao{sVh+QHFKmhOvUA!$*m{;w?RqF-3Fs>we7;L)2W{yQt6mL9$S z;Oh&lA+3Z#a7u+5q{|8Sx zQdn!M>h)G(x%#m?5Bv)!Am7RW_-D(tLcMWwDzELe=OYJgTe{1^f3%ik@|Q|=-9I}o zX}a3O#QF!T^=sw+$Q@oQhrL1IbvFlN^@NCr!~X5XB`dq6^@+eT^}n#hWbwXg>cP3& z=ga2*tfNoY-yiiB+kH;yd9ZxXAo+ifum1P=3KsbP^!Td5?|-*pv?u%DZP@>h+ps@4 zdNup}^W7Qk*4z(gX*>3(_G^FoF!wsgy-oLg@%j@lW&2)pxa(uA6mdw*hWeVjuw7p& z(R$X=u2_Kl^9^F0zHW%7`@h^brRC zZ>)5X&iAzZjQro(5l6WnX|Izl&QpT7&lMg_|FWy%I3M^IjtcagksS{1`g)gwo%JDV9og(3U6w%$Y~;*n%(Dn3&(Qj_sXUp*_9_sI7Zv@Y4RTbAC`v} zb%JL8Z>T2ao*aGe#rE3P9DElkM}Plfj8d4ZU%A*PI6XZL`+p-?@p+&~mcB2N*^LQm zw3c2hOTCNr`QOJVxyAaH@7tRzz?oVsv*QJpXl))M%Zk~l%cdCZR?wS@@#w@9!K^0P zq&&bCwEf^87JU7}6_fIcU>^hflpFh;V1Etv?}EM20uZkfo~u?OOMxXKS0#$`v`*k< zl(=P>CHSS_k1S!$e>TM^;{~f0ted!KjIySLJ>6RF;04w`u*_bOUa0lsLhxB|PYO)$ zP_REl{AXSMC06q(MF~pv&cDRqd6O*Vm$G=(D?*?4i^*#Q;&XM|ud&)PP#a3wV$~}~ z+XVXlQg&Vj{TS%C4EjsZUS&>gEu>7AdX%v(zrco>pv%hG`Cm*CEvZyP5g&<*f~+6D z9Hxy&Y;GB^oVE<~nlct)LJr$OA1LF-(4#Jynmvj58C(=p{o-$7URM!op&$M&RtYI5 zn|ei*%ThcTk=)Sff|&tk0Wn)vKyv_M2XImFV(VMTK|XOHKLyLu@Akc@f7CCgW(ob! zNpMk6S)#uj<~0n_oAryAVzs+K?d`{_%gUo=llCkar~8Teeg*ose!^a+{<74pKev}T z8g!rjGW#yi(pKvT*na(4-&}^~FWO#H}(GvDuW)F1)FF`5|_vk*?^kUkal7X%B((*Z|)2 z?|}SzfT)eN18Q>>jEI4vHXR4bQu;vNdTbpMo7urA!$m>WU;iacn}S&VKz0lr#A`F? zhi>05i@G!-cG4+J4R(QI%%(tq%I+z|mZbo;-%F0%aK)rWAl7Fv@26h4|t|Z2iro-i^q`csxapikD65eMXGSy=us@ z6D^PwG)xGZp8`ZUA_T!}_}(yCx(vou%4_g&Sz11vmC~qVpmz`Fjq1}5TF)T<>2Oiw z^PsN`7d_N6LY7*N5F@%BXxj);+Z#Yvv;973s5WSysfgZmqiCoA>O{Ph#Un*gc!aq} z&bmjsRR}Q_^^R&%=ORY67D|Y;Oj6sK&sM-gBT@$!tX!(#t=?gjETyrf&E!ZnN`c?B zjbah-awf2cakjqhs!16$iVKeicdm=OOmJ@occY8DRdDYE_hA8C4u`BgjrfNEhtn}% z04-G-bAeB#EVZr_b3t3s-72{^@>u|jrXhZyl3JFnOO(A7=ul0x_M>Gfb+mBdr;%Vn zJcmthF2`zRAS;Zdgd~wvBa-GKY2|3)12=%)GFte+1E8N3D*B1vP0FjIskYkNV1F*y zKJ8JM3y23+@q&6qRmoBUYuQ4M)TdoCDLt#WeFlR&PH<_gOh5|Rf&$X z1@wU`mn(k(@v~K;<9q}9Qk83{Xk%o_cMQd2v^Jm<$MDXh<$%6n414-F@GC$s9wThK z0rd7UGOMtmk@t+@HF*ZS<70%Azkm_{o?w3n_BUh1g!l6pRuhBSty-2^RddOfqfqB? z#24bCU~xI}SHZea&5l{nFo!z9V+D`J5X416(YIX)V$+D-+hB1%_wZU7SPRCoQ;I`% zEg1J0aw@ZV$zj@4h`wjYEb&6;`4$ZCaYBhR+G53%F-};b1oVyLMD0%hW>UtD8;U6wt(F0mh>p%FNmZQAWw@VZ))_f5cg+t8#&e-I$oAKkH_Rv zY>94`g2j^r}}NWPO$ zp-Jp3lN_Vn33}%w(K-p#Tu*^<)L`)D`Us4FOcJJOHd&V1PUc9ygwz4?<&!Z=&|$T| zBl5gR^r3asam2rzEQaZ2(9Nc>E3`ffn!+m*F$GJJDI7~IMg%(p>;l1N!DCuL#yju+9o)ADUOcK|H)h_^tLe@JBVg9E|*tT*Hf( z1NIFye2gj;g0&c|HA2!S87Oxk{)#{`zXkfU8no^}bg=Ib88=mQFpWlDn07CsFH9wG z=*6IVo|S~k9kdnGWNH62Zd(NO&uQE3Kz!3{9&))}&NWQKE={2F|k z;`kJiI#Wb6!d*I(m4?dgwcuJ=%B~fSHU#weT3*4HPeWz~;t8`v>AanD)v8%;m~SD% zopsb~$j%nV*{Y&@^aNwYZ1%p7Txfm;^arzzLG>9Tg>wX|nN_~0hHHpzorBJiWR5oP zLZr)F$R?W$w6$~LHs*3vYD=%3i0@?=BQd-m1^Lok)|Yk{CqdihvG^;dLhbH(n6Kxt z#aB!*UVjJu9$Oy;@lQbhFi(hI20eMc5PxC5EWJISi)*Jrn-+-1ZoU9Zw*^AJBWSij z$X^64FBI~=3uUSMLLr|4x?rJ@9{~EYYA2+ z+F~$%1w(;P>AM)qpvAlqSD+c{7dvKse+V|O!vs}F<1j|+40=$Vm~JM5UR)r ztP@ksQ=ng~6Kj-rK%cG?Ym^^B`!5j0yOBVT4X-IQfK^FH8} zEOT}zZ7|{^guFHu^lXvF{QYDvFT__EX=_2>EKl_OXc;V1C9Y*ThB|)4uxVphxkhN(PdMtwgKdJBdNR#I>sI_eyrym!^^TtmV(*q zU9dvCfF)1NDl(>5J8&^)5*NE&Efdi(t9Y;QssX*2C;8-}q$?3UW+c4}`kY8gMbh_( zc5Dz@Ne!}OXVbf4{mf^Y;s&12M6l;`HlHhQLiAxn<7v>xcoOzF%DY1T7ckB2Fktws z(DGKe-djZurJzTOq|V5EJfa(nq)njr@FZTFM-ctMNcs%)PeO}|d==3is|8@@YAhC5 z%Wiy=r3_uo3p^Lhm7K{7umREM4f*4sKjum70<7HZOT=w!L>sDNW~xAR*&4Cz*#-J- z&~%pZ73kmA@W%7$fnu1}V%fGqP7|ILvCq}h<{?FeGB^X zTG8pv>-Z39zYf#yI#)JU#AmHzBfY$0y+E!NNgCyHCuwvb?puP(wo;uw2IH0uqE3Fa z!Pl-L)##xHwN1Ei9{JflV&CpcVUrW(K5WWGQwuKF{$5v-I>SBk zw0p$oJ~y#JZV_eseXMVH%<<>%lcl8l*~kDi)|mS-((V^EzKmKfMts2oqQ))R;qG#n z+HQv!UG)Zg(zoNPSEA!(A}Vw zZ^uP}?pA+)dsI|Z_bHV%6UJ80tQ}K5t-5ARbxqa0o-wuf%5 z>I~bAs__$QtE*~f&8Y5E*I{mYDiUXno;;zdsCwSOsgtUqnK-s8eRT5ZjLO97#PsUq z(H&xDwi}8E78lri#!Q(xyK35uT5$i(a#sDT1^sK~EEzB;t3T@=>=nbFN|HbI zzxMBQ8uus5J7iN*x5NbY`e0K&t9#h1ubMEnntj$o9;POwC9_cnD$*mi2cuU^x zLqZ7|+&B!z8Ne1_L_6%;B)4LyY82b_y&Teb-x>KG4wsP82s5s2W@wR>8eyelJYT?o z(XFw|kMbx(q7i2K%#nzy^TbfO@vxWD(N};pp7&Sgss^O z6dU!GxlPNvL7JsO4HrfIueMR+7*cf?io~E=-egYg#qLYuuIYkSo$2euh z{ZUTk;{JC|HI!Vq+KyeGuSCXD;dm9KKu~lf93at?gG0D9DGK)|*`Wzao51HdgG_yr zQ&jiU5_^4$CB=`H4WfMyi#7~-(aCw4Q@HUEughrx!q7&mz6QQv7$TB%g0oz$S|`w` z+5(MgOS5HjG(}%rFW~wJSCJ}7XK?j|?q&X8iXw`z+oncy2vUsW6bK&TGaY{!)%5R? zc_JDQ?yF^oCK^&_ncXu>i}MqZly4k%{j;=SEE8^AuXXedx%Ni7W#BezR70*`Iayz7_?3UGxH`+U z;!tfqn_OiHYoFXu>euf&$pPa@0C#HSCgnX!d-O%g~w~)<%7Z# zNl0TD6XkZE5U7)O7V?T{Ep-e}7 znOu-OOK(oS4=PM%I~QwR;)1yXO=`k5((EvlOGScu5?6r4D(ba7H8DMjeRQKGgN?1% ztXL0aNIp!qDxqHduPK?W+A@+SsflSxY}+KQQ@uq36kJ<}{b*p_jYFYA_b0Gec$aEc}w z(L_<8L=*J|g)>0ZEC-a3D+DFvXrUtHW`e>wCFJHiSiAxTfmZ0ZxHVC5;9wX!KO#m{khtdkmY{k!D)4Q^N%=Xdy5rPr@BG-EC<^;>Ov ztb&%0H1WR7kw8f8>4tzQk=OAko&?#H9`VOHqMQ4E$EhL|@c5o{sCS;@!@~C?&Y*E} zn#oJFKo(_JTF~T3{`Hfl`kx#ARp?NsKW+HeH-&!{`MLSmFIaGv(pCO}mE=k z7S^7Z$?3LcF||q?U+xuG7spxP6Nl6)sr_g%!X*|$u_zdp-3p7!R@y4<6gE8@lG~l>v`(V{RUSGEiLrky(H)|Et=I&#!t(S?`4=r>rb*2zh` zR_k}qRw9yV&FbFN36SW+dE}-Z#idE~1rPVt`u@e5XiS>Xu5C;kSB^po8q>N!Non7k zbExU;oB|$CE=c7_;0e62qjP9+q_Q2kTgpJ8rqUD4Nq)v8e*vyx}kt^$nMqX!52YOFq2Q^yARj!&n-M@!d_aPB=pQ zp}|NI%}j-*00s|(6xMdGrLAmbz2;g%lo1LWG}qE8sL}ytGMo)fUE>sXVTT&5j5RvR zWQFa>RqP4|*H^hpRHxZI7b_&iUX<)S3p`Dt#lq)+}n464Lc<&Y)>{9v8yR z8OVYQIhQ)UqpPFy`n6vOhM1pk;R2*H0vDM;foUmBZ(6dtf4O8p6n456aVb(TDP+f) z2L0gxm97sh1e#nSJ=90B~{soQCf)M zcefDop+EVfuvK9n7b=m;{mRB7B_^p^zyAFO8AISXfAozb?I5(+!LE#Xsz@AKvSli$t}>hkCOzb;~<7FdFOLpb;Amj|G~FI`~CsiWPn#{0|Tkklj_ z?xOZXKz+zlZv-W3KN^%+;H(8DM&w3Ng=nGx>0VIpagE)e!~%OCD6xicEuxNqXT=pD zy&R4Yq5KZ0x4H6ZP;U^$Di>NhUUy}EnhRdDvNk2ABs7ukzyHhC$)0bAwlxhA)54!qOI7Mi2Uy z15oE(0su|w3`*3vJ1Ai-^3h*GEPxOcTW6uM29(gyK{Z)GCZviQg(haCAq`6; zekU&>M#AS<3KlB^hWyOY$((|srGTJF?{}VKA5cGWidtFlsJD;fx*+&mLZ93fPMa5!45w87c4GP?ht zD+Mi~be>}+D1S~-{RD5K$b0D`Em&!$vS|youK3*8A9=R<_wu~MnP*#Po`UC|XQ+{9 zsK_(s-^+8eGtbV>JO$4^&kjbO9Ymhp|Ghl7JM&C)<|%mYd8Qb7rieUytIjo6OipHrsN;dZ#+4NlGwm6gxZ+R)ZQ)4O)?rlktDxZ*r}I2Yp_ z$8Dq&{d2y{ZZDu{@jS#SJs&YDfMhJ=Y%23#uC@2ILBf5v*royuEDU6^P8!Oe4s2&j zM=2pulSI6^hl3_pqF$ASu0_*`(Q5bSgl&*ooUQO5t76=l837p*PV@GP7gRSe6+LzLi_G+^Ca z14a&K)0lE&qjPgMb$d59`X8~C?J7Gx6l%+0V|5R}$Ek zVOU;0qW;PCjmcQwJfSk{Z|3J&*>EL`X(N;tb#I~Y0TQjHpF#n}IlS0>QxAWWr&13W zJeon?08f)>aVD!9K|;wFdujT$+@LyKsBtI8XD`si1`0s|6(%9C*~j$40}zILm7Lg@(d!l3(JhM=~S} zpKS5Dt~dOY14cpy8%nHqm#ZnM$$X8K@exlVfA<-uK=6kaJc7^*6>W$N#>lE_f6tHUqPm;dlFt7jx&^+)IP(QIVl}fxaTVX9nE5UWYaapIU zNgUiT&$Qy_S;1G|NIpbXUEOHSCe5lu4znSnl*gW~y&ox!l zCtxot4E{17OOzw6r6?$pFE7JYBc~B@mO*rS5I_(7*rxGH%T&h!g1KI+s2^$KTVsn9 z3uPCaV?I%&W*p9B5*sXU)1TTvsm2DXxrt33rgRtEY-H5h^d}vQW8)NTVn2>mlHAT! zI#aEAP+<&7YHMO=E0t~zlPfVMRzF^e`%AU%U=&--h?3Ox&lcO^4--u^QaiF8^Of{K zdhqAI>Fa<}ql55m-B+;owUd4yE^l;mIydpn;2i4ReOcWC`2JKAyM2<<$9Fqe0a91o zx3iBX;SpFCByn0o=OiQjsUP^iXhy1(G170gqaSGS02QW&7crD^L=u8jS3e*q3i!S) z?+5vAc>GM%!0R>A|9W__|07_DFfnU374uDhVc#B}`{`%>Br=n0N8N)Liw2z7Q4cn; zN2e;;@`%RlxypXIPK?~ekP;*J9#A7t_yB1isKK0i(Mg>GC2ZFPok*nSff5=OpuD-p zTxaSnpqg{uoz7H%Ncd_Lxo96OeoU>1QOWLHpu~oC==H19K*e!UIIKE~)4Z4HG>^Tt zKaihp!0Jhcvk{ zVUg0;Qb*6T#j&4j0@%cP^RH86BEF4P?|gjW~9PnvZO7zN7+z;>WSO*(@TfONVl0Ox`_3V;CVeo*T;G@azE z<L@5t<99%b;{Ogx6d&KTkWzTvJA)DhOym^mA0QQj6w32J3FYmegz{UUs3ilW zk3b3KPo0zn&RJ*#gId9h6$whnb#!SREE1Vt`M$kW-9plClJ_QSLv*vfP01L{e(^$D$q^8ulm zO<$#)W6L%u`He>#@I9rhIb4*$XLE|cXK{+aYdO^s)HY6mkb0nK(fGu%8tqY}=`3oE zG9Z?gVXO16u6SuyOIois*!}qfvJ*EQnw7E~o?DvLjU_&#g|canYZm{JkN$G?>eY(n z&&3@fv3~5p8YQa^ig6UJ4?0K4Ba;tt30hJgaF$Lu51NmKog+XAI~Rg_h7XY$p#H`+ z=7AC#H0c>VAC%A_XDS}l()8DNI#dGqdEOqb{2Z7&IYk>5p-lS~q5M85p?r~3{A`(h zzE-&qbr}qAT#7^w<|OF>fVk~ZA7GK|@kF-qdfX9Da_;hP;$YF<1Cl@B>(pX=$AcP) zzU$$SaYCN~G)L8sh>OG35m#`pBeQxA##Sf1eC3F^6mG(X3;d(k53@|Yj?}heCp1gY z0PIY@?)pM@Tvr(zo#0--&+>hgN=cEgO~^GkV?>;Ptq<*j)E9A&0x!VTwtk{*{jT!1 zg>3Mwc;2+u;y6E^U4>{~%*Js?(5BhYx1wGB`e89|?GKK%oLK$(i_?vU3(KDi?)5`$ zXZG}ZC9KZCpanPqkfMQ_e28A$iA0KYnDy1sTVA~$SALx>)sx^jnUYRX+ZSo8_gMK@lXp$Qth|+_Gpo5%36u-i`c{^#NRfd7klRw zErO+PQ3Ct@Xh_rU8*px=><*5rl;7X*u(PE36@g@$79xSw-DC;#?q&!iutl5k z81bi9w7~XR1}~8-k@`@BBXJ`WXST-1y{bjDAX;~(N^E+tC7M0;vet^Vcumuisiny} zUwk&$;wsgCLm}A-_p%$rE5VkGPVJyX9cxQk)FUp%FAcFCPDPC!de;=fQbN#w`)@^W zX>Dfm2PQl3hf1s&zt3#3*O5mc``!H5BTu@}5-}B1=*7|EYpdth#?9rN-ngrB9PZR} znte4fwHMc?#Ck)kH?&08|H0r;s>8@c;|l_~RN0Rm-=wq|kqZkD95F?YsQ||j1ZS}a zXFWLNBwX43%A!BvZ-CGIvcpyEIDn8Mm*JV#6D)$@h>;^Y zXeJ*R{k}5*h<&LC*F!V^35Mm!~QzBW< ztx7A_d^>(VDGZ6!uvHCDxQ(QT4a8jDSqR{`GY^UN*sf%-?@yxth5NFbw=4G81W54K z-lFbw8K*ZL`63h&3Ttt%l9os-8fyGmkNoh7&q z%3H7hF$1Ag2)H?nGH_Hm5_g%>)Ss}56x|OGL=7#HY1(?_68r64B{Wy$fi%qi;Y%*$iw5#A zHuM3^vt#d50>k`iiHtBpUE*3N=cVIZb{eaR+9>YeSz+~FJISY7Dv5&g| zxt6##4EX9!7eGG)dMuz(L$|459As5F7IzwLxg!tG9P6A4W*S=KphX@-l6E+X$ZU65 zf;ty*CN}2ZC;jHi?4Thu9;u=RKQw2RZ<&JS3C#9{8m6c%Sa7T*Md{XpmBv~kN*pkY zCg0V{p@Q=#KKbIC$WE9C435iWk9%-lVB4ZBAu@ip;vFrl-&rGlGRJUtGgH7(<)?6` zaAJ3QTUX8s^w}Ur-Ivb(sCKN z-q7MQ1S36aWGy)EZf_dLp*@1a!Zs@{`<*jHDWPd!$9P4Sw8TIkl3IT}AjMTTZ|WQf zB;%-?n7(F+bzN6OV1|dlOfi2I83Hps1n{f(Sl1;_Gz4n70B+I-gX3zrN8LCRTDegjWT&0PAW;=SPqw3=gEy%F3A8OHT z{BEUH#A2?|1$PS1&3PxzrSV{E*7%`j4>s0E(#BB@SPMAt-f7KReuPvS02Gcak-NE4 z)c`ihJ@xqUhnKoi?E&nChZL9YUBRJ$bEQrVV3DUZJInb{3smk3VDk{+g`jSn^7RLp zVjP7y6u{mfQEi(NXgfu{1mRYXF0mS1RW9bKk`!DL^tcP+XG04zOuq-q`iD+yt>m@X zaN=)7Pd}nq!czG`5YF-huG>txjvd)*iIvy8guP1*;q`3QX^oQhp4Nic#sEuT=2#<1 z2afQg^#;c^ARaI{iudvS{Jq#<{^N0~PAx{_0njr!j1F#$6! z)83Hr-`jnJ%O2kv$c!TsL_{FnML>U}GO^}owU7el0gSl@hK3L2A-&JgXI#LU2F#rv zMZRm}C#c!otnxl=C?^?WcMCDMfxU@seGQZBO%R|?w+WY{`wQ2#SuE)9HV`%o0oVAE zwt(Z>Lq0$Aw3}-S?zOaPeYc0T@8xrTW`9HFUJt|G2jHfQv3os0?q^>k zH9+q70C~UzD1#(sYW^ zDv>hNB=>3lAy%|k39IUFs5}G#;j5+?959^Si)cyOi%XRa?<~FF@e8*p=q)b3`c5smr3AIaB+5p@EvseK5#|9AV2i|!!LDmz)R90LyKfE z4&0V7`z5ycLu`#cG$i(WNE{FnVNsz-LO6h{DaJw8@^4C5ztM)kK`ua|TMZ7;P;qm7 z4};UR6N^f_?gN@VLs5820r ztRHQ{5FX>QA2(v7>+q^K{nYVSs)`8x48bQnO7*ycB>(~yS=$f{B%>(2( zA9{5W5s*bV?opBx;5O|5$2~w^=Q1O3r{4eb*daY#hN?7>U+2gU_d6VNq-D5Yi?Pi* z$^CD@QbXq?Bt%cYGlILmv^T)PS*5|hXG^bd91?hrWoVI1Q@8adcUyJO7y@q!0XHZ0 z7Ke3jv)4T;`8JoiekCs($Zva8lC)4s%B#*epn1n5v$s7mdzZ_QE2H|qZuxS(tDe0g z!9sY~qpjWpw`p6w>xOI`0Kd=vwhzvBn1S-X2g(N?DDS(WP|o;64_5LVK<+j`KJ)F9mE1?s{!(<2MG40*VyD!50KBf4E!JWIgh>)&0G!tqk;T6M}~;BIhyVw z7|zjR^s+D5%RgdSevZr&-q(;j=TW9Fq1?2c&UuvSE4KP!CCvA{0rHg;#UB4ri!S)e zLlUDAXvw(CxZ>J3_H~s$AqFgjZ#>HIExYs+PAtzOQI+T7uHj-+XFlOle!@^bFO=Qp z(C@fbEbg?cSbdX!*?C?pM5I%O)_2gtcFb)vasjET4BPVX;?1|*iZ5c&P@~YA&=5#S z@QT5q8PfoATUA{Ix9L3V%6@^N#bqFe6{mqLIPUZ8_sAbW?k-3Gxlgn|@XY9;F3Al( zwe(3x0Wl&C=pQ^R^dr}zWU|mpjf3NUa9Svk79R*dN>O#jGGnyCA~yPzo6&v($8|oj zw`cNKuI#57S|rm{>*pp~Qzk?lc4_T1v`D6|soF1G&Cx>J-hDsMrFOwkBN@>^^eB!R z2=$SHwOG=uI1i=e)AZgSD5YZ*rcB}K(2@akDfUbx;$KEiI3rNbYKS~iVu~O!#ao~ z>(u9!RtZ-;fMuDATn$m0WxBri&EJI!Al+#l*xct8JF`E6KdxSDAggi|mESFE6TT(z zz9}^9m?1%5PHB3E;>EsxUWvueex3P33A2WGByczjnIpN)=4M_PcS~rp7bM+>l0{}8 zzo~_WXBsFLpddH5@_38#SXUSl-X7)g@yN&Gk&iEaqTnqpwAuRxiZA8E_P?k^;)lXs ze-Tf!KY3FN3~!4q9EE1`wZ*^>$#An&84~o@txe6+f?a$`iBHEul4Wg%Mn91qmUH^ZGEChNB4HUBxHK z7(UIrQm`WNn;5TYp>=kMn81qUB9t}~91A{;tlS(}6witxP3zxSs=0uWoZ_R6HAa45EjgfoMiN+e?vShY+cWXO`oLw0~C#^tnBr3M5I1GFy0D=_w={ zHG9KA$|NLK zc@o1ilPx`oVadO0b&37$q>{yMI*8imBru;hV5kI17^>+ScX9$7gH$%^d5CsMWKWam z^%s=4B(mT)AsX-ko*93e2snfy`P!jc%&QW{Q?W^G3_=SzhHAeg!6q3;M--eAw%`TK zls6dyB_0B$Y(E70J!S}$dI*&9d<_pPm3yg+T=||iv`D6DzU8d#i37den?Ib7s-b^ZeeI9=pF46T9CYU(0y;9AR(4~4(|eaUdQCAKX^ zut`YNZ$3EW01Y5F2RMiWk=r3ja)7SvpER_%42QgGAPbKBP#P?=AKy}96uk6i@6$?X zW>ycbAPtduh25sCp*%JB8ieYy)H%2f^`1A~EuD4Fia=^EXy#!#!Y! zdBBd~GAi^bw14UQVGaY~OB_aCBV=cRyBg3pdO(lxfF8-#zKjx|HBd&nPz;a?50H@_ zAfvbpW;^cWzLxLm=_+4(D%c34WM}yd&2c4Zv{S?+oOz9?0Wd$e~72 z$9h1IXTzUIBXvj<65~BkCb&=xkntWM6S)kHOc2*>!v`726Frb8xsV;@o9F>OnXNyD zo5ExdkQ&hw+ocPE8u{8ro$3Kn;{h^_%Ur*^OfZnAc_7oT-x*V+ zqmt7+pl9$#sD8>onc;yl(}g0cfOmvkyThpEGS_c}f()Tni#7UtA1cwUYdzFwvrZqw zhwsZ^oj+3A$I!vXY%WBarwsi$GI?DJ5k1+AkCc=EI(j8HI+t?iDSA)#_(zx~A9w>Z z>hYc|;1s_2N8S_?QplwdfD#^9k}gA;!a^Ah!WvE~X->h=Ep7I@1c&wFa?)Zh_zML2 zgdO&n!CylBChM4`?7NSZ*iOs1QOE;P?(fuXSmOeX>J1jcGMREe^D)*C%b0u`Ylzuz zqv)f0vxL*A$FrxDz{pL8?9C)g5A{;X(Da9VH{;US?WgfT;bE|7o*Q}eFjfbSN?Q7W zg|Jd4Rrd*^o;7}oXF02Q8EG#|D(^o399)(|d9$Is%0szgZw7~o;o^J& z4)qBaCqENWC&&GaXN}Azo>4;Ooowk@r32e@2ESzbC2Iuj|H8`;Rk$qC$;!^3Rob)Q zvr6;!6RaFgT91+nQ|&Z3l0>zk@T^sQeQX1}`>Ya%Z{M%|1R0!yx^E!W4ME4NV9tD^ zv`x7`?^vY+3BMR-B;O`GcpD9l3w@7`qqFv(D$P4hSw9ud+0kY%7{a%6b@$3{WII2> zB2Tl6>^FJ{Fm@3Fb-9KB^ANZL9M@M#~Fz7eX@runQatg5%zY@8Ur2-B19zpMBiTWiCU%C9awOnEQdN z8@_KK-z`U>(85?o&)me-BB4TI^N9%m=%1Xw5-c8 zL@A-E#do+wA-!ztLfzF)^9(JLY3lsQcQ!TR{T@Z%$z{BNDzmRqh|I?fxSjH~=G^50 zx6=b|H#cX>s|Lt!50E_`AiFupg&Y?7s}{v}om2XT6xs`lvWv1xtsbvMkiS$475>}) zrC%$ZELCZhm5J$ND%s)R@XI+X?zd#><5^JFEAEfSP&jzZY4u!G+HD53NB9e6o#+x?F?ikwmVp}y@_N|&+T{*)lem@tlp^kOD zUA%hSlcIW%*_0;shkDFWuq$uL%S(BdT|Y_EoD$qxM1EOM{K;!;ISL;LtP;dp6zF8RfKEiGKB zi21UI_FBB1EWO#R;5tWb96iWNy~oiW_Tub9TVYZ0pRpvjiqW3s1tq1WwmjP(>(_ZU zs5?MucA2dpyD0mQ0sgsuNl|%maaLK@AL_ThUXNPxvYKC9Y_%8Wmlx-k|FQCQbW!*T zGYe`a)J~|JJYjzI7-OtDUd4__$re9n1JK*q+c;zK?yqylz@0i{-axz#$7-bkJ+8Wz zzj&Pwb5|3O+HbMwOJ=)4g5zzjXxa<>!V<*5ZTi#ha2CMEtEbe~dg*yhAGyz}M^&3x zi;Kz^DjZ(p4;K~uJmu;il&LH!QeES6ZrK}xE$rDy^?a+ja|e!}F?CMP+^XtnwRqEw z6~De6rQWM|JLjkGY~NCU>5FLg?5|3+J}@Yl{r02sl>X8kq56eXFE%w=O*J|EY6LqT zt$HzAxEjk75vyR0KPju&3qLDa`gbckZ+zS`jBS_AX?n7EiN4jlHT(V-rK3LMTqrC2 z73ZvHepRf@_7CJVsJX52@4qQmWPiiSOsSqSW$NteikbR}r8#r!R$OE+&xVmYvm8|o zV>4B?GoL>i-&56GS@*gthi&#!gY|)*_F%g-wT)iUy%md(QDgMj?rqpJntCfcVpaq6 zS6g*td(G-Zeaw==#&0a@tKua_O%^(h-}tC^$-)dP{ZzZ4UiDKW%!LI-#f3#B`kCh= znSV3Yn{91o4q%1NRKsdxnyHyW{QhRDl~ekK^#SbnW@@rt`$n>^bxvpPTBx@*zR^lu zB{b)HGTe?4H`VzP*|!>-X=? z(jR=ts&9=+Wr>02G#1!NozKcUsowgi82CQ0qdTj)0&qdNIg+jH?7;st+#JO|aYiG$ zs9o92E^3_~wJ%@a8k_3yoId)#G#|b3+fe4;O}*X0ZLa&DZ^f>5Q`?)9@zST%^bFQD zUJXu3?U5cuVU$m?++TXEpmoX0dio}b2=QZP;3a0WtBt5chlHQ{=@A#&D@D!O)_AqO z@_Tc==0c1{CmxrYvkSyZZy`PljL%^$*t+iEPclAdbAF7zb(BHopp{+#Lz5Mkn7kh;yCng(DQFv*rxF4B?UBPTNB0-HY7hrE&GF#FY zd>^e2#^*iXw=+)zZ^>+lwfNZ-ZC-`QMnob_UU=0kB99}o_6Jj2A9`B*92g(YVO#8$ z2(1Lr1Lv}7i74|n{ux8>npe)xWrv9O%Uo8Tj?ch(%s)wOuT{;%J{2#R?QeV!%R= zyh(gk@y{3*6_58%@4HF#h<9(|J;K~>1!}v(=w5`G3C8<3vscrR`JP+w<1)9fh*Wco z`32D5-eOev7eve}MMBG!`1OmGMuHuY{zk$G(90bOs}b33BWQN0N=&pPM(RnTQIS4W>=>oYuT*r9d$sW36;Ha*Yt z;)?#CJKqCT?^%Dlo{>WP0$yb^)m(ln&MvCJh4}G@SPI0un4_ZZ&!>S3?&ke zJ458+!cc(&4Rat$!L9h2%6Qz-sRE)Y4AZ6p8ZREc55_MQuiS@xUn*X|7ud?b72zE+ z8jA3G2~mVhb49q8BV5Z7ZV-|8%jmn^kLYh@Y^@+ny8|CI`NcbfUv>Nlbn&Z>dq~|8 zxED&OG49y;LTEFGMwzn}KYrCQLwUg28tk?x58&rlXt9~KoDJxH{7mIM2Py%?88+Di z@Bwl21NsHnc=`Ncy`ZYkgZd=3>?P3s@*LJ)2_3OS{0db_s-t?ZPh!fYE>1|c}Lwzq;zfLVL zU9A#J9!D*Fs~UE&8up>Y#HydYQR2hLb)WiiO~Pk2315l86MFC+TWg-sZ}2R7X2xOX z9b3yh@9kW;T>N;w);5$T`*dAO7)elev2Ao%FntmgBAsEdy>RyXo&v8($EB49NtEZdk16D>bE_n z==+QwgU{05PwbDsC$P-G5yQ)!^U)mAEVZ~L_q zbFRfv_V(h7ZF*c>lEv4aEUM6#p#zk02EJ>uRa=_itaAHW{@tBK#dhS* zSC(8SnN)8q%JCc68kgmjb}4~c5oX8;L+tlap>BzdJMyjk5jaN zN_5<%H`2~;D1QFHgSV?S_r+*8Hx!*d1hRIQ9+P*VA)D08vAIbIQ-aN|T`fvs2p99Z zeP&d2_gcpG5ed6tto>f6J}@@ouqQ>F*@us=y}MhF(e4Zpo%Z9E4e2ky(ru#O3#g(8 z#Af`{j)%bU6G7cy1Yd(t@eCm(q^63Jb-sW$D3rJDw!El!Z#r~9{~?3=_AV-@IZ>~| zwp}+(JomaD<_XZO{PoxMw!ZYd$$66llXB$dT~*3U`aMP+p-;S_m*}OP@_EIDR$b@{ zWaQI|Om2EU`9%x_YK#zkdWprr5U*lXB` zv~zpWj6z=2Ar(Ym${Hb7we=<$Z{md~w4ReHHodJkYZ}N)w;1VP0e;tfjMqtjtcY*_ z6OCRE+cXW@} z_=lqW^qC^c!i)rw{w|iA9{-#k)3v zO|JUPIVp>ui~2?@MAod&+(`D%F4g5f^9s@apHW%yEoxamw|=O5%wwRe{{`M~daAbO zSE+gZVN|jW42vQ{?)a5;N#yfidHu0H9z46A5)XDo>*x#k-cyQrqluAX#{PzPcK#+# zQ$Pk_f!DoJK$(Hgqp{V2&^S~O5z!M}Os5&+XI@x3dEWdds|FuPw8S^fhID78=)W2=##6N53f6dOp!$&lF3t)acN5l8}Xa7JnhV@%NHKGeA@>OZ^dXYnJ*{^<|#!p7fm3bP@ltKE{)o znU&|PF#Rk8swVwIZ#IiX3Lh~Nt2PbyT&uH8`Z=C4p6vXLELt1-hsyj5M|rLsB|4m5c`*#t$66qr}Hgn4P}V!^EbiJq@bD zp7s=|DT5$Xd*y1rR7G>nd2;1Sohu6G!x`lMXFR=-H^;GyH8=HO``0F59^L>{ZU zc&Fz{PZ?EzQ+Ww@uE?(g;)?thAg;)?%)-r~H=qNc`xSCxe^)|Z0HVzID<=UJN+?ak zW37ZP285vlanJ`>x$ay8h#H_@xd-Q>mA7T^RzPgRpl3Cje5aP32cWuSkf_FR}>+AU2^H zAX=z0l~h2?*B;Odl9uJrx&wM%()s}6Bn`pF9$M>`d!SvknBtcB3+cDrhIcTrj?NBjXO)64d@FQdY%K7lmX->N;$$J(*Uu^ zMSwoV&y+6?WN0yPt~tvAeS)7~slc5j)&u$&KU3N0K$`$@CAkX_OTg27_?)>pS>ldY zJzLZxV(@F88IxJ9A5082b)c3GL_;Qr(s&JL}Y15sDWe3aD;ollSmeFN?MXZNDn<>JU2 zp6;Gv@5^0j(X2qdLZt69v(o>m}D~*=jR!J^d>b2}^xRfv!*) zg5QnvKc$3Q*ClA=bcN!FPfKl&;|bbrp$BY7nGASjD5q6R6y5)W!|kP<`RTuQArqWWKR!Envno;Y=$h<(SC z)Wlk1Ql43J0cx<70_9QBkd^~gEIq2Pi1lD>W9d@{?*8b*Amb+%IYCB45uT9Cm};VY z*=|Ufn~U~hF>c=m&Eyr`{T6d`CD|%}`@R0brKLzy+QT(MR5r#+puaJ4 zxs_OTz|%|2B={1s_GPn|=ynhWylL^bflfA{3%?Y9#z9Y8vHyT4MjPrCBX2^n(zYddbySN>0sx2kSv$y#O!N*QKDt6FG|h8GqNX6 z%V{9i?!b~FiO1bz&2Hq9W3@%nY+kd>Y$z>IvXSRo-@__c%ueje4Gw`!K3Uh|R?{Q!$7@4xS9 zqjO^&IcJ86FW30+=B*DszKBbDufwd*uGkNT ziR4NIm4E1o$@_--Fe-IRJPTm$iA)AGe;yjoiH zID+kg9m&3!0mCg88l|e|;vi7?P!0fP*$2b#w3Xnw!IB~#R=hFQ2^2LRf z!?bn27Bmnm-9e@&0g8?*{{T?Z9m?bgUUR+gBTx6(vxb~iIIw5$f|`%ZwB#D2iuP@M z@l`vIdiq<>m|>rI{t?2@pa3(gT7T=gK+Ta$shR2aA}as4mr^s*;dUWcUuh||T>ThJ zscDNfW$ilkcoUPH+pg^j=QdtAJ<*LF-0to!9o!0F^p@^&^<~{h$31v;)&8@6%_aD= z18NO>u&Y~~^}l5gULhn`w*bSugj&#pT-`br_JF?_esocTt7{3BI*?=FInhH5`Ijfr zeDWz(@fOH6)Mx+YX%?H8B~Nve3S0&FZU0$S&ipLC`IjeE>+{RHTmIG3JN6eI9USzmgI&7Q=PxUx}2s zuIRK!%V2T=$VE(+-fh}>OrH$;f?vhy9}!I%;N`!n;!Ke*AtVhpt^wgO5XyQW-bSZX zCH0gT+Yn24Ie^bOC2sl2lWL9!Jm-}7<|j{LL+bV~1HM?|spd6+?>Z%3*CF#X;FnK{ zZNEU~ZNMi^i9`?R-rrQE$#2r&kmHa~0Umi;72$mm=`*KQBT_4>xi8oTIkcyvoOhbrNt-qg(K)SJLoy6c+(1!30#eq_o$+YGl+Y=S%R? z6h1`LM2|?dg|`GPXJ?xzie6G$d-%l=V((=w8e4-vHl*lqSvd@jg8O$b< z59l7f6d2_M3jvAD&|7%-8JhADkTTR7{YyiW?v{=5>BbhOCaaR($z)<4i2XrK!n(9R z%p#5ivD7BcwutjVTxt_nS;Vy<-UgyvCoS@+9`F62yi9E5$9WjYH$bTNdQSpTy&A6{ zhj=w5j*t_Gw}qFD=?UU65KDga$b1?D{AnNOQ<%)BVq|OA239^1veJzl1|rkn318~% z(U8rG90mmgK-d^sQ&U2iOVdm@vfV-iZ6xRsjqqBMT$t%^g!95cD7hgUP?8-%*c+~i zSwA2dBO+Yt?sOwR0+ELGi`0~JBJo`ma(NvOWCjqKRZDc13`8!q>zHy! zM_E~w5XD*58N`7go+0)TKt>Z%glwJ+WCjpfmzMy!9thRtb+do+c<%w>1rTIiegnu6 zAm9FkZ5oDS1F-dG(BDBT7J=1tMjKHx}}jSOJv?*o**a z-F-kFA%v`Z7RYWO(z=5{z5*g!8qHo6zosPkE$dnV$pzwAR|rC1CZu?afJ_G>ty>Ia z4G>w2HxLl3wlqO}&?Y|VXOo`=@l_DzKFqs7ejpC2s|^}!O4G(xNOK@Pfyj`NKuUoi znJN10#;oW{5ZBton=ImkAZ{bE_TCilULXg7$U^)W$iIM~3rO*v0umWxg~Y{ZN;@Dj z6C;ra*)beLUj&S_0@y8i94OOlxdlL$5kgk2wq))H@mUaM#=i*UH9{!k4*)p=L}vU6 zAl@dfY7y52c@2WB7KK2HfjAjI8ia97NbybuvK)xa_zghr1tPQaF(7XNk(u@xke`6a zOjBbyNzt*I(md8;?*L>F5V<1+xD;^G2wzIrY#^WGN0%uo!__!%T2oEw*HpXyAtS*{ z>yNXVA|=hxwc;I3N=7rwn!Z3r0->x7NkQ`*2f~s#G^(?a11a&E(mS4Y(1W4o`saa3b5;!0E0+9`Wa}s!g$bxwW z$n!vC&3F~a2X@G(K)$v^egfi6#u!2yZjs5FvKRYf3?LOJ*RDnLwn> zLLgTIkuqz6sA-nWXsk_610qdZ0OUp>Qs#Cbj{=c0&jD%Q!jf5z59Y21B4suK*$qU> zybk1W3+?(6G|Br?I7$ys5IhzAl|l?5pnn|a3>(0sAug3WHJ!h^kxAm10s!B0c0%@X~bPX zo(Ce0co)d0K&UDTIqV(>rBORHAnM9e+o2W!2?losi zX&`MoT4mC`Bjz4JWW^{2aupC+CY3-o1CeENACSj@$THar*UV#s7{)a z4vH+3-atlJ!7|gwg0K(-ndw&oS!;)q=j2Twe436(!;BRDj(}@o@q-)bn_=}YB12Q| z20~dMQi(Es7=)ggD78kY5I1FN%Kbo`&?i9Xmt}=+&O+Y=ghC_FK=v6Bp3dfutvPjU z>ym%+;0&84b_VnodYTRNvVjRUu*3#70gzLs?YWxr1`rI1a!z*$_!oJsyCHY(gYz|I zc0O{#i?rSVK+b|!!jQPmR!6+0vxXPrTRHtXkdr`cmH0}i64kYu%8P*B*cJRfq<$xm zuL+^%rgp>F3l|}o6{jf<5Sn&JS%)BWBRZ-814NFm z`+*z>BI}c>ZK{Wdbuk8F%V85RNCn6ttpwkEgSm*84aWp z<<2Z1l|ZD88-d&hMB4Z`kZnMujc)?^28gs#>x1m?W7!xFqze#f<4_eRmzL{qYDQjWpBqC*cuQr2O}@Tjg+uGLouDgk1oS` zGDs`3%7~r}7PyfY-AI#S7uysI5%O{BDPZz(iZmQ_^R*e~61volyy8aUhr8HjxRGr@ zFl9-J%!2Ch5g35+qf1}6^S3zfcwloz1S|Drz$?zKR;j-Q8a2|Y)SX9S#zY7e+oeFR z0dgb~d2=0*bH}jcAZlSvLV6>bq7qHHt_0;Ag{u7?09myuo$bcCkjvf3Yi=ZQyo+s? z8`=A zd@?6fo&)nzTCOT}sXgdM+FalYxfF<;m%Vah;B~PN2d=0{#3%Hed^A~E$q=lL|=1~)5wObFu%lOfyi%LU{H-Vi5i%fq2 zhH96fnO#~l{l;ak*hjjNyWPl1AkwY@mut$X%Ux-i2*S2SR$4Bja5Z95c4t7DsdOMw#Qs)npIQ3{g1Fh!!(HH9|u=;_V6z0(lMKWG5c#V>GihtZ8dB!w%4Q zpBj#i%8QLy(XFo$t>%g1zD5G}n)ZLC#)#FQ83~Skjm72djPE#FE1qTTjo};~+0$qy zzL{W5V-#kmR4wjccr+~|T`ZYwEE0OU(Vmb%x^WgW9qnSasru+EBUiQP38Jj4(NGPD z!f%Z8#iR_QvlH-KhB3fFz-5YdnZ`)pj0Mw?%#QQK(hH5zVt1x-W!3np#sF2U&URvx zT21j$wvi^1rW@Tw&zzcJ4^B67yy+!@tn|DLaWundQ9Y_=xyJBnl&S}27@?}TCC@0Y z>N?BluZjyh*VKvlnv0XOjcKBPmzrTWb}@R2ALkgOL{?W*`3T=w5$vfkq$SnjooTWu3lg?7RmFCaN+M>BTUb4fMQsVPal(f*l`?h++Ue%4z(>qtLe`$SLLP|>3Mmh zv+&A;^SgvKY~XG?M=mqsS(Vjtnp|#lw)M!CGxKu8U!&!0eMM~~9vW(fvn1@4%_lJ=NinrT(Bi;a6#vFvH$Ma>hzkcJwtfX>BT+uskqL;$3e3CETsO~9v#Zt=n%Ao$ zugD2*A$CNXapL%LqmMY|CU-A3B-tYd_Qp%h<`%m|!ml=3h%eogZo`l(w_I(U5dDT5 zmy0#m7{00}`kI4P5p}jpP{y^d!dM`_bCYk4GV`U3xO9LyT6{Rt5os=t`^|>pqSZ!} z=sD1=-qOYmGDF3aEj{7l?v|b?aj2yyOguW!oJy@P=`6Iq-se_VlnG3=47O2jm9spRx@;lFlhPf zLl}hYHn>A%PcQ=Rmgk{Z#pa>ZJ+`me{Afhul*ke7BUOx^>@=i%s6kcEK?@mBYIJ1I z>cbiWn~P0z4Uavnl}zDwC8eI6!owPn7NSF@Au#e5UZG~M4>1Q<^~<7><=$yrB_zvl z(~MNn{a+Zw&Y$igZv}#`_rvK%re#nI5uXc{V`jLx9-4ts{{CF!U*f5m7`R6LKJu$Dk zcvBjwx>(k-!r&maxO-jte*MMBuC(MRhkCj8v_~vptMl(dZ%! zSJ#}va5eB^d$?*M#$f1jY3?B2|4mMOxZHC~D z`;*hZICH9FsvN;~i1bfT_75&Xx4Eic^%2ZN@jLCPi!EvoWEa{5djLxin8*cNP22Tf z8itrL1;bXs0Bf+T!ceAZ;`t>`lPeG(jm0b2PM>AuiOE-TrNW#tTC6I^>}l(jMgz?r z$2JYZIM$=wnMO*U7;j$tgAG+txdO&)iZH!ZFApbUCREfBjgJQ2Yek=HYlf}4)+iK<&xVahqD;&^ zSJq^p-1B4w?78BAb80lh#pf7u0-Ag-=2W+>s!LoX4>n_X78s+m<}Y28!O)?!DDnJdn$!4Ubn*OWt~NA#=2jO&;4jKiYK+F-jM z7i(5;_k+f`+I@z&$wB2{`NLT5y z8g%NEOkDHkcbqq6{M^ZtI+ltf)%)*QKU!RApxVSbQ2j~Mo-c>l@gtX9GiEhQ+_%U` zcMsZzOo%wTOpT^Va~E6C?rnn8<^+-16?O8VE6`h6WnlL}&J;S=4#DVMZo{>w(W$C< zYm(s?i}!j>47ic8_@2$CVsu9l-OaOMY3%)BtULE51CB@|Ca ze!2+z*6P#{Q4&Yw5{B9qrI`?xBI0NRt6hOv2eT-6QH@EoOvtLcti|Tf)Fx4qhb#=h zLQ;w(4l`OpIm~BzAR|wPNL$H<1u|)KGjqj|FVq4|ar%T?QDt*#17iFS$lM+?RnwP| z1Fghcc#$_+jGL(@iKpJeV^GCRHM)Z(1{Oq>mEMk`q8u4N3AK+iFqe#i-WH`I@{}4I zU58~CadLcy_Omx4~iux^3plkb+m=Aox_FQ!K0`k3*y9r>JFnPT%WqnTaEf%ICXObxo8 zLPmysp|;9#tPIGUr%cYL?04mzou5|K%*K1H#Quin{0OUf0_aINSE$GvWudaQnCq0e z8lpv;RCseyt%j)4VOg>^$bmZx{a&uoB*Rw3EhQ_bu1XM)jfLVu_k)?Wp*3k-hw%Y? zV`!9`=(DSWc<)QKU8qwp$cj}zBG>1jRP9Dwt%w6KpHd^n9a9sysv$yI`zQ!?u=1E1 zE4ylH99fxG=?1c$;vF0nY>II5H~V(76UenZlWIhbIv$XfJV1FT$%2 zA!vaSHTv*6AJp5Z>0(B(o2x38cG{gQufFw$$nradu8UC6!sd zq}pqEj(T`4)~SPIbU?hl1j{e8(~X8Ul_p|ZPP*8iWyXi)$kb)a3YR0k`$3KN=5YRq zox9a&Uk;}vPs~_nB#P9}u(lP^-ZU*bPK#0HjLylnD`#+IjJ!@R>B(hD%O0BNMKRpx zqM{KcQ>PYB*>|c*0|6s}6S&`X9R1Qay z5ByGiH`GXSKa`VSe0Kmz-clpV(QzU$!i-74R%x{;--1YMvH@2Fmd3n|%Qyolz>X7} zOirBet*Dm9iAlU$MG9!w%FeUVfZcgWZjb%T*)Kg&zHwMGDX3O^Hpn#K_G@@hTvGHmf;L(Jl3sIeGz zqtOhaK+lXcTiJU81sj47R+4U+0=l)t0%{wM5;S|)0L0eTa;DTk`)*i+aWn(2+z{+? z9^7CwwG#$7j`cB>&9A|3dhstBx zHYqB?r!2A&{Ev$8C^v~$Z!-E#rbAo z)uheF`?>>WsK(aCSHXoaX`wRsb76kL$1q(o@xN_NC(@}EJY+Qdf1-QR2e3vCZ{X9CKACNiMW%n+pwybg6No%PjptPezA8x}eu$i1h91*JAZn z^*BDEVYIE9{fP0ZN?bvt3S(J@QBf=xCZmoOKKXC4kIp^T89z! zu!=cndlGBfP!I1^0UMGipCkS_H^`7bnH$XdCv(GMb}XrtCAG>$O4OMpb+gu;C3W-4 zEJ>V9b7Wpfnmc)ZX-TabP+D47IhB^yQBL)-Cbt&*ukyrnC=L7t^0zx4`y0)n;K1l82XWM zy*RlwJZp9;jt?`Eopp>n=g~VuYDV93Po?3qJGq`kDkODtG- zqooad2@OvL|CMacr-iH}XMro2&tk)SMjY(U3*?J^4b61X<3hE&Z2_&L$h67~AM`#l zd^fEfRlIM+$l67VrvVW$RrTju`5)k#!;jJOQSJ}2xw0QV7lv$(6~}kuH3yA$s}03j z9~g;PV}wmvqOw9wa_m7RV|$LN@xAOAWr^~l2rVqpnr*Fe&9IlA0`wfm>o>W9ToE~2 z?cUQ?WYryedD6A+#;Xy$_F+F^&mKQ_{0yr;<6V?;wA%TS+8E7b`-et5Zf*hmOE_$F z{0l=`_z|OnY@cYlBH}!6s+jtU+7fyyUr}SLH{5N9F%p-q$CTuE+N~jyKO#1f{0^Kn z{xj{?$i_3Xt3ugr=YOaH|b=^;>Gtg;kEZyGJ5O8bl`8J=EJG!i#% zxnk#s=s5Sip;9(}_aPR|j(;Mv)bkIcH&5#w4+2c0|DFR}29~^Y90#~0gYay}11{MN zfRLFjrsSDv`tN$du_C#r{HMI&CW#fVd1FQQ&*4e)T%rkQ0$0>~J~wE`7^$lmugcTX941}zs;TpdMXk+iolQrn0s=mf-o_kP2U)rne9*?U{rQ4% z_^LX!HP6%)gLEyB?r=vt)Fmi3c0p!JpSagL*1=%b^f_SfIH%;QdB+viGFpmDpM!Am zKzhCUgDwc{C+AYnesT`M@{@ZqP?xTHy%0*OuFWuuRmV-vQCF{jXpWMeitk&1xOb}fP%)YYj^*39HjeX4Ms%Hw! z_1qr%oBhOwUhpBKJ%0+X<_A~kkfjUUf!?nykNKso@~9(L z&C~5FBE!}WZvQqX%cb|(@A{)CPksXJ&>MG&T<`ASqo9vb-%W7}c zSHY2OD>VdmSaall-(k&a4gVNw9_H5YA8=M<(`;uo_b9`DW3xZS6s+E%1}jX}yw#k9 z{+`1%gi@^iZ`V+R6qkKYo8Y8_+ap`hFv%Q@F#}TW5eI1P5oeuxT#xB6|JSu41&_<*9?jdj!NtpS5rAV#yQkjAsyrL9LG2s?EO?$a1e0szSZ8e zavbGgNccrY6Dr|AZmzuxohOjs-VW1FCL0CGlE`rAUG}*9NwYW=mVYGj@un`|$I%c@=)jzc66nHlNox@7b>gX-+nv!Yc|=Dnag+wDv} z>c{%IJjcOqbWYuf7j8E?2s&Hej$m|f+(o#v`5g=o_6Y1l=Tgo-bPgd_NFTcVYCd#+ zs{sB)XF$$$ut9E9L&~*HoiDIWoq{mzPM?TDSRGrxbQ+|IU^hF*qz>Zu-0Wok@D|ht zw_yJ_JKAxo>g+zqK~2p16nkZL&JL^{tbc=}os+YF+tE(WtX6#ZPhw2I?3V<;P95)%?^iF{EdlLPsc^Bb_1+M4lKiA zrR*=b+R4a*Jsufkb$JwRS}@xB9Pc1ucZVcFC(p>xzwLb&kSU_X1Rb+#d*8968!ha? z4(f3K|MJ93soxWi3dTKXQ0Bq~2ZKgCdtJ0{XS`5w*&~T1gps73@mjTr!j`=raj15WM`21k9g+s(sK0*@Oz$llwtOu_y3V=UW(f_&-O#g4Z0zr80>+& zjwf^(1%rvoT5kRN6N3kNT4$bog#jGOa!Tv6G7BY4I1z^}pEzrgphz(7?272{tiUn^aIp?q0FpnN;la-#VU z`xOWvXQw|JAf<~8`kwpyQelB-Prv_q3R#nY@|uu+QD*Lj-3;bQYFPx7DFaiJ%! zX8Wtx#(K-AJ_#xx>Gu{6s^nl% zaXb4{{bxO?pGw>AICxT5|5=|%{rzWs5>$Q(|3L?_vcKdYhU$13ejVMXBeVb7&{@M_ zEnxudru6AkT1t6^LnbZ{798e*D_C?{jUKm#21kMHaceFF)HjS;j}X+?@)5tF!v*!i zqh0QKooWq!^;$sM>aHR6iPT?1>XQ(M)~c-<_l8?8oq`Bc!~PpiL2R;I^ndRZ#Aji3 zpMv-`tnO0~W%ZnbDEpJAAgby=1+k}|QxHA=LI#;1t9-E56@51yO}pwbpYA;+1A~pMvOB-zf+Ru;cu_QxLbqlba4J(B(d~Q1cW- z75M(lDTq#g_7p^?|2qZo$EP61S*IZIU!HrPqPA+(H_Tjz~GlR}PT!pg_;gd`{`+ygP{^8k& zcH>Oz?1RABhc@E!apte1>plNKJ*a)veg44?lg%j4N}~p8p#$=l&{cn(9dw4iRi`0Bsx?h)gj_KGlAwtu`~>l z#O&Mje9sTFp4lQl4@%!ov- zeGWj`MW-|HZ62#Pu|A(fA@b^rH|Ch_)Is9jvA$Mnv3PT=FD)9ME`o+V@{Gbw}D?-VeE{IQ^936b*noJoNB zK-?4w;U=$+Jqkw-7*bR;e8k|%GYY5EJ0IrHnp;pH11F30o?6q0X+T_WE}SWDSzxvZ zVT}~;6!G0cv#B;(5o0UORv}|0dlc>q#hOY4%u>XXg+9L+HC#&yFTWyG+0#3X4Wiam7NoW}EmH7D; z`n=#82`$2XwS=z0eHDJDvKIFW36X}YB}6f??7e{K1@3<3;Z*!%;cbAH;%6!^dOjUEGZ;;o2Ktd(;56ID_0xgdq!mWv~1%Deb=MyG+3WpWm9 z75>u~JCEVl9J($feYlR}$6y>0cam(5b%0 z>N8^UR9|oPO>ys3U#faQyf)RBpnf2Z(Z8dj!8Bijh6)f-Zng}err}p2abGA3%gv@8 z9|4HZ`R=3)vO37I0JwmI9VE&M*Ku+63^-H#P_=yqzJyigL23Gx?+~B^G8IZ2r;^gf zsnig{scZs>&X}4?BJRxB91!!h2E=?_0g=3^^m6$6JA8v3zV_m-XUsu*7PXlj=1?sc z@3z4IvfLa9qm_b@tTvSflw9Q({Jcu}S83&!r?pnj316Cw(2C2`vX-3*U+M>ZcE$9x zf|1L<4qtv0q80Pg`Y-!2eEB;B_fE@O8TtTe8Qx`Q_>~)pHYTli`A$!lL*Z9m3v`Pf zzHBM}|G29R&}D#jey*+Poz}I_N{# zITYFD+wkSv0j=zFro8N==kl4bICkZNUw3|^mGAWRUD@?<$ya!?d}qYLo+rznk0{>} zQT~m3aNOqy3%)uuCawLU-uM-Nb+G4G2giMNu;6%*cKl$^;|IqbKUna6koNt-p5GrF z_x-_wlZQr~TyS@n-{$o}GW>|OprUL# z;85YI@)tb^`<^P_;XwwLe+es29xOO@Xw<0%pFl%buqUfXFEpm<$PX3i$1i@R=Vrkf#G8wQ9zPh%>h zaA!VhG|V><5c6FOh+3?vT!B0Dl>=hFt10K~MBt?kFFA?8JJu)>dN#e4W}ewNH4dD$ zKV;q>R1>y z+-58C@O*F=6@d3%?Dn{y{T*Uy9Cnbe4??ukMXC#3#Z=1}#h6RJG z=iScgI(A!m$8wZ++c@vkM>tP7PFJ41VCBhvRqT7#(<<%&6mrV(KqW83`v0yNf0gMM zU8eb(i>igbftjDkxFZqFlh(d~HsEI}!*KsthMoh6C#_=uRf=Dx`QR)7u;wiCQ^`(o z-mHkq5-rX{6Mk{?CB8gso=8VkFV+Grjz~_*lg}gsK(2sLw&633vYaezk;xv#MRd`{CzTUZzPd3)A%XgcSY=&~bvq_{)8*>zt+I1CP+n`Ad3u z))mOgwB}lP6FAMaVryl4CeCh9wtp)XP`00z5CGYgQnq`PcJxf3d?nspnuQC@cET10pa_F zkXQkTCFt>nCDs9A32Pdep2G=h7s$^=7*9s&zot;U{76x)59`h?Bsv_y)dD71-44D} zrw-B@i%!F|3B~yG7>uGqV5p-56*v%8e-5RZ{)9AsJRlk*O=Yq}n+j-)7(W!>@NO#h z7i%5x?@Y1QR7?GPF+StOh;LI_F*3~+V=Ioa6~~A#ApM;*?r>tHL7&seG*^sS9Ag&8 zh|dE3ofzp&JElUpiZ9|=F*3~+V_$T1H1dojN3#8~Xa$TU}sLpa7E93wsqW3MWt zh}bL4;oa#$u;zn4KBod&e&{+;;t|p2W;H~CYTxpsO z1{4&4iAHYuSiC@TNs4`=Z=CSr&|tCsY^_C0)r;8e!d5$uP3(CM#dcC57^(!pfIQhjLvoU&1l5lKCNI zs9UG9!BIMg^Mf@|eq85pF9zn$kHs24xsC>h`^gobh~a?6H3K$kVoSN1s-cd&T8^39 z{T7bAAUyJ62zZq*$TZ}Nf{!@vJXed2q`t{TZe@|Fvv85|g)+%aE!sscw8+%#T;w54 z)?ThvsI>Y~vrylWv_CG?$E`wrq>e&8Bz4qXs2|EO%G_gCp}r|8RH*M+h5C_0`6O7V zW`p2VOBU{@k_%$N>kCKuZZ2HbM1}i_!@U@oyKvvI%J25Ov1KG^ek%sj6aWw)=`S{1(5j|_2mMNr18D^w!7%?A7@Z*ta_mZcx ze8AH%eRH1nng(+7pb;N<2WN=X?Uj?xU&W}6=7wUnI)DJSdN?3@O_*Py0hO($TwrJp zAh!AvKy1tvfY_LFK$RllMlAA%=;FMM=8bi#AM*FrN9dy97w-#V=xD8#nD&Y%rfe6I zr_eOS#2+5^WD!t$+aiFMy7^I%W>0NpXfr@eqnQPDkEZ-A4?~n|Jm$~>UrQ;`+mSdM zPzxz>o`lLsxC9^%j5P6Jk!gT9lFI?5N|7ZFv>XsMbyK+(5KB<8v&3zHlBC4l4s<^t zo?tuzh$WuH*S0N@mq4U9)0xWa4)hivo*7YhEC`K2r zE|=eVz-tHUcwZULf-;rge@1W{K7ZnVl_3=2m0a>3yx~hn7`5@5<7>=>8Dr8~XMVj$ zKBlc08@pnrXT{8j6*GM+W`?eq>0fz8{EAL1W_ec3@>S>+MX?pHteBpbeCEv}tm&k$ z3|&S#tt|kocwqvJ^{m!iUYnoZW$#>nMRY|^NSxVMu@~%F1D5TbFUGGl{mljv7Z&Ey zE26_6dZDPIr{^4E!_ESQ$21GaYEgTLv|cg9Q!ylY#YkyQ--s1M#NrAwVGJ!WyB|<8 zVFk}IW&z?^H9flIOTJBjc(!pTAa34NU^z~KaVQm7CALp4_tiZE=_dJ;z{L-6at2bW z=ipV)c&>;O+gF>0cxi<>RnN)Fm5&rHGo=Xi#N8zXK%uX+Q*UWMQl$U83gZRR{+}-l zTIsJ@7(9{rcNIom)0~0s!YH^*%WB!z$?LP~&FfylyiO7yR^lZL|Gd_Gsf^qe+({@z zMwUn#jY(XeE(SD4(x^w_8bdt_*C*kDhhJM`HnQh^vt?*dBcOSXQfnT}8e$PN!{I&+jN&z|=jS4wwK*7Px4WHWWI8TkGPqSCJ_fDAJszUqgu%Nc9l7dr7B*p3r4@P6c9~2{mM$*-}QD6YTV%xc3ikP8Y!34m_F&#$E7epKpc1?XnE`=n=NS$j1Z*%&Wq((T!k8LKq~ zkQ1FRC51+mZzTkv)}-AIrN~J@EJD2zi)d)|YyvGN zb490)rx()dSE!RY8@zsHBp_;l*opweS|$KuEf)YHEoFY?GJuQm^DETL6iGF&0E(8v zv{u5x2LQ3~2Y}dwV}N+7auN`0p_%+*spV|k@nU9}Kywh*G9D0XDFswX>GxySr6@cq zF9O60uK~mgsaw1oKlFIGhe?G;0CB(c4?wK&3qWkvNkFVm0XOR-Io6j3i1Y0{58~&( zY6^%A@iUc~xU=ecfLQfHK&<*IK&*N-AXa@7AXfb-AXfc8p!;ElU-<%%7eBc08JO$= z@+)lt=|n42x&UN_y#TSoB0#M00zj;AHXurksay<*GxKslY~T_=Y~V^jzae5%xz6Fc z(c!x#$T#@aFW^`f@vzym8^;ii2sqXzfWlFrez{#lZX$k#{7bkq?E`aGi z*5n zdEM!$7S1v0O8D}<0H-KO?aD!EzR!V>4@~IF{%4l$T@Z;!Zcjze2n2b~>|45HK_-ME z6tVeU3<>d1n~_b(i3R>7BT2Y;SP>rn5w}t-dOvM;Z3yW!3RuxZtbW?eDkC3Op1gBO zQU}B(nFxqWl6C<;kZpMZplm5Y&sHq41Q1KmY7I-Q1wTvIF8)G@NNft64InBD?N(}pjHWVgJS6$hH6_dJoEjL=fE+n2yU;`tjPT}0tul1 zergkN$V$VnHGZ*Ob}i_9&PsS@6vCd&r^~MSEv67H<_|W#P;Ppmvgu`k zH+Dfs5!h}v>oX`VRK{E~_Q{{%>NJ{so6y?&L%ug!Txc;oJ9-ZTr8lj~^=8!Go<3f_ zJHE?_`EA9vN-eI;QdIHcSgD1iE!CMjB!zmJ4fw%H4|m?mr>4YDcjo~55sBgzS6+pl z1lra3naYK@b3@w+XoM`(y@0q;y#{DBGg32U#>0TPMMgm_H|JzP#ZtH>Aa2g>0I_U3 zAeJS6LY5su5YKqC(5Xsq!3^rn5hH@AP5P8jDr#lOf2Kw#;setc@_$vLdX*^o>s5k# z7^)KV2xTgNy-J9i=b6J|v$>0|Ssg}+^hNM`z$5?J^mUKLTkvAA5RdrjCNsZmA1X}6 zO=(L}YnHtqzWgpiuS=s^xa`gF<$}=OX$x0|-c0q1+($0}Y2_uJifhvr6XlKYD`|R2 z9;Acu|HqvGdINw1>CP|6hjd|ux$;sKOX@3FqT=;lE1pfe7P4pF>|ar};bwETUPj*1 z|0~uZE<+i72J`(2tu&CMmtVOJcV36M2N1XO2LW+YeH0KkRdNX9t-x;pu@+isU@cLA zcpZXPlUPe4Aku<$h~@xU4Q=YN8uCuyb%=8Tu?Rf`aK!Wwz!LKSvBZObsJHhk&jO+e zx2Zf2i2Ij4fLQibKrBnUs&DW*#5V-u=U3W8kf+nM1HtPM1%OyhA3&^TARtyV8W77) zcc7a9u`%}o;&q6vfLIGXL9>>pgX<7vF{}B&ff7(R*qC;JcpaiUAm$tDKvMznI>a16 zoLd(G;z%w7#HP~BpVuK)*?bYoI*0KFVwCF;9XJLWMHxx}#8c-~K-0l!DsmkHm|w07 zq|@UEA9J`sLbNV0LqY(QCjrr%+^e*bbJt1M-05A=thwte0RO9V*XUTxT1TY$Xq3zw zR=m=)63Ud7{XO+eo@nvquT5Sl7ft2QPF{B+_DPWND{lazXD?HE2M||)_XYyw!Z`wn z3+EUh9_}=>A%@6Td4Uv;0mKCx2Z&{<*R&?DdG?$&-&)Mc#79MG@>-Ue#{(tpY`Z6~ zv?ld`Q>Xu#snZHsqco|aN_@E#{_EqUo`n@WnmRSj%0SGur%vS_G2}H* zs%W><99YVm)HGXUh@S2kYUx1j9Vh^(5I?_?@6dWW&{+;N!hyy}NF=}RiB(sLCF`^< z+6AfzJa0bjdH3uytwhQ`&v#Y3KGjnW%J(3J);s8;)k0k@z}<`ACG(2M;h%L?Hghvc;%%Ui$zfuS&^l~L5r!W$h=YO6@FgN57103nfQ? zcJ&?m=)6|)b8i%Qy(QHeQe2iVIi?0PggD_DgrBaSMgQok_*VwJV(h|x#}cDZrZt9Z z_&ew?+KR$w^f;vLOpGr{;b^E55|o=VS!<>gRoVm@V0R$y7mKU`#Rp0-|F$_i$(GtTz2e&gW^9*q@ad{;{Z~hSx;t17Ut4-4kBlLn z)Z&s9vE*ia1U=?vEv9&oMW;?271q@bXq?d17PzbUMIRZCRTw*AGb}0Msb0-9#cDJ! zZH-UF-l8Q6bG>dQahb1M4B3(aH|0l*5-f^%n!m`4>ii=ju*R3r zgl0Fonr8*{t9wW@(ATIEYDvuo9(2P6I&41L|w*%=z_oB@c7mYst-XIFH4Jt))4;Q3Dy$P#k;TlPDg4#}lKi!e$#cGG3@CBA|ZzWm0zgbHW zo_Jqutj+tz8;>ms^4=99+TW)2bV4`8HhU--`c{Zoj!@C(12ZPgh z1s-wVRlXGQ<~ocgL+;R`%4nvgtHTfhrR+R0>b79YdRWu6vumYnWVOgjSERifjI6|x zBA)6d(qd%L#8yxCcn5B_cCEQJOf0_6m)OM4u693Nb4gI?5%KtSG&DS=#U$7h4V0QC z-N~MMAbk%dA${>l7%E4LHG9pt+jH{J`KEU@inj zS80Z;;x~NLs3F0ab1W(1sjl|_mG|ZGQ50L-(=(IqP9TtdCo2#L5VDa36f-l~C&?rr zo1#Dn0fG>=2%=ttJJ+2%N?pAw;DWp28dP`%H$-J~N8E7Vu7V4SY~ORLYNopxyzjl= zcmMn9_v`Lcr_OWgRMn~K>b2&GiB^jgm=hv!j+ki8lmh332+S1|tqY~VTrPl}?H`h3 zRNDpHP`q18(*#iT!2^4Jn_94_VPn^Pad@*`PkFrw_(N*7)1_B&=tHL$n>mOF@-Ms zNX<)%m!M+p0>1$o9?O<}1Tz<{))VYgW7(S&nYq!C5VtIrRM_y!IZDT=wNl-SkJ$5a zPVOa-NYHh>N^;htUs7ia-z)hyu!2w2MA6lL0gv2bB{r*D{7rPVUNpr)_!~BBSM!5o zbTwz$DO-#ZMrd}Xv0xG_ZFCehVSmjXA12&@uatwRc|;sV&&R!ENFKX;or z9m-3htNr~^bhTU8+hcGgP1iE^=i}J>k7B$!d3%2x$4VYUH{1QLGa>GXRCsH!J=)_% z^R9agmp$amG`nZ>7e8xM#xiPkgxfjDeA%-aT;sCm+RLJvZx3Gf+$vG4fx>+W(?oI& zM=|UvY`x;jYh+&Ci0+oKHK2vZL?np$zf^P{^=xt3wH7kg*}jqeNQvSyki$oC;7|}8 z^K9=9aB0HEcHOB*u$aejTECOC=%)hIPbPkJ@y38Tcv>hG-D%4t=P+Fc-o;Tl*ppAb z(lfFDox%EeOQP-qN}Q20>GrP-0VfmDyQyOs^Wuj`6@P1FpP$H9++~_;&+ACvVx)FT zrR$)oC)?6;lKWe+03sNzF&ue>54h(E4KQIbI=n&$Qov1wnbIm*MV z^)7pA#qCn=VPTp{?h&p%FvcQcdpM-f>xGhyQpx%dC69(ESs$Y0F&4eSo|?K>DtQb_ z(A%ajUQBsXc$^hJZBLC$PLWX@g!|1*;XQxA6U4Dxt~?f!%ad#^a;dmbDtS`mVk+-b zAxfSMQPLw!eO4;z2{Cm8H?`sqDX@W?YOejKLvrZ}$>kYgYF#R15uOP#6(idT<$WeZ z$wp!7RZ_{u5K}h^Q(ut+n@Fd{QOK9AwYo7Rm*<43@ig5-crL`$&EQ&f)g;?`yh>~p~N?v0h>`_y@_ev$NL5X$Hew`}` z4BB+>?lrCiY}+A;dR=5~8hf^L)a4|E(0#_(3C8d-I9+f_MvlR3|3B48_TaPjgpBD( zrPIR>o=TDzfurNdb76@a?HTEY3HQpxoe&Tm_sw)RZlk@#emtFBwb4E>YVXqzR2Z43 zWw6bZdP)X6NXH2oZ0II?Ny$RGtBdd!HZdUHBss=>qsPSQQ1Dsu@x5x=fVY9LdZDn+ zJb=H$YkABbso()6*Vs zt&;*HqlFU1Wgv$)9l@a>IOcoe?}O`5Xuf1;skhmqQ(w&ljX()>QV2dzvF&AjaK|q7 z>r6_v+4f?S&E6_5`##_|08{lWp3eVkXBGI6DXVa&yuTFqFhs*g{H9=uOA36%1xQ=F z(!o=xN@Ci9@#E&6sZA4Km=0^O4xjs-+D;|&vKCxx;f*QBQ_Jt_z(IPeYV^&U_!jlKoQ2=OIdPckzVcd>*3YORgj}JR7nIUxq08ioN;^2D7j6 zlnp1h94Ys8h}<_oTkG!Ykj%d2N_tgGCEwzPz%#$7S+U=SNPf>HQ|Cy@??bZtCp+>h z%=$r?b*+^9Aw=%S5VL*=$!xzc>tU&6Kg|06SFCoopY=J2Yp8vD)PylRW#UgF2h)7^ zXP$$()BePB0NeIYiTW8R>-_!~(P{euN8{Wt*zAO`jRc1}t%R65?XTckJFSsXn?!LL zQe(T0EJ6?*bEiEBuDR33J|jgbL3UcU@&$YR;A`@?tq<6RVML1kCdBwSFS_pUqU#=% z0>6g{91>kOx<6774q>zQ?LS1<9V!L>-~uGtA~{CaU1Z{PDEM;#bzS!JZ)yzt;vjBr z9T9!&Mk#hA#3hbGt<_VHgt){p-YvSHl1h$+C^;UYn_R zKq-{WU{yc}LE)$|M9#yubaHwe z8k2=iGmE<`;r}3e(dN=_C(h99d79L9w-b6Q+SC?^Inxb zx7D6w*AzDOJ2jGnw(aM8$LiW*tFeIqXuBLYVl({fGyeG-0sdl8m*!;yCJB zk!-u{`9sqTRI@~pjL@_K938STN;l8mG#b;BLF;I2vRcBp49_Y$Pvj^#=F2sDMaw!G z2|Z??PgLMR5P~RG?3=@tHv$%-*;Rcy~Na!(_m0nbHD93=0BptVJ*v5<6 zO&V8L-(BMlx2{%dnvC`AboOfm$n3Bx6Noa+J>2ZCB&F_dtx=l9<9ne+7qH zO_t5%pctJ|HquLybO*|)k&(j$ipwAsmsj9W5FGP#A)D(FbNn;kQ=`YeC`Bm&wwmVn zIb1XgYCzatkMMW{X5xO8M3GFOgktv4Utu@gW=~18<&y#8Z>Cm>0Sb%S1c_8BF(5?8 zK!rWJSH(9sQeq&NAZ=$#4k}Gn*@Y%fhtkEyykYM*Fc$!8Hx#vHcFjRJf$dULx^I^X z1}UV{?BIh#vI;&Mj zk#wB040-GfN{|{B1B6` zh~t!U887ys)5$Waximy`nW0(Uhr@UNCr+x$#ni0(GO4CKM9oM;4GKV^JVeVVE^{Xa zC=b#P-ZAQaqZQwhnnx+T=qArTg`;SQ4x>SS{FCEKM+H5*tVnq2DS(Ny%Y)!h5FFue zf^#ZYG)6>hzFFfwGqTSrHe>=0AULBC~pdxSnTE6-e%XeEC;dM(6lUqPPrF@h(Ie zKiG)VjWdDiB8o_G8na8OHA%lGc^jPQbFG|%7N+y-n)PWm0$~^UIp#Dpp*=9Q; zVU2)^4qQ_%y0qY!=b24hk@>{K6>&XRNfejioSooM5FB$+eOwRw_APsFcI#VsP}$5G z^dU6-%Vtkh78%!!@gunwTeEUvxmq}?3dtHaHY0LWb8uX`0H{=OTYwV9`WYM=mkrK2 zqeMYlx`RTWfkiXSWb8w^yJ zM3IaYHNk@VF?VCFjeWmg4ex$JLP)?0nP`E$*L0C-Z2d)|NXCkqWI;`scJw{NypqwP zP$VN!-K~Okh6NUT<5hPXur3KB8Ea-|T2LzwZe#}P35g;ZD{8U{g--R%JI&u1uwNvM zgsiZ$OfbB{?CP`5K$V;(ia|0~)Y%r)@$B*&4OG`@vQv|g@Qo*+h2Nq8+05+zbu{j)sFb0-B#ffKk35CoF3R>*V<>WF{XR@0enj*FxCe=Mx9 zHVf?OS!X_DWOlzqk&G2J!-8^tvD9s#zLlsM^2}x$+uJ!TP)B6aw2;Z!OfD0sv!9z* zA28}H57|(dsbrpD&@7G$)S2hwOXtBLI3W*S2_qqEIc8g6KgXnE+!GFi*XJdQWUQzT z3##whVWz&gO`=E!UtgMs&N&vCMkXdRGX$9=W-aoZUBld8@jk^ zGKJnt)^R*n37nqC(MW_~aJGU&h6#?Chjfc~rj~Hk=1G^Kx&%!{FfKzHZFhh}L2%4I zwlt*lLXUaQbsm?Y3)6U-Gd&ag)U!s%jK(>P!g&fWy{S2taaf=^w!F3Qb)(LPOBBgi z{qB4Vs_TQxj~l4@K=~*$60*WBu)qoj{d9=|dqieNGFH@u3Y+|^niBb=l(>*2*wr7| zGrG?Uae|9@79m}Y?6D26|7C4t*dGEa2uoH!(-97cS{Phnj$Qwd-j^Lt)WWq2D_3p8FNvm3?U=V<>eQKw z+S_6D)JoUXg6c+Bb6s;wOG8b)tD>M{#LSMl?G^p2d~FrQ6-7(uFFL29XhF%;qJo0b zilW6!D~e_CiXt}VK|PuEKZ4H*&%9S3RSGpN)pfPi4bA_NmemPZ@BQOJjxeB0>W|h4q{dQxgtLhc;fsk!9F8GksX$tbj}nqa6Udkr0li~O1a#jUsjT^NJxoZr zUr$ks4V@Ve>$(9&OhvQxvT(xGT0{P#hxNWjDq_9ZLl5iWL6$%A+@$V6`2wzmTXjYY zc^a#{Zf`^5e^R%+o@5KR)HnHj?ppVst{SlL|p zr})^D`PlMT9d(V3E|0garLnH%&%r0b!uXEG^_?AE9j$Xa&TpR~J2AEOuJv$S>gluEyWDd-W_9xZZ#40=M|Iu5e2GaV zIN{n)ffuMOgKG=G4lKDSoDr=gW-0WgRBarOzdC{(JmNnEdobC9+sl^n?fU#oc+^%yMx6kmeoid;g zA1eT;ZSuLBJpP3<%EEze@zqxOYS@@jI@i|XMR_an+45jVLC-x$RowpUh|?n3@nh<% zyn1_HJFYHN{MS#4^mlH<`iqL@_W!z~|Eg#7Gua)AHp0K_tW2eeEjXqoD^32?dfM-r zoM3k~F;|?HOvlzZEuN0baWx*Ig)v&cL|VeCoC;lA(cf1&p{ccNQO9|%rkmDiX;5(S zq*MZ&p=cSj0D~B_+1lZHod3D139Q1dC8z~PQ${?C_pmQ`MvneHjde@m4KxLQo z*0~z%s+zr3^==xRG6J0!gOhb6vg$PrI6Rl!K3ZR6NQ#o#AQKbpe}*|UZV7X5($nP7 z{%7g2-Nqn4ua(xd4OmR$b6syHW5o;cIHDs`WJf%K^UsiRX#nAc88Z zYpShk@-^4~+f`;MdqQmc?3w3wwsmyQoR2a7&#GWxu-TXe9@o2qSBNnX(6B3NcGo67 z(S;iFwxD--+ztO>4RtP9aC#S3N*?S;Dua^7nugly%KuCWTVU7X{mUn*Y@S_nIUA>A z2?Cc>s|-?UY>iXX*r;Rn#8ulJ$$b{LwRVo3zjU#yx*m1T#yGU%uw{+Yu|`2%nAVSF zhH1ILil{fKe}UWOa-HFx)r19LL#O%uzgM~L!B)aG?4iL1B&N2)JZ^6ULurT z;n3WIdc~pDh}6DLEk7-E9R!|Ti@T}5o=rP$&tT6ww1|H#_Q-MjfXraInlN868Qnxi z7ZkeuEjMSfJYDP0o(t2$Uaho^Mp` ze1`*7<*?H;KY`k$yt6pgzxL90`# zu9}+sk6xRI(X^!|f!(>ok<9i-Xnlec=TcVQOKa-a;%cr(E*=l&9cPvY+OnP;tgzZ2 ziUM328)DnQYlecQ|qp8ayMEe<_I^|dTS$)jn7-- zZmy}VwTK8eWe~|`>HD3L?6TgPoL5KpfzNO5tql@G0-~|(Xm2f2Z>p^KcpIx~$*T&r znua=`2lck^rA$^3sp)J@q&74NNnrVXHM|2Isfn2YgBp)WUE3FV-q2f9{rk@f^DnOz zGe{EP*`|fCW7(H|v;wPYBd1cUJhT@J(S0%6NY*z>6Z27qXdEkuLOB|1D!rcOs(Sz0 ziTS;Jlh~_a*tQOP=*)MQeG%Xg~H{ ztd?TUE1nIC(-HuUVD&_dJ|lw7iPK_OL5vp8(qptOL3nkL;s&aeX-$yF^FmF=25J%o z;TekYoyry8gS`4Fyi>2duEd{FlOWu_yQY*CrfEt3mDlC_7knWm@howGBimll|0F5C zB(49+Q&tYdnzxws)Ynxvd#Y-^G!HA+=sf22_-GOofwbnD>gEQwkI&J9)KpehvMoa#g+kinZNf~vxsFS#zFK!nLoJ^U_O(tsPFBhb(zJ||m-6#8 zE%U^b!1Wd%qFTG=FT(lbOm=Z^Ej|ik5Y1HF?g{eZH0Z-8P2zr>bJ8TyT~3-*8p?%_ z$FqewT8nTJYEQN~JrEt3u62ooTZ0rDHkUP}YfU17T9YQd-s);h-F!6ZEnrc}T7>Fp zb~pRHe3DGf-BR1^^;TCIGi@|tgn#Nk^=LW&gG_^`&Qs&9XP;nZ?%y=t z*RbeOTv3a=5+k0E9AP-?pQc5zFLMHgLbGO@qG-z;BkDhPcD~8xs;0Wy26wHn`J&TZ znygtGZ%adcO_dl4nroZuF&7nS{)@(yQib(D$pxQDUu8|Bug1-y zM(O2foNABHV-3G+c;5=giPky~ZL zI?zwc1nmVx-bAGDL1&isAtK)(lHm-?(vBh$`2tp|rIf@MaQ~emVPV-S=<5RsHzV>J zBD{3Ui}=yhi|qD%Ejuz5^er!;eh{MkyN@KQEucod%oY?nvN?T~iGE+ue%BvPd)`*I zxe#qyyp`Ho$d290qKX^?)jPJ@Y>#ba9}Pzi8$rAA(kX=~|A9YJwS9;k*k)tb|Lnxl zUQWfS9R^2zRhXwmy=t>%BJ$iHC{Z3F^?{Vr5orsg%tPcFnWC;jWZkQ*vk)!09`yTx zgwGNAMJ5~rJ?J%A2}Q5r`N(Sm(Fl4;AYnNoH^_urK))DB*n!ALGU02`F|SKiX?RvJ z^mU=C6!f$}!W=}-f1MpG(z4adK!_8_x@wmM_z;=0 zO8{nr{$`f|H0{O@Q+5l$EYLUXmVjFjxqmlHFGbc*fZiKO_yUn%Wx{dL1K*H{f;Z4# z-w+5d=!JoV3lOo^x8cg(MCVG>JKKM49RtB#e{*KL7@eaGa99~%ux^$1oVi5U#SPLRAvqwj2 zxk>_4Mq&DVG+&EVW}4`y1U>J7voFicSDbeB1Hc_;{0WsK z3+R-Po4KTY(c+HzouhL{<`?7_6&4f~<(KDXE?6|bt$p$0`HS3(+Gclj;UsqMqW00< zx#yLaBJtemb357^+Lw)+e@;7)g)`gArx#Bj*;?3MSl(VdJvTdZIObvQ@r~me>pi2g z=Ph2+wqQ{gxc`rKw*9+?{@>ZzbozwKv21u!ST?(FwDx_}^wzSXlGdW>r6p}6+NWdj zj7N)=g{<)_EvdF2DqpOwoWd!3yqm|V-q_Q*SFS3`sWj~4IF*V0Fi!QyzL;HHqQv$d z!Wr~{x5P%P+{Go(LujF%o|>xs>2Z|8)|4n4oJGS53)t?7&N|lpMA&K0f*Iu{%8y|XG4Isr04wav=vJCbsUc8v2y8Ls-_j3 zHwOF5vBmKPlrH1EMC5oir_w+PxhzmZt`yY8JasfEAxH1Y3ORa1RLIdQR~K=)wxpmS zy$d4*>0Ob_Y0-jH*fhJI*z?^#wc(0#K~L(B+I9b)!3oeH4DJ`ypB>bnOZt(0ob6Ol z7jr`@K?#HFKnXkOtx6$BFF*-{I}C&AL zOKg)R_B?AnUrk^i{-$+Jeho&%@&D!uin^YlTtRAVkRpE+sq;V`!KU)xZehv>YH5&n zA*h?#wBKOzR+fKIE5f1mAY!}zOIa(Ze~~rbmXcWuo+<0MMb>YNtUvrOWvvFY{vw#Q z;F+@iRAl|B$okv=Qr5kKS^pHwTJTI+|0uHlQDpran{r4iV9TF$#IQAoG#wub6GM5M zU4ICV>=b+gaR`0wQxR2V^hX|5di7NNq1l!0a-JLc^*~q<$1nEga9$MlVmP9amA+7l zv(cGK@W`2H>=i3o3yO3Jn@+kOhiig}l|jTEpi(&Eo*?xgs3X`^+hf>^JT`!$JSM8P7jU4FP_?}hq%ifj z{e|;r5*!CgEVnMq`W(Dio{INYCG`U+dLKyTU%tontU97aD@s()y+^f!_U;;<;)2Z8 z*i>6J_F}a0g1Q`A95;>zt~lFQh|@?L$5&}5)8lOEh`Tv84V18r-T@Tj4~#2c*m`YH?r%Z4b)ZD+ zJQS3BEGS1S5sHjo10`DLji4M~iBPl-Ek$UWC$QP!`Yfe^?F!dVQHHQSz4T;df+aTD z5}PJs4MMWr5}RX=+2(mQ7g$DrX96;aTN}MkF{mul7+(g4)GI5zWe|dEJ#ppWGiFz z%8Wqu9p6E#qf2{i???q=-GS;@XrQ|7XkT{72tJUL`MbI0lNN(`GAr&o+PxeQ;65xHkr)p8CjWFN-q zWo-33&V<6i0s~o)6vf&Tv^-FU2PKEMI$M1U{yN$juj?!=UhiY?9mamR#qMEC@e@t^ zlrVPvyG~Cbjn-7bX$N){81>c@CMqPC=Vf;8LF`aGe)6P>y2JnMsYT2JgW2!|Jyoe; z+wdFzycVIyHe}J?mKbe!g+%9ZG>N<<|GqzjF9MLdPYMj-0%r9?+4{GgsaS`DdFcoI z>4|zmWW7cz#BBsS`$r=F5lz2{A3Pj9hjO_r;qHGb8Ab{JDlx;UgiK39E%TqfzAyQO z;0sr?>%uRWC+V)ZC~|6q;$ZSJnf!hdR?VRQlPcG;)MWh(yEB|Adz~J(AsIiE%?rna zSACMbC7k7@;J755P2P#8Sudx+&u?WPrs$QN4u4+uD>oao3)h%xKm2C@Yssr5TP^=Z ztm1#Di)*HH4nh?+;TkVR2&2DUi~p?Ny$cV|`rKhx*(#`u+$*JPc+t!5fRpfSN@!d} zs0+!ao@dkRA5x$m-}!7$!ymTS-(in|N~t%RMh%1puJ@sr98zzGKd8O>4g4mkg{|zTr$*M%_=?cNl@7lvVCB87vG<+hQ4y|j`p*4FQ z{x&;aXuU)xj^|pBrt3}02dp6jt(Mf6t;^6SDW|jJ8EBJ|4AlQh>P`q}LmJL*dgXiy zJD!E#AWvnZGxb#O4keX4`pXLsZ%%m~6ZXJBRwl={X-E^iiBbH6KBbjC z3$?gGQkkXavSXQgpD`B#B2)Hg?rkdy0K|GME}-y41NA5!nEkb0cW&d5fM{J{2QqegDc*83>=EHOvVNNj-?3T+?T zOAQMccebBbjxwXSqdyh zRW%RLv*4o(2k67>ws^K?0EV}$c=iV!J@IVZK=h_I_Rv864COMW1Hbj zqJQ5_(?^7EsvVskoJPJ-hkdvH1A97^+Uw-hER1e;%aJ%cO>uOvl-qDtya||7<&k(nL*)?VQ z23Af5FOrpTdP*)Hb>A1x9~X&7-5bL3 zs5?7?PEb$dgs~-@J$0I1!=mhX%JViDd&A{JmM;+b6)#cYnX`5rbl(X1=r;!gR52p_ zxf!+aXfVdeQOnLB=2FeJNASmA+H9npC&eRXBjFM-zKvkV=)rXw&J{y@30?kOhn-p} z7%njAiLvHIBZt3*W2htU6+=U$wk$1#;RNoXZ`t)TNRYCi=eZ?a-QkWEF&^0Ey%S7K{qIX4!k{^qL zzG$97mX?IkDi0C<^!zkLd?KY^1l=~81jedpcB}NI5Q>Nq zLNOSPvSVap-GjSZQkj}9|EIJN_^^Q0LMY^%rRFOARiT-OS|FfWd zSAamn8U@uB#;In|n(f*eT|POSApD6t6E%#=Z&b|T6n5Mh=okncAZX!GoxOFge#-Dd zM9JmI7j3rXm!0OOBMoSnTOjB>(*>XmP$7XX#3n#C*J>9cA)sa8X|@S-GU2rqOZ4;- zy4Jh|n^?J37@LZ?VQda!B9O1rV==aJiGJ1?QN|CkF#M%2I;T#d&O=TvR3w8ED!iaX zVa^1#0-Laq9-6!%}Ny_OX|$ZI~fhu0A4I$K`rXcP}K$|HXn*KMaEh z2EeZp5}_Bs4o-c^_hM(A+w}(1p$I87+ru}G3beNP|1Dui#%e@lH;0k!flPaa%uDHV z%c0E1o*Mh;d0!M};VfVGS#A1B6K>F+z#yS}#`I!q_GSVtFCVS88$ma>M?! z!ajM%k_!)U^)A-gX+PFV%Qa@<91Mcd+e9 z%?e;ITG!ZMqz3NSrK3g{uF&HL2gC}y@5W#qP(q7rQ^>$3cvv0dZD(>pCK!g**Bo83 zH6VaD%_N9q1n9Vv)m(!cKxb{j(@Wn*?3QnGsMRzYkwE75mA4x87)zZ1AsbaCYV9=1 zA+At3)!?x1zvA}AE1Miig){(DYkd7+94;6>T zFs{cuW(^JL3FI#)3s?LDXJ%?(Wrjd(@d|LELP)w-sp8c(gi^_)|+CU<2` zeSIa%dJvb??g#WZy{ZwP>@|2vLm|8F0X^P7w=0ijZ_u0lf7vsf9o(QNvBx&(>Hf`o s2C}-R^%N(4kLch3MFu{L=imDWJ_Y3#`+esPh_U<#fV0{9r}Y8<2T3V=PXGV_ delta 108600 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 alNErPG(l5d6O^*2x~QUbbXkR*`2QESJd1V! diff --git a/3rdparty/NPP_staging/npp_staging.h b/3rdparty/NPP_staging/npp_staging.h index c54af5c032..a9c6607dff 100644 --- a/3rdparty/NPP_staging/npp_staging.h +++ b/3rdparty/NPP_staging/npp_staging.h @@ -188,14 +188,14 @@ struct NppStSize32u enum NppStStatus { //already present in NPP - /* NPP_SUCCESS = 0, ///< Successful operation (same as NPP_NO_ERROR) - NPP_ERROR = -1, ///< Unknown error - NPP_CUDA_KERNEL_EXECUTION_ERROR = -3, ///< CUDA kernel execution error - NPP_NULL_POINTER_ERROR = -4, ///< NULL pointer argument error - NPP_TEXTURE_BIND_ERROR = -24, ///< CUDA texture binding error or non-zero offset returned - NPP_MEMCPY_ERROR = -13, ///< CUDA memory copy error - NPP_MEM_ALLOC_ERR = -12, ///< CUDA memory allocation error - NPP_MEMFREE_ERR = -15, ///< CUDA memory deallocation error*/ + //NPP_SUCCESS = 0, ///< Successful operation (same as NPP_NO_ERROR) + //NPP_ERROR = -1, ///< Unknown error + //NPP_CUDA_KERNEL_EXECUTION_ERROR = -3, ///< CUDA kernel execution error + //NPP_NULL_POINTER_ERROR = -4, ///< NULL pointer argument error + //NPP_TEXTURE_BIND_ERROR = -24, ///< CUDA texture binding error or non-zero offset returned + //NPP_MEMCPY_ERROR = -13, ///< CUDA memory copy error + //NPP_MEM_ALLOC_ERR = -12, ///< CUDA memory allocation error + //NPP_MEMFREE_ERR = -15, ///< CUDA memory deallocation error //to be added NPP_INVALID_ROI, ///< Invalid region of interest argument @@ -244,7 +244,7 @@ extern "C" { /** \defgroup core_npp NPP Core * Basic functions for CUDA streams management. - * WARNING: These functions couldn't be exported from NPP_staging library, so they can't be used + * WARNING: These functions couldn't be exported into DLL, so they can be used only with static version of NPP_staging * @{ */ @@ -569,6 +569,13 @@ NppStStatus nppiStTranspose_64f_C1R_host(NppSt64f *h_src, NppSt32u srcStride, NppStStatus nppiStIntegralGetSize_8u32u(NppStSize32u roiSize, NppSt32u *pBufsize); +/** + * Calculates the size of the temporary buffer for integral image creation + * \see nppiStIntegralGetSize_8u32u + */ +NppStStatus nppiStIntegralGetSize_32f32f(NppStSize32u roiSize, NppSt32u *pBufsize); + + /** * Creates an integral image representation for the input image * @@ -587,6 +594,15 @@ NppStStatus nppiStIntegral_8u32u_C1R(NppSt8u *d_src, NppSt32u srcStep, NppSt8u *pBuffer, NppSt32u bufSize); +/** + * Creates an integral image representation for the input image + * \see nppiStIntegral_8u32u_C1R + */ +NppStStatus nppiStIntegral_32f32f_C1R(NppSt32f *d_src, NppSt32u srcStep, + NppSt32f *d_dst, NppSt32u dstStep, NppStSize32u roiSize, + NppSt8u *pBuffer, NppSt32u bufSize); + + /** * Creates an integral image representation for the input image. Host implementation * @@ -602,6 +618,14 @@ NppStStatus nppiStIntegral_8u32u_C1R_host(NppSt8u *h_src, NppSt32u srcStep, NppSt32u *h_dst, NppSt32u dstStep, NppStSize32u roiSize); +/** + * Creates an integral image representation for the input image. Host implementation + * \see nppiStIntegral_8u32u_C1R_host + */ +NppStStatus nppiStIntegral_32f32f_C1R_host(NppSt32f *h_src, NppSt32u srcStep, + NppSt32f *h_dst, NppSt32u dstStep, NppStSize32u roiSize); + + /** * Calculates the size of the temporary buffer for squared integral image creation * diff --git a/modules/gpu/CMakeLists.txt b/modules/gpu/CMakeLists.txt index 3c10b0ee2b..f25036ac24 100644 --- a/modules/gpu/CMakeLists.txt +++ b/modules/gpu/CMakeLists.txt @@ -35,6 +35,13 @@ source_group("Include" FILES ${lib_hdrs}) file(GLOB lib_device_hdrs "src/opencv2/gpu/device/*.h*") source_group("Device" FILES ${lib_device_hdrs}) +if (HAVE_CUDA AND MSVC) + file(GLOB ncv_srcs "src/nvidia/*.cpp") + file(GLOB ncv_hdrs "src/nvidia/*.h*") + file(GLOB ncv_cuda "src/nvidia/*.cu") + source_group("Src\\NVidia" FILES ${ncv_srcs} ${ncv_hdrs} ${ncv_cuda}) +endif() + if (HAVE_CUDA) get_filename_component(_path_to_findnpp "${CMAKE_CURRENT_LIST_FILE}" PATH) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${_path_to_findnpp}) @@ -68,19 +75,16 @@ if (HAVE_CUDA) string(REPLACE "/EHsc-" "/EHs" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}") string(REPLACE "/EHsc-" "/EHs" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") endif() + - CUDA_COMPILE(cuda_objs ${lib_cuda}) + include(FindNPP_staging.cmake) + include_directories(${NPPST_INC}) + + CUDA_COMPILE(cuda_objs ${lib_cuda} ${ncv_cuda}) #CUDA_BUILD_CLEAN_TARGET() endif() - -add_library(${the_target} ${lib_srcs} ${lib_hdrs} ${lib_int_hdrs} ${lib_cuda} ${lib_cuda_hdrs} ${lib_device_hdrs} ${cuda_objs}) - -IF (HAVE_CUDA) - include(FindNPP_staging.cmake) - include_directories(${NPPST_INC}) - target_link_libraries(${the_target} ${NPPST_LIB}) -endif() +add_library(${the_target} ${lib_srcs} ${lib_hdrs} ${lib_int_hdrs} ${lib_cuda} ${lib_cuda_hdrs} ${lib_device_hdrs} ${ncv_srcs} ${ncv_hdrs} ${ncv_cuda} ${cuda_objs}) if(PCHSupport_FOUND) set(pch_header ${CMAKE_CURRENT_SOURCE_DIR}/src/precomp.hpp) @@ -114,6 +118,7 @@ target_link_libraries(${the_target} ${OPENCV_LINKER_LIBS} ${IPP_LIBS} ${DEPS} ) if (HAVE_CUDA) target_link_libraries(${the_target} ${CUDA_LIBRARIES} ${CUDA_NPP_LIBRARIES}) + target_link_libraries(${the_target} ${NPPST_LIB}) CUDA_ADD_CUFFT_TO_TARGET(${the_target}) endif() diff --git a/modules/gpu/include/opencv2/gpu/gpu.hpp b/modules/gpu/include/opencv2/gpu/gpu.hpp index 70a34eee6e..3979d62dd7 100644 --- a/modules/gpu/include/opencv2/gpu/gpu.hpp +++ b/modules/gpu/include/opencv2/gpu/gpu.hpp @@ -1380,87 +1380,39 @@ namespace cv explicit BruteForceMatcher_GPU(L2 /*d*/) : BruteForceMatcher_GPU_base(L2Dist) {} }; - ////////////////////////////////// CascadeClassifier ////////////////////////////////////////// + ////////////////////////////////// CascadeClassifier_GPU ////////////////////////////////////////// // The cascade classifier class for object detection. - class CV_EXPORTS CascadeClassifier + class CV_EXPORTS CascadeClassifier_GPU { - public: - struct CV_EXPORTS DTreeNode - { - int featureIdx; - float threshold; // for ordered features only - int left; - int right; - }; - - struct CV_EXPORTS DTree - { - int nodeCount; - }; - - struct CV_EXPORTS Stage - { - int first; - int ntrees; - float threshold; - }; - - enum { BOOST = 0 }; - enum { DO_CANNY_PRUNING = 1, SCALE_IMAGE = 2,FIND_BIGGEST_OBJECT = 4, DO_ROUGH_SEARCH = 8 }; - - CascadeClassifier(); - CascadeClassifier(const string& filename); - ~CascadeClassifier(); + public: + CascadeClassifier_GPU(); + CascadeClassifier_GPU(const string& filename); + ~CascadeClassifier_GPU(); bool empty() const; bool load(const string& filename); - bool read(const FileNode& node); - - void detectMultiScale( const Mat& image, vector& objects, double scaleFactor=1.1, - int minNeighbors=3, int flags=0, Size minSize=Size(), Size maxSize=Size()); - - bool setImage( Ptr&, const Mat& ); - int runAt( Ptr&, Point ); - - bool isStumpBased; - - int stageType; - int featureType; - int ncategories; - Size origWinSize; - - vector stages; - vector classifiers; - vector nodes; - vector leaves; - vector subsets; + void release(); + + /* returns number of detected objects */ + int detectMultiScale( const GpuMat& image, GpuMat& objectsBuf, double scaleFactor=1.2, int minNeighbors=4, Size minSize=Size()); + + bool findLargestObject; + bool visualizeInPlace; - Ptr feval; - Ptr oldCascade; + Size getClassifierSize() const; + private: + + struct CascadeClassifierImpl; + CascadeClassifierImpl* impl; }; - + ////////////////////////////////// SURF ////////////////////////////////////////// struct CV_EXPORTS SURFParams_GPU { - SURFParams_GPU() : - threshold(0.1f), - nOctaves(4), - nIntervals(4), - initialScale(2.f), - - l1(3.f/1.5f), - l2(5.f/1.5f), - l3(3.f/1.5f), - l4(1.f/1.5f), - edgeScale(0.81f), - initialStep(1), - - extended(true), - - featuresRatio(0.01f) - { - } + SURFParams_GPU() : threshold(0.1f), nOctaves(4), nIntervals(4), initialScale(2.f), + l1(3.f/1.5f), l2(5.f/1.5f), l3(3.f/1.5f), l4(1.f/1.5f), + edgeScale(0.81f), initialStep(1), extended(true), featuresRatio(0.01f) {} //! The interest operator threshold float threshold; diff --git a/modules/gpu/src/arithm.cpp b/modules/gpu/src/arithm.cpp index 28acc6a713..968e192d55 100644 --- a/modules/gpu/src/arithm.cpp +++ b/modules/gpu/src/arithm.cpp @@ -170,8 +170,7 @@ void cv::gpu::LUT(const GpuMat& src, const Mat& lut, GpuMat& dst) if (src.type() == CV_8UC1) { - nppSafeCall( nppiLUT_Linear_8u_C1R(src.ptr(), src.step, dst.ptr(), dst.step, sz, - nppLut.ptr(), lvls.pLevels, 256) ); + nppSafeCall( nppiLUT_Linear_8u_C1R(src.ptr(), src.step, dst.ptr(), dst.step, sz, nppLut.ptr(), lvls.pLevels, 256) ); } else { @@ -186,8 +185,7 @@ void cv::gpu::LUT(const GpuMat& src, const Mat& lut, GpuMat& dst) pValues3[1] = nppLut3[1].ptr(); pValues3[2] = nppLut3[2].ptr(); } - nppSafeCall( nppiLUT_Linear_8u_C3R(src.ptr(), src.step, dst.ptr(), dst.step, sz, - pValues3, lvls.pLevels3, lvls.nValues3) ); + nppSafeCall( nppiLUT_Linear_8u_C3R(src.ptr(), src.step, dst.ptr(), dst.step, sz, pValues3, lvls.pLevels3, lvls.nValues3) ); } } diff --git a/modules/gpu/src/cascadeclassifier.cpp b/modules/gpu/src/cascadeclassifier.cpp index e6a4e7295f..3a7d2ca47d 100644 --- a/modules/gpu/src/cascadeclassifier.cpp +++ b/modules/gpu/src/cascadeclassifier.cpp @@ -42,69 +42,751 @@ #include "precomp.hpp" - - - using namespace cv; using namespace cv::gpu; using namespace std; -#if !defined (HAVE_CUDA) -cv::gpu::CascadeClassifier::CascadeClassifier() { throw_nogpu(); } -cv::gpu::CascadeClassifier::CascadeClassifier(const string&) { throw_nogpu(); } -cv::gpu::CascadeClassifier::~CascadeClassifier() { throw_nogpu(); } +#if !defined (HAVE_CUDA) || (defined(_MSC_VER) && _MSC_VER != 1500) || !defined(_MSC_VER) -bool cv::gpu::CascadeClassifier::empty() const { throw_nogpu(); return true; } -bool cv::gpu::CascadeClassifier::load(const string& filename) { throw_nogpu(); return true; } -bool cv::gpu::CascadeClassifier::read(const FileNode& node) { throw_nogpu(); return true; } +cv::gpu::CascadeClassifier_GPU::CascadeClassifier_GPU() { throw_nogpu(); } +cv::gpu::CascadeClassifier_GPU::CascadeClassifier_GPU(const string&) { throw_nogpu(); } +cv::gpu::CascadeClassifier_GPU::~CascadeClassifier_GPU() { throw_nogpu(); } -void cv::gpu::CascadeClassifier::detectMultiScale( const Mat&, vector&, double, int, int, Size, Size) { throw_nogpu(); } +bool cv::gpu::CascadeClassifier_GPU::empty() const { throw_nogpu(); return true; } +bool cv::gpu::CascadeClassifier_GPU::load(const string&) { throw_nogpu(); return true; } +Size cv::gpu::CascadeClassifier_GPU::getClassifierSize() const { throw_nogpu(); return Size(); } - +int cv::gpu::CascadeClassifier_GPU::detectMultiScale( const GpuMat& , GpuMat& , double , int , Size) { throw_nogpu(); return 0; } +#if defined (HAVE_CUDA) + NCVStatus loadFromXML(const string&, HaarClassifierCascadeDescriptor&, vector&, + vector&, vector&) { throw_nogpu(); return NCVStatus(); } + void groupRectangles(vector&, int, double, vector*) { throw_nogpu(); } +#endif #else +struct cv::gpu::CascadeClassifier_GPU::CascadeClassifierImpl +{ + CascadeClassifierImpl(const string& filename) : lastAllocatedFrameSize(-1, -1) + { + ncvSetDebugOutputHandler(NCVDebugOutputHandler); + if (ncvStat != load(filename)) + CV_Error(CV_GpuApiCallError, "Error in GPU cacade load"); + } + NCVStatus process(const GpuMat& src, GpuMat& objects, float scaleStep, int minNeighbors, bool findLargestObject, bool visualizeInPlace, NcvSize32u ncvMinSize, /*out*/unsigned int& numDetections) + { + calculateMemReqsAndAllocate(src.size()); -cv::gpu::CascadeClassifier::CascadeClassifier() -{ + NCVMemPtr src_beg; + src_beg.ptr = (void*)src.ptr(); + src_beg.memtype = NCVMemoryTypeDevice; -} + NCVMemSegment src_seg; + src_seg.begin = src_beg; + src_seg.size = src.step * src.rows; -cv::gpu::CascadeClassifier::CascadeClassifier(const string& filename) -{ + NCVMatrixReuse d_src(src_seg, devProp.textureAlignment, src.cols, src.rows, src.step, true); + + //NCVMatrixAlloc d_src(*gpuAllocator, src.cols, src.rows); + //ncvAssertReturn(d_src.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); -} + //NCVMatrixAlloc h_src(*cpuAllocator, src.cols, src.rows); + //ncvAssertReturn(h_src.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); -cv::gpu::CascadeClassifier::~CascadeClassifier() -{ + CV_Assert(objects.rows == 1); + + NCVMemPtr objects_beg; + objects_beg.ptr = (void*)objects.ptr(); + objects_beg.memtype = NCVMemoryTypeDevice; + + NCVMemSegment objects_seg; + objects_seg.begin = objects_beg; + objects_seg.size = objects.step * objects.rows; + NCVVectorReuse d_rects(objects_seg, objects.cols); + //NCVVectorAlloc d_rects(*gpuAllocator, 100); + //ncvAssertReturn(d_rects.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + + NcvSize32u roi; + roi.width = d_src.width(); + roi.height = d_src.height(); + + Ncv32u flags = 0; + flags |= findLargestObject? NCVPipeObjDet_FindLargestObject : 0; + flags |= visualizeInPlace ? NCVPipeObjDet_VisualizeInPlace : 0; + + ncvStat = ncvDetectObjectsMultiScale_device( + d_src, roi, d_rects, numDetections, haar, *h_haarStages, + *d_haarStages, *d_haarNodes, *d_haarFeatures, + ncvMinSize, + minNeighbors, + scaleStep, 1, + flags, + *gpuAllocator, *cpuAllocator, devProp.major, devProp.minor, 0); + ncvAssertReturnNcvStat(ncvStat); + ncvAssertCUDAReturn(cudaStreamSynchronize(0), NCV_CUDA_ERROR); + + return NCV_SUCCESS; + } + //// + NcvSize32u getClassifierSize() const { return haar.ClassifierSize; } + cv::Size getClassifierCvSize() const { return cv::Size(haar.ClassifierSize.width, haar.ClassifierSize.height); } +private: + + static void NCVDebugOutputHandler(const char* msg) { CV_Error(CV_GpuApiCallError, msg); } + + NCVStatus load(const string& classifierFile) + { + int devId = cv::gpu::getDevice(); + ncvAssertCUDAReturn(cudaGetDeviceProperties(&devProp, devId), NCV_CUDA_ERROR); + + // Load the classifier from file (assuming its size is about 1 mb) using a simple allocator + gpuCascadeAllocator = new NCVMemNativeAllocator(NCVMemoryTypeDevice); + cpuCascadeAllocator = new NCVMemNativeAllocator(NCVMemoryTypeHostPinned); + + ncvAssertPrintReturn(gpuCascadeAllocator->isInitialized(), "Error creating cascade GPU allocator", NCV_CUDA_ERROR); + ncvAssertPrintReturn(cpuCascadeAllocator->isInitialized(), "Error creating cascade CPU allocator", NCV_CUDA_ERROR); + + Ncv32u haarNumStages, haarNumNodes, haarNumFeatures; + ncvStat = ncvHaarGetClassifierSize(classifierFile, haarNumStages, haarNumNodes, haarNumFeatures); + ncvAssertPrintReturn(ncvStat == NCV_SUCCESS, "Error reading classifier size (check the file)", NCV_FILE_ERROR); + + h_haarStages = new NCVVectorAlloc(*cpuCascadeAllocator, haarNumStages); + h_haarNodes = new NCVVectorAlloc(*cpuCascadeAllocator, haarNumNodes); + h_haarFeatures = new NCVVectorAlloc(*cpuCascadeAllocator, haarNumFeatures); + + ncvAssertPrintReturn(h_haarStages->isMemAllocated(), "Error in cascade CPU allocator", NCV_CUDA_ERROR); + ncvAssertPrintReturn(h_haarNodes->isMemAllocated(), "Error in cascade CPU allocator", NCV_CUDA_ERROR); + ncvAssertPrintReturn(h_haarFeatures->isMemAllocated(), "Error in cascade CPU allocator", NCV_CUDA_ERROR); + + ncvStat = ncvHaarLoadFromFile_host(classifierFile, haar, *h_haarStages, *h_haarNodes, *h_haarFeatures); + ncvAssertPrintReturn(ncvStat == NCV_SUCCESS, "Error loading classifier", NCV_FILE_ERROR); + + d_haarStages = new NCVVectorAlloc(*gpuCascadeAllocator, haarNumStages); + d_haarNodes = new NCVVectorAlloc(*gpuCascadeAllocator, haarNumNodes); + d_haarFeatures = new NCVVectorAlloc(*gpuCascadeAllocator, haarNumFeatures); + + ncvAssertPrintReturn(d_haarStages->isMemAllocated(), "Error in cascade GPU allocator", NCV_CUDA_ERROR); + ncvAssertPrintReturn(d_haarNodes->isMemAllocated(), "Error in cascade GPU allocator", NCV_CUDA_ERROR); + ncvAssertPrintReturn(d_haarFeatures->isMemAllocated(), "Error in cascade GPU allocator", NCV_CUDA_ERROR); + + ncvStat = h_haarStages->copySolid(*d_haarStages, 0); + ncvAssertPrintReturn(ncvStat == NCV_SUCCESS, "Error copying cascade to GPU", NCV_CUDA_ERROR); + ncvStat = h_haarNodes->copySolid(*d_haarNodes, 0); + ncvAssertPrintReturn(ncvStat == NCV_SUCCESS, "Error copying cascade to GPU", NCV_CUDA_ERROR); + ncvStat = h_haarFeatures->copySolid(*d_haarFeatures, 0); + ncvAssertPrintReturn(ncvStat == NCV_SUCCESS, "Error copying cascade to GPU", NCV_CUDA_ERROR); + + return NCV_SUCCESS; + } + //// + + NCVStatus calculateMemReqsAndAllocate(const Size& frameSize) + { + if (lastAllocatedFrameSize == frameSize) + return NCV_SUCCESS; + + // Calculate memory requirements and create real allocators + NCVMemStackAllocator gpuCounter(devProp.textureAlignment); + NCVMemStackAllocator cpuCounter(devProp.textureAlignment); + + ncvAssertPrintReturn(gpuCounter.isInitialized(), "Error creating GPU memory counter", NCV_CUDA_ERROR); + ncvAssertPrintReturn(cpuCounter.isInitialized(), "Error creating CPU memory counter", NCV_CUDA_ERROR); + + NCVMatrixAlloc d_src(gpuCounter, frameSize.width, frameSize.height); + NCVMatrixAlloc h_src(cpuCounter, frameSize.width, frameSize.height); + + ncvAssertReturn(d_src.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + ncvAssertReturn(h_src.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + + NCVVectorAlloc d_rects(gpuCounter, 100); + ncvAssertReturn(d_rects.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + + NcvSize32u roi; + roi.width = d_src.width(); + roi.height = d_src.height(); + Ncv32u numDetections; + ncvStat = ncvDetectObjectsMultiScale_device(d_src, roi, d_rects, numDetections, haar, *h_haarStages, + *d_haarStages, *d_haarNodes, *d_haarFeatures, haar.ClassifierSize, 4, 1.2f, 1, 0, gpuCounter, cpuCounter, devProp.major, devProp.minor, 0); + + ncvAssertReturnNcvStat(ncvStat); + ncvAssertCUDAReturn(cudaStreamSynchronize(0), NCV_CUDA_ERROR); + + gpuAllocator = new NCVMemStackAllocator(NCVMemoryTypeDevice, gpuCounter.maxSize(), devProp.textureAlignment); + cpuAllocator = new NCVMemStackAllocator(NCVMemoryTypeHostPinned, cpuCounter.maxSize(), devProp.textureAlignment); + + ncvAssertPrintReturn(gpuAllocator->isInitialized(), "Error creating GPU memory allocator", NCV_CUDA_ERROR); + ncvAssertPrintReturn(cpuAllocator->isInitialized(), "Error creating CPU memory allocator", NCV_CUDA_ERROR); + return NCV_SUCCESS; + } + //// + + cudaDeviceProp devProp; + NCVStatus ncvStat; + + Ptr gpuCascadeAllocator; + Ptr cpuCascadeAllocator; + + Ptr > h_haarStages; + Ptr > h_haarNodes; + Ptr > h_haarFeatures; + + HaarClassifierCascadeDescriptor haar; + + Ptr > d_haarStages; + Ptr > d_haarNodes; + Ptr > d_haarFeatures; + + Size lastAllocatedFrameSize; + + Ptr gpuAllocator; + Ptr cpuAllocator; +}; + + + +cv::gpu::CascadeClassifier_GPU::CascadeClassifier_GPU() : findLargestObject(false), visualizeInPlace(false), impl(0) {} +cv::gpu::CascadeClassifier_GPU::CascadeClassifier_GPU(const string& filename) : findLargestObject(false), visualizeInPlace(false), impl(0) { load(filename); } +cv::gpu::CascadeClassifier_GPU::~CascadeClassifier_GPU() { release(); } +bool cv::gpu::CascadeClassifier_GPU::empty() const { return impl == 0; } + +void cv::gpu::CascadeClassifier_GPU::release() { if (impl) { delete impl; impl = 0; } } + +bool cv::gpu::CascadeClassifier_GPU::load(const string& filename) +{ + release(); + impl = new CascadeClassifierImpl(filename); + return !this->empty(); } -bool cv::gpu::CascadeClassifier::empty() const +Size cv::gpu::CascadeClassifier_GPU::getClassifierSize() const { - int *a = (int*)&nppiStTranspose_32u_C1R; - return *a == 0xFFFFF; - return true; + return this->empty() ? Size() : impl->getClassifierCvSize(); } + +int cv::gpu::CascadeClassifier_GPU::detectMultiScale( const GpuMat& image, GpuMat& objectsBuf, double scaleFactor, int minNeighbors, Size minSize) +{ + CV_Assert( scaleFactor > 1 && image.depth() == CV_8U); + CV_Assert( !this->empty()); + + const int defaultObjSearchNum = 100; + if (objectsBuf.empty()) + objectsBuf.create(1, defaultObjSearchNum, DataType::type); + + NcvSize32u ncvMinSize = impl->getClassifierSize(); -bool cv::gpu::CascadeClassifier::load(const string& filename) -{ - return true; + if (ncvMinSize.width < (unsigned)minSize.width && ncvMinSize.height < (unsigned)minSize.height) + { + ncvMinSize.width = minSize.width; + ncvMinSize.height = minSize.height; + } + + unsigned int numDetections; + NCVStatus ncvStat = impl->process(image, objectsBuf, (float)scaleFactor, minNeighbors, findLargestObject, visualizeInPlace, ncvMinSize, numDetections); + if (ncvStat != NCV_SUCCESS) + CV_Error(CV_GpuApiCallError, "Error in face detectioln"); + + return numDetections; } -bool cv::gpu::CascadeClassifier::read(const FileNode& node) + struct RectConvert + { + Rect operator()(const NcvRect32u& nr) const { return Rect(nr.x, nr.y, nr.width, nr.height); } + NcvRect32u operator()(const Rect& nr) const + { + NcvRect32u rect; + rect.x = nr.x; + rect.y = nr.y; + rect.width = nr.width; + rect.height = nr.height; + return rect; + } + }; + + void groupRectangles(std::vector &hypotheses, int groupThreshold, double eps, std::vector *weights) + { + vector rects(hypotheses.size()); + std::transform(hypotheses.begin(), hypotheses.end(), rects.begin(), RectConvert()); + + if (weights) + { + vector weights_int; + weights_int.assign(weights->begin(), weights->end()); + cv::groupRectangles(rects, weights_int, groupThreshold, eps); + } + else + { + cv::groupRectangles(rects, groupThreshold, eps); + } + std::transform(rects.begin(), rects.end(), hypotheses.begin(), RectConvert()); + hypotheses.resize(rects.size()); + } + + +#if 1 /* loadFromXML implementation switch */ + +NCVStatus loadFromXML(const std::string &filename, + HaarClassifierCascadeDescriptor &haar, + std::vector &haarStages, + std::vector &haarClassifierNodes, + std::vector &haarFeatures) { - return true; + NCVStatus ncvStat; + + haar.NumStages = 0; + haar.NumClassifierRootNodes = 0; + haar.NumClassifierTotalNodes = 0; + haar.NumFeatures = 0; + haar.ClassifierSize.width = 0; + haar.ClassifierSize.height = 0; + haar.bHasStumpsOnly = true; + haar.bNeedsTiltedII = false; + Ncv32u curMaxTreeDepth; + + std::vector xmlFileCont; + + std::vector h_TmpClassifierNotRootNodes; + haarStages.resize(0); + haarClassifierNodes.resize(0); + haarFeatures.resize(0); + + Ptr oldCascade = (CvHaarClassifierCascade*)cvLoad(filename.c_str(), 0, 0, 0); + if (oldCascade.empty()) + return NCV_HAAR_XML_LOADING_EXCEPTION; + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + haar.ClassifierSize.width = oldCascade->orig_window_size.width; + haar.ClassifierSize.height = oldCascade->orig_window_size.height; + + int stagesCound = oldCascade->count; + for(int s = 0; s < stagesCound; ++s) // by stages + { + HaarStage64 curStage; + curStage.setStartClassifierRootNodeOffset(haarClassifierNodes.size()); + + curStage.setStageThreshold(oldCascade->stage_classifier[s].threshold); + + int treesCount = oldCascade->stage_classifier[s].count; + for(int t = 0; t < treesCount; ++t) // bytrees + { + Ncv32u nodeId = 0; + CvHaarClassifier* tree = &oldCascade->stage_classifier[s].classifier[t]; + + int nodesCount = tree->count; + for(int n = 0; n < nodesCount; ++n) //by features + { + CvHaarFeature* feature = &tree->haar_feature[n]; + + HaarClassifierNode128 curNode; + curNode.setThreshold(tree->threshold[n]); + + HaarClassifierNodeDescriptor32 nodeLeft; + if ( tree->left[n] <= 0 ) + { + Ncv32f leftVal = tree->alpha[-tree->left[n]]; + ncvStat = nodeLeft.create(leftVal); + ncvAssertReturn(ncvStat == NCV_SUCCESS, ncvStat); + } + else + { + Ncv32u leftNodeOffset = tree->left[n]; + nodeLeft.create((Ncv32u)(h_TmpClassifierNotRootNodes.size() + leftNodeOffset - 1)); + haar.bHasStumpsOnly = false; + } + curNode.setLeftNodeDesc(nodeLeft); + + HaarClassifierNodeDescriptor32 nodeRight; + if ( tree->right[n] <= 0 ) + { + Ncv32f rightVal = tree->alpha[-tree->right[n]]; + ncvStat = nodeRight.create(rightVal); + ncvAssertReturn(ncvStat == NCV_SUCCESS, ncvStat); + } + else + { + Ncv32u rightNodeOffset = tree->right[n]; + nodeRight.create((Ncv32u)(h_TmpClassifierNotRootNodes.size() + rightNodeOffset - 1)); + haar.bHasStumpsOnly = false; + } + curNode.setRightNodeDesc(nodeRight); + + Ncv32u tiltedVal = feature->tilted; + haar.bNeedsTiltedII = (tiltedVal != 0); + + Ncv32u featureId = 0; + for(int l = 0; l < CV_HAAR_FEATURE_MAX; ++l) //by rects + { + Ncv32u rectX = feature->rect[l].r.x; + Ncv32u rectY = feature->rect[l].r.y; + Ncv32u rectWidth = feature->rect[l].r.width; + Ncv32u rectHeight = feature->rect[l].r.height; + + Ncv32f rectWeight = feature->rect[l].weight; + + if (rectWeight == 0/* && rectX == 0 &&rectY == 0 && rectWidth == 0 && rectHeight == 0*/) + break; + + HaarFeature64 curFeature; + ncvStat = curFeature.setRect(rectX, rectY, rectWidth, rectHeight, haar.ClassifierSize.width, haar.ClassifierSize.height); + curFeature.setWeight(rectWeight); + ncvAssertReturn(NCV_SUCCESS == ncvStat, ncvStat); + haarFeatures.push_back(curFeature); + + featureId++; + } + + HaarFeatureDescriptor32 tmpFeatureDesc; + ncvStat = tmpFeatureDesc.create(haar.bNeedsTiltedII, featureId, haarFeatures.size() - featureId); + ncvAssertReturn(NCV_SUCCESS == ncvStat, ncvStat); + curNode.setFeatureDesc(tmpFeatureDesc); + + if (!nodeId) + { + //root node + haarClassifierNodes.push_back(curNode); + curMaxTreeDepth = 1; + } + else + { + //other node + h_TmpClassifierNotRootNodes.push_back(curNode); + curMaxTreeDepth++; + } + + nodeId++; + } + } + + curStage.setNumClassifierRootNodes(treesCount); + haarStages.push_back(curStage); + } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + + //fill in cascade stats + haar.NumStages = haarStages.size(); + haar.NumClassifierRootNodes = haarClassifierNodes.size(); + haar.NumClassifierTotalNodes = haar.NumClassifierRootNodes + h_TmpClassifierNotRootNodes.size(); + haar.NumFeatures = haarFeatures.size(); + + //merge root and leaf nodes in one classifiers array + Ncv32u offsetRoot = haarClassifierNodes.size(); + for (Ncv32u i=0; i& objects, double scaleFactor, - int minNeighbors, int flags, Size minSize, Size maxSize) +//// + +#else /* loadFromXML implementation switch */ +#include "e:/devNPP-OpenCV/src/external/_rapidxml-1.13/rapidxml.hpp" + +NCVStatus loadFromXML(const std::string &filename, + HaarClassifierCascadeDescriptor &haar, + std::vector &haarStages, + std::vector &haarClassifierNodes, + std::vector &haarFeatures) { + NCVStatus ncvStat; + + haar.NumStages = 0; + haar.NumClassifierRootNodes = 0; + haar.NumClassifierTotalNodes = 0; + haar.NumFeatures = 0; + haar.ClassifierSize.width = 0; + haar.ClassifierSize.height = 0; + haar.bNeedsTiltedII = false; + haar.bHasStumpsOnly = false; + + FILE *fp; + fopen_s(&fp, filename.c_str(), "r"); + ncvAssertReturn(fp != NULL, NCV_FILE_ERROR); + + //get file size + fseek(fp, 0, SEEK_END); + Ncv32u xmlSize = ftell(fp); + fseek(fp, 0, SEEK_SET); + + //load file to vector + std::vector xmlFileCont; + xmlFileCont.resize(xmlSize+1); + memset(&xmlFileCont[0], 0, xmlSize+1); + fread_s(&xmlFileCont[0], xmlSize, 1, xmlSize, fp); + fclose(fp); + + haar.bHasStumpsOnly = true; + haar.bNeedsTiltedII = false; + Ncv32u curMaxTreeDepth; + + std::vector h_TmpClassifierNotRootNodes; + haarStages.resize(0); + haarClassifierNodes.resize(0); + haarFeatures.resize(0); + + //XML loading and OpenCV XML classifier syntax verification + try + { + rapidxml::xml_document<> doc; + doc.parse<0>(&xmlFileCont[0]); + + //opencv_storage + rapidxml::xml_node<> *parserGlobal = doc.first_node(); + ncvAssertReturn(!strcmp(parserGlobal->name(), "opencv_storage"), NCV_HAAR_XML_LOADING_EXCEPTION); + + //classifier type + parserGlobal = parserGlobal->first_node(); + ncvAssertReturn(parserGlobal, NCV_HAAR_XML_LOADING_EXCEPTION); + rapidxml::xml_attribute<> *attr = parserGlobal->first_attribute("type_id"); + ncvAssertReturn(!strcmp(attr->value(), "opencv-haar-classifier"), NCV_HAAR_XML_LOADING_EXCEPTION); + //classifier size + parserGlobal = parserGlobal->first_node("size"); + ncvAssertReturn(parserGlobal, NCV_HAAR_XML_LOADING_EXCEPTION); + sscanf_s(parserGlobal->value(), "%d %d", &(haar.ClassifierSize.width), &(haar.ClassifierSize.height)); + + //parse stages + parserGlobal = parserGlobal->next_sibling("stages"); + ncvAssertReturn(parserGlobal, NCV_HAAR_XML_LOADING_EXCEPTION); + parserGlobal = parserGlobal->first_node("_"); + ncvAssertReturn(parserGlobal, NCV_HAAR_XML_LOADING_EXCEPTION); + + while (parserGlobal) + { + HaarStage64 curStage; + curStage.setStartClassifierRootNodeOffset(haarClassifierNodes.size()); + Ncv32u tmpNumClassifierRootNodes = 0; + + rapidxml::xml_node<> *parserStageThreshold = parserGlobal->first_node("stage_threshold"); + ncvAssertReturn(parserStageThreshold, NCV_HAAR_XML_LOADING_EXCEPTION); + Ncv32f tmpStageThreshold; + sscanf_s(parserStageThreshold->value(), "%f", &tmpStageThreshold); + curStage.setStageThreshold(tmpStageThreshold); + + //parse trees + rapidxml::xml_node<> *parserTree; + parserTree = parserGlobal->first_node("trees"); + ncvAssertReturn(parserTree, NCV_HAAR_XML_LOADING_EXCEPTION); + parserTree = parserTree->first_node("_"); + ncvAssertReturn(parserTree, NCV_HAAR_XML_LOADING_EXCEPTION); + + while (parserTree) + { + rapidxml::xml_node<> *parserNode; + parserNode = parserTree->first_node("_"); + ncvAssertReturn(parserNode, NCV_HAAR_XML_LOADING_EXCEPTION); + Ncv32u nodeId = 0; + + while (parserNode) + { + HaarClassifierNode128 curNode; + + rapidxml::xml_node<> *parserNodeThreshold = parserNode->first_node("threshold"); + ncvAssertReturn(parserNodeThreshold, NCV_HAAR_XML_LOADING_EXCEPTION); + Ncv32f tmpThreshold; + sscanf_s(parserNodeThreshold->value(), "%f", &tmpThreshold); + curNode.setThreshold(tmpThreshold); + + rapidxml::xml_node<> *parserNodeLeft = parserNode->first_node("left_val"); + HaarClassifierNodeDescriptor32 nodeLeft; + if (parserNodeLeft) + { + Ncv32f leftVal; + sscanf_s(parserNodeLeft->value(), "%f", &leftVal); + ncvStat = nodeLeft.create(leftVal); + ncvAssertReturn(ncvStat == NCV_SUCCESS, ncvStat); + } + else + { + parserNodeLeft = parserNode->first_node("left_node"); + ncvAssertReturn(parserNodeLeft, NCV_HAAR_XML_LOADING_EXCEPTION); + Ncv32u leftNodeOffset; + sscanf_s(parserNodeLeft->value(), "%d", &leftNodeOffset); + nodeLeft.create(h_TmpClassifierNotRootNodes.size() + leftNodeOffset - 1); + haar.bHasStumpsOnly = false; + } + curNode.setLeftNodeDesc(nodeLeft); + + rapidxml::xml_node<> *parserNodeRight = parserNode->first_node("right_val"); + HaarClassifierNodeDescriptor32 nodeRight; + if (parserNodeRight) + { + Ncv32f rightVal; + sscanf_s(parserNodeRight->value(), "%f", &rightVal); + ncvStat = nodeRight.create(rightVal); + ncvAssertReturn(ncvStat == NCV_SUCCESS, ncvStat); + } + else + { + parserNodeRight = parserNode->first_node("right_node"); + ncvAssertReturn(parserNodeRight, NCV_HAAR_XML_LOADING_EXCEPTION); + Ncv32u rightNodeOffset; + sscanf_s(parserNodeRight->value(), "%d", &rightNodeOffset); + nodeRight.create(h_TmpClassifierNotRootNodes.size() + rightNodeOffset - 1); + haar.bHasStumpsOnly = false; + } + curNode.setRightNodeDesc(nodeRight); + + rapidxml::xml_node<> *parserNodeFeatures = parserNode->first_node("feature"); + ncvAssertReturn(parserNodeFeatures, NCV_HAAR_XML_LOADING_EXCEPTION); + + rapidxml::xml_node<> *parserNodeFeaturesTilted = parserNodeFeatures->first_node("tilted"); + ncvAssertReturn(parserNodeFeaturesTilted, NCV_HAAR_XML_LOADING_EXCEPTION); + Ncv32u tiltedVal; + sscanf_s(parserNodeFeaturesTilted->value(), "%d", &tiltedVal); + haar.bNeedsTiltedII = (tiltedVal != 0); + + rapidxml::xml_node<> *parserNodeFeaturesRects = parserNodeFeatures->first_node("rects"); + ncvAssertReturn(parserNodeFeaturesRects, NCV_HAAR_XML_LOADING_EXCEPTION); + parserNodeFeaturesRects = parserNodeFeaturesRects->first_node("_"); + ncvAssertReturn(parserNodeFeaturesRects, NCV_HAAR_XML_LOADING_EXCEPTION); + Ncv32u featureId = 0; + + while (parserNodeFeaturesRects) + { + Ncv32u rectX, rectY, rectWidth, rectHeight; + Ncv32f rectWeight; + sscanf_s(parserNodeFeaturesRects->value(), "%d %d %d %d %f", &rectX, &rectY, &rectWidth, &rectHeight, &rectWeight); + HaarFeature64 curFeature; + ncvStat = curFeature.setRect(rectX, rectY, rectWidth, rectHeight, haar.ClassifierSize.width, haar.ClassifierSize.height); + curFeature.setWeight(rectWeight); + ncvAssertReturn(NCV_SUCCESS == ncvStat, ncvStat); + haarFeatures.push_back(curFeature); + + parserNodeFeaturesRects = parserNodeFeaturesRects->next_sibling("_"); + featureId++; + } + + HaarFeatureDescriptor32 tmpFeatureDesc; + ncvStat = tmpFeatureDesc.create(haar.bNeedsTiltedII, featureId, haarFeatures.size() - featureId); + ncvAssertReturn(NCV_SUCCESS == ncvStat, ncvStat); + curNode.setFeatureDesc(tmpFeatureDesc); + + if (!nodeId) + { + //root node + haarClassifierNodes.push_back(curNode); + curMaxTreeDepth = 1; + } + else + { + //other node + h_TmpClassifierNotRootNodes.push_back(curNode); + curMaxTreeDepth++; + } + + parserNode = parserNode->next_sibling("_"); + nodeId++; + } + + parserTree = parserTree->next_sibling("_"); + tmpNumClassifierRootNodes++; + } + + curStage.setNumClassifierRootNodes(tmpNumClassifierRootNodes); + haarStages.push_back(curStage); + + parserGlobal = parserGlobal->next_sibling("_"); + } + } + catch (...) + { + return NCV_HAAR_XML_LOADING_EXCEPTION; + } + + //fill in cascade stats + haar.NumStages = haarStages.size(); + haar.NumClassifierRootNodes = haarClassifierNodes.size(); + haar.NumClassifierTotalNodes = haar.NumClassifierRootNodes + h_TmpClassifierNotRootNodes.size(); + haar.NumFeatures = haarFeatures.size(); + + //merge root and leaf nodes in one classifiers array + Ncv32u offsetRoot = haarClassifierNodes.size(); + for (Ncv32u i=0; i(), src1.step, - src2.ptr(), src2.step, - dst.ptr(), dst.step, sz, 0) ); + nppSafeCall( npp_func_8uc1(src1.ptr(), src1.step, src2.ptr(), src2.step, dst.ptr(), dst.step, sz, 0) ); break; case CV_8UC4: - nppSafeCall( npp_func_8uc4(src1.ptr(), src1.step, - src2.ptr(), src2.step, - dst.ptr(), dst.step, sz, 0) ); + nppSafeCall( npp_func_8uc4(src1.ptr(), src1.step, src2.ptr(), src2.step, dst.ptr(), dst.step, sz, 0) ); break; case CV_32SC1: - nppSafeCall( npp_func_32sc1(src1.ptr(), src1.step, - src2.ptr(), src2.step, - dst.ptr(), dst.step, sz) ); + nppSafeCall( npp_func_32sc1(src1.ptr(), src1.step, src2.ptr(), src2.step, dst.ptr(), dst.step, sz) ); break; case CV_32FC1: - nppSafeCall( npp_func_32fc1(src1.ptr(), src1.step, - src2.ptr(), src2.step, - dst.ptr(), dst.step, sz) ); + nppSafeCall( npp_func_32fc1(src1.ptr(), src1.step, src2.ptr(), src2.step, dst.ptr(), dst.step, sz) ); break; default: CV_Assert(!"Unsupported source type"); @@ -133,16 +120,15 @@ namespace template struct NppArithmScalarFunc; template<> struct NppArithmScalarFunc<1> { - typedef NppStatus (*func_ptr)(const Npp32f *pSrc, int nSrcStep, Npp32f nValue, Npp32f *pDst, - int nDstStep, NppiSize oSizeROI); + typedef NppStatus (*func_ptr)(const Npp32f *pSrc, int nSrcStep, Npp32f nValue, Npp32f *pDst, int nDstStep, NppiSize oSizeROI); }; template<> struct NppArithmScalarFunc<2> { - typedef NppStatus (*func_ptr)(const Npp32fc *pSrc, int nSrcStep, Npp32fc nValue, Npp32fc *pDst, - int nDstStep, NppiSize oSizeROI); + typedef NppStatus (*func_ptr)(const Npp32fc *pSrc, int nSrcStep, Npp32fc nValue, Npp32fc *pDst, int nDstStep, NppiSize oSizeROI); }; template::func_ptr func> struct NppArithmScalar; + template::func_ptr func> struct NppArithmScalar<1, func> { static void calc(const GpuMat& src, const Scalar& sc, GpuMat& dst) @@ -254,24 +240,16 @@ void cv::gpu::absdiff(const GpuMat& src1, const GpuMat& src2, GpuMat& dst) switch (src1.type()) { case CV_8UC1: - nppSafeCall( nppiAbsDiff_8u_C1R(src1.ptr(), src1.step, - src2.ptr(), src2.step, - dst.ptr(), dst.step, sz) ); + nppSafeCall( nppiAbsDiff_8u_C1R(src1.ptr(), src1.step, src2.ptr(), src2.step, dst.ptr(), dst.step, sz) ); break; case CV_8UC4: - nppSafeCall( nppiAbsDiff_8u_C4R(src1.ptr(), src1.step, - src2.ptr(), src2.step, - dst.ptr(), dst.step, sz) ); + nppSafeCall( nppiAbsDiff_8u_C4R(src1.ptr(), src1.step, src2.ptr(), src2.step, dst.ptr(), dst.step, sz) ); break; case CV_32SC1: - nppSafeCall( nppiAbsDiff_32s_C1R(src1.ptr(), src1.step, - src2.ptr(), src2.step, - dst.ptr(), dst.step, sz) ); + nppSafeCall( nppiAbsDiff_32s_C1R(src1.ptr(), src1.step, src2.ptr(), src2.step, dst.ptr(), dst.step, sz) ); break; case CV_32FC1: - nppSafeCall( nppiAbsDiff_32f_C1R(src1.ptr(), src1.step, - src2.ptr(), src2.step, - dst.ptr(), dst.step, sz) ); + nppSafeCall( nppiAbsDiff_32f_C1R(src1.ptr(), src1.step, src2.ptr(), src2.step, dst.ptr(), dst.step, sz) ); break; default: CV_Assert(!"Unsupported source type"); diff --git a/modules/gpu/src/nvidia/FaceDetectionFeed.cpp_NvidiaAPI_sample b/modules/gpu/src/nvidia/FaceDetectionFeed.cpp_NvidiaAPI_sample new file mode 100644 index 0000000000..c1926a38c2 --- /dev/null +++ b/modules/gpu/src/nvidia/FaceDetectionFeed.cpp_NvidiaAPI_sample @@ -0,0 +1,362 @@ +/*M/////////////////////////////////////////////////////////////////////////////////////// +// +// IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. +// +// By downloading, copying, installing or using the software you agree to this license. +// If you do not agree to this license, do not download, install, +// copy or use the software. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright (C) 2009-2010, NVIDIA Corporation, all rights reserved. +// Third party copyrights are property of their respective owners. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistribution's of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistribution's in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * The name of the copyright holders may not be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// This software is provided by the copyright holders and contributors "as is" and +// any express or implied warranties, including, but not limited to, the implied +// warranties of merchantability and fitness for a particular purpose are disclaimed. +// In no event shall the Intel Corporation or contributors be liable for any direct, +// indirect, incidental, special, exemplary, or consequential damages +// (including, but not limited to, procurement of substitute goods or services; +// loss of use, data, or profits; or business interruption) however caused +// and on any theory of liability, whether in contract, strict liability, +// or tort (including negligence or otherwise) arising in any way out of +// the use of this software, even if advised of the possibility of such damage. +// +//M*/ + +#include +#include + +#define CV_NO_BACKWARD_COMPATIBILITY + +#include "opencv2/opencv.hpp" + +#include "NCVHaarObjectDetection.hpp" + +using namespace cv; +using namespace std; + +const Size preferredVideoFrameSize(640, 480); + +string preferredClassifier = "haarcascade_frontalface_alt.xml"; +string wndTitle = "NVIDIA Computer Vision SDK :: Face Detection in Video Feed"; + + +void printSyntax(void) +{ + printf("Syntax: FaceDetectionFeed.exe [-c cameranum | -v filename] classifier.xml\n"); +} + + +void imagePrintf(Mat& img, int lineOffsY, Scalar color, const char *format, ...) +{ + int fontFace = CV_FONT_HERSHEY_PLAIN; + double fontScale = 1; + + int baseline; + Size textSize = cv::getTextSize("T", fontFace, fontScale, 1, &baseline); + + va_list arg_ptr; + va_start(arg_ptr, format); + int len = _vscprintf(format, arg_ptr) + 1; + + vector strBuf(len); + vsprintf_s(&strBuf[0], len, format, arg_ptr); + + Point org(1, 3 * textSize.height * (lineOffsY + 1) / 2); + putText(img, &strBuf[0], org, fontFace, fontScale, color); + va_end(arg_ptr); +} + + +NCVStatus process(Mat *srcdst, + Ncv32u width, Ncv32u height, + NcvBool bShowAllHypotheses, NcvBool bLargestFace, + HaarClassifierCascadeDescriptor &haar, + NCVVector &d_haarStages, NCVVector &d_haarNodes, + NCVVector &d_haarFeatures, NCVVector &h_haarStages, + INCVMemAllocator &gpuAllocator, + INCVMemAllocator &cpuAllocator, + cudaDeviceProp &devProp) +{ + ncvAssertReturn(!((srcdst == NULL) ^ gpuAllocator.isCounting()), NCV_NULL_PTR); + + NCVStatus ncvStat; + + NCV_SET_SKIP_COND(gpuAllocator.isCounting()); + + NCVMatrixAlloc d_src(gpuAllocator, width, height); + ncvAssertReturn(d_src.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + NCVMatrixAlloc h_src(cpuAllocator, width, height); + ncvAssertReturn(h_src.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + NCVVectorAlloc d_rects(gpuAllocator, 100); + ncvAssertReturn(d_rects.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + + Mat h_src_hdr(Size(width, height), CV_8U, h_src.ptr(), h_src.stride()); + + NCV_SKIP_COND_BEGIN + + (*srcdst).copyTo(h_src_hdr); + + ncvStat = h_src.copySolid(d_src, 0); + ncvAssertReturnNcvStat(ncvStat); + ncvAssertCUDAReturn(cudaStreamSynchronize(0), NCV_CUDA_ERROR); + + NCV_SKIP_COND_END + + NcvSize32u roi; + roi.width = d_src.width(); + roi.height = d_src.height(); + + Ncv32u numDetections; + ncvStat = ncvDetectObjectsMultiScale_device( + d_src, roi, d_rects, numDetections, haar, h_haarStages, + d_haarStages, d_haarNodes, d_haarFeatures, + haar.ClassifierSize, + bShowAllHypotheses ? 0 : 4, + 1.2f, 1, + (bLargestFace ? NCVPipeObjDet_FindLargestObject : 0) | NCVPipeObjDet_VisualizeInPlace, + gpuAllocator, cpuAllocator, devProp.major, devProp.minor, 0); + ncvAssertReturnNcvStat(ncvStat); + ncvAssertCUDAReturn(cudaStreamSynchronize(0), NCV_CUDA_ERROR); + + NCV_SKIP_COND_BEGIN + + ncvStat = d_src.copySolid(h_src, 0); + ncvAssertReturnNcvStat(ncvStat); + ncvAssertCUDAReturn(cudaStreamSynchronize(0), NCV_CUDA_ERROR); + + h_src_hdr.copyTo(*srcdst); + + NCV_SKIP_COND_END + + return NCV_SUCCESS; +} + + +int main( int argc, const char** argv ) +{ + NCVStatus ncvStat; + + printf("NVIDIA Computer Vision SDK\n"); + printf("Face Detection in video and live feed\n"); + printf("=========================================\n"); + printf(" Esc - Quit\n"); + printf(" Space - Switch between NCV and OpenCV\n"); + printf(" L - Switch between FullSearch and LargestFace modes\n"); + printf(" U - Toggle unfiltered hypotheses visualization in FullSearch\n"); + + if (argc != 4 && argc != 1) + return printSyntax(), -1; + + VideoCapture capture; + Size frameSize; + + if (argc == 1 || strcmp(argv[1], "-c") == 0) + { + // Camera input is specified + int camIdx = (argc == 3) ? atoi(argv[2]) : 0; + if(!capture.open(camIdx)) + return printf("Error opening camera\n"), -1; + + capture.set(CV_CAP_PROP_FRAME_WIDTH, preferredVideoFrameSize.width); + capture.set(CV_CAP_PROP_FRAME_HEIGHT, preferredVideoFrameSize.height); + capture.set(CV_CAP_PROP_FPS, 25); + frameSize = preferredVideoFrameSize; + } + else if (strcmp(argv[1], "-v") == 0) + { + // Video file input (avi) + if(!capture.open(argv[2])) + return printf("Error opening video file\n"), -1; + + frameSize.width = (int)capture.get(CV_CAP_PROP_FRAME_WIDTH); + frameSize.height = (int)capture.get(CV_CAP_PROP_FRAME_HEIGHT); + } + else + return printSyntax(), -1; + + NcvBool bUseOpenCV = true; + NcvBool bLargestFace = true; + NcvBool bShowAllHypotheses = false; + + string classifierFile = (argc == 1) ? preferredClassifier : argv[3]; + + CascadeClassifier classifierOpenCV; + if (!classifierOpenCV.load(classifierFile)) + return printf("Error (in OpenCV) opening classifier\n"), printSyntax(), -1; + + int devId; + ncvAssertCUDAReturn(cudaGetDevice(&devId), -1); + cudaDeviceProp devProp; + ncvAssertCUDAReturn(cudaGetDeviceProperties(&devProp, devId), -1); + printf("Using GPU %d %s, arch=%d.%d\n", devId, devProp.name, devProp.major, devProp.minor); + + //============================================================================== + // + // Load the classifier from file (assuming its size is about 1 mb) + // using a simple allocator + // + //============================================================================== + + NCVMemNativeAllocator gpuCascadeAllocator(NCVMemoryTypeDevice); + ncvAssertPrintReturn(gpuCascadeAllocator.isInitialized(), "Error creating cascade GPU allocator", -1); + NCVMemNativeAllocator cpuCascadeAllocator(NCVMemoryTypeHostPinned); + ncvAssertPrintReturn(cpuCascadeAllocator.isInitialized(), "Error creating cascade CPU allocator", -1); + + Ncv32u haarNumStages, haarNumNodes, haarNumFeatures; + ncvStat = ncvHaarGetClassifierSize(classifierFile, haarNumStages, haarNumNodes, haarNumFeatures); + ncvAssertPrintReturn(ncvStat == NCV_SUCCESS, "Error reading classifier size (check the file)", -1); + + NCVVectorAlloc h_haarStages(cpuCascadeAllocator, haarNumStages); + ncvAssertPrintReturn(h_haarStages.isMemAllocated(), "Error in cascade CPU allocator", -1); + NCVVectorAlloc h_haarNodes(cpuCascadeAllocator, haarNumNodes); + ncvAssertPrintReturn(h_haarNodes.isMemAllocated(), "Error in cascade CPU allocator", -1); + NCVVectorAlloc h_haarFeatures(cpuCascadeAllocator, haarNumFeatures); + ncvAssertPrintReturn(h_haarFeatures.isMemAllocated(), "Error in cascade CPU allocator", -1); + + HaarClassifierCascadeDescriptor haar; + ncvStat = ncvHaarLoadFromFile_host(classifierFile, haar, h_haarStages, h_haarNodes, h_haarFeatures); + ncvAssertPrintReturn(ncvStat == NCV_SUCCESS, "Error loading classifier", -1); + + NCVVectorAlloc d_haarStages(gpuCascadeAllocator, haarNumStages); + ncvAssertPrintReturn(d_haarStages.isMemAllocated(), "Error in cascade GPU allocator", -1); + NCVVectorAlloc d_haarNodes(gpuCascadeAllocator, haarNumNodes); + ncvAssertPrintReturn(d_haarNodes.isMemAllocated(), "Error in cascade GPU allocator", -1); + NCVVectorAlloc d_haarFeatures(gpuCascadeAllocator, haarNumFeatures); + ncvAssertPrintReturn(d_haarFeatures.isMemAllocated(), "Error in cascade GPU allocator", -1); + + ncvStat = h_haarStages.copySolid(d_haarStages, 0); + ncvAssertPrintReturn(ncvStat == NCV_SUCCESS, "Error copying cascade to GPU", -1); + ncvStat = h_haarNodes.copySolid(d_haarNodes, 0); + ncvAssertPrintReturn(ncvStat == NCV_SUCCESS, "Error copying cascade to GPU", -1); + ncvStat = h_haarFeatures.copySolid(d_haarFeatures, 0); + ncvAssertPrintReturn(ncvStat == NCV_SUCCESS, "Error copying cascade to GPU", -1); + + //============================================================================== + // + // Calculate memory requirements and create real allocators + // + //============================================================================== + + NCVMemStackAllocator gpuCounter(devProp.textureAlignment); + ncvAssertPrintReturn(gpuCounter.isInitialized(), "Error creating GPU memory counter", -1); + NCVMemStackAllocator cpuCounter(devProp.textureAlignment); + ncvAssertPrintReturn(cpuCounter.isInitialized(), "Error creating CPU memory counter", -1); + + ncvStat = process(NULL, frameSize.width, frameSize.height, + false, false, haar, + d_haarStages, d_haarNodes, + d_haarFeatures, h_haarStages, + gpuCounter, cpuCounter, devProp); + ncvAssertPrintReturn(ncvStat == NCV_SUCCESS, "Error in memory counting pass", -1); + + NCVMemStackAllocator gpuAllocator(NCVMemoryTypeDevice, gpuCounter.maxSize(), devProp.textureAlignment); + ncvAssertPrintReturn(gpuAllocator.isInitialized(), "Error creating GPU memory allocator", -1); + NCVMemStackAllocator cpuAllocator(NCVMemoryTypeHostPinned, cpuCounter.maxSize(), devProp.textureAlignment); + ncvAssertPrintReturn(cpuAllocator.isInitialized(), "Error creating CPU memory allocator", -1); + + printf("Initialized for frame size [%dx%d]\n", frameSize.width, frameSize.height); + + //============================================================================== + // + // Main processing loop + // + //============================================================================== + + namedWindow(wndTitle, 1); + + Mat frame, gray, frameDisp; + + for(;;) + { + // For camera and video file, capture the next image + capture >> frame; + if (frame.empty()) + break; + + cvtColor(frame, gray, CV_BGR2GRAY); + + // process + NcvSize32u minSize = haar.ClassifierSize; + if (bLargestFace) + { + Ncv32u ratioX = preferredVideoFrameSize.width / minSize.width; + Ncv32u ratioY = preferredVideoFrameSize.height / minSize.height; + Ncv32u ratioSmallest = std::min(ratioX, ratioY); + ratioSmallest = (Ncv32u)std::max(ratioSmallest / 2.5f, 1.f); + minSize.width *= ratioSmallest; + minSize.height *= ratioSmallest; + } + + NcvTimer timer = ncvStartTimer(); + + if (!bUseOpenCV) + { + ncvStat = process(&gray, frameSize.width, frameSize.height, + bShowAllHypotheses, bLargestFace, haar, + d_haarStages, d_haarNodes, + d_haarFeatures, h_haarStages, + gpuAllocator, cpuAllocator, devProp); + ncvAssertPrintReturn(ncvStat == NCV_SUCCESS, "Error in memory counting pass", -1); + } + else + { + vector rectsOpenCV; + + classifierOpenCV.detectMultiScale( + gray, + rectsOpenCV, + 1.2f, + bShowAllHypotheses && !bLargestFace ? 0 : 4, + (bLargestFace ? CV_HAAR_FIND_BIGGEST_OBJECT : 0) | CV_HAAR_SCALE_IMAGE, + Size(minSize.width, minSize.height)); + + for (size_t rt = 0; rt < rectsOpenCV.size(); ++rt) + rectangle(gray, rectsOpenCV[rt], Scalar(255)); + } + + Ncv32f avgTime = (Ncv32f)ncvEndQueryTimerMs(timer); + + cvtColor(gray, frameDisp, CV_GRAY2BGR); + + imagePrintf(frameDisp, 0, CV_RGB(255, 0,0), "Space - Switch NCV%s / OpenCV%s", bUseOpenCV?"":" (ON)", bUseOpenCV?" (ON)":""); + imagePrintf(frameDisp, 1, CV_RGB(255, 0,0), "L - Switch FullSearch%s / LargestFace%s modes", bLargestFace?"":" (ON)", bLargestFace?" (ON)":""); + imagePrintf(frameDisp, 2, CV_RGB(255, 0,0), "U - Toggle unfiltered hypotheses visualization in FullSearch %s", bShowAllHypotheses?"(ON)":"(OFF)"); + imagePrintf(frameDisp, 3, CV_RGB(118,185,0), " Running at %f FPS on %s", 1000.0f / avgTime, bUseOpenCV?"CPU":"GPU"); + + cv::imshow(wndTitle, frameDisp); + + switch (cvWaitKey(1)) + { + case ' ': + bUseOpenCV = !bUseOpenCV; + break; + case 'L':case 'l': + bLargestFace = !bLargestFace; + break; + case 'U':case 'u': + bShowAllHypotheses = !bShowAllHypotheses; + break; + case 27: + return 0; + } + } + + return 0; +} diff --git a/modules/gpu/src/nvidia/NCV.cpp b/modules/gpu/src/nvidia/NCV.cpp new file mode 100644 index 0000000000..fef9c08f5b --- /dev/null +++ b/modules/gpu/src/nvidia/NCV.cpp @@ -0,0 +1,571 @@ +/*M/////////////////////////////////////////////////////////////////////////////////////// +// +// IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. +// +// By downloading, copying, installing or using the software you agree to this license. +// If you do not agree to this license, do not download, install, +// copy or use the software. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright (C) 2009-2010, NVIDIA Corporation, all rights reserved. +// Third party copyrights are property of their respective owners. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistribution's of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistribution's in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * The name of the copyright holders may not be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// This software is provided by the copyright holders and contributors "as is" and +// any express or implied warranties, including, but not limited to, the implied +// warranties of merchantability and fitness for a particular purpose are disclaimed. +// In no event shall the Intel Corporation or contributors be liable for any direct, +// indirect, incidental, special, exemplary, or consequential damages +// (including, but not limited to, procurement of substitute goods or services; +// loss of use, data, or profits; or business interruption) however caused +// and on any theory of liability, whether in contract, strict liability, +// or tort (including negligence or otherwise) arising in any way out of +// the use of this software, even if advised of the possibility of such damage. +// +//M*/ + + +#include + + +#if !defined (HAVE_CUDA) + + +#else /* !defined (HAVE_CUDA) */ + + +#include +#include "NCV.hpp" + + +//============================================================================== +// +// Error handling helpers +// +//============================================================================== + + +static void stdioDebugOutput(const char *msg) +{ + printf("%s", msg); +} + + +static NCVDebugOutputHandler *debugOutputHandler = stdioDebugOutput; + + +void ncvDebugOutput(const char *msg, ...) +{ + const int K_DEBUG_STRING_MAXLEN = 1024; + char buffer[K_DEBUG_STRING_MAXLEN]; + va_list args; + va_start(args, msg); + vsnprintf_s(buffer, K_DEBUG_STRING_MAXLEN, K_DEBUG_STRING_MAXLEN-1, msg, args); + va_end (args); + debugOutputHandler(buffer); +} + + +void ncvSetDebugOutputHandler(NCVDebugOutputHandler *func) +{ + debugOutputHandler = func; +} + + +//============================================================================== +// +// Memory wrappers and helpers +// +//============================================================================== + + +NCVStatus GPUAlignmentValue(Ncv32u &alignment) +{ + int curDev; + cudaDeviceProp curProp; + ncvAssertCUDAReturn(cudaGetDevice(&curDev), NCV_CUDA_ERROR); + ncvAssertCUDAReturn(cudaGetDeviceProperties(&curProp, curDev), NCV_CUDA_ERROR); + alignment = curProp.textureAlignment; //GPUAlignmentValue(curProp.major); + return NCV_SUCCESS; +} + + +Ncv32u alignUp(Ncv32u what, Ncv32u alignment) +{ + Ncv32u alignMask = alignment-1; + Ncv32u inverseAlignMask = ~alignMask; + Ncv32u res = (what + alignMask) & inverseAlignMask; + return res; +} + + +void NCVMemPtr::clear() +{ + ptr = NULL; + memtype = NCVMemoryTypeNone; +} + + +void NCVMemSegment::clear() +{ + begin.clear(); + size = 0; +} + + +NCVStatus memSegCopyHelper(void *dst, NCVMemoryType dstType, const void *src, NCVMemoryType srcType, size_t sz, cudaStream_t cuStream) +{ + NCVStatus ncvStat; + switch (dstType) + { + case NCVMemoryTypeHostPageable: + case NCVMemoryTypeHostPinned: + switch (srcType) + { + case NCVMemoryTypeHostPageable: + case NCVMemoryTypeHostPinned: + memcpy(dst, src, sz); + ncvStat = NCV_SUCCESS; + break; + case NCVMemoryTypeDevice: + if (cuStream != 0) + { + ncvAssertCUDAReturn(cudaMemcpyAsync(dst, src, sz, cudaMemcpyDeviceToHost, cuStream), NCV_CUDA_ERROR); + } + else + { + ncvAssertCUDAReturn(cudaMemcpy(dst, src, sz, cudaMemcpyDeviceToHost), NCV_CUDA_ERROR); + } + ncvStat = NCV_SUCCESS; + break; + default: + ncvStat = NCV_MEM_RESIDENCE_ERROR; + } + break; + case NCVMemoryTypeDevice: + switch (srcType) + { + case NCVMemoryTypeHostPageable: + case NCVMemoryTypeHostPinned: + if (cuStream != 0) + { + ncvAssertCUDAReturn(cudaMemcpyAsync(dst, src, sz, cudaMemcpyHostToDevice, cuStream), NCV_CUDA_ERROR); + } + else + { + ncvAssertCUDAReturn(cudaMemcpy(dst, src, sz, cudaMemcpyHostToDevice), NCV_CUDA_ERROR); + } + ncvStat = NCV_SUCCESS; + break; + case NCVMemoryTypeDevice: + if (cuStream != 0) + { + ncvAssertCUDAReturn(cudaMemcpyAsync(dst, src, sz, cudaMemcpyDeviceToDevice, cuStream), NCV_CUDA_ERROR); + } + else + { + ncvAssertCUDAReturn(cudaMemcpy(dst, src, sz, cudaMemcpyDeviceToDevice), NCV_CUDA_ERROR); + } + ncvStat = NCV_SUCCESS; + break; + default: + ncvStat = NCV_MEM_RESIDENCE_ERROR; + } + break; + default: + ncvStat = NCV_MEM_RESIDENCE_ERROR; + } + + return ncvStat; +} + + +//=================================================================== +// +// NCVMemStackAllocator class members implementation +// +//=================================================================== + + +NCVMemStackAllocator::NCVMemStackAllocator(Ncv32u alignment) + : + currentSize(0), + _maxSize(0), + allocBegin(NULL), + begin(NULL), + _memType(NCVMemoryTypeNone), + _alignment(alignment) +{ + NcvBool bProperAlignment = (alignment & (alignment-1)) == 0; + ncvAssertPrintCheck(bProperAlignment, "NCVMemStackAllocator ctor:: alignment not power of 2"); +} + + +NCVMemStackAllocator::NCVMemStackAllocator(NCVMemoryType memT, size_t capacity, Ncv32u alignment) + : + currentSize(0), + _maxSize(0), + allocBegin(NULL), + _memType(memT), + _alignment(alignment) +{ + NcvBool bProperAlignment = (alignment & (alignment-1)) == 0; + ncvAssertPrintCheck(bProperAlignment, "NCVMemStackAllocator ctor:: _alignment not power of 2"); + + allocBegin = NULL; + + switch (memT) + { + case NCVMemoryTypeDevice: + ncvAssertCUDAReturn(cudaMalloc(&allocBegin, capacity), ); + break; + case NCVMemoryTypeHostPinned: + ncvAssertCUDAReturn(cudaMallocHost(&allocBegin, capacity), ); + break; + case NCVMemoryTypeHostPageable: + allocBegin = (Ncv8u *)malloc(capacity); + break; + } + + if (capacity == 0) + { + allocBegin = (Ncv8u *)(0x1); + } + + if (!isCounting()) + { + begin = allocBegin; + end = begin + capacity; + } +} + + +NCVMemStackAllocator::~NCVMemStackAllocator() +{ + if (allocBegin != NULL) + { + ncvAssertPrintCheck(currentSize == 0, "NCVMemStackAllocator dtor:: not all objects were deallocated properly, forcing destruction"); + switch (_memType) + { + case NCVMemoryTypeDevice: + ncvAssertCUDAReturn(cudaFree(allocBegin), ); + break; + case NCVMemoryTypeHostPinned: + ncvAssertCUDAReturn(cudaFreeHost(allocBegin), ); + break; + case NCVMemoryTypeHostPageable: + free(allocBegin); + break; + } + allocBegin = NULL; + } +} + + +NCVStatus NCVMemStackAllocator::alloc(NCVMemSegment &seg, size_t size) +{ + seg.clear(); + ncvAssertReturn(isInitialized(), NCV_ALLOCATOR_BAD_ALLOC); + + size = alignUp(size, this->_alignment); + this->currentSize += size; + this->_maxSize = std::max(this->_maxSize, this->currentSize); + + if (!isCounting()) + { + size_t availSize = end - begin; + ncvAssertReturn(size <= availSize, NCV_ALLOCATOR_INSUFFICIENT_CAPACITY); + } + + seg.begin.ptr = begin; + seg.begin.memtype = this->_memType; + seg.size = size; + begin += size; + + return NCV_SUCCESS; +} + + +NCVStatus NCVMemStackAllocator::dealloc(NCVMemSegment &seg) +{ + ncvAssertReturn(isInitialized(), NCV_ALLOCATOR_BAD_ALLOC); + ncvAssertReturn(seg.begin.memtype == this->_memType, NCV_ALLOCATOR_BAD_DEALLOC); + ncvAssertReturn(seg.begin.ptr != NULL || isCounting(), NCV_ALLOCATOR_BAD_DEALLOC); + ncvAssertReturn(seg.begin.ptr == begin - seg.size, NCV_ALLOCATOR_DEALLOC_ORDER); + + currentSize -= seg.size; + begin -= seg.size; + + seg.clear(); + + ncvAssertReturn(allocBegin <= begin, NCV_ALLOCATOR_BAD_DEALLOC); + + return NCV_SUCCESS; +} + + +NcvBool NCVMemStackAllocator::isInitialized(void) const +{ + return ((this->_alignment & (this->_alignment-1)) == 0) && isCounting() || this->allocBegin != NULL; +} + + +NcvBool NCVMemStackAllocator::isCounting(void) const +{ + return this->_memType == NCVMemoryTypeNone; +} + + +NCVMemoryType NCVMemStackAllocator::memType(void) const +{ + return this->_memType; +} + + +Ncv32u NCVMemStackAllocator::alignment(void) const +{ + return this->_alignment; +} + + +size_t NCVMemStackAllocator::maxSize(void) const +{ + return this->_maxSize; +} + + +//=================================================================== +// +// NCVMemNativeAllocator class members implementation +// +//=================================================================== + + +NCVMemNativeAllocator::NCVMemNativeAllocator(NCVMemoryType memT) + : + currentSize(0), + _maxSize(0), + _memType(memT) +{ + ncvAssertPrintReturn(memT != NCVMemoryTypeNone, "NCVMemNativeAllocator ctor:: counting not permitted for this allocator type", ); + ncvAssertPrintReturn(NCV_SUCCESS == GPUAlignmentValue(this->_alignment), "NCVMemNativeAllocator ctor:: couldn't get device _alignment", ); +} + + +NCVMemNativeAllocator::~NCVMemNativeAllocator() +{ + ncvAssertPrintCheck(currentSize == 0, "NCVMemNativeAllocator dtor:: detected memory leak"); +} + + +NCVStatus NCVMemNativeAllocator::alloc(NCVMemSegment &seg, size_t size) +{ + seg.clear(); + ncvAssertReturn(isInitialized(), NCV_ALLOCATOR_BAD_ALLOC); + + switch (this->_memType) + { + case NCVMemoryTypeDevice: + ncvAssertCUDAReturn(cudaMalloc(&seg.begin.ptr, size), NCV_CUDA_ERROR); + break; + case NCVMemoryTypeHostPinned: + ncvAssertCUDAReturn(cudaMallocHost(&seg.begin.ptr, size), NCV_CUDA_ERROR); + break; + case NCVMemoryTypeHostPageable: + seg.begin.ptr = (Ncv8u *)malloc(size); + break; + } + + this->currentSize += alignUp(size, this->_alignment); + this->_maxSize = std::max(this->_maxSize, this->currentSize); + + seg.begin.memtype = this->_memType; + seg.size = size; + + return NCV_SUCCESS; +} + + +NCVStatus NCVMemNativeAllocator::dealloc(NCVMemSegment &seg) +{ + ncvAssertReturn(isInitialized(), NCV_ALLOCATOR_BAD_ALLOC); + ncvAssertReturn(seg.begin.memtype == this->_memType, NCV_ALLOCATOR_BAD_DEALLOC); + ncvAssertReturn(seg.begin.ptr != NULL, NCV_ALLOCATOR_BAD_DEALLOC); + + ncvAssertReturn(currentSize >= alignUp(seg.size, this->_alignment), NCV_ALLOCATOR_BAD_DEALLOC); + currentSize -= alignUp(seg.size, this->_alignment); + + switch (this->_memType) + { + case NCVMemoryTypeDevice: + ncvAssertCUDAReturn(cudaFree(seg.begin.ptr), NCV_CUDA_ERROR); + break; + case NCVMemoryTypeHostPinned: + ncvAssertCUDAReturn(cudaFreeHost(seg.begin.ptr), NCV_CUDA_ERROR); + break; + case NCVMemoryTypeHostPageable: + free(seg.begin.ptr); + break; + } + + seg.clear(); + + return NCV_SUCCESS; +} + + +NcvBool NCVMemNativeAllocator::isInitialized(void) const +{ + return (this->_alignment != 0); +} + + +NcvBool NCVMemNativeAllocator::isCounting(void) const +{ + return false; +} + + +NCVMemoryType NCVMemNativeAllocator::memType(void) const +{ + return this->_memType; +} + + +Ncv32u NCVMemNativeAllocator::alignment(void) const +{ + return this->_alignment; +} + + +size_t NCVMemNativeAllocator::maxSize(void) const +{ + return this->_maxSize; +} + + +//=================================================================== +// +// Time and timer routines +// +//=================================================================== + + +typedef struct _NcvTimeMoment NcvTimeMoment; + +#if defined(_WIN32) || defined(_WIN64) + + #include + + typedef struct _NcvTimeMoment + { + LONGLONG moment, freq; + } NcvTimeMoment; + + + static void _ncvQueryMoment(NcvTimeMoment *t) + { + QueryPerformanceFrequency((LARGE_INTEGER *)&(t->freq)); + QueryPerformanceCounter((LARGE_INTEGER *)&(t->moment)); + } + + + double _ncvMomentToMicroseconds(NcvTimeMoment *t) + { + return 1000000.0 * t->moment / t->freq; + } + + + double _ncvMomentsDiffToMicroseconds(NcvTimeMoment *t1, NcvTimeMoment *t2) + { + return 1000000.0 * 2 * ((t2->moment) - (t1->moment)) / (t1->freq + t2->freq); + } + + + double _ncvMomentsDiffToMilliseconds(NcvTimeMoment *t1, NcvTimeMoment *t2) + { + return 1000.0 * 2 * ((t2->moment) - (t1->moment)) / (t1->freq + t2->freq); + } + +#elif defined(__unix__) + + #include + + typedef struct _NcvTimeMoment + { + struct timeval tv; + struct timezone tz; + } NcvTimeMoment; + + + void _ncvQueryMoment(NcvTimeMoment *t) + { + gettimeofday(& t->tv, & t->tz); + } + + + double _ncvMomentToMicroseconds(NcvTimeMoment *t) + { + return 1000000.0 * t->tv.tv_sec + (double)t->tv.tv_usec; + } + + + double _ncvMomentsDiffToMicroseconds(NcvTimeMoment *t1, NcvTimeMoment *t2) + { + return (((double)t2->tv.tv_sec - (double)t1->tv.tv_sec) * 1000000 + (double)t2->tv.tv_usec - (double)t1->tv.tv_usec); + } + + +#endif //#if defined(_WIN32) || defined(_WIN64) + + +struct _NcvTimer +{ + NcvTimeMoment t1, t2; +}; + + +NcvTimer ncvStartTimer(void) +{ + struct _NcvTimer *t; + t = (struct _NcvTimer *)malloc(sizeof(struct _NcvTimer)); + _ncvQueryMoment(&t->t1); + return t; +} + + +double ncvEndQueryTimerUs(NcvTimer t) +{ + double res; + _ncvQueryMoment(&t->t2); + res = _ncvMomentsDiffToMicroseconds(&t->t1, &t->t2); + free(t); + return res; +} + + +double ncvEndQueryTimerMs(NcvTimer t) +{ + double res; + _ncvQueryMoment(&t->t2); + res = _ncvMomentsDiffToMilliseconds(&t->t1, &t->t2); + free(t); + return res; +} + +#endif /* !defined (HAVE_CUDA) */ \ No newline at end of file diff --git a/modules/gpu/src/nvidia/NCV.hpp b/modules/gpu/src/nvidia/NCV.hpp new file mode 100644 index 0000000000..a71f650252 --- /dev/null +++ b/modules/gpu/src/nvidia/NCV.hpp @@ -0,0 +1,837 @@ +/*M/////////////////////////////////////////////////////////////////////////////////////// +// +// IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. +// +// By downloading, copying, installing or using the software you agree to this license. +// If you do not agree to this license, do not download, install, +// copy or use the software. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright (C) 2009-2010, NVIDIA Corporation, all rights reserved. +// Third party copyrights are property of their respective owners. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistribution's of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistribution's in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * The name of the copyright holders may not be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// This software is provided by the copyright holders and contributors "as is" and +// any express or implied warranties, including, but not limited to, the implied +// warranties of merchantability and fitness for a particular purpose are disclaimed. +// In no event shall the Intel Corporation or contributors be liable for any direct, +// indirect, incidental, special, exemplary, or consequential damages +// (including, but not limited to, procurement of substitute goods or services; +// loss of use, data, or profits; or business interruption) however caused +// and on any theory of liability, whether in contract, strict liability, +// or tort (including negligence or otherwise) arising in any way out of +// the use of this software, even if advised of the possibility of such damage. +// +//M*/ + +#ifndef _ncv_hpp_ +#define _ncv_hpp_ + +#include +#include "npp_staging.h" + + +//============================================================================== +// +// Alignment macros +// +//============================================================================== + + +#if !defined(__align__) && !defined(__CUDACC__) + #if defined(_WIN32) || defined(_WIN64) + #define __align__(n) __declspec(align(n)) + #elif defined(__unix__) + #define __align__(n) __attribute__((__aligned__(n))) + #endif +#endif + + +//============================================================================== +// +// Integral and compound types of guaranteed size +// +//============================================================================== + + +typedef bool NcvBool; +typedef long long Ncv64s; +typedef unsigned long long Ncv64u; +typedef int Ncv32s; +typedef unsigned int Ncv32u; +typedef short Ncv16s; +typedef unsigned short Ncv16u; +typedef char Ncv8s; +typedef unsigned char Ncv8u; +typedef float Ncv32f; +typedef double Ncv64f; + + +typedef struct +{ + Ncv8u x; + Ncv8u y; + Ncv8u width; + Ncv8u height; +} NcvRect8u; + + +typedef struct +{ + Ncv32s x; ///< x-coordinate of upper left corner. + Ncv32s y; ///< y-coordinate of upper left corner. + Ncv32s width; ///< Rectangle width. + Ncv32s height; ///< Rectangle height. +} NcvRect32s; + + +typedef struct +{ + Ncv32u x; ///< x-coordinate of upper left corner. + Ncv32u y; ///< y-coordinate of upper left corner. + Ncv32u width; ///< Rectangle width. + Ncv32u height; ///< Rectangle height. +} NcvRect32u; + + +typedef struct +{ + Ncv32s width; ///< Rectangle width. + Ncv32s height; ///< Rectangle height. +} NcvSize32s; + + +typedef struct +{ + Ncv32u width; ///< Rectangle width. + Ncv32u height; ///< Rectangle height. +} NcvSize32u; + + +NPPST_CT_ASSERT(sizeof(NcvBool) <= 4); +NPPST_CT_ASSERT(sizeof(Ncv64s) == 8); +NPPST_CT_ASSERT(sizeof(Ncv64u) == 8); +NPPST_CT_ASSERT(sizeof(Ncv32s) == 4); +NPPST_CT_ASSERT(sizeof(Ncv32u) == 4); +NPPST_CT_ASSERT(sizeof(Ncv16s) == 2); +NPPST_CT_ASSERT(sizeof(Ncv16u) == 2); +NPPST_CT_ASSERT(sizeof(Ncv8s) == 1); +NPPST_CT_ASSERT(sizeof(Ncv8u) == 1); +NPPST_CT_ASSERT(sizeof(Ncv32f) == 4); +NPPST_CT_ASSERT(sizeof(Ncv64f) == 8); +NPPST_CT_ASSERT(sizeof(NcvRect8u) == sizeof(Ncv32u)); +NPPST_CT_ASSERT(sizeof(NcvRect32s) == 4 * sizeof(Ncv32s)); +NPPST_CT_ASSERT(sizeof(NcvRect32u) == 4 * sizeof(Ncv32u)); +NPPST_CT_ASSERT(sizeof(NcvSize32u) == 2 * sizeof(Ncv32u)); + + +//============================================================================== +// +// Persistent constants +// +//============================================================================== + + +const Ncv32u K_WARP_SIZE = 32; +const Ncv32u K_LOG2_WARP_SIZE = 5; + + +//============================================================================== +// +// Error handling +// +//============================================================================== + + +#define NCV_CT_PREP_STRINGIZE_AUX(x) #x +#define NCV_CT_PREP_STRINGIZE(x) NCV_CT_PREP_STRINGIZE_AUX(x) + + +void ncvDebugOutput(const char *msg, ...); + + +typedef void NCVDebugOutputHandler(const char* msg); + + +void ncvSetDebugOutputHandler(NCVDebugOutputHandler* func); + + +#define ncvAssertPrintCheck(pred, msg) \ + ((pred) ? true : (ncvDebugOutput("\n%s\n", \ + "NCV Assertion Failed: " msg ", file=" __FILE__ ", line=" NCV_CT_PREP_STRINGIZE(__LINE__) \ + ), false)) + + +#define ncvAssertPrintReturn(pred, msg, err) \ + if (ncvAssertPrintCheck(pred, msg)) ; else return err + + +#define ncvAssertReturn(pred, err) \ + do \ + { \ + if (!(pred)) \ + { \ + ncvDebugOutput("\n%s%d%s\n", "NCV Assertion Failed: retcode=", (int)err, ", file=" __FILE__ ", line=" NCV_CT_PREP_STRINGIZE(__LINE__)); \ + return err; \ + } \ + } while (0) + + +#define ncvAssertReturnNcvStat(ncvOp) \ + do \ + { \ + NCVStatus _ncvStat = ncvOp; \ + if (NCV_SUCCESS != _ncvStat) \ + { \ + ncvDebugOutput("\n%s%d%s\n", "NCV Assertion Failed: NcvStat=", (int)_ncvStat, ", file=" __FILE__ ", line=" NCV_CT_PREP_STRINGIZE(__LINE__)); \ + return _ncvStat; \ + } \ + } while (0) + + +#define ncvAssertCUDAReturn(cudacall, errCode) \ + do \ + { \ + cudaError_t resCall = cudacall; \ + cudaError_t resGLE = cudaGetLastError(); \ + if (cudaSuccess != resCall || cudaSuccess != resGLE) \ + { \ + ncvDebugOutput("\n%s%d%s\n", "NCV CUDA Assertion Failed: cudaError_t=", (int)(resCall | resGLE), ", file=" __FILE__ ", line=" NCV_CT_PREP_STRINGIZE(__LINE__)); \ + return errCode; \ + } \ + } while (0) + + +/** +* Return-codes for status notification, errors and warnings +*/ +enum NCVStatus +{ + NCV_SUCCESS, + + NCV_CUDA_ERROR, + NCV_NPP_ERROR, + NCV_FILE_ERROR, + + NCV_NULL_PTR, + NCV_INCONSISTENT_INPUT, + NCV_TEXTURE_BIND_ERROR, + NCV_DIMENSIONS_INVALID, + + NCV_INVALID_ROI, + NCV_INVALID_STEP, + NCV_INVALID_SCALE, + + NCV_ALLOCATOR_NOT_INITIALIZED, + NCV_ALLOCATOR_BAD_ALLOC, + NCV_ALLOCATOR_BAD_DEALLOC, + NCV_ALLOCATOR_INSUFFICIENT_CAPACITY, + NCV_ALLOCATOR_DEALLOC_ORDER, + NCV_ALLOCATOR_BAD_REUSE, + + NCV_MEM_COPY_ERROR, + NCV_MEM_RESIDENCE_ERROR, + NCV_MEM_INSUFFICIENT_CAPACITY, + + NCV_HAAR_INVALID_PIXEL_STEP, + NCV_HAAR_TOO_MANY_FEATURES_IN_CLASSIFIER, + NCV_HAAR_TOO_MANY_FEATURES_IN_CASCADE, + NCV_HAAR_TOO_LARGE_FEATURES, + NCV_HAAR_XML_LOADING_EXCEPTION, + + NCV_NOIMPL_HAAR_TILTED_FEATURES, + + NCV_WARNING_HAAR_DETECTIONS_VECTOR_OVERFLOW, +}; + + +#define NCV_SET_SKIP_COND(x) \ + bool __ncv_skip_cond = x + + +#define NCV_RESET_SKIP_COND(x) \ + __ncv_skip_cond = x + + +#define NCV_SKIP_COND_BEGIN \ + if (!__ncv_skip_cond) { + + +#define NCV_SKIP_COND_END \ + } + + +//============================================================================== +// +// Timer +// +//============================================================================== + + +typedef struct _NcvTimer *NcvTimer; + +NcvTimer ncvStartTimer(void); + +double ncvEndQueryTimerUs(NcvTimer t); + +double ncvEndQueryTimerMs(NcvTimer t); + + +//============================================================================== +// +// Memory management classes template compound types +// +//============================================================================== + + +/** +* Alignment of GPU memory chunks in bytes +*/ +NCVStatus GPUAlignmentValue(Ncv32u &alignment); + + +/** +* Calculates the aligned top bound value +*/ +Ncv32u alignUp(Ncv32u what, Ncv32u alignment); + + +/** +* NCVMemoryType +*/ +enum NCVMemoryType +{ + NCVMemoryTypeNone, + NCVMemoryTypeHostPageable, + NCVMemoryTypeHostPinned, + NCVMemoryTypeDevice +}; + + +/** +* NCVMemPtr +*/ +struct NCVMemPtr +{ + void *ptr; + NCVMemoryType memtype; + void clear(); +}; + + +/** +* NCVMemSegment +*/ +struct NCVMemSegment +{ + NCVMemPtr begin; + size_t size; + void clear(); +}; + + +/** +* INCVMemAllocator (Interface) +*/ +class INCVMemAllocator +{ +public: + virtual ~INCVMemAllocator() = 0; + + virtual NCVStatus alloc(NCVMemSegment &seg, size_t size) = 0; + virtual NCVStatus dealloc(NCVMemSegment &seg) = 0; + + virtual NcvBool isInitialized(void) const = 0; + virtual NcvBool isCounting(void) const = 0; + + virtual NCVMemoryType memType(void) const = 0; + virtual Ncv32u alignment(void) const = 0; + virtual size_t maxSize(void) const = 0; +}; + +inline INCVMemAllocator::~INCVMemAllocator() {} + + +/** +* NCVMemStackAllocator +*/ +class NCVMemStackAllocator : public INCVMemAllocator +{ + NCVMemStackAllocator(); + NCVMemStackAllocator(const NCVMemStackAllocator &); + +public: + + explicit NCVMemStackAllocator(Ncv32u alignment); + NCVMemStackAllocator(NCVMemoryType memT, size_t capacity, Ncv32u alignment); + virtual ~NCVMemStackAllocator(); + + virtual NCVStatus alloc(NCVMemSegment &seg, size_t size); + virtual NCVStatus dealloc(NCVMemSegment &seg); + + virtual NcvBool isInitialized(void) const; + virtual NcvBool isCounting(void) const; + + virtual NCVMemoryType memType(void) const; + virtual Ncv32u alignment(void) const; + virtual size_t maxSize(void) const; + +private: + + NCVMemoryType _memType; + Ncv32u _alignment; + Ncv8u *allocBegin; + Ncv8u *begin; + Ncv8u *end; + size_t currentSize; + size_t _maxSize; +}; + + +/** +* NCVMemNativeAllocator +*/ +class NCVMemNativeAllocator : public INCVMemAllocator +{ +public: + + NCVMemNativeAllocator(NCVMemoryType memT); + virtual ~NCVMemNativeAllocator(); + + virtual NCVStatus alloc(NCVMemSegment &seg, size_t size); + virtual NCVStatus dealloc(NCVMemSegment &seg); + + virtual NcvBool isInitialized(void) const; + virtual NcvBool isCounting(void) const; + + virtual NCVMemoryType memType(void) const; + virtual Ncv32u alignment(void) const; + virtual size_t maxSize(void) const; + +private: + + NCVMemNativeAllocator(); + NCVMemNativeAllocator(const NCVMemNativeAllocator &); + + NCVMemoryType _memType; + Ncv32u _alignment; + size_t currentSize; + size_t _maxSize; +}; + + +/** +* Copy dispatcher +*/ +NCVStatus memSegCopyHelper(void *dst, NCVMemoryType dstType, + const void *src, NCVMemoryType srcType, + size_t sz, cudaStream_t cuStream); + + +/** +* NCVVector (1D) +*/ +template +class NCVVector +{ + NCVVector(const NCVVector &); + +public: + + NCVVector() + { + clear(); + } + + virtual ~NCVVector() {} + + void clear() + { + _ptr = NULL; + _length = 0; + _memtype = NCVMemoryTypeNone; + } + + NCVStatus copySolid(NCVVector &dst, cudaStream_t cuStream, size_t howMuch=0) + { + if (howMuch == 0) + { + ncvAssertReturn(dst._length == this->_length, NCV_MEM_COPY_ERROR); + howMuch = this->_length * sizeof(T); + } + else + { + ncvAssertReturn(dst._length * sizeof(T) >= howMuch && + this->_length * sizeof(T) >= howMuch && + howMuch > 0, NCV_MEM_COPY_ERROR); + } + ncvAssertReturn((this->_ptr != NULL || this->_memtype == NCVMemoryTypeNone) && + (dst._ptr != NULL || dst._memtype == NCVMemoryTypeNone), NCV_NULL_PTR); + + NCVStatus ncvStat = NCV_SUCCESS; + if (this->_memtype != NCVMemoryTypeNone) + { + ncvStat = memSegCopyHelper(dst._ptr, dst._memtype, + this->_ptr, this->_memtype, + howMuch, cuStream); + } + + return ncvStat; + } + + T *ptr() const {return this->_ptr;} + size_t length() const {return this->_length;} + NCVMemoryType memType() const {return this->_memtype;} + +protected: + + T *_ptr; + size_t _length; + NCVMemoryType _memtype; +}; + + +/** +* NCVVectorAlloc +*/ +template +class NCVVectorAlloc : public NCVVector +{ + NCVVectorAlloc(); + NCVVectorAlloc(const NCVVectorAlloc &); + +public: + + NCVVectorAlloc(INCVMemAllocator &allocator, Ncv32u length) + : + allocator(allocator) + { + NCVStatus ncvStat; + + this->clear(); + this->allocatedMem.clear(); + + ncvStat = allocator.alloc(this->allocatedMem, length * sizeof(T)); + ncvAssertPrintReturn(ncvStat == NCV_SUCCESS, "NCVVectorAlloc ctor:: alloc failed", ); + + this->_ptr = (T *)this->allocatedMem.begin.ptr; + this->_length = length; + this->_memtype = this->allocatedMem.begin.memtype; + } + + + ~NCVVectorAlloc() + { + NCVStatus ncvStat; + + ncvStat = allocator.dealloc(this->allocatedMem); + ncvAssertPrintCheck(ncvStat == NCV_SUCCESS, "NCVVectorAlloc dtor:: dealloc failed"); + + this->clear(); + } + + + NcvBool isMemAllocated() const + { + return (this->allocatedMem.begin.ptr != NULL) || (this->allocator.isCounting()); + } + + + Ncv32u getAllocatorsAlignment() const + { + return allocator.alignment(); + } + + + NCVMemSegment getSegment() const + { + return allocatedMem; + } + +private: + + INCVMemAllocator &allocator; + NCVMemSegment allocatedMem; +}; + + +/** +* NCVVectorReuse +*/ +template +class NCVVectorReuse : public NCVVector +{ + NCVVectorReuse(); + NCVVectorReuse(const NCVVectorReuse &); + +public: + + explicit NCVVectorReuse(const NCVMemSegment &memSegment) + { + this->bReused = false; + this->clear(); + + this->_length = memSegment.size / sizeof(T); + this->_ptr = (T *)memSegment.begin.ptr; + this->_memtype = memSegment.begin.memtype; + + this->bReused = true; + } + + + NCVVectorReuse(const NCVMemSegment &memSegment, Ncv32u length) + { + this->bReused = false; + this->clear(); + + ncvAssertPrintReturn(length * sizeof(T) <= memSegment.size, \ + "NCVVectorReuse ctor:: memory binding failed due to size mismatch", ); + + this->_length = length; + this->_ptr = (T *)memSegment.begin.ptr; + this->_memtype = memSegment.begin.memtype; + + this->bReused = true; + } + + + NcvBool isMemReused() const + { + return this->bReused; + } + +private: + + NcvBool bReused; +}; + + +/** +* NCVMatrix (2D) +*/ +template +class NCVMatrix +{ + NCVMatrix(const NCVMatrix &); + +public: + + NCVMatrix() + { + clear(); + } + + virtual ~NCVMatrix() {} + + + void clear() + { + _ptr = NULL; + _pitch = 0; + _width = 0; + _height = 0; + _memtype = NCVMemoryTypeNone; + } + + + Ncv32u stride() const + { + return _pitch / sizeof(T); + } + + + NCVStatus copySolid(NCVMatrix &dst, cudaStream_t cuStream, size_t howMuch=0) + { + if (howMuch == 0) + { + ncvAssertReturn(dst._pitch == this->_pitch && + dst._height == this->_height, NCV_MEM_COPY_ERROR); + howMuch = this->_pitch * this->_height; + } + else + { + ncvAssertReturn(dst._pitch * dst._height >= howMuch && + this->_pitch * this->_height >= howMuch && + howMuch > 0, NCV_MEM_COPY_ERROR); + } + ncvAssertReturn((this->_ptr != NULL || this->_memtype == NCVMemoryTypeNone) && + (dst._ptr != NULL || dst._memtype == NCVMemoryTypeNone), NCV_NULL_PTR); + + NCVStatus ncvStat = NCV_SUCCESS; + if (this->_memtype != NCVMemoryTypeNone) + { + ncvStat = memSegCopyHelper(dst._ptr, dst._memtype, + this->_ptr, this->_memtype, + howMuch, cuStream); + } + + return ncvStat; + } + + T *ptr() const {return this->_ptr;} + Ncv32u width() const {return this->_width;} + Ncv32u height() const {return this->_height;} + Ncv32u pitch() const {return this->_pitch;} + NCVMemoryType memType() const {return this->_memtype;} + +protected: + + T *_ptr; + Ncv32u _width; + Ncv32u _height; + Ncv32u _pitch; + NCVMemoryType _memtype; +}; + + +/** +* NCVMatrixAlloc +*/ +template +class NCVMatrixAlloc : public NCVMatrix +{ + NCVMatrixAlloc(); + NCVMatrixAlloc(const NCVMatrixAlloc &); + +public: + + NCVMatrixAlloc(INCVMemAllocator &allocator, Ncv32u width, Ncv32u height, Ncv32u pitch=0) + : + allocator(allocator) + { + NCVStatus ncvStat; + + this->clear(); + this->allocatedMem.clear(); + + Ncv32u widthBytes = width * sizeof(T); + Ncv32u pitchBytes = alignUp(widthBytes, allocator.alignment()); + + if (pitch != 0) + { + ncvAssertPrintReturn(pitch >= pitchBytes && + (pitch & (allocator.alignment() - 1)) == 0, + "NCVMatrixAlloc ctor:: incorrect pitch passed", ); + pitchBytes = pitch; + } + + Ncv32u requiredAllocSize = pitchBytes * height; + + ncvStat = allocator.alloc(this->allocatedMem, requiredAllocSize); + ncvAssertPrintReturn(ncvStat == NCV_SUCCESS, "NCVMatrixAlloc ctor:: alloc failed", ); + + this->_ptr = (T *)this->allocatedMem.begin.ptr; + this->_width = width; + this->_height = height; + this->_pitch = pitchBytes; + this->_memtype = this->allocatedMem.begin.memtype; + } + + ~NCVMatrixAlloc() + { + NCVStatus ncvStat; + + ncvStat = allocator.dealloc(this->allocatedMem); + ncvAssertPrintCheck(ncvStat == NCV_SUCCESS, "NCVMatrixAlloc dtor:: dealloc failed"); + + this->clear(); + } + + + NcvBool isMemAllocated() const + { + return (this->allocatedMem.begin.ptr != NULL) || (this->allocator.isCounting()); + } + + + Ncv32u getAllocatorsAlignment() const + { + return allocator.alignment(); + } + + + NCVMemSegment getSegment() const + { + return allocatedMem; + } + +private: + + INCVMemAllocator &allocator; + NCVMemSegment allocatedMem; +}; + + +/** +* NCVMatrixReuse +*/ +template +class NCVMatrixReuse : public NCVMatrix +{ + NCVMatrixReuse(); + NCVMatrixReuse(const NCVMatrixReuse &); + +public: + + NCVMatrixReuse(const NCVMemSegment &memSegment, Ncv32u alignment, Ncv32u width, Ncv32u height, Ncv32u pitch=0, NcvBool bSkipPitchCheck=false) + { + this->bReused = false; + this->clear(); + + Ncv32u widthBytes = width * sizeof(T); + Ncv32u pitchBytes = alignUp(widthBytes, alignment); + + if (pitch != 0) + { + if (!bSkipPitchCheck) + { + ncvAssertPrintReturn(pitch >= pitchBytes && + (pitch & (alignment - 1)) == 0, + "NCVMatrixReuse ctor:: incorrect pitch passed", ); + } + else + { + ncvAssertPrintReturn(pitch >= widthBytes, "NCVMatrixReuse ctor:: incorrect pitch passed", ); + } + pitchBytes = pitch; + } + + ncvAssertPrintReturn(pitchBytes * height <= memSegment.size, \ + "NCVMatrixReuse ctor:: memory binding failed due to size mismatch", ); + + this->_width = width; + this->_height = height; + this->_pitch = pitchBytes; + this->_ptr = (T *)memSegment.begin.ptr; + this->_memtype = memSegment.begin.memtype; + + this->bReused = true; + } + + + NcvBool isMemReused() const + { + return this->bReused; + } + +private: + + NcvBool bReused; +}; + +#endif // _ncv_hpp_ diff --git a/modules/gpu/src/nvidia/NCVHaarObjectDetection.cu b/modules/gpu/src/nvidia/NCVHaarObjectDetection.cu new file mode 100644 index 0000000000..a501d6525e --- /dev/null +++ b/modules/gpu/src/nvidia/NCVHaarObjectDetection.cu @@ -0,0 +1,2603 @@ +/*M/////////////////////////////////////////////////////////////////////////////////////// +// +// IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. +// +// By downloading, copying, installing or using the software you agree to this license. +// If you do not agree to this license, do not download, install, +// copy or use the software. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright (C) 2009-2010, NVIDIA Corporation, all rights reserved. +// Third party copyrights are property of their respective owners. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistribution's of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistribution's in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * The name of the copyright holders may not be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// This software is provided by the copyright holders and contributors "as is" and +// any express or implied warranties, including, but not limited to, the implied +// warranties of merchantability and fitness for a particular purpose are disclaimed. +// In no event shall the Intel Corporation or contributors be liable for any direct, +// indirect, incidental, special, exemplary, or consequential damages +// (including, but not limited to, procurement of substitute goods or services; +// loss of use, data, or profits; or business interruption) however caused +// and on any theory of liability, whether in contract, strict liability, +// or tort (including negligence or otherwise) arising in any way out of +// the use of this software, even if advised of the possibility of such damage. +// +//M*/ + +//////////////////////////////////////////////////////////////////////////////// +// +// NVIDIA CUDA implementation of Viola-Jones Object Detection Framework +// +// The algorithm and code are explained in the upcoming GPU Computing Gems +// chapter in detail: +// +// Anton Obukhov, "Haar Classifiers for Object Detection with CUDA" +// PDF URL placeholder +// email: aobukhov@nvidia.com, devsupport@nvidia.com +// +// Credits for help with the code to: +// Alexey Mendelenko, Cyril Crassin, and Mikhail Smirnov. +// +//////////////////////////////////////////////////////////////////////////////// + +#include + +#include "npp.h" +#include "NCV.hpp" +#include "NCVRuntimeTemplates.hpp" +#include "NCVHaarObjectDetection.hpp" + +void groupRectangles(std::vector &hypotheses, int groupThreshold, double eps, std::vector *weights); + + +//============================================================================== +// +// BlockScan file +// +//============================================================================== + + +//Almost the same as naive scan1Inclusive, but doesn't need __syncthreads() +//assuming size <= WARP_SIZE and size is power of 2 +template +inline __device__ T warpScanInclusive(T idata, volatile T *s_Data) +{ + Ncv32u pos = 2 * threadIdx.x - (threadIdx.x & (K_WARP_SIZE - 1)); + s_Data[pos] = 0; + pos += K_WARP_SIZE; + s_Data[pos] = idata; + + for(Ncv32u offset = 1; offset < K_WARP_SIZE; offset <<= 1) + { + s_Data[pos] += s_Data[pos - offset]; + } + + return s_Data[pos]; +} + + +template +inline __device__ T warpScanExclusive(T idata, volatile T *s_Data) +{ + return warpScanInclusive(idata, s_Data) - idata; +} + + +template +inline __device__ T blockScanInclusive(T idata, volatile T *s_Data) +{ + if (tiNumScanThreads > K_WARP_SIZE) + { + //Bottom-level inclusive warp scan + T warpResult = warpScanInclusive(idata, s_Data); + + //Save top elements of each warp for exclusive warp scan + //sync to wait for warp scans to complete (because s_Data is being overwritten) + __syncthreads(); + if( (threadIdx.x & (K_WARP_SIZE - 1)) == (K_WARP_SIZE - 1) ) + { + s_Data[threadIdx.x >> K_LOG2_WARP_SIZE] = warpResult; + } + + //wait for warp scans to complete + __syncthreads(); + + if( threadIdx.x < (tiNumScanThreads / K_WARP_SIZE) ) + { + //grab top warp elements + T val = s_Data[threadIdx.x]; + //calculate exclusive scan and write back to shared memory + s_Data[threadIdx.x] = warpScanExclusive(val, s_Data); + } + + //return updated warp scans with exclusive scan results + __syncthreads(); + return warpResult + s_Data[threadIdx.x >> K_LOG2_WARP_SIZE]; + } + else + { + return warpScanInclusive(idata, s_Data); + } +} + + +//============================================================================== +// +// HaarClassifierCascade file +// +//============================================================================== + + +const Ncv32u MAX_GRID_DIM = 65535; + + +const Ncv32u NUM_THREADS_ANCHORSPARALLEL = 64; + + +#define NUM_THREADS_CLASSIFIERPARALLEL_LOG2 6 +#define NUM_THREADS_CLASSIFIERPARALLEL (1 << NUM_THREADS_CLASSIFIERPARALLEL_LOG2) + + +/** \internal +* Haar features solid array. +*/ +texture texHaarFeatures; + + +/** \internal +* Haar classifiers flattened trees container. +* Two parts: first contains root nodes, second - nodes that are referred by root nodes. +* Drawback: breaks tree locality (might cause more cache misses +* Advantage: No need to introduce additional 32-bit field to index root nodes offsets +*/ +texture texHaarClassifierNodes; + + +texture texIImage; + + +__device__ HaarStage64 getStage(Ncv32u iStage, HaarStage64 *d_Stages) +{ + return d_Stages[iStage]; +} + + +template +__device__ HaarClassifierNode128 getClassifierNode(Ncv32u iNode, HaarClassifierNode128 *d_ClassifierNodes) +{ + HaarClassifierNode128 tmpNode; + if (tbCacheTextureCascade) + { + tmpNode._ui4 = tex1Dfetch(texHaarClassifierNodes, iNode); + } + else + { + tmpNode = d_ClassifierNodes[iNode]; + } + return tmpNode; +} + + +template +__device__ void getFeature(Ncv32u iFeature, HaarFeature64 *d_Features, + Ncv32f *weight, + Ncv32u *rectX, Ncv32u *rectY, Ncv32u *rectWidth, Ncv32u *rectHeight) +{ + HaarFeature64 feature; + if (tbCacheTextureCascade) + { + feature._ui2 = tex1Dfetch(texHaarFeatures, iFeature); + } + else + { + feature = d_Features[iFeature]; + } + feature.getRect(rectX, rectY, rectWidth, rectHeight); + *weight = feature.getWeight(); +} + + +template +__device__ Ncv32u getElemIImg(Ncv32u x, Ncv32u *d_IImg) +{ + if (tbCacheTextureIImg) + { + return tex1Dfetch(texIImage, x); + } + else + { + return d_IImg[x]; + } +} + + +__device__ Ncv32f reduceSpecialization(Ncv32f partialSum) +{ + __shared__ volatile Ncv32f reductor[NUM_THREADS_CLASSIFIERPARALLEL]; + reductor[threadIdx.x] = partialSum; + __syncthreads(); + +#if defined CPU_FP_COMPLIANCE + if (!threadIdx.x) + { + Ncv32f sum = 0.0f; + for (int i=0; i= 8 + if (threadIdx.x < 128) + { + reductor[threadIdx.x] += reductor[threadIdx.x + 128]; + } + __syncthreads(); +#endif +#if NUM_THREADS_CLASSIFIERPARALLEL_LOG2 >= 7 + if (threadIdx.x < 64) + { + reductor[threadIdx.x] += reductor[threadIdx.x + 64]; + } + __syncthreads(); +#endif + + if (threadIdx.x < 32) + { +#if NUM_THREADS_CLASSIFIERPARALLEL_LOG2 >= 6 + reductor[threadIdx.x] += reductor[threadIdx.x + 32]; +#endif +#if NUM_THREADS_CLASSIFIERPARALLEL_LOG2 >= 5 + reductor[threadIdx.x] += reductor[threadIdx.x + 16]; +#endif + reductor[threadIdx.x] += reductor[threadIdx.x + 8]; + reductor[threadIdx.x] += reductor[threadIdx.x + 4]; + reductor[threadIdx.x] += reductor[threadIdx.x + 2]; + reductor[threadIdx.x] += reductor[threadIdx.x + 1]; + } +#endif + + __syncthreads(); + + return reductor[0]; +} + + +__device__ Ncv32u d_outMaskPosition; + + +__inline __device__ void compactBlockWriteOutAnchorParallel(NcvBool threadPassFlag, + Ncv32u threadElem, + Ncv32u *vectorOut) +{ +#if __CUDA_ARCH__ >= 110 + Ncv32u passMaskElem = threadPassFlag ? 1 : 0; + __shared__ Ncv32u shmem[NUM_THREADS_ANCHORSPARALLEL * 2]; + Ncv32u incScan = blockScanInclusive(passMaskElem, shmem); + __syncthreads(); + Ncv32u excScan = incScan - passMaskElem; + + __shared__ Ncv32u numPassed; + __shared__ Ncv32u outMaskOffset; + if (threadIdx.x == NUM_THREADS_ANCHORSPARALLEL-1) + { + numPassed = incScan; + outMaskOffset = atomicAdd(&d_outMaskPosition, incScan); + } + __syncthreads(); + + if (threadPassFlag) + { + shmem[excScan] = threadElem; + } + __syncthreads(); + + if (threadIdx.x < numPassed) + { + vectorOut[outMaskOffset + threadIdx.x] = shmem[threadIdx.x]; + } +#endif +} + + +template +__global__ void applyHaarClassifierAnchorParallel(Ncv32u *d_IImg, Ncv32u IImgStride, + Ncv32f *d_weights, Ncv32u weightsStride, + HaarFeature64 *d_Features, HaarClassifierNode128 *d_ClassifierNodes, HaarStage64 *d_Stages, + Ncv32u *d_inMask, Ncv32u *d_outMask, + Ncv32u mask1Dlen, Ncv32u mask2Dstride, + NcvSize32u anchorsRoi, Ncv32u startStageInc, Ncv32u endStageExc, Ncv32f scaleArea) +{ + Ncv32u y_offs; + Ncv32u x_offs; + Ncv32u maskOffset; + Ncv32u outMaskVal; + + NcvBool bInactiveThread = false; + + if (tbReadPixelIndexFromVector) + { + maskOffset = (MAX_GRID_DIM * blockIdx.y + blockIdx.x) * NUM_THREADS_ANCHORSPARALLEL + threadIdx.x; + + if (maskOffset >= mask1Dlen) + { + if (tbDoAtomicCompaction) bInactiveThread = true; else return; + } + + if (!tbDoAtomicCompaction || tbDoAtomicCompaction && !bInactiveThread) + { + outMaskVal = d_inMask[maskOffset]; + y_offs = outMaskVal >> 16; + x_offs = outMaskVal & 0xFFFF; + } + } + else + { + y_offs = blockIdx.y; + x_offs = blockIdx.x * NUM_THREADS_ANCHORSPARALLEL + threadIdx.x; + + if (x_offs >= mask2Dstride) + { + if (tbDoAtomicCompaction) bInactiveThread = true; else return; + } + + if (!tbDoAtomicCompaction || tbDoAtomicCompaction && !bInactiveThread) + { + maskOffset = y_offs * mask2Dstride + x_offs; + + if ((x_offs >= anchorsRoi.width) || + (!tbInitMaskPositively && + d_inMask != d_outMask && + d_inMask[maskOffset] == OBJDET_MASK_ELEMENT_INVALID_32U)) + { + if (tbDoAtomicCompaction) + { + bInactiveThread = true; + } + else + { + d_outMask[maskOffset] = OBJDET_MASK_ELEMENT_INVALID_32U; + return; + } + } + + outMaskVal = (y_offs << 16) | x_offs; + } + } + + NcvBool bPass = true; + + if (!tbDoAtomicCompaction || tbDoAtomicCompaction && !bInactiveThread) + { + Ncv32f pixelStdDev = d_weights[y_offs * weightsStride + x_offs]; + + for (Ncv32u iStage = startStageInc; iStage(iNode, d_ClassifierNodes); + HaarFeatureDescriptor32 featuresDesc = curNode.getFeatureDesc(); + Ncv32u curNodeFeaturesNum = featuresDesc.getNumFeatures(); + Ncv32u iFeature = featuresDesc.getFeaturesOffset(); + + Ncv32f curNodeVal = 0.0f; + + for (Ncv32u iRect=0; iRect + (iFeature + iRect, d_Features, + &rectWeight, &rectX, &rectY, &rectWidth, &rectHeight); + + Ncv32u iioffsTL = (y_offs + rectY) * IImgStride + (x_offs + rectX); + Ncv32u iioffsTR = iioffsTL + rectWidth; + Ncv32u iioffsBL = iioffsTL + rectHeight * IImgStride; + Ncv32u iioffsBR = iioffsBL + rectWidth; + + Ncv32u rectSum = getElemIImg(iioffsBR, d_IImg) - + getElemIImg(iioffsBL, d_IImg) + + getElemIImg(iioffsTL, d_IImg) - + getElemIImg(iioffsTR, d_IImg); + +#if defined CPU_FP_COMPLIANCE || defined DISABLE_MAD_SELECTIVELY + curNodeVal += __fmul_rn((Ncv32f)rectSum, rectWeight); +#else + curNodeVal += (Ncv32f)rectSum * rectWeight; +#endif + } + + HaarClassifierNodeDescriptor32 nodeLeft = curNode.getLeftNodeDesc(); + HaarClassifierNodeDescriptor32 nodeRight = curNode.getRightNodeDesc(); + Ncv32f nodeThreshold = curNode.getThreshold(); + HaarClassifierNodeDescriptor32 nextNodeDescriptor; + nextNodeDescriptor = (curNodeVal < scaleArea * pixelStdDev * nodeThreshold) ? nodeLeft : nodeRight; + + if (nextNodeDescriptor.isLeaf()) + { + Ncv32f tmpLeafValue = nextNodeDescriptor.getLeafValue(); + curStageSum += tmpLeafValue; + bMoreNodesToTraverse = false; + } + else + { + iNode = nextNodeDescriptor.getNextNodeOffset(); + } + } + + __syncthreads(); + curRootNodeOffset++; + } + + if (curStageSum < stageThreshold) + { + bPass = false; + outMaskVal = OBJDET_MASK_ELEMENT_INVALID_32U; + break; + } + } + } + + __syncthreads(); + + if (!tbDoAtomicCompaction) + { + if (!tbReadPixelIndexFromVector || + (tbReadPixelIndexFromVector && (!bPass || d_inMask != d_outMask))) + { + d_outMask[maskOffset] = outMaskVal; + } + } + else + { + compactBlockWriteOutAnchorParallel(bPass && !bInactiveThread, + outMaskVal, + d_outMask); + } +} + + +template +__global__ void applyHaarClassifierClassifierParallel(Ncv32u *d_IImg, Ncv32u IImgStride, + Ncv32f *d_weights, Ncv32u weightsStride, + HaarFeature64 *d_Features, HaarClassifierNode128 *d_ClassifierNodes, HaarStage64 *d_Stages, + Ncv32u *d_inMask, Ncv32u *d_outMask, + Ncv32u mask1Dlen, Ncv32u mask2Dstride, + NcvSize32u anchorsRoi, Ncv32u startStageInc, Ncv32u endStageExc, Ncv32f scaleArea) +{ + Ncv32u maskOffset = MAX_GRID_DIM * blockIdx.y + blockIdx.x; + + if (maskOffset >= mask1Dlen) + { + return; + } + + Ncv32u outMaskVal = d_inMask[maskOffset]; + Ncv32u y_offs = outMaskVal >> 16; + Ncv32u x_offs = outMaskVal & 0xFFFF; + + Ncv32f pixelStdDev = d_weights[y_offs * weightsStride + x_offs]; + NcvBool bPass = true; + + for (Ncv32u iStage = startStageInc; iStage> NUM_THREADS_CLASSIFIERPARALLEL_LOG2; + + for (Ncv32u chunkId=0; chunkId(iNode, d_ClassifierNodes); + HaarFeatureDescriptor32 featuresDesc = curNode.getFeatureDesc(); + Ncv32u curNodeFeaturesNum = featuresDesc.getNumFeatures(); + Ncv32u iFeature = featuresDesc.getFeaturesOffset(); + + Ncv32f curNodeVal = 0.0f; + //TODO: fetch into shmem if size suffices. Shmem can be shared with reduce + for (Ncv32u iRect=0; iRect + (iFeature + iRect, d_Features, + &rectWeight, &rectX, &rectY, &rectWidth, &rectHeight); + + Ncv32u iioffsTL = (y_offs + rectY) * IImgStride + (x_offs + rectX); + Ncv32u iioffsTR = iioffsTL + rectWidth; + Ncv32u iioffsBL = iioffsTL + rectHeight * IImgStride; + Ncv32u iioffsBR = iioffsBL + rectWidth; + + Ncv32u rectSum = getElemIImg(iioffsBR, d_IImg) - + getElemIImg(iioffsBL, d_IImg) + + getElemIImg(iioffsTL, d_IImg) - + getElemIImg(iioffsTR, d_IImg); + +#if defined CPU_FP_COMPLIANCE || defined DISABLE_MAD_SELECTIVELY + curNodeVal += __fmul_rn((Ncv32f)rectSum, rectWeight); +#else + curNodeVal += (Ncv32f)rectSum * rectWeight; +#endif + } + + HaarClassifierNodeDescriptor32 nodeLeft = curNode.getLeftNodeDesc(); + HaarClassifierNodeDescriptor32 nodeRight = curNode.getRightNodeDesc(); + Ncv32f nodeThreshold = curNode.getThreshold(); + HaarClassifierNodeDescriptor32 nextNodeDescriptor; + nextNodeDescriptor = (curNodeVal < scaleArea * pixelStdDev * nodeThreshold) ? nodeLeft : nodeRight; + + if (nextNodeDescriptor.isLeaf()) + { + Ncv32f tmpLeafValue = nextNodeDescriptor.getLeafValue(); + curStageSum += tmpLeafValue; + bMoreNodesToTraverse = false; + } + else + { + iNode = nextNodeDescriptor.getNextNodeOffset(); + } + } + } + __syncthreads(); + + curRootNodeOffset += NUM_THREADS_CLASSIFIERPARALLEL; + } + + Ncv32f finalStageSum = reduceSpecialization(curStageSum); + + if (finalStageSum < stageThreshold) + { + bPass = false; + outMaskVal = OBJDET_MASK_ELEMENT_INVALID_32U; + break; + } + } + + if (!tbDoAtomicCompaction) + { + if (!bPass || d_inMask != d_outMask) + { + if (!threadIdx.x) + { + d_outMask[maskOffset] = outMaskVal; + } + } + } + else + { +#if __CUDA_ARCH__ >= 110 + if (bPass && !threadIdx.x) + { + Ncv32u outMaskOffset = atomicAdd(&d_outMaskPosition, 1); + d_outMask[outMaskOffset] = outMaskVal; + } +#endif + } +} + + +template +__global__ void initializeMaskVector(Ncv32u *d_inMask, Ncv32u *d_outMask, + Ncv32u mask1Dlen, Ncv32u mask2Dstride, + NcvSize32u anchorsRoi, Ncv32u step) +{ + Ncv32u y_offs = blockIdx.y; + Ncv32u x_offs = blockIdx.x * NUM_THREADS_ANCHORSPARALLEL + threadIdx.x; + Ncv32u outMaskOffset = y_offs * gridDim.x * blockDim.x + x_offs; + + Ncv32u y_offs_upsc = step * y_offs; + Ncv32u x_offs_upsc = step * x_offs; + Ncv32u inMaskOffset = y_offs_upsc * mask2Dstride + x_offs_upsc; + + Ncv32u outElem = OBJDET_MASK_ELEMENT_INVALID_32U; + + if (x_offs_upsc < anchorsRoi.width && + (!tbMaskByInmask || d_inMask[inMaskOffset] != OBJDET_MASK_ELEMENT_INVALID_32U)) + { + outElem = (y_offs_upsc << 16) | x_offs_upsc; + } + + if (!tbDoAtomicCompaction) + { + d_outMask[outMaskOffset] = outElem; + } + else + { + compactBlockWriteOutAnchorParallel(outElem != OBJDET_MASK_ELEMENT_INVALID_32U, + outElem, + d_outMask); + } +} + + +struct applyHaarClassifierAnchorParallelFunctor +{ + dim3 gridConf, blockConf; + cudaStream_t cuStream; + + //Kernel arguments are stored as members; + Ncv32u *d_IImg; + Ncv32u IImgStride; + Ncv32f *d_weights; + Ncv32u weightsStride; + HaarFeature64 *d_Features; + HaarClassifierNode128 *d_ClassifierNodes; + HaarStage64 *d_Stages; + Ncv32u *d_inMask; + Ncv32u *d_outMask; + Ncv32u mask1Dlen; + Ncv32u mask2Dstride; + NcvSize32u anchorsRoi; + Ncv32u startStageInc; + Ncv32u endStageExc; + Ncv32f scaleArea; + + //Arguments are passed through the constructor + applyHaarClassifierAnchorParallelFunctor(dim3 _gridConf, dim3 _blockConf, cudaStream_t _cuStream, + Ncv32u *_d_IImg, Ncv32u _IImgStride, + Ncv32f *_d_weights, Ncv32u _weightsStride, + HaarFeature64 *_d_Features, HaarClassifierNode128 *_d_ClassifierNodes, HaarStage64 *_d_Stages, + Ncv32u *_d_inMask, Ncv32u *_d_outMask, + Ncv32u _mask1Dlen, Ncv32u _mask2Dstride, + NcvSize32u _anchorsRoi, Ncv32u _startStageInc, + Ncv32u _endStageExc, Ncv32f _scaleArea) : + gridConf(_gridConf), + blockConf(_blockConf), + cuStream(_cuStream), + d_IImg(_d_IImg), + IImgStride(_IImgStride), + d_weights(_d_weights), + weightsStride(_weightsStride), + d_Features(_d_Features), + d_ClassifierNodes(_d_ClassifierNodes), + d_Stages(_d_Stages), + d_inMask(_d_inMask), + d_outMask(_d_outMask), + mask1Dlen(_mask1Dlen), + mask2Dstride(_mask2Dstride), + anchorsRoi(_anchorsRoi), + startStageInc(_startStageInc), + endStageExc(_endStageExc), + scaleArea(_scaleArea) + {} + + template + void call(TList tl) + { + applyHaarClassifierAnchorParallel < + Loki::TL::TypeAt::Result::value, + Loki::TL::TypeAt::Result::value, + Loki::TL::TypeAt::Result::value, + Loki::TL::TypeAt::Result::value, + Loki::TL::TypeAt::Result::value > + <<>> + (d_IImg, IImgStride, + d_weights, weightsStride, + d_Features, d_ClassifierNodes, d_Stages, + d_inMask, d_outMask, + mask1Dlen, mask2Dstride, + anchorsRoi, startStageInc, + endStageExc, scaleArea); + } +}; + + +void applyHaarClassifierAnchorParallelDynTemplate(NcvBool tbInitMaskPositively, + NcvBool tbCacheTextureIImg, + NcvBool tbCacheTextureCascade, + NcvBool tbReadPixelIndexFromVector, + NcvBool tbDoAtomicCompaction, + + dim3 gridConf, dim3 blockConf, cudaStream_t cuStream, + + Ncv32u *d_IImg, Ncv32u IImgStride, + Ncv32f *d_weights, Ncv32u weightsStride, + HaarFeature64 *d_Features, HaarClassifierNode128 *d_ClassifierNodes, HaarStage64 *d_Stages, + Ncv32u *d_inMask, Ncv32u *d_outMask, + Ncv32u mask1Dlen, Ncv32u mask2Dstride, + NcvSize32u anchorsRoi, Ncv32u startStageInc, + Ncv32u endStageExc, Ncv32f scaleArea) +{ + //Second parameter is the number of "dynamic" template parameters + NCVRuntimeTemplateBool::KernelCaller + ::call( applyHaarClassifierAnchorParallelFunctor(gridConf, blockConf, cuStream, + d_IImg, IImgStride, + d_weights, weightsStride, + d_Features, d_ClassifierNodes, d_Stages, + d_inMask, d_outMask, + mask1Dlen, mask2Dstride, + anchorsRoi, startStageInc, + endStageExc, scaleArea), + 0xC001C0DE, //this is dummy int for the va_args C compatibility + tbInitMaskPositively, + tbCacheTextureIImg, + tbCacheTextureCascade, + tbReadPixelIndexFromVector, + tbDoAtomicCompaction); +} + + +struct applyHaarClassifierClassifierParallelFunctor +{ + dim3 gridConf, blockConf; + cudaStream_t cuStream; + + //Kernel arguments are stored as members; + Ncv32u *d_IImg; + Ncv32u IImgStride; + Ncv32f *d_weights; + Ncv32u weightsStride; + HaarFeature64 *d_Features; + HaarClassifierNode128 *d_ClassifierNodes; + HaarStage64 *d_Stages; + Ncv32u *d_inMask; + Ncv32u *d_outMask; + Ncv32u mask1Dlen; + Ncv32u mask2Dstride; + NcvSize32u anchorsRoi; + Ncv32u startStageInc; + Ncv32u endStageExc; + Ncv32f scaleArea; + + //Arguments are passed through the constructor + applyHaarClassifierClassifierParallelFunctor(dim3 _gridConf, dim3 _blockConf, cudaStream_t _cuStream, + Ncv32u *_d_IImg, Ncv32u _IImgStride, + Ncv32f *_d_weights, Ncv32u _weightsStride, + HaarFeature64 *_d_Features, HaarClassifierNode128 *_d_ClassifierNodes, HaarStage64 *_d_Stages, + Ncv32u *_d_inMask, Ncv32u *_d_outMask, + Ncv32u _mask1Dlen, Ncv32u _mask2Dstride, + NcvSize32u _anchorsRoi, Ncv32u _startStageInc, + Ncv32u _endStageExc, Ncv32f _scaleArea) : + gridConf(_gridConf), + blockConf(_blockConf), + cuStream(_cuStream), + d_IImg(_d_IImg), + IImgStride(_IImgStride), + d_weights(_d_weights), + weightsStride(_weightsStride), + d_Features(_d_Features), + d_ClassifierNodes(_d_ClassifierNodes), + d_Stages(_d_Stages), + d_inMask(_d_inMask), + d_outMask(_d_outMask), + mask1Dlen(_mask1Dlen), + mask2Dstride(_mask2Dstride), + anchorsRoi(_anchorsRoi), + startStageInc(_startStageInc), + endStageExc(_endStageExc), + scaleArea(_scaleArea) + {} + + template + void call(TList tl) + { + applyHaarClassifierClassifierParallel < + Loki::TL::TypeAt::Result::value, + Loki::TL::TypeAt::Result::value, + Loki::TL::TypeAt::Result::value > + <<>> + (d_IImg, IImgStride, + d_weights, weightsStride, + d_Features, d_ClassifierNodes, d_Stages, + d_inMask, d_outMask, + mask1Dlen, mask2Dstride, + anchorsRoi, startStageInc, + endStageExc, scaleArea); + } +}; + + +void applyHaarClassifierClassifierParallelDynTemplate(NcvBool tbCacheTextureIImg, + NcvBool tbCacheTextureCascade, + NcvBool tbDoAtomicCompaction, + + dim3 gridConf, dim3 blockConf, cudaStream_t cuStream, + + Ncv32u *d_IImg, Ncv32u IImgStride, + Ncv32f *d_weights, Ncv32u weightsStride, + HaarFeature64 *d_Features, HaarClassifierNode128 *d_ClassifierNodes, HaarStage64 *d_Stages, + Ncv32u *d_inMask, Ncv32u *d_outMask, + Ncv32u mask1Dlen, Ncv32u mask2Dstride, + NcvSize32u anchorsRoi, Ncv32u startStageInc, + Ncv32u endStageExc, Ncv32f scaleArea) +{ + //Second parameter is the number of "dynamic" template parameters + NCVRuntimeTemplateBool::KernelCaller + ::call( applyHaarClassifierClassifierParallelFunctor(gridConf, blockConf, cuStream, + d_IImg, IImgStride, + d_weights, weightsStride, + d_Features, d_ClassifierNodes, d_Stages, + d_inMask, d_outMask, + mask1Dlen, mask2Dstride, + anchorsRoi, startStageInc, + endStageExc, scaleArea), + 0xC001C0DE, //this is dummy int for the va_args C compatibility + tbCacheTextureIImg, + tbCacheTextureCascade, + tbDoAtomicCompaction); +} + + +struct initializeMaskVectorFunctor +{ + dim3 gridConf, blockConf; + cudaStream_t cuStream; + + //Kernel arguments are stored as members; + Ncv32u *d_inMask; + Ncv32u *d_outMask; + Ncv32u mask1Dlen; + Ncv32u mask2Dstride; + NcvSize32u anchorsRoi; + Ncv32u step; + + //Arguments are passed through the constructor + initializeMaskVectorFunctor(dim3 _gridConf, dim3 _blockConf, cudaStream_t _cuStream, + Ncv32u *_d_inMask, Ncv32u *_d_outMask, + Ncv32u _mask1Dlen, Ncv32u _mask2Dstride, + NcvSize32u _anchorsRoi, Ncv32u _step) : + gridConf(_gridConf), + blockConf(_blockConf), + cuStream(_cuStream), + d_inMask(_d_inMask), + d_outMask(_d_outMask), + mask1Dlen(_mask1Dlen), + mask2Dstride(_mask2Dstride), + anchorsRoi(_anchorsRoi), + step(_step) + {} + + template + void call(TList tl) + { + initializeMaskVector < + Loki::TL::TypeAt::Result::value, + Loki::TL::TypeAt::Result::value > + <<>> + (d_inMask, d_outMask, + mask1Dlen, mask2Dstride, + anchorsRoi, step); + } +}; + + +void initializeMaskVectorDynTemplate(NcvBool tbMaskByInmask, + NcvBool tbDoAtomicCompaction, + + dim3 gridConf, dim3 blockConf, cudaStream_t cuStream, + + Ncv32u *d_inMask, Ncv32u *d_outMask, + Ncv32u mask1Dlen, Ncv32u mask2Dstride, + NcvSize32u anchorsRoi, Ncv32u step) +{ + //Second parameter is the number of "dynamic" template parameters + NCVRuntimeTemplateBool::KernelCaller + ::call( initializeMaskVectorFunctor(gridConf, blockConf, cuStream, + d_inMask, d_outMask, + mask1Dlen, mask2Dstride, + anchorsRoi, step), + 0xC001C0DE, //this is dummy int for the va_args C compatibility + tbMaskByInmask, + tbDoAtomicCompaction); +} + + +Ncv32u getStageNumWithNotLessThanNclassifiers(Ncv32u N, HaarClassifierCascadeDescriptor &haar, + NCVVector &h_HaarStages) +{ + Ncv32u i = 0; + for (; i= N) + { + break; + } + } + return i; +} + + +template +void swap(T &p1, T &p2) +{ + T tmp = p1; + p1 = p2; + p2 = tmp; +} + + +NCVStatus ncvApplyHaarClassifierCascade_device(NCVMatrix &d_integralImage, + NCVMatrix &d_weights, + NCVMatrixAlloc &d_pixelMask, + Ncv32u &numDetections, + HaarClassifierCascadeDescriptor &haar, + NCVVector &h_HaarStages, + NCVVector &d_HaarStages, + NCVVector &d_HaarNodes, + NCVVector &d_HaarFeatures, + NcvBool bMaskElements, + NcvSize32u anchorsRoi, + Ncv32u pixelStep, + Ncv32f scaleArea, + INCVMemAllocator &gpuAllocator, + INCVMemAllocator &cpuAllocator, + Ncv32u devPropMajor, + Ncv32u devPropMinor, + cudaStream_t cuStream) +{ + ncvAssertReturn(d_integralImage.memType() == d_weights.memType() && + d_integralImage.memType() == d_pixelMask.memType() && + d_integralImage.memType() == gpuAllocator.memType() && + (d_integralImage.memType() == NCVMemoryTypeDevice || + d_integralImage.memType() == NCVMemoryTypeNone), NCV_MEM_RESIDENCE_ERROR); + ncvAssertReturn(d_HaarStages.memType() == d_HaarNodes.memType() && + d_HaarStages.memType() == d_HaarFeatures.memType() && + (d_HaarStages.memType() == NCVMemoryTypeDevice || + d_HaarStages.memType() == NCVMemoryTypeNone), NCV_MEM_RESIDENCE_ERROR); + ncvAssertReturn(h_HaarStages.memType() != NCVMemoryTypeDevice, NCV_MEM_RESIDENCE_ERROR); + ncvAssertReturn(gpuAllocator.isInitialized() && cpuAllocator.isInitialized(), NCV_ALLOCATOR_NOT_INITIALIZED); + ncvAssertReturn((d_integralImage.ptr() != NULL && d_weights.ptr() != NULL && d_pixelMask.ptr() != NULL && + h_HaarStages.ptr() != NULL && d_HaarStages.ptr() != NULL && d_HaarNodes.ptr() != NULL && + d_HaarFeatures.ptr() != NULL) || gpuAllocator.isCounting(), NCV_NULL_PTR); + ncvAssertReturn(anchorsRoi.width > 0 && anchorsRoi.height > 0 && + d_pixelMask.width() >= anchorsRoi.width && d_pixelMask.height() >= anchorsRoi.height && + d_weights.width() >= anchorsRoi.width && d_weights.height() >= anchorsRoi.height && + d_integralImage.width() >= anchorsRoi.width + haar.ClassifierSize.width && + d_integralImage.height() >= anchorsRoi.height + haar.ClassifierSize.height, NCV_DIMENSIONS_INVALID); + ncvAssertReturn(scaleArea > 0, NCV_INVALID_SCALE); + ncvAssertReturn(d_HaarStages.length() >= haar.NumStages && + d_HaarNodes.length() >= haar.NumClassifierTotalNodes && + d_HaarFeatures.length() >= haar.NumFeatures && + d_HaarStages.length() == h_HaarStages.length() && + haar.NumClassifierRootNodes <= haar.NumClassifierTotalNodes, NCV_DIMENSIONS_INVALID); + ncvAssertReturn(haar.bNeedsTiltedII == false || gpuAllocator.isCounting(), NCV_NOIMPL_HAAR_TILTED_FEATURES); + ncvAssertReturn(pixelStep == 1 || pixelStep == 2, NCV_HAAR_INVALID_PIXEL_STEP); + + NCV_SET_SKIP_COND(gpuAllocator.isCounting()); + +#if defined _SELF_TEST_ + + NCVStatus ncvStat; + + NCVMatrixAlloc h_integralImage(cpuAllocator, d_integralImage.width, d_integralImage.height, d_integralImage.pitch); + ncvAssertReturn(h_integralImage.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + NCVMatrixAlloc h_weights(cpuAllocator, d_weights.width, d_weights.height, d_weights.pitch); + ncvAssertReturn(h_weights.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + NCVMatrixAlloc h_pixelMask(cpuAllocator, d_pixelMask.width, d_pixelMask.height, d_pixelMask.pitch); + ncvAssertReturn(h_pixelMask.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + NCVVectorAlloc h_HaarNodes(cpuAllocator, d_HaarNodes.length); + ncvAssertReturn(h_HaarNodes.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + NCVVectorAlloc h_HaarFeatures(cpuAllocator, d_HaarFeatures.length); + ncvAssertReturn(h_HaarFeatures.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + + NCVMatrixAlloc h_pixelMask_d(cpuAllocator, d_pixelMask.width, d_pixelMask.height, d_pixelMask.pitch); + ncvAssertReturn(h_pixelMask_d.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + + NCV_SKIP_COND_BEGIN + + ncvStat = d_pixelMask.copySolid(h_pixelMask, 0); + ncvAssertReturnNcvStat(ncvStat); + ncvStat = d_integralImage.copySolid(h_integralImage, 0); + ncvAssertReturnNcvStat(ncvStat); + ncvStat = d_weights.copySolid(h_weights, 0); + ncvAssertReturnNcvStat(ncvStat); + ncvStat = d_HaarNodes.copySolid(h_HaarNodes, 0); + ncvAssertReturnNcvStat(ncvStat); + ncvStat = d_HaarFeatures.copySolid(h_HaarFeatures, 0); + ncvAssertReturnNcvStat(ncvStat); + ncvAssertCUDAReturn(cudaStreamSynchronize(0), NCV_CUDA_ERROR); + + for (Ncv32u i=0; i<(Ncv32u)anchorsRoi.height; i++) + { + for (Ncv32u j=0; j d_vecPixelMask(d_pixelMask.getSegment(), anchorsRoi.height * d_pixelMask.stride()); + ncvAssertReturn(d_vecPixelMask.isMemReused(), NCV_ALLOCATOR_BAD_REUSE); + + NCVVectorAlloc d_vecPixelMaskTmp(gpuAllocator, d_vecPixelMask.length()); + ncvAssertReturn(d_vecPixelMaskTmp.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + + NCVVectorAlloc hp_pool32u(cpuAllocator, 2); + ncvAssertReturn(hp_pool32u.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + Ncv32u *hp_zero = &hp_pool32u.ptr()[0]; + Ncv32u *hp_numDet = &hp_pool32u.ptr()[1]; + + NCV_SKIP_COND_BEGIN + *hp_zero = 0; + *hp_numDet = 0; + NCV_SKIP_COND_END + + Ncv32f scaleAreaPixels = scaleArea * ((haar.ClassifierSize.width - 2*HAAR_STDDEV_BORDER) * + (haar.ClassifierSize.height - 2*HAAR_STDDEV_BORDER)); + + NcvBool bTexCacheCascade = devPropMajor < 2; + NcvBool bTexCacheIImg = true; //this works better even on Fermi so far + NcvBool bDoAtomicCompaction = devPropMajor >= 2 || (devPropMajor == 1 && devPropMinor >= 3); + + NCVVector *d_ptrNowData = &d_vecPixelMask; + NCVVector *d_ptrNowTmp = &d_vecPixelMaskTmp; + + Ncv32u szNppCompactTmpBuf; + nppsStCompactGetSize_32u(d_vecPixelMask.length(), &szNppCompactTmpBuf); + if (bDoAtomicCompaction) + { + szNppCompactTmpBuf = 0; + } + NCVVectorAlloc d_tmpBufCompact(gpuAllocator, szNppCompactTmpBuf); + + NCV_SKIP_COND_BEGIN + + if (bTexCacheIImg) + { + cudaChannelFormatDesc cfdTexIImage; + cfdTexIImage = cudaCreateChannelDesc(); + + size_t alignmentOffset; + ncvAssertCUDAReturn(cudaBindTexture(&alignmentOffset, texIImage, d_integralImage.ptr(), cfdTexIImage, + (anchorsRoi.height + haar.ClassifierSize.height) * d_integralImage.pitch()), NCV_CUDA_ERROR); + ncvAssertReturn(alignmentOffset==0, NCV_TEXTURE_BIND_ERROR); + } + + if (bTexCacheCascade) + { + cudaChannelFormatDesc cfdTexHaarFeatures; + cudaChannelFormatDesc cfdTexHaarClassifierNodes; + cfdTexHaarFeatures = cudaCreateChannelDesc(); + cfdTexHaarClassifierNodes = cudaCreateChannelDesc(); + + size_t alignmentOffset; + ncvAssertCUDAReturn(cudaBindTexture(&alignmentOffset, texHaarFeatures, + d_HaarFeatures.ptr(), cfdTexHaarFeatures,sizeof(HaarFeature64) * haar.NumFeatures), NCV_CUDA_ERROR); + ncvAssertReturn(alignmentOffset==0, NCV_TEXTURE_BIND_ERROR); + ncvAssertCUDAReturn(cudaBindTexture(&alignmentOffset, texHaarClassifierNodes, + d_HaarNodes.ptr(), cfdTexHaarClassifierNodes, sizeof(HaarClassifierNode128) * haar.NumClassifierTotalNodes), NCV_CUDA_ERROR); + ncvAssertReturn(alignmentOffset==0, NCV_TEXTURE_BIND_ERROR); + } + + Ncv32u stageStartAnchorParallel = 0; + Ncv32u stageMiddleSwitch = getStageNumWithNotLessThanNclassifiers(NUM_THREADS_CLASSIFIERPARALLEL, + haar, h_HaarStages); + Ncv32u stageEndClassifierParallel = haar.NumStages; + if (stageMiddleSwitch == 0) + { + stageMiddleSwitch = 1; + } + + //create stages subdivision for pixel-parallel processing + const Ncv32u compactEveryNstage = bDoAtomicCompaction ? 7 : 1; + Ncv32u curStop = stageStartAnchorParallel; + std::vector pixParallelStageStops; + while (curStop < stageMiddleSwitch) + { + pixParallelStageStops.push_back(curStop); + curStop += compactEveryNstage; + } + if (curStop > compactEveryNstage && curStop - stageMiddleSwitch > compactEveryNstage / 2) + { + pixParallelStageStops[pixParallelStageStops.size()-1] = + (stageMiddleSwitch - (curStop - 2 * compactEveryNstage)) / 2; + } + pixParallelStageStops.push_back(stageMiddleSwitch); + Ncv32u pixParallelStageStopsIndex = 0; + + if (pixelStep != 1 || bMaskElements) + { + if (bDoAtomicCompaction) + { + ncvAssertCUDAReturn(cudaMemcpyToSymbolAsync(d_outMaskPosition, hp_zero, sizeof(Ncv32u), + 0, cudaMemcpyHostToDevice, cuStream), NCV_CUDA_ERROR); + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + } + + dim3 gridInit((((anchorsRoi.width + pixelStep - 1) / pixelStep + NUM_THREADS_ANCHORSPARALLEL - 1) / NUM_THREADS_ANCHORSPARALLEL), + (anchorsRoi.height + pixelStep - 1) / pixelStep); + dim3 blockInit(NUM_THREADS_ANCHORSPARALLEL); + + if (gridInit.x == 0 || gridInit.y == 0) + { + numDetections = 0; + return NCV_SUCCESS; + } + + initializeMaskVectorDynTemplate(bMaskElements, + bDoAtomicCompaction, + gridInit, blockInit, cuStream, + d_ptrNowData->ptr(), + d_ptrNowTmp->ptr(), + d_vecPixelMask.length(), d_pixelMask.stride(), + anchorsRoi, pixelStep); + ncvAssertCUDAReturn(cudaGetLastError(), NCV_CUDA_ERROR); + + if (bDoAtomicCompaction) + { + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + ncvAssertCUDAReturn(cudaMemcpyFromSymbolAsync(hp_numDet, d_outMaskPosition, sizeof(Ncv32u), + 0, cudaMemcpyDeviceToHost, cuStream), NCV_CUDA_ERROR); + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + swap(d_ptrNowData, d_ptrNowTmp); + } + else + { + NppStStatus nppSt; + nppSt = nppsStCompact_32u(d_ptrNowTmp->ptr(), d_vecPixelMask.length(), + d_ptrNowData->ptr(), hp_numDet, OBJDET_MASK_ELEMENT_INVALID_32U, + d_tmpBufCompact.ptr(), szNppCompactTmpBuf); + ncvAssertReturn(nppSt == NPP_SUCCESS, NCV_NPP_ERROR); + } + numDetections = *hp_numDet; + } + else + { + // + // 1. Run the first pixel-input pixel-parallel classifier for few stages + // + + if (bDoAtomicCompaction) + { + ncvAssertCUDAReturn(cudaMemcpyToSymbolAsync(d_outMaskPosition, hp_zero, sizeof(Ncv32u), + 0, cudaMemcpyHostToDevice, cuStream), NCV_CUDA_ERROR); + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + } + + dim3 grid1(((d_pixelMask.stride() + NUM_THREADS_ANCHORSPARALLEL - 1) / NUM_THREADS_ANCHORSPARALLEL), + anchorsRoi.height); + dim3 block1(NUM_THREADS_ANCHORSPARALLEL); + applyHaarClassifierAnchorParallelDynTemplate( + true, //tbInitMaskPositively + bTexCacheIImg, //tbCacheTextureIImg + bTexCacheCascade, //tbCacheTextureCascade + pixParallelStageStops[pixParallelStageStopsIndex] != 0,//tbReadPixelIndexFromVector + bDoAtomicCompaction, //tbDoAtomicCompaction + grid1, + block1, + cuStream, + d_integralImage.ptr(), d_integralImage.stride(), + d_weights.ptr(), d_weights.stride(), + d_HaarFeatures.ptr(), d_HaarNodes.ptr(), d_HaarStages.ptr(), + d_ptrNowData->ptr(), + bDoAtomicCompaction ? d_ptrNowTmp->ptr() : d_ptrNowData->ptr(), + 0, + d_pixelMask.stride(), + anchorsRoi, + pixParallelStageStops[pixParallelStageStopsIndex], + pixParallelStageStops[pixParallelStageStopsIndex+1], + scaleAreaPixels); + ncvAssertCUDAReturn(cudaGetLastError(), NCV_CUDA_ERROR); + + if (bDoAtomicCompaction) + { + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + ncvAssertCUDAReturn(cudaMemcpyFromSymbolAsync(hp_numDet, d_outMaskPosition, sizeof(Ncv32u), + 0, cudaMemcpyDeviceToHost, cuStream), NCV_CUDA_ERROR); + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + } + else + { + NppStStatus nppSt; + nppSt = nppsStCompact_32u(d_ptrNowData->ptr(), d_vecPixelMask.length(), + d_ptrNowTmp->ptr(), hp_numDet, OBJDET_MASK_ELEMENT_INVALID_32U, + d_tmpBufCompact.ptr(), szNppCompactTmpBuf); + ncvAssertReturn(nppSt == NPP_SUCCESS, NCV_NPP_ERROR); + } + + swap(d_ptrNowData, d_ptrNowTmp); + numDetections = *hp_numDet; + + pixParallelStageStopsIndex++; + } + + // + // 2. Run pixel-parallel stages + // + + for (; pixParallelStageStopsIndex < pixParallelStageStops.size()-1; pixParallelStageStopsIndex++) + { + if (numDetections == 0) + { + break; + } + + if (bDoAtomicCompaction) + { + ncvAssertCUDAReturn(cudaMemcpyToSymbolAsync(d_outMaskPosition, hp_zero, sizeof(Ncv32u), + 0, cudaMemcpyHostToDevice, cuStream), NCV_CUDA_ERROR); + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + } + + dim3 grid2((numDetections + NUM_THREADS_ANCHORSPARALLEL - 1) / NUM_THREADS_ANCHORSPARALLEL); + if (numDetections > MAX_GRID_DIM) + { + grid2.x = MAX_GRID_DIM; + grid2.y = (numDetections + MAX_GRID_DIM - 1) / MAX_GRID_DIM; + } + dim3 block2(NUM_THREADS_ANCHORSPARALLEL); + + applyHaarClassifierAnchorParallelDynTemplate( + false, //tbInitMaskPositively + bTexCacheIImg, //tbCacheTextureIImg + bTexCacheCascade, //tbCacheTextureCascade + pixParallelStageStops[pixParallelStageStopsIndex] != 0 || pixelStep != 1 || bMaskElements,//tbReadPixelIndexFromVector + bDoAtomicCompaction, //tbDoAtomicCompaction + grid2, + block2, + cuStream, + d_integralImage.ptr(), d_integralImage.stride(), + d_weights.ptr(), d_weights.stride(), + d_HaarFeatures.ptr(), d_HaarNodes.ptr(), d_HaarStages.ptr(), + d_ptrNowData->ptr(), + bDoAtomicCompaction ? d_ptrNowTmp->ptr() : d_ptrNowData->ptr(), + numDetections, + d_pixelMask.stride(), + anchorsRoi, + pixParallelStageStops[pixParallelStageStopsIndex], + pixParallelStageStops[pixParallelStageStopsIndex+1], + scaleAreaPixels); + ncvAssertCUDAReturn(cudaGetLastError(), NCV_CUDA_ERROR); + + if (bDoAtomicCompaction) + { + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + ncvAssertCUDAReturn(cudaMemcpyFromSymbolAsync(hp_numDet, d_outMaskPosition, sizeof(Ncv32u), + 0, cudaMemcpyDeviceToHost, cuStream), NCV_CUDA_ERROR); + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + } + else + { + NppStStatus nppSt; + nppSt = nppsStCompact_32u(d_ptrNowData->ptr(), numDetections, + d_ptrNowTmp->ptr(), hp_numDet, OBJDET_MASK_ELEMENT_INVALID_32U, + d_tmpBufCompact.ptr(), szNppCompactTmpBuf); + ncvAssertReturn(nppSt == NPP_SUCCESS, NCV_NPP_ERROR); + } + + swap(d_ptrNowData, d_ptrNowTmp); + numDetections = *hp_numDet; + } + + // + // 3. Run all left stages in one stage-parallel kernel + // + + if (numDetections > 0 && stageMiddleSwitch < stageEndClassifierParallel) + { + if (bDoAtomicCompaction) + { + ncvAssertCUDAReturn(cudaMemcpyToSymbolAsync(d_outMaskPosition, hp_zero, sizeof(Ncv32u), + 0, cudaMemcpyHostToDevice, cuStream), NCV_CUDA_ERROR); + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + } + + dim3 grid3(numDetections); + if (numDetections > MAX_GRID_DIM) + { + grid3.x = MAX_GRID_DIM; + grid3.y = (numDetections + MAX_GRID_DIM - 1) / MAX_GRID_DIM; + } + dim3 block3(NUM_THREADS_CLASSIFIERPARALLEL); + + applyHaarClassifierClassifierParallelDynTemplate( + bTexCacheIImg, //tbCacheTextureIImg + bTexCacheCascade, //tbCacheTextureCascade + bDoAtomicCompaction, //tbDoAtomicCompaction + grid3, + block3, + cuStream, + d_integralImage.ptr(), d_integralImage.stride(), + d_weights.ptr(), d_weights.stride(), + d_HaarFeatures.ptr(), d_HaarNodes.ptr(), d_HaarStages.ptr(), + d_ptrNowData->ptr(), + bDoAtomicCompaction ? d_ptrNowTmp->ptr() : d_ptrNowData->ptr(), + numDetections, + d_pixelMask.stride(), + anchorsRoi, + stageMiddleSwitch, + stageEndClassifierParallel, + scaleAreaPixels); + ncvAssertCUDAReturn(cudaGetLastError(), NCV_CUDA_ERROR); + + if (bDoAtomicCompaction) + { + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + ncvAssertCUDAReturn(cudaMemcpyFromSymbolAsync(hp_numDet, d_outMaskPosition, sizeof(Ncv32u), + 0, cudaMemcpyDeviceToHost, cuStream), NCV_CUDA_ERROR); + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + } + else + { + NppStStatus nppSt; + nppSt = nppsStCompact_32u(d_ptrNowData->ptr(), numDetections, + d_ptrNowTmp->ptr(), hp_numDet, OBJDET_MASK_ELEMENT_INVALID_32U, + d_tmpBufCompact.ptr(), szNppCompactTmpBuf); + ncvAssertReturn(nppSt == NPP_SUCCESS, NCV_NPP_ERROR); + } + + swap(d_ptrNowData, d_ptrNowTmp); + numDetections = *hp_numDet; + } + + if (d_ptrNowData != &d_vecPixelMask) + { + d_vecPixelMaskTmp.copySolid(d_vecPixelMask, cuStream); + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + } + +#if defined _SELF_TEST_ + + ncvStat = d_pixelMask.copySolid(h_pixelMask_d, 0); + ncvAssertReturnNcvStat(ncvStat); + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + + if (bDoAtomicCompaction) + { + std::sort(h_pixelMask_d.ptr, h_pixelMask_d.ptr + numDetections); + } + + Ncv32u fpu_oldcw, fpu_cw; + _controlfp_s(&fpu_cw, 0, 0); + fpu_oldcw = fpu_cw; + _controlfp_s(&fpu_cw, _PC_24, _MCW_PC); + Ncv32u numDetGold; + ncvStat = ncvApplyHaarClassifierCascade_host(h_integralImage, h_weights, h_pixelMask, numDetGold, haar, + h_HaarStages, h_HaarNodes, h_HaarFeatures, + bMaskElements, anchorsRoi, pixelStep, scaleArea); + ncvAssertReturnNcvStat(ncvStat); + _controlfp_s(&fpu_cw, fpu_oldcw, _MCW_PC); + + bool bPass = true; + + if (numDetGold != numDetections) + { + printf("NCVHaarClassifierCascade::applyHaarClassifierCascade numdetections don't match: cpu=%d, gpu=%d\n", numDetGold, numDetections); + bPass = false; + } + else + { + for (Ncv32u i=0; i> 16)); + res.width = (Ncv32u)(scale * width); + res.height = (Ncv32u)(scale * height); + return res; +} + + +__global__ void growDetectionsKernel(Ncv32u *pixelMask, Ncv32u numElements, + NcvRect32u *hypotheses, + Ncv32u rectWidth, Ncv32u rectHeight, Ncv32f curScale) +{ + Ncv32u blockId = blockIdx.y * 65535 + blockIdx.x; + Ncv32u elemAddr = blockId * NUM_GROW_THREADS + threadIdx.x; + if (elemAddr >= numElements) + { + return; + } + hypotheses[elemAddr] = pixelToRect(pixelMask[elemAddr], rectWidth, rectHeight, curScale); +} + + +NCVStatus ncvGrowDetectionsVector_device(NCVVector &pixelMask, + Ncv32u numPixelMaskDetections, + NCVVector &hypotheses, + Ncv32u &totalDetections, + Ncv32u totalMaxDetections, + Ncv32u rectWidth, + Ncv32u rectHeight, + Ncv32f curScale, + cudaStream_t cuStream) +{ + ncvAssertReturn(pixelMask.ptr() != NULL && hypotheses.ptr() != NULL, NCV_NULL_PTR); + ncvAssertReturn(pixelMask.memType() == hypotheses.memType() && + pixelMask.memType() == NCVMemoryTypeDevice, NCV_MEM_RESIDENCE_ERROR); + ncvAssertReturn(rectWidth > 0 && rectHeight > 0 && curScale > 0, NCV_INVALID_ROI); + ncvAssertReturn(curScale > 0, NCV_INVALID_SCALE); + ncvAssertReturn(totalMaxDetections <= hypotheses.length() && + numPixelMaskDetections <= pixelMask.length() && + totalMaxDetections <= totalMaxDetections, NCV_INCONSISTENT_INPUT); + + NCVStatus ncvStat = NCV_SUCCESS; + Ncv32u numDetsToCopy = numPixelMaskDetections; + + if (numDetsToCopy == 0) + { + return ncvStat; + } + + if (totalDetections + numPixelMaskDetections > totalMaxDetections) + { + ncvStat = NCV_WARNING_HAAR_DETECTIONS_VECTOR_OVERFLOW; + numDetsToCopy = totalMaxDetections - totalDetections; + } + + dim3 block(NUM_GROW_THREADS); + dim3 grid((numDetsToCopy + NUM_GROW_THREADS - 1) / NUM_GROW_THREADS); + if (grid.x > 65535) + { + grid.y = (grid.x + 65534) / 65535; + grid.x = 65535; + } + growDetectionsKernel<<>>(pixelMask.ptr(), numDetsToCopy, + hypotheses.ptr() + totalDetections, + rectWidth, rectHeight, curScale); + ncvAssertCUDAReturn(cudaGetLastError(), NCV_CUDA_ERROR); + + totalDetections += numDetsToCopy; + return ncvStat; +} + + +//============================================================================== +// +// Visualize file +// +//============================================================================== + + +const Ncv32u NUMTHREADS_DRAWRECTS = 32; +const Ncv32u NUMTHREADS_DRAWRECTS_LOG2 = 5; + + +template +__global__ void drawRects(T *d_dst, + Ncv32u dstStride, + Ncv32u dstWidth, + Ncv32u dstHeight, + NcvRect32u *d_rects, + Ncv32u numRects, + T color) +{ + Ncv32u blockId = blockIdx.y * 65535 + blockIdx.x; + if (blockId > numRects * 4) + { + return; + } + + NcvRect32u curRect = d_rects[blockId >> 2]; + NcvBool bVertical = blockId & 0x1; + NcvBool bTopLeft = blockId & 0x2; + + Ncv32u pt0x, pt0y; + if (bVertical) + { + Ncv32u numChunks = (curRect.height + NUMTHREADS_DRAWRECTS - 1) >> NUMTHREADS_DRAWRECTS_LOG2; + + pt0x = bTopLeft ? curRect.x : curRect.x + curRect.width - 1; + pt0y = curRect.y; + + if (pt0x < dstWidth) + { + for (Ncv32u chunkId = 0; chunkId < numChunks; chunkId++) + { + Ncv32u ptY = pt0y + chunkId * NUMTHREADS_DRAWRECTS + threadIdx.x; + if (ptY < pt0y + curRect.height && ptY < dstHeight) + { + d_dst[ptY * dstStride + pt0x] = color; + } + } + } + } + else + { + Ncv32u numChunks = (curRect.width + NUMTHREADS_DRAWRECTS - 1) >> NUMTHREADS_DRAWRECTS_LOG2; + + pt0x = curRect.x; + pt0y = bTopLeft ? curRect.y : curRect.y + curRect.height - 1; + + if (pt0y < dstHeight) + { + for (Ncv32u chunkId = 0; chunkId < numChunks; chunkId++) + { + Ncv32u ptX = pt0x + chunkId * NUMTHREADS_DRAWRECTS + threadIdx.x; + if (ptX < pt0x + curRect.width && ptX < dstWidth) + { + d_dst[pt0y * dstStride + ptX] = color; + } + } + } + } +} + + +template +static NCVStatus drawRectsWrapperDevice(T *d_dst, + Ncv32u dstStride, + Ncv32u dstWidth, + Ncv32u dstHeight, + NcvRect32u *d_rects, + Ncv32u numRects, + T color, + cudaStream_t cuStream) +{ + ncvAssertReturn(d_dst != NULL && d_rects != NULL, NCV_NULL_PTR); + ncvAssertReturn(dstWidth > 0 && dstHeight > 0, NCV_DIMENSIONS_INVALID); + ncvAssertReturn(dstStride >= dstWidth, NCV_INVALID_STEP); + ncvAssertReturn(numRects <= dstWidth * dstHeight, NCV_DIMENSIONS_INVALID); + + if (numRects == 0) + { + return NCV_SUCCESS; + } + +#if defined _SELF_TEST_ + T *h_dst; + ncvAssertCUDAReturn(cudaMallocHost(&h_dst, dstStride * dstHeight * sizeof(T)), NCV_CUDA_ERROR); + ncvAssertCUDAReturn(cudaMemcpy(h_dst, d_dst, dstStride * dstHeight * sizeof(T), cudaMemcpyDeviceToHost), NCV_CUDA_ERROR); + NcvRect32s *h_rects; + ncvAssertCUDAReturn(cudaMallocHost(&h_rects, numRects * sizeof(NcvRect32s)), NCV_CUDA_ERROR); + ncvAssertCUDAReturn(cudaMemcpy(h_rects, d_rects, numRects * sizeof(NcvRect32s), cudaMemcpyDeviceToHost), NCV_CUDA_ERROR); + ncvAssertReturnNcvStat(drawRectsWrapperHost(h_dst, dstStride, dstWidth, dstHeight, h_rects, numRects, color)); +#endif + + dim3 grid(numRects * 4); + dim3 block(NUMTHREADS_DRAWRECTS); + if (grid.x > 65535) + { + grid.y = (grid.x + 65534) / 65535; + grid.x = 65535; + } + + drawRects<<>>(d_dst, dstStride, dstWidth, dstHeight, d_rects, numRects, color); + + ncvAssertCUDAReturn(cudaGetLastError(), NCV_CUDA_ERROR); + +#if defined _SELF_TEST_ + T *h_dst_after; + ncvAssertCUDAReturn(cudaMallocHost(&h_dst_after, dstStride * dstHeight * sizeof(T)), NCV_CUDA_ERROR); + ncvAssertCUDAReturn(cudaMemcpy(h_dst_after, d_dst, dstStride * dstHeight * sizeof(T), cudaMemcpyDeviceToHost), NCV_CUDA_ERROR); + bool bPass = true; + for (Ncv32u i=0; i &d_srcImg, + NcvSize32u srcRoi, + NCVVector &d_dstRects, + Ncv32u &dstNumRects, + + HaarClassifierCascadeDescriptor &haar, + NCVVector &h_HaarStages, + NCVVector &d_HaarStages, + NCVVector &d_HaarNodes, + NCVVector &d_HaarFeatures, + + NcvSize32u minObjSize, + Ncv32u minNeighbors, //default 4 + Ncv32f scaleStep, //default 1.2f + Ncv32u pixelStep, //default 1 + Ncv32u flags, //default NCVPipeObjDet_Default + + INCVMemAllocator &gpuAllocator, + INCVMemAllocator &cpuAllocator, + Ncv32u devPropMajor, + Ncv32u devPropMinor, + cudaStream_t cuStream) +{ + ncvAssertReturn(d_srcImg.memType() == d_dstRects.memType() && + d_srcImg.memType() == gpuAllocator.memType() && + (d_srcImg.memType() == NCVMemoryTypeDevice || + d_srcImg.memType() == NCVMemoryTypeNone), NCV_MEM_RESIDENCE_ERROR); + ncvAssertReturn(d_HaarStages.memType() == d_HaarNodes.memType() && + d_HaarStages.memType() == d_HaarFeatures.memType() && + (d_HaarStages.memType() == NCVMemoryTypeDevice || + d_HaarStages.memType() == NCVMemoryTypeNone), NCV_MEM_RESIDENCE_ERROR); + ncvAssertReturn(h_HaarStages.memType() != NCVMemoryTypeDevice, NCV_MEM_RESIDENCE_ERROR); + ncvAssertReturn(gpuAllocator.isInitialized() && cpuAllocator.isInitialized(), NCV_ALLOCATOR_NOT_INITIALIZED); + ncvAssertReturn((d_srcImg.ptr() != NULL && d_dstRects.ptr() != NULL && + h_HaarStages.ptr() != NULL && d_HaarStages.ptr() != NULL && d_HaarNodes.ptr() != NULL && + d_HaarFeatures.ptr() != NULL) || gpuAllocator.isCounting(), NCV_NULL_PTR); + ncvAssertReturn(srcRoi.width > 0 && srcRoi.height > 0 && + d_srcImg.width() >= srcRoi.width && d_srcImg.height() >= srcRoi.height && + srcRoi.width >= minObjSize.width && srcRoi.height >= minObjSize.height && + d_dstRects.length() >= 1, NCV_DIMENSIONS_INVALID); + ncvAssertReturn(scaleStep > 1.0f, NCV_INVALID_SCALE); + ncvAssertReturn(d_HaarStages.length() >= haar.NumStages && + d_HaarNodes.length() >= haar.NumClassifierTotalNodes && + d_HaarFeatures.length() >= haar.NumFeatures && + d_HaarStages.length() == h_HaarStages.length() && + haar.NumClassifierRootNodes <= haar.NumClassifierTotalNodes, NCV_DIMENSIONS_INVALID); + ncvAssertReturn(haar.bNeedsTiltedII == false, NCV_NOIMPL_HAAR_TILTED_FEATURES); + ncvAssertReturn(pixelStep == 1 || pixelStep == 2, NCV_HAAR_INVALID_PIXEL_STEP); + + //TODO: set NPP active stream to cuStream + + NCVStatus ncvStat; + NCV_SET_SKIP_COND(gpuAllocator.isCounting()); + + Ncv32u integralWidth = d_srcImg.width() + 1; + Ncv32u integralHeight = d_srcImg.height() + 1; + + NCVMatrixAlloc d_integralImage(gpuAllocator, integralWidth, integralHeight); + ncvAssertReturn(d_integralImage.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + NCVMatrixAlloc d_sqIntegralImage(gpuAllocator, integralWidth, integralHeight); + ncvAssertReturn(d_sqIntegralImage.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + + NCVMatrixAlloc d_rectStdDev(gpuAllocator, d_srcImg.width(), d_srcImg.height()); + ncvAssertReturn(d_rectStdDev.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + NCVMatrixAlloc d_pixelMask(gpuAllocator, d_srcImg.width(), d_srcImg.height()); + ncvAssertReturn(d_pixelMask.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + + NCVMatrixAlloc d_scaledIntegralImage(gpuAllocator, integralWidth, integralHeight); + ncvAssertReturn(d_scaledIntegralImage.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + NCVMatrixAlloc d_scaledSqIntegralImage(gpuAllocator, integralWidth, integralHeight); + ncvAssertReturn(d_scaledSqIntegralImage.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + + NCVVectorAlloc d_hypothesesIntermediate(gpuAllocator, d_srcImg.width() * d_srcImg.height()); + ncvAssertReturn(d_hypothesesIntermediate.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + NCVVectorAlloc h_hypothesesIntermediate(cpuAllocator, d_srcImg.width() * d_srcImg.height()); + ncvAssertReturn(h_hypothesesIntermediate.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + + NppStStatus nppStat; + Ncv32u szTmpBufIntegral, szTmpBufSqIntegral; + nppStat = nppiStIntegralGetSize_8u32u(NppStSize32u(d_srcImg.width(), d_srcImg.height()), &szTmpBufIntegral); + ncvAssertReturn(nppStat == NPP_SUCCESS, NCV_NPP_ERROR); + nppStat = nppiStSqrIntegralGetSize_8u64u(NppStSize32u(d_srcImg.width(), d_srcImg.height()), &szTmpBufSqIntegral); + ncvAssertReturn(nppStat == NPP_SUCCESS, NCV_NPP_ERROR); + NCVVectorAlloc d_tmpIIbuf(gpuAllocator, std::max(szTmpBufIntegral, szTmpBufSqIntegral)); + ncvAssertReturn(d_tmpIIbuf.isMemAllocated(), NCV_ALLOCATOR_BAD_ALLOC); + + NCV_SKIP_COND_BEGIN + + nppStat = nppiStIntegral_8u32u_C1R(d_srcImg.ptr(), d_srcImg.pitch(), + d_integralImage.ptr(), d_integralImage.pitch(), + NppStSize32u(d_srcImg.width(), d_srcImg.height()), + d_tmpIIbuf.ptr(), szTmpBufIntegral); + ncvAssertReturn(nppStat == NPP_SUCCESS, NCV_NPP_ERROR); + + nppStat = nppiStSqrIntegral_8u64u_C1R(d_srcImg.ptr(), d_srcImg.pitch(), + d_sqIntegralImage.ptr(), d_sqIntegralImage.pitch(), + NppStSize32u(d_srcImg.width(), d_srcImg.height()), + d_tmpIIbuf.ptr(), szTmpBufSqIntegral); + ncvAssertReturn(nppStat == NPP_SUCCESS, NCV_NPP_ERROR); + + NCV_SKIP_COND_END + + dstNumRects = 0; + + Ncv32u lastCheckedScale = 0; + NcvBool bReverseTraverseScale = ((flags & NCVPipeObjDet_FindLargestObject) != 0); + std::vector scalesVector; + + NcvBool bFoundLargestFace = false; + + for (Ncv32f scaleIter = 1.0f; ; scaleIter *= scaleStep) + { + Ncv32u scale = (Ncv32u)scaleIter; + if (lastCheckedScale == scale) + { + continue; + } + lastCheckedScale = scale; + + if (haar.ClassifierSize.width * (Ncv32s)scale < minObjSize.width || + haar.ClassifierSize.height * (Ncv32s)scale < minObjSize.height) + { + continue; + } + + NcvSize32s srcRoi, srcIIRoi, scaledIIRoi, searchRoi; + + srcRoi.width = d_srcImg.width(); + srcRoi.height = d_srcImg.height(); + + srcIIRoi.width = srcRoi.width + 1; + srcIIRoi.height = srcRoi.height + 1; + + scaledIIRoi.width = srcIIRoi.width / scale; + scaledIIRoi.height = srcIIRoi.height / scale; + + searchRoi.width = scaledIIRoi.width - haar.ClassifierSize.width; + searchRoi.height = scaledIIRoi.height - haar.ClassifierSize.height; + + if (searchRoi.width <= 0 || searchRoi.height <= 0) + { + break; + } + + scalesVector.push_back(scale); + + if (gpuAllocator.isCounting()) + { + break; + } + } + + if (bReverseTraverseScale) + { + std::reverse(scalesVector.begin(), scalesVector.end()); + } + + //TODO: handle _fair_scale_ flag + for (Ncv32u i=0; i d_vecPixelMask(d_pixelMask.getSegment()); + ncvStat = ncvGrowDetectionsVector_device( + d_vecPixelMask, + detectionsOnThisScale, + d_hypothesesIntermediate, + dstNumRects, + d_hypothesesIntermediate.length(), + haar.ClassifierSize.width, + haar.ClassifierSize.height, + (Ncv32f)scale, + cuStream); + ncvAssertReturn(ncvStat == NCV_SUCCESS, ncvStat); + + if (flags & NCVPipeObjDet_FindLargestObject) + { + if (dstNumRects == 0) + { + continue; + } + + if (dstNumRects != 0) + { + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + ncvStat = d_hypothesesIntermediate.copySolid(h_hypothesesIntermediate, cuStream, + dstNumRects * sizeof(NcvRect32u)); + ncvAssertReturnNcvStat(ncvStat); + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + } + + Ncv32u numStrongHypothesesNow = dstNumRects; + ncvStat = ncvFilterHypotheses_host( + h_hypothesesIntermediate, + numStrongHypothesesNow, + minNeighbors, + RECT_SIMILARITY_PROPORTION, + NULL); + ncvAssertReturnNcvStat(ncvStat); + + if (numStrongHypothesesNow > 0) + { + NcvRect32u maxRect = h_hypothesesIntermediate.ptr()[0]; + for (Ncv32u j=1; j d_dstRects.length()) + { + ncvRetCode = NCV_WARNING_HAAR_DETECTIONS_VECTOR_OVERFLOW; + dstNumRects = d_dstRects.length(); + } + + if (dstNumRects != 0) + { + ncvStat = h_hypothesesIntermediate.copySolid(d_dstRects, cuStream, + dstNumRects * sizeof(NcvRect32u)); + ncvAssertReturnNcvStat(ncvStat); + } + } + + if (flags & NCVPipeObjDet_VisualizeInPlace) + { + ncvAssertCUDAReturn(cudaStreamSynchronize(cuStream), NCV_CUDA_ERROR); + ncvDrawRects_8u_device(d_srcImg.ptr(), d_srcImg.stride(), + d_srcImg.width(), d_srcImg.height(), + d_dstRects.ptr(), dstNumRects, 255, cuStream); + } + + NCV_SKIP_COND_END + + return ncvRetCode; +} + + +//============================================================================== +// +// Purely Host code: classifier IO, mock-ups +// +//============================================================================== + + +#ifdef _SELF_TEST_ +#include +#endif + + +NCVStatus ncvApplyHaarClassifierCascade_host(NCVMatrix &h_integralImage, + NCVMatrix &h_weights, + NCVMatrixAlloc &h_pixelMask, + Ncv32u &numDetections, + HaarClassifierCascadeDescriptor &haar, + NCVVector &h_HaarStages, + NCVVector &h_HaarNodes, + NCVVector &h_HaarFeatures, + NcvBool bMaskElements, + NcvSize32u anchorsRoi, + Ncv32u pixelStep, + Ncv32f scaleArea) +{ + ncvAssertReturn(h_integralImage.memType() == h_weights.memType() && + h_integralImage.memType() == h_pixelMask.memType() && + (h_integralImage.memType() == NCVMemoryTypeHostPageable || + h_integralImage.memType() == NCVMemoryTypeHostPinned), NCV_MEM_RESIDENCE_ERROR); + ncvAssertReturn(h_HaarStages.memType() == h_HaarNodes.memType() && + h_HaarStages.memType() == h_HaarFeatures.memType() && + (h_HaarStages.memType() == NCVMemoryTypeHostPageable || + h_HaarStages.memType() == NCVMemoryTypeHostPinned), NCV_MEM_RESIDENCE_ERROR); + ncvAssertReturn(h_integralImage.ptr() != NULL && h_weights.ptr() != NULL && h_pixelMask.ptr() != NULL && + h_HaarStages.ptr() != NULL && h_HaarNodes.ptr() != NULL && h_HaarFeatures.ptr() != NULL, NCV_NULL_PTR); + ncvAssertReturn(anchorsRoi.width > 0 && anchorsRoi.height > 0 && + h_pixelMask.width() >= anchorsRoi.width && h_pixelMask.height() >= anchorsRoi.height && + h_weights.width() >= anchorsRoi.width && h_weights.height() >= anchorsRoi.height && + h_integralImage.width() >= anchorsRoi.width + haar.ClassifierSize.width && + h_integralImage.height() >= anchorsRoi.height + haar.ClassifierSize.height, NCV_DIMENSIONS_INVALID); + ncvAssertReturn(scaleArea > 0, NCV_INVALID_SCALE); + ncvAssertReturn(h_HaarStages.length() >= haar.NumStages && + h_HaarNodes.length() >= haar.NumClassifierTotalNodes && + h_HaarFeatures.length() >= haar.NumFeatures && + h_HaarStages.length() == h_HaarStages.length() && + haar.NumClassifierRootNodes <= haar.NumClassifierTotalNodes, NCV_DIMENSIONS_INVALID); + ncvAssertReturn(haar.bNeedsTiltedII == false, NCV_NOIMPL_HAAR_TILTED_FEATURES); + ncvAssertReturn(pixelStep == 1 || pixelStep == 2, NCV_HAAR_INVALID_PIXEL_STEP); + + Ncv32f scaleAreaPixels = scaleArea * ((haar.ClassifierSize.width - 2*HAAR_STDDEV_BORDER) * + (haar.ClassifierSize.height - 2*HAAR_STDDEV_BORDER)); + + for (Ncv32u i=0; i= anchorsRoi.width) + { + h_pixelMask.ptr()[i * h_pixelMask.stride() + j] = OBJDET_MASK_ELEMENT_INVALID_32U; + } + else + { + for (Ncv32u iStage = 0; iStage < haar.NumStages; iStage++) + { + Ncv32f curStageSum = 0.0f; + Ncv32u numRootNodesInStage = h_HaarStages.ptr()[iStage].getNumClassifierRootNodes(); + Ncv32u curRootNodeOffset = h_HaarStages.ptr()[iStage].getStartClassifierRootNodeOffset(); + + if (iStage == 0) + { + if (bMaskElements && h_pixelMask.ptr()[i * h_pixelMask.stride() + j] == OBJDET_MASK_ELEMENT_INVALID_32U) + { + break; + } + else + { + h_pixelMask.ptr()[i * h_pixelMask.stride() + j] = ((i << 16) | j); + } + } + else if (h_pixelMask.ptr()[i * h_pixelMask.stride() + j] == OBJDET_MASK_ELEMENT_INVALID_32U) + { + break; + } + + while (numRootNodesInStage--) + { + NcvBool bMoreNodesToTraverse = true; + Ncv32u curNodeOffset = curRootNodeOffset; + + while (bMoreNodesToTraverse) + { + HaarClassifierNode128 curNode = h_HaarNodes.ptr()[curNodeOffset]; + Ncv32u curNodeFeaturesNum = curNode.getFeatureDesc().getNumFeatures(); + Ncv32u curNodeFeaturesOffs = curNode.getFeatureDesc().getFeaturesOffset(); + + Ncv32f curNodeVal = 0.f; + for (Ncv32u iRect=0; iRect &pixelMask, + Ncv32u numPixelMaskDetections, + NCVVector &hypotheses, + Ncv32u &totalDetections, + Ncv32u totalMaxDetections, + Ncv32u rectWidth, + Ncv32u rectHeight, + Ncv32f curScale) +{ + ncvAssertReturn(pixelMask.ptr() != NULL && hypotheses.ptr() != NULL, NCV_NULL_PTR); + ncvAssertReturn(pixelMask.memType() == hypotheses.memType() && + pixelMask.memType() != NCVMemoryTypeDevice, NCV_MEM_RESIDENCE_ERROR); + ncvAssertReturn(rectWidth > 0 && rectHeight > 0 && curScale > 0, NCV_INVALID_ROI); + ncvAssertReturn(curScale > 0, NCV_INVALID_SCALE); + ncvAssertReturn(totalMaxDetections <= hypotheses.length() && + numPixelMaskDetections <= pixelMask.length() && + totalMaxDetections <= totalMaxDetections, NCV_INCONSISTENT_INPUT); + + NCVStatus ncvStat = NCV_SUCCESS; + Ncv32u numDetsToCopy = numPixelMaskDetections; + + if (numDetsToCopy == 0) + { + return ncvStat; + } + + if (totalDetections + numPixelMaskDetections > totalMaxDetections) + { + ncvStat = NCV_WARNING_HAAR_DETECTIONS_VECTOR_OVERFLOW; + numDetsToCopy = totalMaxDetections - totalDetections; + } + + for (Ncv32u i=0; i &hypotheses, + Ncv32u &numHypotheses, + Ncv32u minNeighbors, + Ncv32f intersectEps, + NCVVector *hypothesesWeights) +{ + ncvAssertReturn(hypotheses.memType() == NCVMemoryTypeHostPageable || + hypotheses.memType() == NCVMemoryTypeHostPinned, NCV_MEM_RESIDENCE_ERROR); + if (hypothesesWeights != NULL) + { + ncvAssertReturn(hypothesesWeights->memType() == NCVMemoryTypeHostPageable || + hypothesesWeights->memType() == NCVMemoryTypeHostPinned, NCV_MEM_RESIDENCE_ERROR); + } + + if (numHypotheses == 0) + { + return NCV_SUCCESS; + } + + std::vector rects(numHypotheses); + memcpy(&rects[0], hypotheses.ptr(), numHypotheses * sizeof(NcvRect32u)); + + std::vector weights; + if (hypothesesWeights != NULL) + { + groupRectangles(rects, minNeighbors, intersectEps, &weights); + } + else + { + groupRectangles(rects, minNeighbors, intersectEps, NULL); + } + + numHypotheses = (Ncv32u)rects.size(); + if (numHypotheses > 0) + { + memcpy(hypotheses.ptr(), &rects[0], numHypotheses * sizeof(NcvRect32u)); + } + + if (hypothesesWeights != NULL) + { + memcpy(hypothesesWeights->ptr(), &weights[0], numHypotheses * sizeof(Ncv32u)); + } + + return NCV_SUCCESS; +} + + +template +static NCVStatus drawRectsWrapperHost(T *h_dst, + Ncv32u dstStride, + Ncv32u dstWidth, + Ncv32u dstHeight, + NcvRect32u *h_rects, + Ncv32u numRects, + T color) +{ + ncvAssertReturn(h_dst != NULL && h_rects != NULL, NCV_NULL_PTR); + ncvAssertReturn(dstWidth > 0 && dstHeight > 0, NCV_DIMENSIONS_INVALID); + ncvAssertReturn(dstStride >= dstWidth, NCV_INVALID_STEP); + ncvAssertReturn(numRects != 0, NCV_SUCCESS); + ncvAssertReturn(numRects <= dstWidth * dstHeight, NCV_DIMENSIONS_INVALID); + + for (Ncv32u i=0; i &haarStages, + std::vector &haarClassifierNodes, + std::vector &haarFeatures); + + +#define NVBIN_HAAR_SIZERESERVED 16 +#define NVBIN_HAAR_VERSION 0x1 + + +static NCVStatus loadFromNVBIN(const std::string &filename, + HaarClassifierCascadeDescriptor &haar, + std::vector &haarStages, + std::vector &haarClassifierNodes, + std::vector &haarFeatures) +{ + FILE *fp; + fopen_s(&fp, filename.c_str(), "rb"); + ncvAssertReturn(fp != NULL, NCV_FILE_ERROR); + Ncv32u fileVersion; + fread_s(&fileVersion, sizeof(Ncv32u), sizeof(Ncv32u), 1, fp); + ncvAssertReturn(fileVersion == NVBIN_HAAR_VERSION, NCV_FILE_ERROR); + Ncv32u fsize; + fread_s(&fsize, sizeof(Ncv32u), sizeof(Ncv32u), 1, fp); + fseek(fp, 0, SEEK_END); + Ncv32u fsizeActual = ftell(fp); + ncvAssertReturn(fsize == fsizeActual, NCV_FILE_ERROR); + + std::vector fdata; + fdata.resize(fsize); + Ncv32u dataOffset = 0; + fseek(fp, 0, SEEK_SET); + fread_s(&fdata[0], fsize, fsize, 1, fp); + fclose(fp); + + //data + dataOffset = NVBIN_HAAR_SIZERESERVED; + haar.NumStages = *(Ncv32u *)(&fdata[0]+dataOffset); + dataOffset += sizeof(Ncv32u); + haar.NumClassifierRootNodes = *(Ncv32u *)(&fdata[0]+dataOffset); + dataOffset += sizeof(Ncv32u); + haar.NumClassifierTotalNodes = *(Ncv32u *)(&fdata[0]+dataOffset); + dataOffset += sizeof(Ncv32u); + haar.NumFeatures = *(Ncv32u *)(&fdata[0]+dataOffset); + dataOffset += sizeof(Ncv32u); + haar.ClassifierSize = *(NcvSize32u *)(&fdata[0]+dataOffset); + dataOffset += sizeof(NcvSize32u); + haar.bNeedsTiltedII = *(NcvBool *)(&fdata[0]+dataOffset); + dataOffset += sizeof(NcvBool); + haar.bHasStumpsOnly = *(NcvBool *)(&fdata[0]+dataOffset); + dataOffset += sizeof(NcvBool); + + haarStages.resize(haar.NumStages); + haarClassifierNodes.resize(haar.NumClassifierTotalNodes); + haarFeatures.resize(haar.NumFeatures); + + Ncv32u szStages = haar.NumStages * sizeof(HaarStage64); + Ncv32u szClassifiers = haar.NumClassifierTotalNodes * sizeof(HaarClassifierNode128); + Ncv32u szFeatures = haar.NumFeatures * sizeof(HaarFeature64); + + memcpy(&haarStages[0], &fdata[0]+dataOffset, szStages); + dataOffset += szStages; + memcpy(&haarClassifierNodes[0], &fdata[0]+dataOffset, szClassifiers); + dataOffset += szClassifiers; + memcpy(&haarFeatures[0], &fdata[0]+dataOffset, szFeatures); + dataOffset += szFeatures; + + return NCV_SUCCESS; +} + + +NCVStatus ncvHaarGetClassifierSize(const std::string &filename, Ncv32u &numStages, + Ncv32u &numNodes, Ncv32u &numFeatures) +{ + NCVStatus ncvStat; + + std::string fext = filename.substr(filename.find_last_of(".") + 1); + std::transform(fext.begin(), fext.end(), fext.begin(), ::tolower); + + if (fext == "nvbin") + { + FILE *fp; + fopen_s(&fp, filename.c_str(), "rb"); + ncvAssertReturn(fp != NULL, NCV_FILE_ERROR); + Ncv32u fileVersion; + fread_s(&fileVersion, sizeof(Ncv32u), sizeof(Ncv32u), 1, fp); + ncvAssertReturn(fileVersion == NVBIN_HAAR_VERSION, NCV_FILE_ERROR); + fseek(fp, NVBIN_HAAR_SIZERESERVED, SEEK_SET); + Ncv32u tmp; + fread_s(&numStages, sizeof(Ncv32u), sizeof(Ncv32u), 1, fp); + fread_s(&tmp, sizeof(Ncv32u), sizeof(Ncv32u), 1, fp); + fread_s(&numNodes, sizeof(Ncv32u), sizeof(Ncv32u), 1, fp); + fread_s(&numFeatures, sizeof(Ncv32u), sizeof(Ncv32u), 1, fp); + fclose(fp); + } + else if (fext == "xml") + { + HaarClassifierCascadeDescriptor haar; + std::vector haarStages; + std::vector haarNodes; + std::vector haarFeatures; + + ncvStat = loadFromXML(filename, haar, haarStages, haarNodes, haarFeatures); + ncvAssertReturnNcvStat(ncvStat); + + numStages = haar.NumStages; + numNodes = haar.NumClassifierTotalNodes; + numFeatures = haar.NumFeatures; + } + else + { + return NCV_HAAR_XML_LOADING_EXCEPTION; + } + + return NCV_SUCCESS; +} + + +NCVStatus ncvHaarLoadFromFile_host(const std::string &filename, + HaarClassifierCascadeDescriptor &haar, + NCVVector &h_HaarStages, + NCVVector &h_HaarNodes, + NCVVector &h_HaarFeatures) +{ + ncvAssertReturn(h_HaarStages.memType() == NCVMemoryTypeHostPinned && + h_HaarNodes.memType() == NCVMemoryTypeHostPinned && + h_HaarFeatures.memType() == NCVMemoryTypeHostPinned, NCV_MEM_RESIDENCE_ERROR); + + NCVStatus ncvStat; + + std::string fext = filename.substr(filename.find_last_of(".") + 1); + std::transform(fext.begin(), fext.end(), fext.begin(), ::tolower); + + std::vector haarStages; + std::vector haarNodes; + std::vector haarFeatures; + + if (fext == "nvbin") + { + ncvStat = loadFromNVBIN(filename, haar, haarStages, haarNodes, haarFeatures); + ncvAssertReturnNcvStat(ncvStat); + } + else if (fext == "xml") + { + ncvStat = loadFromXML(filename, haar, haarStages, haarNodes, haarFeatures); + ncvAssertReturnNcvStat(ncvStat); + } + else + { + return NCV_HAAR_XML_LOADING_EXCEPTION; + } + + ncvAssertReturn(h_HaarStages.length() >= haarStages.size(), NCV_MEM_INSUFFICIENT_CAPACITY); + ncvAssertReturn(h_HaarNodes.length() >= haarNodes.size(), NCV_MEM_INSUFFICIENT_CAPACITY); + ncvAssertReturn(h_HaarFeatures.length() >= haarFeatures.size(), NCV_MEM_INSUFFICIENT_CAPACITY); + + memcpy(h_HaarStages.ptr(), &haarStages[0], haarStages.size()*sizeof(HaarStage64)); + memcpy(h_HaarNodes.ptr(), &haarNodes[0], haarNodes.size()*sizeof(HaarClassifierNode128)); + memcpy(h_HaarFeatures.ptr(), &haarFeatures[0], haarFeatures.size()*sizeof(HaarFeature64)); + + return NCV_SUCCESS; +} + + +NCVStatus ncvHaarStoreNVBIN_host(std::string &filename, + HaarClassifierCascadeDescriptor haar, + NCVVector &h_HaarStages, + NCVVector &h_HaarNodes, + NCVVector &h_HaarFeatures) +{ + ncvAssertReturn(h_HaarStages.length() >= haar.NumStages, NCV_INCONSISTENT_INPUT); + ncvAssertReturn(h_HaarNodes.length() >= haar.NumClassifierTotalNodes, NCV_INCONSISTENT_INPUT); + ncvAssertReturn(h_HaarFeatures.length() >= haar.NumFeatures, NCV_INCONSISTENT_INPUT); + ncvAssertReturn(h_HaarStages.memType() == NCVMemoryTypeHostPinned && + h_HaarNodes.memType() == NCVMemoryTypeHostPinned && + h_HaarFeatures.memType() == NCVMemoryTypeHostPinned, NCV_MEM_RESIDENCE_ERROR); + + Ncv32u szStages = haar.NumStages * sizeof(HaarStage64); + Ncv32u szClassifiers = haar.NumClassifierTotalNodes * sizeof(HaarClassifierNode128); + Ncv32u szFeatures = haar.NumFeatures * sizeof(HaarFeature64); + + Ncv32u dataOffset = 0; + std::vector fdata; + fdata.resize(szStages+szClassifiers+szFeatures+1024, 0); + + //header + *(Ncv32u *)(&fdata[0]+dataOffset) = NVBIN_HAAR_VERSION; + + //data + dataOffset = NVBIN_HAAR_SIZERESERVED; + *(Ncv32u *)(&fdata[0]+dataOffset) = haar.NumStages; + dataOffset += sizeof(Ncv32u); + *(Ncv32u *)(&fdata[0]+dataOffset) = haar.NumClassifierRootNodes; + dataOffset += sizeof(Ncv32u); + *(Ncv32u *)(&fdata[0]+dataOffset) = haar.NumClassifierTotalNodes; + dataOffset += sizeof(Ncv32u); + *(Ncv32u *)(&fdata[0]+dataOffset) = haar.NumFeatures; + dataOffset += sizeof(Ncv32u); + *(NcvSize32u *)(&fdata[0]+dataOffset) = haar.ClassifierSize; + dataOffset += sizeof(NcvSize32u); + *(NcvBool *)(&fdata[0]+dataOffset) = haar.bNeedsTiltedII; + dataOffset += sizeof(NcvBool); + *(NcvBool *)(&fdata[0]+dataOffset) = haar.bHasStumpsOnly; + dataOffset += sizeof(NcvBool); + + memcpy(&fdata[0]+dataOffset, h_HaarStages.ptr(), szStages); + dataOffset += szStages; + memcpy(&fdata[0]+dataOffset, h_HaarNodes.ptr(), szClassifiers); + dataOffset += szClassifiers; + memcpy(&fdata[0]+dataOffset, h_HaarFeatures.ptr(), szFeatures); + dataOffset += szFeatures; + Ncv32u fsize = dataOffset; + + //TODO: CRC32 here + + //update header + dataOffset = sizeof(Ncv32u); + *(Ncv32u *)(&fdata[0]+dataOffset) = fsize; + + FILE *fp; + fopen_s(&fp, filename.c_str(), "wb"); + ncvAssertReturn(fp != NULL, NCV_FILE_ERROR); + fwrite(&fdata[0], fsize, 1, fp); + fclose(fp); + return NCV_SUCCESS; +} diff --git a/modules/gpu/src/nvidia/NCVHaarObjectDetection.hpp b/modules/gpu/src/nvidia/NCVHaarObjectDetection.hpp new file mode 100644 index 0000000000..d9f500d7a0 --- /dev/null +++ b/modules/gpu/src/nvidia/NCVHaarObjectDetection.hpp @@ -0,0 +1,501 @@ +/*M/////////////////////////////////////////////////////////////////////////////////////// +// +// IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. +// +// By downloading, copying, installing or using the software you agree to this license. +// If you do not agree to this license, do not download, install, +// copy or use the software. +// +// +// License Agreement +// For Open Source Computer Vision Library +// +// Copyright (C) 2009-2010, NVIDIA Corporation, all rights reserved. +// Third party copyrights are property of their respective owners. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistribution's of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistribution's in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * The name of the copyright holders may not be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// This software is provided by the copyright holders and contributors "as is" and +// any express or implied warranties, including, but not limited to, the implied +// warranties of merchantability and fitness for a particular purpose are disclaimed. +// In no event shall the Intel Corporation or contributors be liable for any direct, +// indirect, incidental, special, exemplary, or consequential damages +// (including, but not limited to, procurement of substitute goods or services; +// loss of use, data, or profits; or business interruption) however caused +// and on any theory of liability, whether in contract, strict liability, +// or tort (including negligence or otherwise) arising in any way out of +// the use of this software, even if advised of the possibility of such damage. +// +//M*/ + +//////////////////////////////////////////////////////////////////////////////// +// +// NVIDIA CUDA implementation of Viola-Jones Object Detection Framework +// +// The algorithm and code are explained in the upcoming GPU Computing Gems +// chapter in detail: +// +// Anton Obukhov, "Haar Classifiers for Object Detection with CUDA" +// PDF URL placeholder +// email: aobukhov@nvidia.com, devsupport@nvidia.com +// +// Credits for help with the code to: +// Alexey Mendelenko, Cyril Crassin, and Mikhail Smirnov. +// +//////////////////////////////////////////////////////////////////////////////// + +#ifndef _ncvhaarobjectdetection_hpp_ +#define _ncvhaarobjectdetection_hpp_ + +#include +#include "NCV.hpp" + + +//============================================================================== +// +// Guaranteed size cross-platform classifier structures +// +//============================================================================== + + +struct HaarFeature64 +{ + uint2 _ui2; + +#define HaarFeature64_CreateCheck_MaxRectField 0xFF + + __host__ NCVStatus setRect(Ncv32u rectX, Ncv32u rectY, Ncv32u rectWidth, Ncv32u rectHeight, Ncv32u clsWidth, Ncv32u clsHeight) + { + ncvAssertReturn(rectWidth <= HaarFeature64_CreateCheck_MaxRectField && rectHeight <= HaarFeature64_CreateCheck_MaxRectField, NCV_HAAR_TOO_LARGE_FEATURES); + ((NcvRect8u*)&(this->_ui2.x))->x = rectX; + ((NcvRect8u*)&(this->_ui2.x))->y = rectY; + ((NcvRect8u*)&(this->_ui2.x))->width = rectWidth; + ((NcvRect8u*)&(this->_ui2.x))->height = rectHeight; + return NCV_SUCCESS; + } + + __host__ NCVStatus setWeight(Ncv32f weight) + { + ((Ncv32f*)&(this->_ui2.y))[0] = weight; + return NCV_SUCCESS; + } + + __device__ __host__ void getRect(Ncv32u *rectX, Ncv32u *rectY, Ncv32u *rectWidth, Ncv32u *rectHeight) + { + NcvRect8u tmpRect = *(NcvRect8u*)(&this->_ui2.x); + *rectX = tmpRect.x; + *rectY = tmpRect.y; + *rectWidth = tmpRect.width; + *rectHeight = tmpRect.height; + } + + __device__ __host__ Ncv32f getWeight(void) + { + return *(Ncv32f*)(&this->_ui2.y); + } +}; + + +struct HaarFeatureDescriptor32 +{ +private: + +#define HaarFeatureDescriptor32_Interpret_MaskFlagTilted 0x80000000 +#define HaarFeatureDescriptor32_CreateCheck_MaxNumFeatures 0x7F +#define HaarFeatureDescriptor32_NumFeatures_Shift 24 +#define HaarFeatureDescriptor32_CreateCheck_MaxFeatureOffset 0x00FFFFFF + + Ncv32u desc; + +public: + + __host__ NCVStatus create(NcvBool bTilted, Ncv32u numFeatures, Ncv32u offsetFeatures) + { + if (numFeatures > HaarFeatureDescriptor32_CreateCheck_MaxNumFeatures) + { + return NCV_HAAR_TOO_MANY_FEATURES_IN_CLASSIFIER; + } + if (offsetFeatures > HaarFeatureDescriptor32_CreateCheck_MaxFeatureOffset) + { + return NCV_HAAR_TOO_MANY_FEATURES_IN_CASCADE; + } + this->desc = 0; + this->desc |= (bTilted ? HaarFeatureDescriptor32_Interpret_MaskFlagTilted : 0); + this->desc |= (numFeatures << HaarFeatureDescriptor32_NumFeatures_Shift); + this->desc |= offsetFeatures; + return NCV_SUCCESS; + } + + __device__ __host__ NcvBool isTilted(void) + { + return (this->desc & HaarFeatureDescriptor32_Interpret_MaskFlagTilted) != 0; + } + + __device__ __host__ Ncv32u getNumFeatures(void) + { + return (this->desc & ~HaarFeatureDescriptor32_Interpret_MaskFlagTilted) >> HaarFeatureDescriptor32_NumFeatures_Shift; + } + + __device__ __host__ Ncv32u getFeaturesOffset(void) + { + return this->desc & HaarFeatureDescriptor32_CreateCheck_MaxFeatureOffset; + } +}; + + +struct HaarClassifierNodeDescriptor32 +{ + uint1 _ui1; + +#define HaarClassifierNodeDescriptor32_Interpret_MaskSwitch (1 << 30) + + __host__ NCVStatus create(Ncv32f leafValue) + { + if ((*(Ncv32u *)&leafValue) & HaarClassifierNodeDescriptor32_Interpret_MaskSwitch) + { + return NCV_HAAR_XML_LOADING_EXCEPTION; + } + *(Ncv32f *)&this->_ui1 = leafValue; + return NCV_SUCCESS; + } + + __host__ NCVStatus create(Ncv32u offsetHaarClassifierNode) + { + if (offsetHaarClassifierNode >= HaarClassifierNodeDescriptor32_Interpret_MaskSwitch) + { + return NCV_HAAR_XML_LOADING_EXCEPTION; + } + this->_ui1.x = offsetHaarClassifierNode; + this->_ui1.x |= HaarClassifierNodeDescriptor32_Interpret_MaskSwitch; + return NCV_SUCCESS; + } + + __device__ __host__ NcvBool isLeaf(void) + { + return !(this->_ui1.x & HaarClassifierNodeDescriptor32_Interpret_MaskSwitch); + } + + __host__ Ncv32f getLeafValueHost(void) + { + return *(Ncv32f *)&this->_ui1.x; + } + +#ifdef __CUDACC__ + __device__ Ncv32f getLeafValue(void) + { + return __int_as_float(this->_ui1.x); + } +#endif + + __device__ __host__ Ncv32u getNextNodeOffset(void) + { + return (this->_ui1.x & ~HaarClassifierNodeDescriptor32_Interpret_MaskSwitch); + } +}; + + +struct HaarClassifierNode128 +{ + uint4 _ui4; + + __host__ NCVStatus setFeatureDesc(HaarFeatureDescriptor32 f) + { + this->_ui4.x = *(Ncv32u *)&f; + return NCV_SUCCESS; + } + + __host__ NCVStatus setThreshold(Ncv32f t) + { + this->_ui4.y = *(Ncv32u *)&t; + return NCV_SUCCESS; + } + + __host__ NCVStatus setLeftNodeDesc(HaarClassifierNodeDescriptor32 nl) + { + this->_ui4.z = *(Ncv32u *)&nl; + return NCV_SUCCESS; + } + + __host__ NCVStatus setRightNodeDesc(HaarClassifierNodeDescriptor32 nr) + { + this->_ui4.w = *(Ncv32u *)&nr; + return NCV_SUCCESS; + } + + __host__ __device__ HaarFeatureDescriptor32 getFeatureDesc(void) + { + return *(HaarFeatureDescriptor32 *)&this->_ui4.x; + } + + __host__ __device__ Ncv32f getThreshold(void) + { + return *(Ncv32f*)&this->_ui4.y; + } + + __host__ __device__ HaarClassifierNodeDescriptor32 getLeftNodeDesc(void) + { + return *(HaarClassifierNodeDescriptor32 *)&this->_ui4.z; + } + + __host__ __device__ HaarClassifierNodeDescriptor32 getRightNodeDesc(void) + { + return *(HaarClassifierNodeDescriptor32 *)&this->_ui4.w; + } +}; + + +struct HaarStage64 +{ +#define HaarStage64_Interpret_MaskRootNodes 0x0000FFFF +#define HaarStage64_Interpret_MaskRootNodeOffset 0xFFFF0000 +#define HaarStage64_Interpret_ShiftRootNodeOffset 16 + + uint2 _ui2; + + __host__ NCVStatus setStageThreshold(Ncv32f t) + { + this->_ui2.x = *(Ncv32u *)&t; + return NCV_SUCCESS; + } + + __host__ NCVStatus setStartClassifierRootNodeOffset(Ncv32u val) + { + if (val > (HaarStage64_Interpret_MaskRootNodeOffset >> HaarStage64_Interpret_ShiftRootNodeOffset)) + { + return NCV_HAAR_XML_LOADING_EXCEPTION; + } + this->_ui2.y = (val << HaarStage64_Interpret_ShiftRootNodeOffset) | (this->_ui2.y & HaarStage64_Interpret_MaskRootNodes); + return NCV_SUCCESS; + } + + __host__ NCVStatus setNumClassifierRootNodes(Ncv32u val) + { + if (val > HaarStage64_Interpret_MaskRootNodes) + { + return NCV_HAAR_XML_LOADING_EXCEPTION; + } + this->_ui2.y = val | (this->_ui2.y & HaarStage64_Interpret_MaskRootNodeOffset); + return NCV_SUCCESS; + } + + __host__ __device__ Ncv32f getStageThreshold(void) + { + return *(Ncv32f*)&this->_ui2.x; + } + + __host__ __device__ Ncv32u getStartClassifierRootNodeOffset(void) + { + return (this->_ui2.y >> HaarStage64_Interpret_ShiftRootNodeOffset); + } + + __host__ __device__ Ncv32u getNumClassifierRootNodes(void) + { + return (this->_ui2.y & HaarStage64_Interpret_MaskRootNodes); + } +}; + + +NPPST_CT_ASSERT(sizeof(HaarFeature64) == 8); +NPPST_CT_ASSERT(sizeof(HaarFeatureDescriptor32) == 4); +NPPST_CT_ASSERT(sizeof(HaarClassifierNodeDescriptor32) == 4); +NPPST_CT_ASSERT(sizeof(HaarClassifierNode128) == 16); +NPPST_CT_ASSERT(sizeof(HaarStage64) == 8); + + +//============================================================================== +// +// Classifier cascade descriptor +// +//============================================================================== + + +struct HaarClassifierCascadeDescriptor +{ + Ncv32u NumStages; + Ncv32u NumClassifierRootNodes; + Ncv32u NumClassifierTotalNodes; + Ncv32u NumFeatures; + NcvSize32u ClassifierSize; + NcvBool bNeedsTiltedII; + NcvBool bHasStumpsOnly; +}; + + +//============================================================================== +// +// Functional interface +// +//============================================================================== + + +enum +{ + NCVPipeObjDet_Default = 0x000, + NCVPipeObjDet_UseFairImageScaling = 0x001, + NCVPipeObjDet_FindLargestObject = 0x002, + NCVPipeObjDet_VisualizeInPlace = 0x004, +}; + + +NCVStatus ncvDetectObjectsMultiScale_device(NCVMatrix &d_srcImg, + NcvSize32u srcRoi, + NCVVector &d_dstRects, + Ncv32u &dstNumRects, + + HaarClassifierCascadeDescriptor &haar, + NCVVector &h_HaarStages, + NCVVector &d_HaarStages, + NCVVector &d_HaarNodes, + NCVVector &d_HaarFeatures, + + NcvSize32u minObjSize, + Ncv32u minNeighbors, //default 4 + Ncv32f scaleStep, //default 1.2f + Ncv32u pixelStep, //default 1 + Ncv32u flags, //default NCVPipeObjDet_Default + + INCVMemAllocator &gpuAllocator, + INCVMemAllocator &cpuAllocator, + Ncv32u devPropMajor, + Ncv32u devPropMinor, + cudaStream_t cuStream); + + +#define OBJDET_MASK_ELEMENT_INVALID_32U 0xFFFFFFFF +#define HAAR_STDDEV_BORDER 1 + + +NCVStatus ncvApplyHaarClassifierCascade_device(NCVMatrix &d_integralImage, + NCVMatrix &d_weights, + NCVMatrixAlloc &d_pixelMask, + Ncv32u &numDetections, + HaarClassifierCascadeDescriptor &haar, + NCVVector &h_HaarStages, + NCVVector &d_HaarStages, + NCVVector &d_HaarNodes, + NCVVector &d_HaarFeatures, + NcvBool bMaskElements, + NcvSize32u anchorsRoi, + Ncv32u pixelStep, + Ncv32f scaleArea, + INCVMemAllocator &gpuAllocator, + INCVMemAllocator &cpuAllocator, + Ncv32u devPropMajor, + Ncv32u devPropMinor, + cudaStream_t cuStream); + + +NCVStatus ncvApplyHaarClassifierCascade_host(NCVMatrix &h_integralImage, + NCVMatrix &h_weights, + NCVMatrixAlloc &h_pixelMask, + Ncv32u &numDetections, + HaarClassifierCascadeDescriptor &haar, + NCVVector &h_HaarStages, + NCVVector &h_HaarNodes, + NCVVector &h_HaarFeatures, + NcvBool bMaskElements, + NcvSize32u anchorsRoi, + Ncv32u pixelStep, + Ncv32f scaleArea); + + +NCVStatus ncvDrawRects_8u_device(Ncv8u *d_dst, + Ncv32u dstStride, + Ncv32u dstWidth, + Ncv32u dstHeight, + NcvRect32u *d_rects, + Ncv32u numRects, + Ncv8u color, + cudaStream_t cuStream); + + +NCVStatus ncvDrawRects_32u_device(Ncv32u *d_dst, + Ncv32u dstStride, + Ncv32u dstWidth, + Ncv32u dstHeight, + NcvRect32u *d_rects, + Ncv32u numRects, + Ncv32u color, + cudaStream_t cuStream); + + +NCVStatus ncvDrawRects_8u_host(Ncv8u *h_dst, + Ncv32u dstStride, + Ncv32u dstWidth, + Ncv32u dstHeight, + NcvRect32u *h_rects, + Ncv32u numRects, + Ncv8u color); + + +NCVStatus ncvDrawRects_32u_host(Ncv32u *h_dst, + Ncv32u dstStride, + Ncv32u dstWidth, + Ncv32u dstHeight, + NcvRect32u *h_rects, + Ncv32u numRects, + Ncv32u color); + + +#define RECT_SIMILARITY_PROPORTION 0.2f + + +NCVStatus ncvGrowDetectionsVector_device(NCVVector &pixelMask, + Ncv32u numPixelMaskDetections, + NCVVector &hypotheses, + Ncv32u &totalDetections, + Ncv32u totalMaxDetections, + Ncv32u rectWidth, + Ncv32u rectHeight, + Ncv32f curScale, + cudaStream_t cuStream); + + +NCVStatus ncvGrowDetectionsVector_host(NCVVector &pixelMask, + Ncv32u numPixelMaskDetections, + NCVVector &hypotheses, + Ncv32u &totalDetections, + Ncv32u totalMaxDetections, + Ncv32u rectWidth, + Ncv32u rectHeight, + Ncv32f curScale); + + +NCVStatus ncvFilterHypotheses_host(NCVVector &hypotheses, + Ncv32u &numHypotheses, + Ncv32u minNeighbors, + Ncv32f intersectEps, + NCVVector *hypothesesWeights); + + +NCVStatus ncvHaarGetClassifierSize(const std::string &filename, Ncv32u &numStages, + Ncv32u &numNodes, Ncv32u &numFeatures); + + +NCVStatus ncvHaarLoadFromFile_host(const std::string &filename, + HaarClassifierCascadeDescriptor &haar, + NCVVector &h_HaarStages, + NCVVector &h_HaarNodes, + NCVVector &h_HaarFeatures); + + +NCVStatus ncvHaarStoreNVBIN_host(const std::string &filename, + HaarClassifierCascadeDescriptor haar, + NCVVector &h_HaarStages, + NCVVector &h_HaarNodes, + NCVVector &h_HaarFeatures); + + + +#endif // _ncvhaarobjectdetection_hpp_ diff --git a/modules/gpu/src/nvidia/NCVRuntimeTemplates.hpp b/modules/gpu/src/nvidia/NCVRuntimeTemplates.hpp new file mode 100644 index 0000000000..14d16bb3b9 --- /dev/null +++ b/modules/gpu/src/nvidia/NCVRuntimeTemplates.hpp @@ -0,0 +1,174 @@ +//////////////////////////////////////////////////////////////////////////////// +// The Loki Library +// Copyright (c) 2001 by Andrei Alexandrescu +// This code accompanies the book: +// Alexandrescu, Andrei. "Modern C++ Design: Generic Programming and Design +// Patterns Applied". Copyright (c) 2001. Addison-Wesley. +// Permission to use, copy, modify, distribute and sell this software for any +// purpose is hereby granted without fee, provided that the above copyright +// notice appear in all copies and that both that copyright notice and this +// permission notice appear in supporting documentation. +// The author or Addison-Welsey Longman make no representations about the +// suitability of this software for any purpose. It is provided "as is" +// without express or implied warranty. +// http://loki-lib.sourceforge.net/index.php?n=Main.License +//////////////////////////////////////////////////////////////////////////////// + +#ifndef _ncvruntimetemplates_hpp_ +#define _ncvruntimetemplates_hpp_ + +#include +#include + + +namespace Loki +{ + //============================================================================== + // class NullType + // Used as a placeholder for "no type here" + // Useful as an end marker in typelists + //============================================================================== + + class NullType {}; + + //============================================================================== + // class template Typelist + // The building block of typelists of any length + // Use it through the LOKI_TYPELIST_NN macros + // Defines nested types: + // Head (first element, a non-typelist type by convention) + // Tail (second element, can be another typelist) + //============================================================================== + + template + struct Typelist + { + typedef T Head; + typedef U Tail; + }; + + //============================================================================== + // class template Int2Type + // Converts each integral constant into a unique type + // Invocation: Int2Type where v is a compile-time constant integral + // Defines 'value', an enum that evaluates to v + //============================================================================== + + template + struct Int2Type + { + enum { value = v }; + }; + + namespace TL + { + //============================================================================== + // class template TypeAt + // Finds the type at a given index in a typelist + // Invocation (TList is a typelist and index is a compile-time integral + // constant): + // TypeAt::Result + // returns the type in position 'index' in TList + // If you pass an out-of-bounds index, the result is a compile-time error + //============================================================================== + + template struct TypeAt; + + template + struct TypeAt, 0> + { + typedef Head Result; + }; + + template + struct TypeAt, i> + { + typedef typename TypeAt::Result Result; + }; + } +} + + +//////////////////////////////////////////////////////////////////////////////// +// Runtime boolean template instance dispatcher +// Cyril Crassin +// NVIDIA, 2010 +//////////////////////////////////////////////////////////////////////////////// + +namespace NCVRuntimeTemplateBool +{ + //This struct is used to transform a list of parameters into template arguments + //The idea is to build a typelist containing the arguments + //and to pass this typelist to a user defined functor + template + struct KernelCaller + { + //Convenience function used by the user + //Takes a variable argument list, transforms it into a list + static void call(Func &functor, int dummy, ...) + { + //Vector used to collect arguments + std::vector templateParamList; + + //Variable argument list manipulation + va_list listPointer; + va_start(listPointer, dummy); + //Collect parameters into the list + for(int i=0; i &templateParamList) + { + //Get current parameter value in the list + int val = templateParamList[templateParamList.size() - 1]; + templateParamList.pop_back(); + + //Select the compile time value to add into the typelist + //depending on the runtime variable and make recursive call. + //Both versions are really instantiated + if(val) + { + KernelCaller< + Loki::Typelist, TList >, + NumArguments-1, Func > + ::call(functor, templateParamList); + } + else + { + KernelCaller< + Loki::Typelist, TList >, + NumArguments-1, Func > + ::call(functor, templateParamList); + } + } + }; + + //Specialization for 0 value left in the list + //-> actual kernel functor call + template + struct KernelCaller + { + static void call(Func &functor) + { + //Call to the functor's kernel call method + functor.call(TList()); //TList instantiated to get the method template parameter resolved + } + + static void call(Func &functor, std::vector &templateParams) + { + functor.call(TList()); + } + }; +} + +#endif //_ncvruntimetemplates_hpp_ diff --git a/modules/gpu/src/precomp.hpp b/modules/gpu/src/precomp.hpp index eb906e3368..a7ba6ffaba 100644 --- a/modules/gpu/src/precomp.hpp +++ b/modules/gpu/src/precomp.hpp @@ -71,6 +71,9 @@ #include "npp_staging.h" #include "surf_key_point.h" + #include "nvidia/NCV.hpp" + #include "nvidia/NCVHaarObjectDetection.hpp" + #define CUDART_MINIMUM_REQUIRED_VERSION 3020 #define NPP_MINIMUM_REQUIRED_VERSION 3216 diff --git a/samples/gpu/cascadeclassifier.cpp b/samples/gpu/cascadeclassifier.cpp new file mode 100644 index 0000000000..7f0a0e74db --- /dev/null +++ b/samples/gpu/cascadeclassifier.cpp @@ -0,0 +1,193 @@ +// WARNING: this sample is under construction! Use it on your own risk. + +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace std; +using namespace cv; +using namespace cv::gpu; + +void help() +{ + cout << "Usage: ./cascadeclassifier \n" + "Using OpenCV version " << CV_VERSION << endl << endl; +} + +void DetectAndDraw(Mat& img, CascadeClassifier_GPU& cascade); + +String cascadeName = "../../data/haarcascades/haarcascade_frontalface_alt.xml"; +String nestedCascadeName = "../../data/haarcascades/haarcascade_eye_tree_eyeglasses.xml"; + + + +template void convertAndReseize(const T& src, T& gray, T& resized, double scale = 2.0) +{ + if (src.channels() == 3) + cvtColor( src, gray, CV_BGR2GRAY ); + else + gray = src; + + Size sz(cvRound(gray.cols * scale), cvRound(gray.rows * scale)); + if (scale != 1) + resize(gray, resized, sz); + else + resized = gray; +} + + + +int main( int argc, const char** argv ) +{ + if (argc != 3) + return help(), -1; + + if (cv::gpu::getCudaEnabledDeviceCount() == 0) + return cerr << "No GPU found or the library is compiled without GPU support" << endl, -1; + + VideoCapture capture; + + string cascadeName = argv[1]; + string inputName = argv[2]; + + cv::gpu::CascadeClassifier_GPU cascade_gpu; + if( !cascade_gpu.load( cascadeName ) ) + return cerr << "ERROR: Could not load cascade classifier \"" << cascadeName << "\"" << endl, help(), -1; + + cv::CascadeClassifier cascade_cpu; + if( !cascade_cpu.load( cascadeName ) ) + return cerr << "ERROR: Could not load cascade classifier \"" << cascadeName << "\"" << endl, help(), -1; + + Mat image = imread( inputName); + if( image.empty() ) + if (!capture.open(inputName)) + { + int camid = 0; + sscanf(inputName.c_str(), "%d", &camid); + if(!capture.open(camid)) + cout << "Can't open source" << endl; + } + + namedWindow( "result", 1 ); + Size fontSz = cv::getTextSize("T[]", FONT_HERSHEY_SIMPLEX, 1.0, 2, 0); + + Mat frame, frame_cpu, gray_cpu, resized_cpu, faces_downloaded, frameDisp; + vector facesBuf_cpu; + + GpuMat frame_gpu, gray_gpu, resized_gpu, facesBuf_gpu; + + /* parameters */ + bool useGPU = true; + double scale_factor = 2; + + bool visualizeInPlace = false; + bool findLargestObject = false; + + printf("\t - toggle GPU/CPU\n"); + printf("\tL - toggle lagest faces\n"); + printf("\tV - toggle visualisation in-place (for GPU only)\n"); + printf("\t1/q - inc/dec scale\n"); + + int detections_num; + for(;;) + { + if( capture.isOpened() ) + { + capture >> frame; + if( frame.empty()) + break; + } + + (image.empty() ? frame : image).copyTo(frame_cpu); + frame_gpu.upload( image.empty() ? frame : image); + + convertAndReseize(frame_gpu, gray_gpu, resized_gpu, scale_factor); + convertAndReseize(frame_cpu, gray_cpu, resized_cpu, scale_factor); + + cv::TickMeter tm; + tm.start(); + + if (useGPU) + { + cascade_gpu.visualizeInPlace = visualizeInPlace; + cascade_gpu.findLargestObject = findLargestObject; + + detections_num = cascade_gpu.detectMultiScale( resized_gpu, facesBuf_gpu ); + facesBuf_gpu.colRange(0, detections_num).download(faces_downloaded); + + } + else /* so use CPU */ + { + Size minSize = cascade_gpu.getClassifierSize(); + if (findLargestObject) + { + float ratio = (float)std::min(frame.cols / minSize.width, frame.rows / minSize.height); + ratio = std::max(ratio / 2.5f, 1.f); + minSize = Size(cvRound(minSize.width * ratio), cvRound(minSize.height * ratio)); + } + + cascade_cpu.detectMultiScale(resized_cpu, facesBuf_cpu, 1.2, 4, (findLargestObject ? CV_HAAR_FIND_BIGGEST_OBJECT : 0) | CV_HAAR_SCALE_IMAGE, minSize); + detections_num = (int)facesBuf_cpu.size(); + } + + tm.stop(); + printf( "detection time = %g ms\n", tm.getTimeMilli() ); + + if (useGPU) + resized_gpu.download(resized_cpu); + + if (!visualizeInPlace || !useGPU) + if (detections_num) + { + Rect* faces = useGPU ? faces_downloaded.ptr() : &facesBuf_cpu[0]; + for(int i = 0; i < detections_num; ++i) + cv::rectangle(resized_cpu, faces[i], Scalar(255)); + } + + Point text_pos(5, 25); + int offs = fontSz.height + 5; + Scalar color = CV_RGB(255, 0, 0); + + + cv::cvtColor(resized_cpu, frameDisp, CV_GRAY2BGR); + + char buf[4096]; + sprintf(buf, "%s, FPS = %0.3g", useGPU ? "GPU" : "CPU", 1.0/tm.getTimeSec()); + putText(frameDisp, buf, text_pos, FONT_HERSHEY_SIMPLEX, 1.0, color, 2); + sprintf(buf, "scale = %0.3g, [%d*scale x %d*scale]", scale_factor, frame.cols, frame.rows); + putText(frameDisp, buf, text_pos+=Point(0,offs), FONT_HERSHEY_SIMPLEX, 1.0, color, 2); + putText(frameDisp, "Hotkeys: space, 1, Q, L, V, Esc", text_pos+=Point(0,offs), FONT_HERSHEY_SIMPLEX, 1.0, color, 2); + + if (findLargestObject) + putText(frameDisp, "FindLargestObject", text_pos+=Point(0,offs), FONT_HERSHEY_SIMPLEX, 1.0, color, 2); + + if (visualizeInPlace && useGPU) + putText(frameDisp, "VisualizeInPlace", text_pos+Point(0,offs), FONT_HERSHEY_SIMPLEX, 1.0, color, 2); + + cv::imshow( "result", frameDisp); + + int key = waitKey( 5 ); + if( key == 27) + break; + + switch (key) + { + case (int)' ': useGPU = !useGPU; printf("Using %s\n", useGPU ? "GPU" : "CPU");break; + case (int)'v': case (int)'V': visualizeInPlace = !visualizeInPlace; printf("VisualizeInPlace = %d\n", visualizeInPlace); break; + case (int)'l': case (int)'L': findLargestObject = !findLargestObject; printf("FindLargestObject = %d\n", findLargestObject); break; + case (int)'1': scale_factor*=1.05; printf("Scale factor = %g\n", scale_factor); break; + case (int)'q': case (int)'Q':scale_factor/=1.05; printf("Scale factor = %g\n", scale_factor); break; + } + + } + return 0; +} + + +