From 066daacdb92b72a3242ae732317012a6de5c2307 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 30 Jul 2020 09:20:50 +0300 Subject: [PATCH 001/173] Commit. --- scripts/demo/car.png | Bin 310725 -> 131 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/scripts/demo/car.png b/scripts/demo/car.png index f22d8d66fa83c4a51f76cfc44be8f261d1c10084..bdceaccab7fb392caf3b07648ded397a43d3b214 100644 GIT binary patch literal 131 zcmWN?NfN>!5CFhCuiyiQEyI%Ez%V0JsiXvR@b%i4zUV#sc*(ZbIS-}oecc{BxBu-^ zHXKhiPeOGWF?yA=WefhO5%(0U1Cs-T3xy+DLwiY(BgRy$B|-p;6*-&g2n%Vhku`gt NiM009FC1^@s6P7_(;00001b5ch_0Itp) z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf|D{PpK~#8Nl>O(E zZP|6$39fWE&AsWpdES;cs47%}Bmja?vn6WO-Rh3+`E&HA%%_@&{&hNMOJ@35&p@^B2@4R#FIs5Fr_S(yD*YV!niHpwYh@Q^Q z7_8T$-q#x|i)*pJzZrM$PQ;)6{EuU+xfQ?s)oxhlkacu4%#{LPgb=TtL{36acFNB*jg!Swq zI>E*Mek)<`?d>J+-stG+j8pR6+1ZWO!FhCcb;Llu#=CyfckyXQerG-%iHlC^d`v$3 z~-d7o_)W<5CR!=e_@3@Xm4z?s;C} zowKvEBf5dRq_>`y*a2Q6PWX=`)IVs`)7V(w1jf#2G#bFsPv+-wd~_IRXW;Vm6ugjM z8_s%PJ!ZytPS2v%Y9-!JXdh}s`a(D2@r3lpr`%KSG3VjoG3{`W=?@Q&$nPwU z4i95{Ym;*;PLB_%PsHKDKKYzPt-A}@#cdZjq1{i9Vq|nUE?>SB-L%avw7I{xmt`Iu z9LC;WD^7t)+;(-*9^Kv9V&`p~i~g>j?id;#$!n{%PaeB*46GoEx3>QoI2JeH9DJXG z$5ZOi)lG|ZoJDV6cMOlzV|-#HCZ>m?G149VjUMu8#`DF+*xcBp+zx00oYx1kU4-YD zd~E;endulF9*&F7i`d=YjrGm-*xufZUhv!7Q%f+cyLcEG8jktdxtN=s2Ui!dwYeQD z%PX<5u?Ag$=hMR&9HH&&eMu`vC)5MUP^(D`o#fXa^?^q8)cWF}bpTvlQEv=DkmuCt z7@9hZQ*d_-Ep>JEM6IWfvSwp;YBKKM`zF@cmSbt@dD2^N{{ZdY1I`ZO{PZIF;q9@J ziI|+2NLtz7-;0f{jo93xKFw|NJ&C@7KFX^{54gBQo7qll%e>!c(w^NhI5-Gx4O7<+ z=<1aE90AJ#c$Vi*q5~YA^85@s?Se>c7wa-GFu=7pPHEF!%H4+s`}_Km&iD8B;rC{A zf{!!WZDn~2S}F8i+8ZM{X^3<4QxSPL7oxA zgFV1977Me}v{7F?dHg7LnoVf`IB9osay)L`yd(LN$6@^L%ZKs!!D@h(AbG$J@G?!w zwLVZwnj9V(h?&{(7@HhK#?_KHSC-deWo12fn@92RKK$3bh#}~8ViLN9-qzPPVr2>1 z0X{k4>ecD^AOFukkALy6|GQ|>7GHh!b^PM*|2`hyzlY4O#VImjAO2ig*^62~`ApYS zo-QoR$DjV}r^wfOe0T3&eDTE>vAnVpt7~hqxp@rzod;qbTnG3H+5{PP2$gd6P_CUL z?|q*#kBZ52UErz{HbyP*JHW3z)X%jJFV?^{vJSdBl>c`Q;loqfui*RE?ONQtbv34^ zr;?5qmzQFhQ{J$=8jUeDI1E3IMgzD92OH6Zc2`%HfpstOaejJ^HtK@D>d9NJgCojl zQJ)j)bpWlGIzSG{#4hMren;kltKJ$s&9zbQho0)lmIL(HA?=L3+uY?mi{|!4(7Z7* z1;0-qhsV(A!|2n&I<{ zoFQ{Mj=%|oUeiWX=ptvkQ|>9R(dXI?e4F|V{!q88FQn5Bi9o&Ol_!}7U5!wM_RnGz z9rEFie;Rjx^y6q;x&hHrMu!!n5v-Jl8I}JyeD1aVJ1;(X_4KH&uN*RP9$wpaPady4 ze>DpZpsjAG2f7-ed4i2_e5{?I?yTNp4N=cd(sbD%GP1A8Qd#KxGi1Usa!PweJLEL& zkrS@kBmIWN>xwhQR$La2Ej zo*l*!ym4YWzLPJd8P6`5lB5L_i6`0$8FPvbRu5^9b;8){mo5b#c5wbi_-S{geVLzu z2Y3!eNA-aZU9KIcjr*biPRq_R$qQ+k`DJs1CuyYP{X0|GfoIVJa03X$iLvomUtK|A zti-Kblku}Z`=i*|-Hu=U@;C9|(FTGQBc-Q7jd~OJ8mgs*1qgI0b$F)IHeV~GkQQ-A z7!NnEUyTpmeJ3tsXzXlm#HauG%UFUj#|8&tvVp+>0&C>a+uxT)w~j$K3?Z=1tsRur zdW;VB#l-MX=sv(6(buEV(+$&gqErr|bwWe&@iCRgw24Cu%>xXm-NVy7TN=`6mQ$S# z(w)(I8rd?K#<93Np>an?hSR9fFgYYoVVInp1Re|x>avS5ZKF}~M6d=xVx3c5q)`UZ zXb5{>F{kOg*OBn5!Qb241N;gP4T>J{NKzWr#ubmkmV|EJ>8#+K(7GD7#ew1Ez4>LE zm@+S=ZSpzaWj!%&g&VpfUla?(w~o;5gvdrmMi5jOSJoNhs*_C5fIkVDtomsemF6Le z(Dq0nWC?0++FIjxczh&ozxQUm_wk2u{q`+z*qaPz*$OlnK_w}C68ZoJgwDV|T21HR z48S^2wx<~53ceGJ)#DSG5=K>+Nn3|7v<}JvLPCZ;LP=^sZEbADE?W77cXIIIQ7h|k z3}fp^G#b4ymX1;@W@l$&c5a5>&%w8yXJ`l*@7B_GM)K0W`Lcp;KsPb8|C| zx6KUWIr%uK;5g#@1@At*s}#8oIsRSc#-NJi?j6Q5YE+ zjQ8F{@nKk~EX32&^X1sU08FEvXA{u&==f+l6dGLOU3?!NNWUG?7#PIx8Y>1b7Dqa* z{H3x1#|kOa>+A{RAdczO)D#M>J03iE7^|zR+3p$;XMB4C4N8+;6sAKVosb;@mo@;G zD)b#3E8Bel$7ObYF3X>y-43x>n_GLdv$Bi)uwHZ=kB>02pp`QPGnv&;#L}qklQIcd z^Gf+W7{FuHSEE%&NN3^zLsHmveC4|%=xc9h2N;oQ&|C)=fzGwGq2u$`TW@BaSHb-* z4yeHiVey+*3)zL0Gdez$ieUo>4!uI1sU)X{!q}MQH{$;N2c+$RmIh*P|0r6}z{qF= zV+X_S!7~h%U0^w*Z80u5H>f-IHO`gBUPp|M^@^SL$-_N#1P*ZzbkRVroOfcloy7JIw2w$seqFgb8UN~E|0MqV|LK3gaNUg0fBQvz z`st@Qm5UfaJ$!@GJ2}9a0KYYi#;GCN8HaYD9`C;YUQFVMZf|_GnB`UU&v6ugXg$EN+akDIx!G~Lo$SsKG7M&L){p!gK_)T z9i0AI`13TLKV6N7_nyV$hpQOl7wHt89O1Z8LJ#FwKXnQw4KOM*tsC%greRB66qdH3 zdM*P1)J4Z;e4-Jv3nMYJFc8!8eX($53_8R^LZhD|&kkDq*a9o@)$i`b^T*_k`k@)) zjn{9)TW`Dxu9a6qaqqhav9_|AI!+^L?*O{p$I(VUHwL?6{?Y_GU;-Rb-@_J;(NIhy zvuxXMzJ3tjefKo3U7Lu%_?Q1O#&I(4;Ryfg*T0GnikAC!%=WPJF^AIFO8Z*zoXz4v8)9<6@7r9hml6fNB2_LD2LC^myJj-lcQxkaODZ9NXXm2du+D z9d$R*D}3^SG<<}9NW-2(K2(ky5INAk;Co?G2Uxd~ug-wtNSDUG>61r=MLIPPf4A$JcrF3w$~h4kX+!a@*it1;mPNaFBEL`0i>x`O z4gA)CR1I09u`dM51oJCxSMo_?Kik3laXxILQ=X&8ZCho6^2*?x!B921xXVDLG)37q zhcu|uZ=EXmkxJ-7Xabun@sn-FzaQM4x==C7+zxE`%TJfd3LnGi5ek(EX*nHC^4SYdEZ2}lAspdn9b;1PuJ6pS#00eNfwcDA7LaO zNK8=e2}snLIHrCF5a0IRQS2Znk3mK!4RQ?Z$2t}u_gt7kqjfQ|DQnGPv}kNd7)E`K zJiqt;`x%+p+S-nR|9`p2-Yt97qI}Gf@aiVltX#IS?9YB#MkY=}2H; z^r7@RI)H`-)S)={Bwe-`r^czoU7Ql1TO98a0Df@h@AeNL=TE+sugywBPCch%aRPH` z==fe?I-nzmU_Zh*(-5}}WiD+XovVR93{vW_xv`y$sZ*OCHD#z6h9*Yi`kQy-U7U&A zuiedvy3UJr_E7oi;R>V1u|~2Ubuzm~yh@d%8j7hbQJN=bGOH04NgV~OfS{dK!Y#hh zxiRv#!MU?loC#r4L0mv1I)@FMRh@|olQ*r!ZKIM5p0Me$uM2?(ZJZ?(&5O z0)`K88*R`C7VpxUj&mnYZ1!4EcLmlFG@Vfa=;{~+?a|lQkFhrz^9wVva0x^BWG|jR zzlQ+a=h{^U&Ll}9h&zWjhR-`n8rsPEjf-6Jw)4R0Pmsc7nhcz+1!iESFUE% zZ5^X)-N+;D4PF zd(`Xp?DWBJ!)aLFzyA=Ub^wEAk~F)qxV#B2YjNZHbZo9{WTae!tQ#EJBfPl{4Lu8F zV`hiZ*AdsRH{u8HUjz1ySbn}9>#Ik=9?|IUh_Mk3$*z>88|xSz&5P&*KeN!qG{%9n z^!(W_d7sDYZ%@a2AH4+(Pm>&8 z8Nooi8V!thgErz;CrG*%_sX*_jF%oyWN$C+s6#2Q+SazM3Q~Qfp`ncQxl!#-u9B$A zRK+hZm*57q7-OY5A{whR#SMCLJi{6nqF? zJ(XADK`i!xj*4M<=+VeqntG$u!j-c(SbwS*8y*O_NMNe7@xtQ>&(F3S6|+b z&wq0-o<5>o07qSb{Zn)@cq$H4(K9*~)>mAUBwEqS7_bqvdX3D>Ibtc|_=LQD)AZdH!>#x3v)#Wvj10H&k z*48)oVy~s%hc||LVv2I6rX3m38{^t0cu>l^Cr=jR+4Hr`^Jjnd)9f96_WW5qUwoc= zeQ{|O8L^j+wzxH7-P*UeNm&Rz59)yRMIZd#fi2R)pf5fAovE+vt!V9EAX|FkU;K-I z6*sQk!qHyF;r=YXz4sti*LIP6LpWC*IC=(1?Dee2<;!z1Gd&6YL&sFQ(O<)H>O+>p zBbz&Mx_=(cHRRXM0cmPD6N3SFK#NpZlA%GQN9pj??n#akg|=17(Wcm>F`eKv$T4Ph zu%))Z^4-^w$0OKN{n%3IZ1T^53a54TJ9Y6fvh_F({9?-#N3y^ouM|Bm zeYbm#!S@5YGQtoJDOaCOZSkA(? z);IH$FK8oqzTgP>VW>3d;foAR0H`*Tc_n{ynuqDNalD$3G%24MBs0*eBVs$s2i7H> zdh(DYbi{Px)s5b(pNr$5ZC}ZVVn>Ka{-d3^*Qw*akQF%k!JTR98kV^E_kr*}V#XUI{gqH?UXcZs|krbhPl95Id zX181ogjU|P&VWi<2VqcS8ksIEN{17^-{rfV9gP6KHOe>;YJ+;O zuC8Geu0#`CR5r4WC5`Fn-gxuPYdH)?;l7LUXqRn2ZD~YIgLYzK0%LYOdjS+=hv3aJ zcXVE!FK2Iw^>HY`9)|no)>Z}yY>-8d<{@9V`F-iGIQ$lGYxISx=Q<`%}|(&dF1)v$$!>_Kp7z{Q2VN+<~A z$jb6kIw1D&=@8q)=P@=qM%%SAvaJIoKKgOc6q*AldvR-&CJhtFPBqH1XK4pRZ742XxfHv!>%+&(v9aMWC1@>c&P)Ox5F!x39!SCvp&4=mVDrI901lI4O%8@$B&)^;hZJ8{jYuhxpiQHJ(o1!$-@p zwy_&ylcVvopMMyi{P7=A@74J7cNliir%bHVX#3c;@6mywSA%YbN5^9vr*?2?C}pZj z&bBsaV6;&ORp(ykAsq;Ngi|W&oG4E?(IF~fP?a)EJ~c(9$i~I2Gq4Xqj?4r3~Hp&Px7MtOTMN{eaJumOUhA3`jbRON^kzzhaLod${LAlrq`}jEmcf|JgWo>#weX`c?alKl zn$$fxECjh}`ZVA;?3J!#oZ1t$+ibLp4?p4^NOW(vsM67+z26 zoH}yewocv0nZcq>hsK^JU;g;Ve;jYT{wA{E96tOG$NP)8cYhh#ZqGzF{C*tUP2>Tx z?dsLRxP%@VB;O%)n#01ouvvDtH{%ppHPkyCN4v+dx4j?Qxb@x!HdhaFfCfil8O={> zB%PFC-vM<)a9nhvE2d-N^0k+v=B@O$WDh% zD?igA@Pr;JCm`uh9w6@|KlAn(@6>I{XZ}uJgyt-t^y1b)oz8TE37mzh<%x3VL*dSs zSs9Kjr2MRmbZsXi`v$MnOWIRXcN)z!oH$ATFjr~6{awly+Rp4VkSKk_C&`b%VUM$R zn>5{%!A5PpQUyO!{wO1qGv;TxK1;iheC3PMQ=&5j3@HPsZ@Vt$<7v4CKJzeZf8#WO zAMFM07oB+9*r2gNF^A7trn1gYwH^KL6ubBcFYpjOXIg6_+zD?fwOugn;(ewl&+~En z-ltu26ODgUh^H9wDtisBlam7oc_41wxRyfq`Qi%3gi%NlP=lV@h|SD0>%5we|CRE2 zN(IEA0!iad$00pD5|Ap9ua{MWNxE^hJGmOAI_3QljeA7d2ZvM?VXvdoKTrg@rK$Yw zy>QOM1+aT>uS82@K!(u>Oy#SPvNT{4Fb?bL6C-3P@3>D7lM_0loL+>_K-3Thr)Mzc z{!V&+SFT*nDDfT&e-S~otr0<9)nP+RbmG*;6L+5C!Fm^GLVTC^60k7wGx1QoSsT=} zz{c-F#Q9bJrSjytr+D}J0)$e%#D8X$X}vG$W#sCt=tD+MLmr|Mv$bwLZygq{oOW4f z?*r*ATPEy5`5!{0WK`Odtf*#2Ci(}uV;m!DW?>%VbP~beTNo9AB%!A>1hHGM6e_}l z5J_TB*t|+MK2rcmsyr81VQGJ>p=ob`#)gW;sHJV>kR`hrR4I>t#J5ab6N4&ExTKi`hY$!1lq_nMLlExN>wUK;>Ch6Q7@zAl~-`hp0 z9j3A55Gx&`9?Efu*b0i$;aZ(24|}+3H6yY(K>ZQJ!}Y{-&Ns2AkoLA7I&jt{g&S=t zy=u7GR?Z30soKr?1H0+q?CtKv+RA#Y8~xfp%9g>O;#+ZBhFK{`#GY+oluu=3FV@!9 zZmh32`F0zpXEmNa-2%6#$!j`w!r=T971s&MPkymSX?O^n%Zn&--%Gzm86b@!3AiWw7VDy zYimb1P=PU1oS>Y?1tFcEcgMqrd(fSV3{{yD5z|xMF*`dDBhbR= z@Gvw)yLTyb!C}g`PGBx{3i_bii~N24>Ui9_b1OzhhoILzVBdv@!C~FuStB{DM+Vu) zSzq7Sj^*Wz*w{LUmb+tOdL(8sYUVG^#r);VF+IBwy#v$}TC5NCQTMT!nVaQ)DE2Tk z9ZJ*JKNu5}GdNiWYG!eA=VEUDDhAt?m>9nd&K6>Fd;z*2rETlfd5HEHp&f_8{TS^s zOPfsOeQ#}q_k*MxgiZ!&OXu+PgQFU_gCH*)PUjG~ZrT-RAE)i)*ttvwP-yp4oIjtT z(@uHr?{m_gpTVO(!)7#yrID;McG%((29vx}PNVr4PZ*HL@KVYHXv@?2QkjP9F;{s_ zdOka~Z1dGomF^6p$dgEgLx)&880(ciqr?&oDeZ68*iS;CVdy zYNI?iIDmuK;d~A9JMPK*{=N~~V-y)Ql5{>eI0QWoLRfeU$0@T*4qYk6I>o3E9xoplobc9qvT_U=vn{H4nu@{sc^U6bemQyZeV6Yf>MQ2~6 z!=kJ8l~1?N#oHoxSaN{NxdIgWMU>k+SWvBM=e>>x3r_)YPlg z5!*?fFYb$;EwG9BqRoqOO;YI1=(WLC(?VUeh1b3P{T$x8w73*cpFF{-dk~K~pFMmS z&mTXGl_!s5eeqdrtt`d%ngOo0*xg!>ovn>@e0I0Da?Z~l_j?FH&wbjs<@`m?19Xi# z-T4ISS@m+Uqikn@!XW8z4jZWNfmUiYbdPgR`-|>-q5pWN&QBT2z0U`*Bm2Q$8Tu%F z8r0MVwFk1BXI;n~-`g8m(m1@-9?xp{Q92Y{)x%Ca%XtSn7wGfiz(a%Rwv75~TTqXj z=XtOnd+6W&gI4M#uRFWDu>n5THa0o8V{^M1O`dJyylwC7#rE!gUboP}TRdCeLWgcN zV|9}&=USfIv9`6tyB(hI#4@^WX>}`>)<~z`#ra>u!Cc$f=bmeGFIM@zy48w}=3#8@ zqKEd6DTDj=xy{++`Oe;P?DB4}r44cty9Z^;z#eDbA7DRxaC=(nOvcc#A43uC?QFqF zn{jzzJpSy@ehe`m6u7p0uNs9m$8bGE<$bO@+=K92~x^~V8yvz(%osC zLIfZ>C!LV4zpp#H>UwEJ4FeUt9-6}(9J6g+y)?9u)oeSBZ3xQl)IJ*7F}SE&lpcl% zLO=oA-Q&b*jNT&*9t9aGj#L0JmCvn%(Nvj5&-4&!)PiU`A<8cDYoG-CF#;4u+cdOB z*4@`$$B0;V0dtCanuKGLOyz@wx4OlyGd<}A`9_3jIZQh?Lu$3L0 z3K@qG82zw2dt}hzDdHIh#Nf^esNl_RTq8XF{q+=93Ot#-lu14y(W1vD`{MSyugANe z{4j36_8NxNAoa8hvv}hQBTt>nb7;Vil7;|A0j84HQ7AnV8fyNgd`{a@rnW8`&zmri z4&4#LKMh@&ZDVaM4eoRjj55I7j>n}qgotxARKiBRPoTSEu!|?7Y6rBd<3pV^?L3&C zE{uFm>*ug8ouF}y(_V~9C+pgCR&NX@qNR@_dU0woC=YPT9rr#xgAp`7$o)V}Obt_y z17LC@c5!SJU=DFP0aoj(Q>#Ezm}v+)#@jhHI|y4N;L^A>Z3PEVfJ4KhY3zUf&6D`% zn>7@Az)Khg3O>HqAyd#zPK?uj@(o55?WPlP0UhZ0IIdFT+la8r&N1PZfBn`?YSWR! zs-8c4mN1+l)c1D9w{@i~6!PeJZw?u%*<+XltI_7%hl8Xw+tSIV>+6RIPJ5YDN}Rwr zJu?`yb5rEgNQJS#w*x)yP^!HW{V_5;l+K2D5S&S-MuHVfsieD@4%>Sa5`rJ0~8z?*^x&_a3bm$y`)NG(4GnBmgr1t zJV>w38(FwCpYv{xf!T@T>l^FjzY)#7%{VyT$-E~f$76D83P)}r!h2m5#tjMnZwA%FtpdBzaKo44=99R zooL(0c`8n1-&6V6a|B&T=i-+i&9j!w0ne zNnE}%6Sr>L_P_^UY;U1>aK;v&Zy?ja(eOY_%}i0Z8DJX&*BE@Xg~o8s6QfNGh}3~; zcyttFM0qDAsigbiyZ)42_O|HgR0g(2v%NI{oH{_$rn=Xl(Wz55_&Wm-DSXTW*+;vi zK}sb(B}0DEw#rToBY-fy6iv28Z~2T8Jb6~6CI`HgzXO!x{@Zp1Avs}l#%d>{dUp}Cbge?HR#im`4#xe@F(iUy9`Qk zRy{G|P}!~AH?t~ekiUV&I%I?tzR#f>91eHLAQ8XCQOO_K#3xQVHIQfjZ;lb*UBG8)s<~Opf)&G`wf8fW0KjD`6eOk*L-B z;t__f#+^<}2fEfE&&b$lwyO?+!&A34=9LucU3D(h```9~W>0HrVK#`gDWp*nlK$Uv?12{oi`llBXIXi!FHSDww`(&U$9 z%-04Ty&9~ofiLB1+F&HONcxkm{7xR!;nKm8PYq@|%)6u!|BjeR!##f&7>oT^3M+ik zJ|$nDaqm@Gtll-a!S36`(m^gjTgq4kt0vA=4zGQ=S?I1+a%VzrMDT z#;5|@XixTLL09#9Z+1EN^w2h^(8%#&rlFb1zf6cqhXw=E?(Jigl->DV9S*0cfgcQ9 zXv!#@0@b-1MyhhC3f~F4^|8H9&S=|{Rt${{#N5IJ#&%gD$6-#CPWh#5S7vb-iB9u& z)2O-8i+%FZu*47>D!mf+lsSaQ9jZA^PIW=W*}e5#PXPd|w}MVs5=e zub?j-K!s;rBLMh~`nY10^CxV7>#kEXXitY}ar8uvMT2%@1LJxd1A|Ww51mgCw2y`(#`9bU2J2`zv2k*0I-~c;w8<{=M_J@* zCF4fN$yY;09(2_$BVy8=!wY+Qib1o!z77qY$0&xY!?o-k*`hGVAUm{ab#;^9PBcdu z4;5$Kc^J-#Qid4O+rt5suLc`K)JQ z#Wxt_TRWS%a*%R$c(|W7pNpB9Ir6@UFTVJa`s3*G?&v@nQ6g=7d&n;EBMT3Y^_Sgim{@bl@FhUa!|O zlAEh|K)=mxjIc$Ax%I@GZ!W|~AAX!e!0tbIfU~*-=hH^A5Ukl`)+PdAOs~`3$}AkHMlYVCl&q3!Im2u46^> zCCsTikTXTmE4!3nMNX9=hf<$sk%x*Y{!bhzpE5nNrpUjx>@=vr)s#gn@=STc^ZeS6 zd7rRyuYAcNDJiG=+He>UQs;o@J}1JLIT|*gye~f%L!`*wG}0l0bU5-^ku#>r(?1)W zcgl@4n4o*VP1yP8^PQwBSM~_IwRw55&aO2u}NSOwSvrIO6#^&(-BR%d5G1pG~;eL>H{Mf?roG z%nsp9OvW)5xH3&g!d}mmdB~8}wbkVN>AC4Nh_-fil8%()_Q>uum9vh{C<`9Uz<5r; zx5r?=wF@nuBU9_-eG;>?)94siJ3EMFWX=8iPmno>$VBrS&)vP zTX|O+ZS##+vyva=y*9;X{yIJW@~B2HNSjWyx7~dvZmZt$(%Y`Q)rN%D_+9uy#~B#3 zM|el^d~04LrvPoqlw-+fIsqr#x93IBE>L6Y%H%-}c%1=lA?hMZApd;k)x15-S8RcX z7tcz~UjFu|ew+5y=ZRjg<&ycOsPI(Wi|-z!7?-go}? zxxcMX4wX{u^%x8+v}v#T3unSHk{a?Z51W%8TbnU8-iROl=%Xy;^Dn-MhYwd{|A5A? z4Wzf4la?uW4z(S8bWdl*tclHSOll~(X5 zm^6koN{!%aOlgQ~U|NnuWh7RiVVyNxRZ7_{4Z+w=rE*hw2v0S{Ng|U-U6n<~*@w{& z@H}@gBG0=nF!@kuq*dJ<9xLoHBKmqzw!+by5fmc}4)t)pfvd9g_YaUK!Wr0%z*vUE za8CKHppz+`Lm)BdxNnGAqE{#zA<{^V5Q+FPKKmZ^4eCNx5aom+rGAxB(bK{$~)BU^lH#LMWjf_Hs$ zCq3zuA$%ucNlT5N1XA6L>l>Ze6~$fq`6)nYqbSPTjS0 zaK96~JKLbaiN6|17>(2m*&f?CCmN4C7+pFP5Cb^q#A(@~Jx|jabZo7W_wAVJ z-W`_~CNWrtfX|*bd9FAv+bBVYP*5r`oWYyYdvUZ~npM#&zi1pfCqPF;M|^B#Bxa{) zG6MAc+4FdS5o`}y3TO=^aO+vdhvR%vuJau%Unz40+zwYXVwBNgp5@9!<6{Zi21dN| zDY6%3&q@4pKgKf7*W&ZjSXtG`L^(D(Fq-=^!hiY76`ZXR@Y~8IWP6|sC!SA|X9Fk0 z=(V(+p9bQ}<%t*>#X0EiNXJt}sesq{vG-to&0ZjvkG#O?=tQoRw$ykCoMV>; zc5=8;*eh4A#r119;hBvbBQ71A&K|FsSskFjd2^*RhmSx@{ew8b7&72SnJG>ct4Vii z81h%{r13zrtJob1loK*}XOEwnj}&EGH1Cy_%B5mlkSFw{f#JR77vuJVy0+S~PYN&Z z%IPz1c;>%y_`AqF={n!~n*Y+ML7vz%>N^krFK1hlX1elDx#g{SiARk`+ssJ5iHrO$ zd?Q=}WnK87ln;+*UILMIQJxh!Uh-|%)xUP7DtOb$Fu;~h1SikIR?r zk~e1MVSTFrP4&z+NP~%QOrP*(W_;JqM;NO}tOcgyl9KMW{m5>*5!9@ys(#dkj zuE8{y#J_pta_05?`I9)<-Hkg}Zp6&UByK}z9AfM@H<~#i+g|9&@#(m7>1xc*;A|kv znm8XWyXQDJ>px-%f4jpv0OK1Uvx#Hc}=s0i>CyWm5)8TTCl)*gb@oDQwFKC3s zvH0?8kFqwG!Xk&eLWlA@DM&*Cu9lnjQqqsK=1{#t-Rge%m4fp-aV$d~`5>k^PIaHI_4j27i`szNZ)vZ}rw((n z5WQ`Uss_fe&cxpCRtyjJ#U~%Xlg$0iw-0e9)?#-b)1|u~C^fPbNMxQxdK3ibRbaAK zyyapFBbxmP;sKPm!==Pd#mBR~VlvmYZy>>6#B_>AC=U~IVRjN{83mSZ&c z00LAYJ4DF!o*mz8#a&O^sEFx|Dj41Eg1Dg~;5myfVABDao0*B(+1WHuj}V&MFz9o< z^~V@A>y&c<;U1Ir*ainKz4X35*U7i{f#(=;hZvPk+Ux^&j;Z#Yic-(lao8&RVu&~_ zrR*1w-C?{_pcW&&G_re5XqY+NOJl*dDa2TqBkyc{Z$YX<5{ffnqQVIIq9lEBElSNH z5>&*4c9WsRt)92bULGPGoE)zs(Fd_P0o#?fXiVUT$|*GGwLnN3na@`dtVb??SqwwR zS&NSj1n=wbz7_8wtgqj=4NmGgR7AS1b?ZQ&j3Gpg_Z)xOR$8f0+RlVRzkhqe{?bg$Uzv`f(SGo4*R&Dy z7H}OWj*Ela0nOoPGm={8RAdg<0^cbBv+avP#cc`;R|s*L%icbO z`SC8Wp2zrPBd%Yci(9v@C1V>gUtC;*&)l=YeIyz<9!@Mas=SJkzlVU8vOtFo zIg#2(P8uS#-^RvXE_Zu?GIGe_$Y?EQ7v^JnW;Tsjov9p31>Y&VhH+wCqS?q^_Bvs( zo2LrHXpqCn>>*rRbAqM@fb%LGmwt``Qo~WX(&pE$lne5T!m*l5Q+oZ*P|SO=vc8Hl z(M%&zheUq1mqMJbF0Vi*E+x!4{Qf z)iY7_gNn5DL|AE!UUfG?Oq&{ zBL58Ns1!9yDw^<+j-8Y#&UN@KKV>60)>zFt32+uiKG2AB_=LlE9%)!--cE*eSXAi= zNT&5`>RjZY`mtT-HcVL_Fj=NDuNYF^amZ}>s~AoBito~y;K%n)V8}YQ>z1&hTP#~6 z&_8pkdRn}ipFH6kkK_^f|3%$MkUXKoM7~xkvB5Rlj&zlyVI2%?ID|u}v&UA!&#!6) zHSsGhEwAbkc6g)1ejHLQJPwa@&R9Bp;uVMD

>*y3d!=fw(o0h5iFCr^gtw$NStL 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

^jw>oFGdwK* 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

!E?49oa)av{I~Iv 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%0DXz)P{20$dBhs<$zhO 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 Date: Mon, 28 Sep 2020 16:12:33 +0300 Subject: [PATCH 002/173] started to write the nGraph class for NonMaxSuppression-5. --- .../include/ngraph/op/non_max_suppression.hpp | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/ngraph/core/include/ngraph/op/non_max_suppression.hpp b/ngraph/core/include/ngraph/op/non_max_suppression.hpp index 3bc8c152f7fef9..33daceded11c40 100644 --- a/ngraph/core/include/ngraph/op/non_max_suppression.hpp +++ b/ngraph/core/include/ngraph/op/non_max_suppression.hpp @@ -235,6 +235,128 @@ namespace ngraph clone_with_new_inputs(const OutputVector& new_args) const override; }; } // namespace v4 + + namespace v5 + { + /// \brief NonMaxSuppression operation + /// + class NGRAPH_API NonMaxSuppression : public Op + { + public: + enum class BoxEncodingType + { + CORNER, + CENTER + }; + + static constexpr NodeTypeInfo type_info{"NonMaxSuppression", 5}; + const NodeTypeInfo& get_type_info() const override { return type_info; } + NonMaxSuppression() = default; + + /// \brief Constructs a NonMaxSuppression operation with default values in the last. + /// 4 inputs. + /// + /// \param boxes Node producing the box coordinates + /// \param scores Node producing the box scores + /// \param box_encoding Specifies the format of boxes data encoding + /// \param sort_result_descending Specifies whether it is necessary to sort selected + /// boxes across batches + /// \param output_type Specifies the output tensor type + NonMaxSuppression(const Output& boxes, + const Output& scores, + const BoxEncodingType box_encoding = BoxEncodingType::CORNER, + const bool sort_result_descending = true, + const ngraph::element::Type& output_type = ngraph::element::i64); + + /// \brief Constructs a NonMaxSuppression operation with default values in the last. + /// 3 inputs. + /// + /// \param boxes Node producing the box coordinates + /// \param scores Node producing the box scores + /// \param max_output_boxes_per_class Node producing maximum number of boxes to be + /// selected per class + /// \param box_encoding Specifies the format of boxes data encoding + /// \param sort_result_descending Specifies whether it is necessary to sort selected + /// boxes across batches + /// \param output_type Specifies the output tensor type + NonMaxSuppression(const Output& boxes, + const Output& scores, + const Output& max_output_boxes_per_class, + const BoxEncodingType box_encoding = BoxEncodingType::CORNER, + const bool sort_result_descending = true, + const ngraph::element::Type& output_type = ngraph::element::i64); + + /// \brief Constructs a NonMaxSuppression operation with default values in the last. + /// 2 inputs. + /// + /// \param boxes Node producing the box coordinates + /// \param scores Node producing the box scores + /// \param max_output_boxes_per_class Node producing maximum number of boxes to be + /// selected per class + /// \param iou_threshold Node producing intersection over union threshold + /// \param box_encoding Specifies the format of boxes data encoding + /// \param sort_result_descending Specifies whether it is necessary to sort selected + /// boxes across batches + /// \param output_type Specifies the output tensor type + NonMaxSuppression(const Output& boxes, + const Output& scores, + const Output& max_output_boxes_per_class, + const Output& iou_threshold, + const BoxEncodingType box_encoding = BoxEncodingType::CORNER, + const bool sort_result_descending = true, + const ngraph::element::Type& output_type = ngraph::element::i64); + + /// \brief Constructs a NonMaxSuppression operation with default value in the last. + /// input. + /// + /// \param boxes Node producing the box coordinates + /// \param scores Node producing the box scores + /// \param max_output_boxes_per_class Node producing maximum number of boxes to be + /// selected per class + /// \param iou_threshold Node producing intersection over union threshold + /// \param score_threshold Node producing minimum score threshold + /// \param box_encoding Specifies the format of boxes data encoding + /// \param sort_result_descending Specifies whether it is necessary to sort selected + /// boxes across batches + /// \param output_type Specifies the output tensor type + NonMaxSuppression(const Output& boxes, + const Output& scores, + const Output& max_output_boxes_per_class, + const Output& iou_threshold, + const Output& score_threshold, + const BoxEncodingType box_encoding = BoxEncodingType::CORNER, + const bool sort_result_descending = true, + const ngraph::element::Type& output_type = ngraph::element::i64); + + /// \brief Constructs a NonMaxSuppression operation. + /// + /// \param boxes Node producing the box coordinates + /// \param scores Node producing the box scores + /// \param max_output_boxes_per_class Node producing maximum number of boxes to be + /// selected per class + /// \param iou_threshold Node producing intersection over union threshold + /// \param score_threshold Node producing minimum score threshold + /// \param soft_nms_sigma Node specifying the sigma parameter for Soft-NMS + /// \param box_encoding Specifies the format of boxes data encoding + /// \param sort_result_descending Specifies whether it is necessary to sort selected + /// boxes across batches + /// \param output_type Specifies the output tensor type + NonMaxSuppression(const Output& boxes, + const Output& scores, + const Output& max_output_boxes_per_class, + const Output& iou_threshold, + const Output& score_threshold, + const Output& soft_nms_sigma, + const BoxEncodingType box_encoding = BoxEncodingType::CORNER, + const bool sort_result_descending = true, + const ngraph::element::Type& output_type = ngraph::element::i64); + + void validate_and_infer_types() override; + + std::shared_ptr + clone_with_new_inputs(const OutputVector& new_args) const override; + }; + } // namespace v5 } // namespace op NGRAPH_API From 6ceca93c963e8e937ed8d3a5cb0aabb19aabb5bb Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 29 Sep 2020 09:44:49 +0300 Subject: [PATCH 003/173] Added NonMaxSuppression-5 to opset5_tbl.hpp. --- .../include/ngraph/op/non_max_suppression.hpp | 19 +++++++++++++++++++ .../core/include/ngraph/opsets/opset5_tbl.hpp | 1 + 2 files changed, 20 insertions(+) diff --git a/ngraph/core/include/ngraph/op/non_max_suppression.hpp b/ngraph/core/include/ngraph/op/non_max_suppression.hpp index 33daceded11c40..ad93e1ab48722e 100644 --- a/ngraph/core/include/ngraph/op/non_max_suppression.hpp +++ b/ngraph/core/include/ngraph/op/non_max_suppression.hpp @@ -396,4 +396,23 @@ namespace ngraph "AttributeAdapter", 1}; const DiscreteTypeInfo& get_type_info() const override { return type_info; } }; + + NGRAPH_API + std::ostream& operator<<(std::ostream& s, + const op::v5::NonMaxSuppression::BoxEncodingType& type); + + template <> + class NGRAPH_API AttributeAdapter + : public EnumAttributeAdapterBase + { + public: + AttributeAdapter(op::v5::NonMaxSuppression::BoxEncodingType& value) + : EnumAttributeAdapterBase(value) + { + } + + static constexpr DiscreteTypeInfo type_info{ + "AttributeAdapter", 1}; + const DiscreteTypeInfo& get_type_info() const override { return type_info; } + }; } // namespace ngraph diff --git a/ngraph/core/include/ngraph/opsets/opset5_tbl.hpp b/ngraph/core/include/ngraph/opsets/opset5_tbl.hpp index 145cb866826564..c7c293a00a5b4f 100644 --- a/ngraph/core/include/ngraph/opsets/opset5_tbl.hpp +++ b/ngraph/core/include/ngraph/opsets/opset5_tbl.hpp @@ -165,5 +165,6 @@ NGRAPH_OP(Swish, ngraph::op::v4) // New operations added in opset5 NGRAPH_OP(LSTMSequence, ngraph::op::v5) +NGRAPH_OP(NonMaxSuppression, ngraph::op::v5) NGRAPH_OP(GRUSequence, ngraph::op::v5) NGRAPH_OP(RNNSequence, ngraph::op::v5) \ No newline at end of file From c035301eed5c196239e832bf408751745c95ab81 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 29 Sep 2020 10:11:38 +0300 Subject: [PATCH 004/173] Written all constructors of NonMaxSuppression-5. --- .../include/ngraph/op/non_max_suppression.hpp | 25 ++++ ngraph/core/src/op/non_max_suppression.cpp | 114 ++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/ngraph/core/include/ngraph/op/non_max_suppression.hpp b/ngraph/core/include/ngraph/op/non_max_suppression.hpp index ad93e1ab48722e..27101831912078 100644 --- a/ngraph/core/include/ngraph/op/non_max_suppression.hpp +++ b/ngraph/core/include/ngraph/op/non_max_suppression.hpp @@ -355,6 +355,31 @@ namespace ngraph std::shared_ptr clone_with_new_inputs(const OutputVector& new_args) const override; + + BoxEncodingType get_box_encoding() const { return m_box_encoding; } + void set_box_encoding(const BoxEncodingType box_encoding) + { + m_box_encoding = box_encoding; + } + bool get_sort_result_descending() const { return m_sort_result_descending; } + void set_sort_result_descending(const bool sort_result_descending) + { + m_sort_result_descending = sort_result_descending; + } + + element::Type get_output_type() const { return m_output_type; } + void set_output_type(const element::Type& output_type) + { + m_output_type = output_type; + } + using Node::set_output_type; + + protected: + BoxEncodingType m_box_encoding = BoxEncodingType::CORNER; + bool m_sort_result_descending = true; + ngraph::element::Type m_output_type = ngraph::element::i64; + // void validate(); + // int64_t max_boxes_output_from_input() const; }; } // namespace v5 } // namespace op diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index f07a5e39c8103a..d4a832a1ad1e00 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -530,3 +530,117 @@ void op::v4::NonMaxSuppression::validate_and_infer_types() } set_output_type(0, m_output_type, out_shape); } + +// ------------------------------ V5 ------------------------------ + +constexpr NodeTypeInfo op::v5::NonMaxSuppression::type_info; + +op::v5::NonMaxSuppression::NonMaxSuppression( + const Output& boxes, + const Output& scores, + const op::v5::NonMaxSuppression::BoxEncodingType box_encoding, + const bool sort_result_descending, + const element::Type& output_type) + : Op({boxes, + scores, + op::Constant::create(element::i64, Shape{}, {0}), + op::Constant::create(element::f32, Shape{}, {.0f}), + op::Constant::create(element::f32, Shape{}, {.0f}), + op::Constant::create(element::f32, Shape{}, {.0f})}) + , m_box_encoding{box_encoding} + , m_sort_result_descending{sort_result_descending} + , m_output_type{output_type} +{ + constructor_validate_and_infer_types(); +} + +op::v5::NonMaxSuppression::NonMaxSuppression( + const Output& boxes, + const Output& scores, + const Output& max_output_boxes_per_class, + const op::v5::NonMaxSuppression::BoxEncodingType box_encoding, + const bool sort_result_descending, + const element::Type& output_type) + : Op({boxes, + scores, + max_output_boxes_per_class, + op::Constant::create(element::f32, Shape{}, {.0f}), + op::Constant::create(element::f32, Shape{}, {.0f}), + op::Constant::create(element::f32, Shape{}, {.0f})}) + , m_box_encoding{box_encoding} + , m_sort_result_descending{sort_result_descending} + , m_output_type{output_type} +{ + constructor_validate_and_infer_types(); +} + +op::v5::NonMaxSuppression::NonMaxSuppression( + const Output& boxes, + const Output& scores, + const Output& max_output_boxes_per_class, + const Output& iou_threshold, + const op::v5::NonMaxSuppression::BoxEncodingType box_encoding, + const bool sort_result_descending, + const element::Type& output_type) + : Op({boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + op::Constant::create(element::f32, Shape{}, {.0f}), + op::Constant::create(element::f32, Shape{}, {.0f})}) + , m_box_encoding{box_encoding} + , m_sort_result_descending{sort_result_descending} + , m_output_type{output_type} +{ + constructor_validate_and_infer_types(); +} + +op::v5::NonMaxSuppression::NonMaxSuppression( + const Output& boxes, + const Output& scores, + const Output& max_output_boxes_per_class, + const Output& iou_threshold, + const Output& score_threshold, + const op::v5::NonMaxSuppression::BoxEncodingType box_encoding, + const bool sort_result_descending, + const element::Type& output_type) + : Op({boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + op::Constant::create(element::f32, Shape{}, {.0f})}) + , m_box_encoding{box_encoding} + , m_sort_result_descending{sort_result_descending} + , m_output_type{output_type} +{ + constructor_validate_and_infer_types(); +} + +op::v5::NonMaxSuppression::NonMaxSuppression( + const Output& boxes, + const Output& scores, + const Output& max_output_boxes_per_class, + const Output& iou_threshold, + const Output& score_threshold, + const Output& soft_nms_sigma, + const op::v5::NonMaxSuppression::BoxEncodingType box_encoding, + const bool sort_result_descending, + const element::Type& output_type) + : Op({boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma}) + , m_box_encoding{box_encoding} + , m_sort_result_descending{sort_result_descending} + , m_output_type{output_type} +{ + constructor_validate_and_infer_types(); +} + +void op::v5::NonMaxSuppression::validate_and_infer_types() +{ + set_output_type(0, m_output_type, out_shape); +} \ No newline at end of file From 4ca722106329e66b812de0f3f9be04d32665767c Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 29 Sep 2020 10:34:33 +0300 Subject: [PATCH 005/173] Wriiten the method op::v5::NonMaxSuppression::clone_with_new_inputs. --- ngraph/core/src/op/non_max_suppression.cpp | 37 ++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index d4a832a1ad1e00..180712770a43eb 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -640,7 +640,40 @@ op::v5::NonMaxSuppression::NonMaxSuppression( constructor_validate_and_infer_types(); } + +shared_ptr + op::v5::NonMaxSuppression::clone_with_new_inputs(const OutputVector& new_args) const +{ + check_new_args_count(this, new_args); + NODE_VALIDATION_CHECK(this, + new_args.size() >= 2 && new_args.size() <= 6, + "Number of inputs must be 2, 3, 4, 5 or 6"); + + const auto& arg2 = new_args.size() > 2 + ? new_args.at(2) + : ngraph::op::Constant::create(element::i32, Shape{}, {0}); + const auto& arg3 = new_args.size() > 3 + ? new_args.at(3) + : ngraph::op::Constant::create(element::f32, Shape{}, {.0f}); + const auto& arg4 = new_args.size() > 4 + ? new_args.at(4) + : ngraph::op::Constant::create(element::f32, Shape{}, {.0f}); + const auto& arg5 = new_args.size() > 5 + ? new_args.at(5) + : ngraph::op::Constant::create(element::f32, Shape{}, {.0f}); + + return std::make_shared(new_args.at(0), + new_args.at(1), + arg2, + arg3, + arg4, + arg5, + m_box_encoding, + m_sort_result_descending, + m_output_type); +} + void op::v5::NonMaxSuppression::validate_and_infer_types() { - set_output_type(0, m_output_type, out_shape); -} \ No newline at end of file + // set_output_type(0, m_output_type, out_shape); +} From ce514c5e8098c010ffbeb25b30b20ecd0934c3aa Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 29 Sep 2020 10:38:36 +0300 Subject: [PATCH 006/173] Started to write validate_and_infer_type(). --- ngraph/core/src/op/non_max_suppression.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 180712770a43eb..463567e6a2342b 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -675,5 +675,12 @@ shared_ptr void op::v5::NonMaxSuppression::validate_and_infer_types() { + const auto boxes_ps = get_input_partial_shape(0); + const auto scores_ps = get_input_partial_shape(1); + + // NonMaxSuppression produces triplets + // that have the following format: [batch_index, class_index, box_index] + PartialShape out_shape = {Dimension::dynamic(), 3}; + // set_output_type(0, m_output_type, out_shape); } From 66118f74535cc65e1ee8f4a2d4785ead093baa24 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 29 Sep 2020 11:43:06 +0300 Subject: [PATCH 007/173] Written the method validate_and_infer_type(). --- .../include/ngraph/op/non_max_suppression.hpp | 5 +- ngraph/core/src/op/non_max_suppression.cpp | 151 +++++++++++++++++- 2 files changed, 153 insertions(+), 3 deletions(-) diff --git a/ngraph/core/include/ngraph/op/non_max_suppression.hpp b/ngraph/core/include/ngraph/op/non_max_suppression.hpp index 27101831912078..41fbdb86cc0466 100644 --- a/ngraph/core/include/ngraph/op/non_max_suppression.hpp +++ b/ngraph/core/include/ngraph/op/non_max_suppression.hpp @@ -351,6 +351,7 @@ namespace ngraph const bool sort_result_descending = true, const ngraph::element::Type& output_type = ngraph::element::i64); + bool visit_attributes(AttributeVisitor& visitor) override; void validate_and_infer_types() override; std::shared_ptr @@ -378,8 +379,8 @@ namespace ngraph BoxEncodingType m_box_encoding = BoxEncodingType::CORNER; bool m_sort_result_descending = true; ngraph::element::Type m_output_type = ngraph::element::i64; - // void validate(); - // int64_t max_boxes_output_from_input() const; + void validate(); + int64_t max_boxes_output_from_input() const; }; } // namespace v5 } // namespace op diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 463567e6a2342b..b440a05f33b05e 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -673,6 +673,113 @@ shared_ptr m_output_type); } +void op::v5::NonMaxSuppression::validate() +{ + const auto boxes_ps = get_input_partial_shape(0); + const auto scores_ps = get_input_partial_shape(1); + + NODE_VALIDATION_CHECK(this, + m_output_type == element::i64 || m_output_type == element::i32, + "Output type must be i32 or i64"); + + if (boxes_ps.is_dynamic() || scores_ps.is_dynamic()) + { + return; + } + + NODE_VALIDATION_CHECK(this, + boxes_ps.rank().is_static() && boxes_ps.rank().get_length() == 3, + "Expected a 3D tensor for the 'boxes' input. Got: ", + boxes_ps); + + NODE_VALIDATION_CHECK(this, + scores_ps.rank().is_static() && scores_ps.rank().get_length() == 3, + "Expected a 3D tensor for the 'scores' input. Got: ", + scores_ps); + + if (inputs().size() >= 3) + { + const auto max_boxes_ps = get_input_partial_shape(2); + NODE_VALIDATION_CHECK(this, + max_boxes_ps.is_dynamic() || is_scalar(max_boxes_ps.to_shape()), + "Expected a scalar for the 'max_output_boxes_per_class' input. Got: ", + max_boxes_ps); + } + + if (inputs().size() >= 4) + { + const auto iou_threshold_ps = get_input_partial_shape(3); + NODE_VALIDATION_CHECK(this, + iou_threshold_ps.is_dynamic() || + is_scalar(iou_threshold_ps.to_shape()), + "Expected a scalar for the 'iou_threshold' input. Got: ", + iou_threshold_ps); + } + + if (inputs().size() >= 5) + { + const auto score_threshold_ps = get_input_partial_shape(4); + NODE_VALIDATION_CHECK(this, + score_threshold_ps.is_dynamic() || + is_scalar(score_threshold_ps.to_shape()), + "Expected a scalar for the 'score_threshold' input. Got: ", + score_threshold_ps); + } + + if (inputs().size() >= 6) + { + const auto soft_nms_sigma = get_input_partial_shape(5); + NODE_VALIDATION_CHECK(this, + soft_nms_sigma.is_dynamic() || + is_scalar(soft_nms_sigma.to_shape()), + "Expected a scalar for the 'soft_nms_sigma' input. Got: ", + soft_nms_sigma); + } + + const auto num_batches_boxes = boxes_ps[0]; + const auto num_batches_scores = scores_ps[0]; + NODE_VALIDATION_CHECK(this, + num_batches_boxes.same_scheme(num_batches_scores), + "The first dimension of both 'boxes' and 'scores' must match. Boxes: ", + num_batches_boxes, + "; Scores: ", + num_batches_scores); + + const auto num_boxes_boxes = boxes_ps[1]; + const auto num_boxes_scores = scores_ps[2]; + NODE_VALIDATION_CHECK(this, + num_boxes_boxes.same_scheme(num_boxes_scores), + "'boxes' and 'scores' input shapes must match at the second and third " + "dimension respectively. Boxes: ", + num_boxes_boxes, + "; Scores: ", + num_boxes_scores); + + NODE_VALIDATION_CHECK(this, + boxes_ps[2].is_static() && boxes_ps[2].get_length() == 4u, + "The last dimension of the 'boxes' input must be equal to 4. Got:", + boxes_ps[2]); +} + +int64_t op::v5::NonMaxSuppression::max_boxes_output_from_input() const +{ + int64_t max_output_boxes{0}; + + const auto max_output_boxes_input = + as_type_ptr(input_value(2).get_node_shared_ptr()); + max_output_boxes = max_output_boxes_input->cast_vector().at(0); + + return max_output_boxes; +} + +bool ngraph::op::v5::NonMaxSuppression::visit_attributes(AttributeVisitor& visitor) +{ + visitor.on_attribute("box_encoding", m_box_encoding); + visitor.on_attribute("sort_result_descending", m_sort_result_descending); + visitor.on_attribute("output_type", m_output_type); + return true; +} + void op::v5::NonMaxSuppression::validate_and_infer_types() { const auto boxes_ps = get_input_partial_shape(0); @@ -682,5 +789,47 @@ void op::v5::NonMaxSuppression::validate_and_infer_types() // that have the following format: [batch_index, class_index, box_index] PartialShape out_shape = {Dimension::dynamic(), 3}; - // set_output_type(0, m_output_type, out_shape); + validate() + + if (boxes_ps.rank().is_static() && scores_ps.rank().is_static()) + { + const auto num_boxes_boxes = boxes_ps[1]; + const auto max_output_boxes_per_class_node = input_value(2).get_node_shared_ptr(); + if (num_boxes_boxes.is_static() && scores_ps[0].is_static() && scores_ps[1].is_static() && + op::is_constant(max_output_boxes_per_class_node)) + { + const auto num_boxes = num_boxes_boxes.get_length(); + const auto num_classes = scores_ps[1].get_length(); + const auto max_output_boxes_per_class = max_boxes_output_from_input(); + + out_shape[0] = std::min(num_boxes, max_output_boxes_per_class) * num_classes * + scores_ps[0].get_length(); + } + } + set_output_type(0, m_output_type, out_shape); + set_output_type(1, element::f32, out_shape); + set_output_type(2, element::f32, Shape{}); } + +namespace ngraph +{ + template <> + EnumNames& + EnumNames::get() + { + static auto enum_names = EnumNames( + "op::v5::NonMaxSuppression::BoxEncodingType", + {{"corner", op::v5::NonMaxSuppression::BoxEncodingType::CORNER}, + {"center", op::v5::NonMaxSuppression::BoxEncodingType::CENTER}}); + return enum_names; + } + + constexpr DiscreteTypeInfo + AttributeAdapter::type_info; + + std::ostream& operator<<(std::ostream& s, + const op::v5::NonMaxSuppression::BoxEncodingType& type) + { + return s << as_string(type); + } +} // namespace ngraph From 709a12154e325f78c7200e86cd555cf84ff4e26d Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 29 Sep 2020 11:58:12 +0300 Subject: [PATCH 008/173] Fixed typo. --- ngraph/core/src/op/non_max_suppression.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index b440a05f33b05e..4203730e00d487 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -789,7 +789,7 @@ void op::v5::NonMaxSuppression::validate_and_infer_types() // that have the following format: [batch_index, class_index, box_index] PartialShape out_shape = {Dimension::dynamic(), 3}; - validate() + validate(); if (boxes_ps.rank().is_static() && scores_ps.rank().is_static()) { From 2983f6b70ac44b10c3ee98b5b91612eed769e29d Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 29 Sep 2020 12:10:59 +0300 Subject: [PATCH 009/173] Fixed opset5_tbl.hpp. --- ngraph/core/include/ngraph/opsets/opset5_tbl.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/ngraph/core/include/ngraph/opsets/opset5_tbl.hpp b/ngraph/core/include/ngraph/opsets/opset5_tbl.hpp index c7c293a00a5b4f..398ae8c2b548d0 100644 --- a/ngraph/core/include/ngraph/opsets/opset5_tbl.hpp +++ b/ngraph/core/include/ngraph/opsets/opset5_tbl.hpp @@ -157,7 +157,6 @@ NGRAPH_OP(CTCLoss, ngraph::op::v4) NGRAPH_OP(HSwish, ngraph::op::v4) NGRAPH_OP(Interpolate, ngraph::op::v4) NGRAPH_OP(Mish, ngraph::op::v4) -NGRAPH_OP(NonMaxSuppression, ngraph::op::v4) NGRAPH_OP(ReduceL1, ngraph::op::v4) NGRAPH_OP(ReduceL2, ngraph::op::v4) NGRAPH_OP(SoftPlus, ngraph::op::v4) From 64ce75aebe68aada164b76322a1b6ba7b14b0932 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 29 Sep 2020 16:18:06 +0300 Subject: [PATCH 010/173] Some code style fixes. --- ngraph/core/src/op/non_max_suppression.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 4203730e00d487..2a9ad1040d0222 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -640,7 +640,6 @@ op::v5::NonMaxSuppression::NonMaxSuppression( constructor_validate_and_infer_types(); } - shared_ptr op::v5::NonMaxSuppression::clone_with_new_inputs(const OutputVector& new_args) const { @@ -730,8 +729,7 @@ void op::v5::NonMaxSuppression::validate() { const auto soft_nms_sigma = get_input_partial_shape(5); NODE_VALIDATION_CHECK(this, - soft_nms_sigma.is_dynamic() || - is_scalar(soft_nms_sigma.to_shape()), + soft_nms_sigma.is_dynamic() || is_scalar(soft_nms_sigma.to_shape()), "Expected a scalar for the 'soft_nms_sigma' input. Got: ", soft_nms_sigma); } From 88081f110f5bf12f705c02e96fe5699d8dc94744 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 29 Sep 2020 16:40:30 +0300 Subject: [PATCH 011/173] Started to write the method v5::NonMaxSuppression::evaluate(). --- .../include/ngraph/op/non_max_suppression.hpp | 2 ++ ngraph/core/src/op/non_max_suppression.cpp | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/ngraph/core/include/ngraph/op/non_max_suppression.hpp b/ngraph/core/include/ngraph/op/non_max_suppression.hpp index 41fbdb86cc0466..5f080503033bdb 100644 --- a/ngraph/core/include/ngraph/op/non_max_suppression.hpp +++ b/ngraph/core/include/ngraph/op/non_max_suppression.hpp @@ -356,6 +356,8 @@ namespace ngraph std::shared_ptr clone_with_new_inputs(const OutputVector& new_args) const override; + bool evaluate(const HostTensorVector& outputs, + const HostTensorVector& inputs) const override; BoxEncodingType get_box_encoding() const { return m_box_encoding; } void set_box_encoding(const BoxEncodingType box_encoding) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 2a9ad1040d0222..49e15bc52e8900 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -831,3 +831,20 @@ namespace ngraph return s << as_string(type); } } // namespace ngraph + +bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, + const HostTensorVector& inputs) const +{ + element::Type input_et = get_input_element_type(0); + + switch (input_et) + { + case element::Type_t::f32: + break; + case element::Type_t::f16: + break; + default:; + } + + return true; +} From 9fcc84254d4810ebd046301bb97d92d5aa96c1ce Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 29 Sep 2020 17:47:27 +0300 Subject: [PATCH 012/173] Started to write nGraph reference implementation of NMS-5. --- .../include/ngraph/op/non_max_suppression.hpp | 3 ++ .../runtime/reference/non_max_suppression.hpp | 53 +++++++++++++++++++ ngraph/core/src/op/non_max_suppression.cpp | 43 ++++++++++++++- 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp diff --git a/ngraph/core/include/ngraph/op/non_max_suppression.hpp b/ngraph/core/include/ngraph/op/non_max_suppression.hpp index 5f080503033bdb..5ba5b63b52a8a1 100644 --- a/ngraph/core/include/ngraph/op/non_max_suppression.hpp +++ b/ngraph/core/include/ngraph/op/non_max_suppression.hpp @@ -383,6 +383,9 @@ namespace ngraph ngraph::element::Type m_output_type = ngraph::element::i64; void validate(); int64_t max_boxes_output_from_input() const; + float iou_threshold_from_input() const; + float score_threshold_from_input() const; + float soft_nms_sigma_from_input() const; }; } // namespace v5 } // namespace op diff --git a/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp b/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp new file mode 100644 index 00000000000000..fcdd34713c5640 --- /dev/null +++ b/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp @@ -0,0 +1,53 @@ +//***************************************************************************** +// Copyright 2017-2020 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//***************************************************************************** + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "ngraph/coordinate_transform.hpp" +#include "ngraph/shape_util.hpp" + +namespace ngraph +{ + namespace runtime + { + namespace reference + { + template + void non_max_suppression(const float* boxes_data, + const Shape& boxes_data_shape, + const float* scores_data, + const Shape& scores_data_shape, + int64_t max_output_boxes_per_class, + float iou_threshold, + float score_threshold, + float soft_nms_sigma, + T* selected_indices, + const Shape& selected_indices_shape, + float* selected_scores, + const Shape& selected_scores_shape, + float* valid_outputs) + { + } + } + } +} diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 49e15bc52e8900..9a45acf4b6bcfd 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -650,7 +650,7 @@ shared_ptr const auto& arg2 = new_args.size() > 2 ? new_args.at(2) - : ngraph::op::Constant::create(element::i32, Shape{}, {0}); + : ngraph::op::Constant::create(element::i64, Shape{}, {0}); const auto& arg3 = new_args.size() > 3 ? new_args.at(3) : ngraph::op::Constant::create(element::f32, Shape{}, {.0f}); @@ -770,6 +770,43 @@ int64_t op::v5::NonMaxSuppression::max_boxes_output_from_input() const return max_output_boxes; } +static constexpr size_t iou_threshold_port = 3; +static constexpr size_t score_threshold_port = 4; +static constexpr size_t soft_nms_sigma_port = 5; + +float op::v5::NonMaxSuppression::iou_threshold_from_input() const +{ + float iou_threshold = 0.0f; + + const auto iou_threshold_input = + as_type_ptr(input_value(iou_threshold_port).get_node_shared_ptr()); + iou_threshold = iou_threshold_input->cast_vector().at(0); + + return iou_threshold; +} + +float op::v5::NonMaxSuppression::score_threshold_from_input() const +{ + float score_threshold = 0.0f; + + const auto score_threshold_input = + as_type_ptr(input_value(score_threshold_port).get_node_shared_ptr()); + score_threshold = score_threshold_input->cast_vector().at(0); + + return score_threshold; +} + +float op::v5::NonMaxSuppression::soft_nms_sigma_from_input() const +{ + float soft_nms_sigma = 0.0f; + + const auto soft_nms_sigma_input = + as_type_ptr(input_value(soft_nms_sigma_port).get_node_shared_ptr()); + soft_nms_sigma = soft_nms_sigma_input->cast_vector().at(0); + + return soft_nms_sigma; +} + bool ngraph::op::v5::NonMaxSuppression::visit_attributes(AttributeVisitor& visitor) { visitor.on_attribute("box_encoding", m_box_encoding); @@ -836,6 +873,10 @@ bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, const HostTensorVector& inputs) const { element::Type input_et = get_input_element_type(0); + int64_t max_output_boxes_per_class = max_boxes_output_from_input(); + float iou_threshold = iou_threshold_from_input(); + float score_threshold = score_threshold_from_input(); + float soft_nms_sigma = soft_nms_sigma_from_input(); switch (input_et) { From e610ce1044338f9f93421f8d9b720dec3114f5eb Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 30 Sep 2020 11:12:04 +0300 Subject: [PATCH 013/173] Some additions. --- .../runtime/reference/non_max_suppression.hpp | 8 +-- ngraph/core/src/op/non_max_suppression.cpp | 62 +++++++++++++++++-- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp b/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp index fcdd34713c5640..6399b574e57d57 100644 --- a/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp +++ b/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp @@ -24,6 +24,7 @@ #include #include #include "ngraph/coordinate_transform.hpp" +#include "ngraph/op/util/op_types.hpp" #include "ngraph/shape_util.hpp" namespace ngraph @@ -32,7 +33,6 @@ namespace ngraph { namespace reference { - template void non_max_suppression(const float* boxes_data, const Shape& boxes_data_shape, const float* scores_data, @@ -41,13 +41,11 @@ namespace ngraph float iou_threshold, float score_threshold, float soft_nms_sigma, - T* selected_indices, + int64_t* selected_indices, const Shape& selected_indices_shape, float* selected_scores, const Shape& selected_scores_shape, - float* valid_outputs) - { - } + float* valid_outputs); } } } diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 9a45acf4b6bcfd..9694aad2c3f417 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -18,6 +18,7 @@ #include "ngraph/attribute_visitor.hpp" #include "ngraph/op/constant.hpp" #include "ngraph/op/util/op_types.hpp" +#include "ngraph/type/float16.hpp" using namespace std; using namespace ngraph; @@ -869,23 +870,72 @@ namespace ngraph } } // namespace ngraph +static PartialShape infer_selected_indices_shape(const HostTensorVector& inputs, + int64_t max_output_boxes_per_class) +{ + const auto boxes_ps = inputs[0]->get_partial_shape(); + const auto scores_ps = inputs[1]->get_partial_shape(); + + // NonMaxSuppression produces triplets + // that have the following format: [batch_index, class_index, box_index] + PartialShape result = {Dimension::dynamic(), 3}; + + if (boxes_ps.rank().is_static() && scores_ps.rank().is_static()) + { + const auto num_boxes_boxes = boxes_ps[1]; + const auto max_output_boxes_per_class_node = input_value(2).get_node_shared_ptr(); + if (num_boxes_boxes.is_static() && scores_ps[0].is_static() && scores_ps[1].is_static()) + { + const auto num_boxes = num_boxes_boxes.get_length(); + const auto num_classes = scores_ps[1].get_length(); + + result[0] = std::min(num_boxes, max_output_boxes_per_class) * num_classes * + scores_ps[0].get_length(); + } + } + + return result; +} + +static std::vector prepare_boxes_data(const HostTensorPtr& boxes, const Shape& boxes_shape) +{ + std::vector result; + element::Type boxes_input_et = boxes->get_element_type(); + + return result; +} + bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, const HostTensorVector& inputs) const { element::Type input_et = get_input_element_type(0); + int64_t max_output_boxes_per_class = max_boxes_output_from_input(); float iou_threshold = iou_threshold_from_input(); float score_threshold = score_threshold_from_input(); float soft_nms_sigma = soft_nms_sigma_from_input(); - switch (input_et) + auto selected_indices_shape = infer_selected_indices_shape(inputs, max_output_boxes_per_class); + Shape out_shape = selected_indices_shape.to_shape(); + + outputs[0]->set_element_type(m_output_type); + outputs[0]->set_shape(out_shape); + + size_t num_of_outputs = outputs.size(); + + if (num_of_outputs >= 2) { - case element::Type_t::f32: - break; - case element::Type_t::f16: - break; - default:; + outputs[1]->set_element_type(element::f32); + outputs[1]->set_shape(out_shape); } + if (num_of_outputs >= 3) + { + outputs[2]->set_element_type(element::f32); + outputs[2]->set_shape(Shape{}); + } + + Shape boxes_shape = outputs[0]->get_shape(); + return true; } From 8461e5724d6973ffc4800282b2aba2607bc0f6b2 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 30 Sep 2020 14:01:14 +0300 Subject: [PATCH 014/173] Written preprocessing of boxes data. --- ngraph/core/src/op/non_max_suppression.cpp | 111 +++++++++++++++++++-- 1 file changed, 105 insertions(+), 6 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 9694aad2c3f417..11f4cbf40ee561 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -14,6 +14,7 @@ // limitations under the License. //***************************************************************************** +#include #include "ngraph/op/non_max_suppression.hpp" #include "ngraph/attribute_visitor.hpp" #include "ngraph/op/constant.hpp" @@ -771,6 +772,7 @@ int64_t op::v5::NonMaxSuppression::max_boxes_output_from_input() const return max_output_boxes; } +static constexpr size_t boxes_port = 0; static constexpr size_t iou_threshold_port = 3; static constexpr size_t score_threshold_port = 4; static constexpr size_t soft_nms_sigma_port = 5; @@ -873,7 +875,7 @@ namespace ngraph static PartialShape infer_selected_indices_shape(const HostTensorVector& inputs, int64_t max_output_boxes_per_class) { - const auto boxes_ps = inputs[0]->get_partial_shape(); + const auto boxes_ps = inputs[boxes_port]->get_partial_shape(); const auto scores_ps = inputs[1]->get_partial_shape(); // NonMaxSuppression produces triplets @@ -883,7 +885,6 @@ static PartialShape infer_selected_indices_shape(const HostTensorVector& inputs, if (boxes_ps.rank().is_static() && scores_ps.rank().is_static()) { const auto num_boxes_boxes = boxes_ps[1]; - const auto max_output_boxes_per_class_node = input_value(2).get_node_shared_ptr(); if (num_boxes_boxes.is_static() && scores_ps[0].is_static() && scores_ps[1].is_static()) { const auto num_boxes = num_boxes_boxes.get_length(); @@ -897,19 +898,115 @@ static PartialShape infer_selected_indices_shape(const HostTensorVector& inputs, return result; } -static std::vector prepare_boxes_data(const HostTensorPtr& boxes, const Shape& boxes_shape) +using V5BoxEncoding = op::v5::NonMaxSuppression::BoxEncodingType; + +static void normalize_corner(float* boxes, const Shape& boxes_shape) +{ + size_t num_batches = boxes_shape[0]; + size_t num_boxes = boxes_shape[1]; + + float* box_ptr = boxes; + + for (size_t batch = 0; batch < num_batches; ++batch) + { + for (size_t box = 0; box < num_boxes; ++num_boxes) + { + float y1 = box_ptr[0]; + float x1 = box_ptr[1]; + float y2 = box_ptr[2]; + float x2 = box_ptr[3]; + + float ymin = std::min(y1, y2); + float ymax = std::max(y1, y2); + float xmin = std::min(x1, x2); + float xmax = std::max(x1, x2); + + box_ptr[0] = ymin; + box_ptr[1] = xmin; + box_ptr[2] = ymax; + box_ptr[3] = xmax; + + box_ptr += 4; + } + } +} + +static void normalize_center(float* boxes, const Shape& boxes_shape) +{ + size_t num_batches = boxes_shape[0]; + size_t num_boxes = boxes_shape[1]; + + float* box_ptr = boxes; + + for (size_t batch = 0; batch < num_batches; ++batch) + { + for (size_t box = 0; box < num_boxes; ++num_boxes) + { + float x_center = box_ptr[0]; + float y_center = box_ptr[1]; + float width = box_ptr[2]; + float height = box_ptr[3]; + + float y1 = y_center - height / 2.0; + float x1 = x_center - width / 2.0; + float y2 = y_center + height / 2.0; + float x2 = x_center + width / 2.0;; + + box_ptr[0] = y1; + box_ptr[1] = x1; + box_ptr[2] = y2; + box_ptr[3] = x2; + + box_ptr += 4; + } + } +} + +static void normalize_box_encoding(float* boxes, + const Shape& boxes_shape, + const V5BoxEncoding box_encoding) +{ + if (box_encoding == V5BoxEncoding::CORNER) + { + normalize_corner(boxes, boxes_shape); + } + else + { + normalize_center(boxes, boxes_shape); + } +} + +static std::vector prepare_boxes_data(const HostTensorPtr& boxes, + const Shape& boxes_shape, + const V5BoxEncoding box_encoding) { - std::vector result; element::Type boxes_input_et = boxes->get_element_type(); + size_t boxes_size = shape_size(boxes_shape); + std::vector result(boxes_size); + + if (boxes_input_et == element::Type_t::f32) + { + float* boxes_ptr = boxes->get_data_ptr(); + memcpy(result.data(), boxes_ptr, boxes_size * sizeof(float)); + } + else + { + float16* boxes_ptr = boxes->get_data_ptr(); + for (size_t i = 0; i < boxes_size; ++i) + { + result[i] = float(boxes_ptr[i]); + } + } + + normalize_box_encoding(result.data(), boxes_shape, box_encoding); + return result; } bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, const HostTensorVector& inputs) const { - element::Type input_et = get_input_element_type(0); - int64_t max_output_boxes_per_class = max_boxes_output_from_input(); float iou_threshold = iou_threshold_from_input(); float score_threshold = score_threshold_from_input(); @@ -937,5 +1034,7 @@ bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, Shape boxes_shape = outputs[0]->get_shape(); + auto boxes_data = prepare_boxes_data(inputs[boxes_port], boxes_shape, m_box_encoding); + return true; } From 06b2edda74f910fb260ec15a4cede60be7a7a0a7 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 30 Sep 2020 14:47:30 +0300 Subject: [PATCH 015/173] Started to write the function non_max_suppression() that calculates NonMaxSuppression-5 operation. --- .../runtime/reference/non_max_suppression.hpp | 2 +- .../runtime/reference/non_max_suppression.cpp | 37 ++++++++++++++ ngraph/core/src/op/non_max_suppression.cpp | 51 ++++++++++++++++++- 3 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp diff --git a/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp b/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp index 6399b574e57d57..9f76239f08753c 100644 --- a/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp +++ b/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp @@ -45,7 +45,7 @@ namespace ngraph const Shape& selected_indices_shape, float* selected_scores, const Shape& selected_scores_shape, - float* valid_outputs); + int64_t* valid_outputs); } } } diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp new file mode 100644 index 00000000000000..249b46825085cd --- /dev/null +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -0,0 +1,37 @@ +//***************************************************************************** +// Copyright 2017-2020 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//***************************************************************************** + + +#include "ngraph/op/non_max_suppression.hpp" +#include "ngraph/runtime/reference/non_max_suppression.hpp" + +using namespace ngraph::runtime::reference; + +void non_max_suppression(const float* boxes_data, + const Shape& boxes_data_shape, + const float* scores_data, + const Shape& scores_data_shape, + int64_t max_output_boxes_per_class, + float iou_threshold, + float score_threshold, + float soft_nms_sigma, + int64_t* selected_indices, + const Shape& selected_indices_shape, + float* selected_scores, + const Shape& selected_scores_shape, + int64_t* valid_outputs) +{ +} diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 11f4cbf40ee561..80d1eedb7a679c 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -19,6 +19,7 @@ #include "ngraph/attribute_visitor.hpp" #include "ngraph/op/constant.hpp" #include "ngraph/op/util/op_types.hpp" +#include "ngraph/runtime/reference/non_max_suppression.hpp" #include "ngraph/type/float16.hpp" using namespace std; @@ -773,6 +774,7 @@ int64_t op::v5::NonMaxSuppression::max_boxes_output_from_input() const } static constexpr size_t boxes_port = 0; +static constexpr size_t scores_port = 1; static constexpr size_t iou_threshold_port = 3; static constexpr size_t score_threshold_port = 4; static constexpr size_t soft_nms_sigma_port = 5; @@ -985,7 +987,7 @@ static std::vector prepare_boxes_data(const HostTensorPtr& boxes, size_t boxes_size = shape_size(boxes_shape); std::vector result(boxes_size); - if (boxes_input_et == element::Type_t::f32) + if (boxes_input_et == ngraph::element::f32) { float* boxes_ptr = boxes->get_data_ptr(); memcpy(result.data(), boxes_ptr, boxes_size * sizeof(float)); @@ -1004,6 +1006,31 @@ static std::vector prepare_boxes_data(const HostTensorPtr& boxes, return result; } +static std::vector prepare_scores_data(const HostTensorPtr& scores, + const Shape& scores_shape) +{ + element::Type scores_input_et = scores->get_element_type(); + + size_t scores_size = shape_size(scores_shape); + std::vector result(scores_size); + + if (scores_input_et == ngraph::element::f32) + { + float* scores_ptr = scores->get_data_ptr(); + memcpy(result.data(), scores_ptr, scores_size * sizeof(float)); + } + else + { + float16* scores_ptr = scores->get_data_ptr(); + for (size_t i = 0; i < scores_size; ++i) + { + result[i] = float(scores_ptr[i]); + } + } + + return result; +} + bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, const HostTensorVector& inputs) const { @@ -1032,9 +1059,29 @@ bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, outputs[2]->set_shape(Shape{}); } - Shape boxes_shape = outputs[0]->get_shape(); + Shape boxes_shape = inputs[boxes_port]->get_shape(); + Shape scores_shape = inputs[scores_port]->get_shape(); auto boxes_data = prepare_boxes_data(inputs[boxes_port], boxes_shape, m_box_encoding); + auto scores_data = prepare_scores_data(inputs[scores_port], scores_shape); + + std::vector selected_indices(out_shape); + std::vector selected_scores(out_shape); + int64_t valid_outputs = 0; + + runtime::reference::non_max_suppression(boxes_data, + boxes_shape, + scores_data, + scores_shape, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + selected_indices.data(), + out_shape, + selected_scores.data(), + out_shape, + &valid_outputs); return true; } From 06da1dd86bffc73807527416de03fd028f6b239f Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 30 Sep 2020 15:14:57 +0300 Subject: [PATCH 016/173] Written postprocessing of the evaluate(). --- ngraph/core/src/op/non_max_suppression.cpp | 60 +++++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 80d1eedb7a679c..6389a3344b4bf8 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -1031,6 +1031,54 @@ static std::vector prepare_scores_data(const HostTensorPtr& scores, return result; } +static void evaluate_postprocessing(const HostTensorVector& outputs, + const ngraph::element::Type output_type, + const std::vector& selected_indices, + const std::vector& selected_scores, + int64_t valid_outputs) +{ + size_t num_of_outputs = outputs.size(); + + if (output_type == ngraph::element::i64) + { + int64_t* indices_ptr = outputs[0]->get_data_ptr(); + memcpy(indices_ptr, selected_indices.data(), selected_indices.size() * sizeof(int64_t)); + } + else + { + int32_t* indices_ptr = outputs[0]->get_data_ptr(); + for (int64_t idx : selected_indices) + { + *indices_ptr = static_cast(idx); + indices_ptr++; + } + } + + if (num_of_outputs < 2) + { + return; + } + + float* scores_ptr = outputs[1]->get_data_ptr(); + memcpy(scores_ptr, selected_scores.data(), selected_scores.size() * sizeof(float)); + + if (num_of_outputs < 3) + { + return; + } + + if (output_type == ngraph::element::i64) + { + int64_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); + *valid_outputs_ptr = valid_outputs; + } + else + { + int32_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); + *valid_outputs_ptr = static_cast(valid_outputs); + } +} + bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, const HostTensorVector& inputs) const { @@ -1065,8 +1113,10 @@ bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, auto boxes_data = prepare_boxes_data(inputs[boxes_port], boxes_shape, m_box_encoding); auto scores_data = prepare_scores_data(inputs[scores_port], scores_shape); - std::vector selected_indices(out_shape); - std::vector selected_scores(out_shape); + size_t out_shape_size = shape_size(out_shape); + + std::vector selected_indices(out_shape_size); + std::vector selected_scores(out_shape_size); int64_t valid_outputs = 0; runtime::reference::non_max_suppression(boxes_data, @@ -1083,5 +1133,11 @@ bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, out_shape, &valid_outputs); + evaluate_postprocessing(outputs, + m_output_type, + selected_indices, + selected_scores, + valid_outputs); + return true; } From 0b6ca39a06de3aa97bce4ff37d54be071ff7bda3 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 30 Sep 2020 15:27:50 +0300 Subject: [PATCH 017/173] Small fixes. --- ngraph/core/src/op/non_max_suppression.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 6389a3344b4bf8..b1363822e69cb1 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -1119,9 +1119,9 @@ bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, std::vector selected_scores(out_shape_size); int64_t valid_outputs = 0; - runtime::reference::non_max_suppression(boxes_data, + runtime::reference::non_max_suppression(boxes_data.data(), boxes_shape, - scores_data, + scores_data.data(), scores_shape, max_output_boxes_per_class, iou_threshold, From 78b946d2a1a0cee7640fe91d3f377f406ed1c4e3 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 30 Sep 2020 15:41:19 +0300 Subject: [PATCH 018/173] Small fix. --- .../core/reference/src/runtime/reference/non_max_suppression.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index 249b46825085cd..54ab43892462ac 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -17,6 +17,7 @@ #include "ngraph/op/non_max_suppression.hpp" #include "ngraph/runtime/reference/non_max_suppression.hpp" +#include "ngraph/shape_util.hpp" using namespace ngraph::runtime::reference; From 14db60daf34aeca96f7f384893a3e752a6fa6305 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 30 Sep 2020 15:53:53 +0300 Subject: [PATCH 019/173] Added include for ngraph/shape.hpp. --- .../reference/src/runtime/reference/non_max_suppression.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index 54ab43892462ac..a0aa13880d677b 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -17,8 +17,9 @@ #include "ngraph/op/non_max_suppression.hpp" #include "ngraph/runtime/reference/non_max_suppression.hpp" -#include "ngraph/shape_util.hpp" +#include "ngraph/shape.hpp" +using namespace ngraph; using namespace ngraph::runtime::reference; void non_max_suppression(const float* boxes_data, From 951815aaf029c7c6e06ceef791f1aa549985b96e Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 1 Oct 2020 10:25:07 +0300 Subject: [PATCH 020/173] Written the function intersectionOverUnion. --- .../runtime/reference/non_max_suppression.cpp | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index a0aa13880d677b..8a0542b0c8c3ae 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -15,6 +15,10 @@ //***************************************************************************** +#include +#include +#include +#include #include "ngraph/op/non_max_suppression.hpp" #include "ngraph/runtime/reference/non_max_suppression.hpp" #include "ngraph/shape.hpp" @@ -22,6 +26,46 @@ using namespace ngraph; using namespace ngraph::runtime::reference; +struct Rectangle +{ + float y1; + float x1; + float y2; + float x2; +}; + +static float intersectionOverUnion(float* boxesI, float* boxesJ) +{ + Rectangle boxI = *(reinterpret_cast(boxesI)); + Rectangle boxJ = *(reinterpret_cast(boxesJ)); + + float areaI = (boxI.y2 - boxI.y1) * (boxI.x2 - boxI.x1); + float areaJ = (boxJ.y2 - boxJ.y1) * (boxJ.x2 - boxJ.x1); + + if (areaI <= 0.0f || areaJ <= 0.0f) + { + return 0.0f; + } + + float intersection_ymin = std::max(boxI.y1, boxJ.y1); + float intersection_xmin = std::max(boxI.x1, boxJ.x1); + float intersection_ymax = std::min(boxI.y2, boxJ.y2); + float intersection_xmax = std::min(boxI.x2, boxJ.x2); + + float intersection_area = std::max(intersection_ymax - intersection_ymin, 0.0f) * + std::max(intersection_xmax - intersection_xmin, 0.0f); + + return intersection_area / (areaI + areaJ - intersection_area); +} + +struct FilteredBoxes +{ + float score; + int batch_index; + int class_index; + int box_index; +}; + void non_max_suppression(const float* boxes_data, const Shape& boxes_data_shape, const float* scores_data, @@ -36,4 +80,27 @@ void non_max_suppression(const float* boxes_data, const Shape& selected_scores_shape, int64_t* valid_outputs) { + float scale = 0.0f; + if (soft_nms_sigma > 0.0f) { + scale = - 0.5f / soft_nms_sigma; + } + + auto func = [iou_threshold, scale](float iou) { + const float weight = std::exp(scale * iou * iou))); + return iou <= iou_threshold ? weight : 0.0f; + }; + + // boxes shape: {num_batches, num_boxes, 4} + // scores shape: {num_batches, num_classes, num_boxes} + size_t num_batches = scores_data_shape[0]; + size_t num_classes = scores_data_shape[1]; + + std::vector fb; + + for (int batch = 0; batch < num_batches; batch++) + { + for (int class_idx = 0; class_idx < num_classes; class_idx++) + { + } + } } From 8dd9b5610cf0c6caa15b08fb3a8feee8b637cfb7 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 1 Oct 2020 10:44:07 +0300 Subject: [PATCH 021/173] Some additions. --- .../runtime/reference/non_max_suppression.cpp | 73 +++++++++++-------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index 8a0542b0c8c3ae..33f20ab177c60b 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -66,41 +66,50 @@ struct FilteredBoxes int box_index; }; -void non_max_suppression(const float* boxes_data, - const Shape& boxes_data_shape, - const float* scores_data, - const Shape& scores_data_shape, - int64_t max_output_boxes_per_class, - float iou_threshold, - float score_threshold, - float soft_nms_sigma, - int64_t* selected_indices, - const Shape& selected_indices_shape, - float* selected_scores, - const Shape& selected_scores_shape, - int64_t* valid_outputs) +namespace ngraph { - float scale = 0.0f; - if (soft_nms_sigma > 0.0f) { - scale = - 0.5f / soft_nms_sigma; - } - - auto func = [iou_threshold, scale](float iou) { - const float weight = std::exp(scale * iou * iou))); - return iou <= iou_threshold ? weight : 0.0f; - }; - - // boxes shape: {num_batches, num_boxes, 4} - // scores shape: {num_batches, num_classes, num_boxes} - size_t num_batches = scores_data_shape[0]; - size_t num_classes = scores_data_shape[1]; - - std::vector fb; - - for (int batch = 0; batch < num_batches; batch++) + namespace runtime { - for (int class_idx = 0; class_idx < num_classes; class_idx++) + namespace reference { + void non_max_suppression(const float* boxes_data, + const Shape& boxes_data_shape, + const float* scores_data, + const Shape& scores_data_shape, + int64_t max_output_boxes_per_class, + float iou_threshold, + float score_threshold, + float soft_nms_sigma, + int64_t* selected_indices, + const Shape& selected_indices_shape, + float* selected_scores, + const Shape& selected_scores_shape, + int64_t* valid_outputs) + { + float scale = 0.0f; + if (soft_nms_sigma > 0.0f) { + scale = - 0.5f / soft_nms_sigma; + } + + auto func = [iou_threshold, scale](float iou) { + const float weight = std::exp(scale * iou * iou))); + return iou <= iou_threshold ? weight : 0.0f; + }; + + // boxes shape: {num_batches, num_boxes, 4} + // scores shape: {num_batches, num_classes, num_boxes} + size_t num_batches = scores_data_shape[0]; + size_t num_classes = scores_data_shape[1]; + + std::vector fb; + + for (int batch = 0; batch < num_batches; batch++) + { + for (int class_idx = 0; class_idx < num_classes; class_idx++) + { + } + } + } } } } From 28076b47f67341f639f11b33a47dcfb281238316 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 1 Oct 2020 10:53:57 +0300 Subject: [PATCH 022/173] Small fix. --- .../reference/src/runtime/reference/non_max_suppression.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index 33f20ab177c60b..50749dbc9b004e 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -92,7 +92,7 @@ namespace ngraph } auto func = [iou_threshold, scale](float iou) { - const float weight = std::exp(scale * iou * iou))); + const float weight = std::exp(scale * iou * iou); return iou <= iou_threshold ? weight : 0.0f; }; From f13d8b279e87a0d163988c925fe67927df93f135 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 1 Oct 2020 16:23:39 +0300 Subject: [PATCH 023/173] Continued to write the function non_max_suppression(). --- .../runtime/reference/non_max_suppression.cpp | 167 ++++++++++++++++-- 1 file changed, 150 insertions(+), 17 deletions(-) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index 50749dbc9b004e..aa982c804986c9 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -16,9 +16,10 @@ #include +#include #include -#include #include +#include #include "ngraph/op/non_max_suppression.hpp" #include "ngraph/runtime/reference/non_max_suppression.hpp" #include "ngraph/shape.hpp" @@ -28,17 +29,24 @@ using namespace ngraph::runtime::reference; struct Rectangle { - float y1; - float x1; - float y2; - float x2; + Rectangle(float y_left, float x_left, float y_right, float x_right) + : y1{y_left} + , x1{x_left} + , y2{y_right} + , x2{x_right} + { + } + + Rectangle() = default; + + float y1 = 0.0f; + float x1 = 0.0f; + float y2 = 0.f; + float x2 = 0.0f; }; -static float intersectionOverUnion(float* boxesI, float* boxesJ) +static float intersectionOverUnion(const Rectangle& boxI, const Rectangle& boxJ) { - Rectangle boxI = *(reinterpret_cast(boxesI)); - Rectangle boxJ = *(reinterpret_cast(boxesJ)); - float areaI = (boxI.y2 - boxI.y1) * (boxI.x2 - boxI.x1); float areaJ = (boxJ.y2 - boxJ.y1) * (boxJ.x2 - boxJ.x1); @@ -58,12 +66,61 @@ static float intersectionOverUnion(float* boxesI, float* boxesJ) return intersection_area / (areaI + areaJ - intersection_area); } -struct FilteredBoxes +struct SelectedIndex +{ + SelectedIndex(int64_t batch_idx, int64_t class_idx, int64_t box_idx) + : batch_index(batch_idx) + , class_index(class_idx) + , box_index(box_idx) + { + } + + SelectedIndex() = default; + + int64_t batch_index = 0; + int64_t class_index = 0; + int64_t box_index = 0; +}; + +struct SelectedScore +{ + SelectedScore(float batch_idx, float class_idx, float score) + : batch_index{batch_idx} + , class_index{class_idx} + , box_score{score} + { + } + + SelectedScore() = default; + + float batch_index = 0.0f; + float class_index = 0.0f; + float box_score = 0.0f; +}; + +struct BoxInfo { - float score; - int batch_index; - int class_index; - int box_index; + BoxInfo(const Rectangle& r, int64_t idx, float sc, int64_t suppress_idx) + : box{r} + , index{idx} + , score{sc} + , suppress_begin_index{suppress_idx} + { + area = (box.y2 - box.y1) * (box.x2 - box.x1); + } + + BoxInfo() = default; + + inline bool operator<(const BoxInfo& rhs) const + { + return score < rhs.score || (score == rhs.score && index > rhs.index); + } + + Rectangle box; + int64_t index = 0; + float score = 0.0f; + int64_t suppress_begin_index = 0; + float area = 0.0f; }; namespace ngraph @@ -100,13 +157,89 @@ namespace ngraph // scores shape: {num_batches, num_classes, num_boxes} size_t num_batches = scores_data_shape[0]; size_t num_classes = scores_data_shape[1]; + size_t num_boxes = boxes_data_shape[1]; + + SelectedIndex* selected_indices_ptr = + reinterpret_cast(selected_indices); + SelectedScore* selected_scores_ptr = + reinterpret_cast(selected_scores); + + size_t boxesStrides = num_boxes * 4; + std::array scoresStrides = {num_classes * num_boxes, num_boxes}; - std::vector fb; + size_t boxes_per_class = static_cast(max_output_boxes_per_class); - for (int batch = 0; batch < num_batches; batch++) + for (size_t batch = 0; batch < num_batches; batch++) { - for (int class_idx = 0; class_idx < num_classes; class_idx++) + float* boxesPtr = boxes + batch * boxesStrides; + for (size_t class_idx = 0; class_idx < num_classes; class_idx++) { + float* scoresPtr = scores + batch * scoresStrides[0] + + class_idx * scoresStrides[1]; + std::vector candidate_boxes; + candidate_boxes.reserve(num_boxes); + + for (size_t box_idx = 0; box_idx < num_boxes; box_idx++) + { + if (scoresPtr[box_idx] > score_threshold) + { + Rectangle* r = reinterpret_cast(boxesPtr) + box_idx; + candidate_boxes.emplace_back(*r, box_idx, scoresPtr[box_idx], 0); + } + } + + std::priority_queue sorted_boxes(std::less(), + std::move(candidate_boxes)); + + std::vector selected; + // Get the next box with top score, filter by iou_threshold + + BoxInfo next_candidate; + float original_score; + + while (!sorted_boxes.empty() && selected.size() < boxes_per_class) + { + next_candidate = sorted_boxes.top(); + original_score = next_candidate.score; + sorted_boxes.pop(); + + bool should_hard_suppress = false; + for (int64_t j = static_cast(selected.size()) - 1; + j >= next_candidate.suppress_begin_index; --j) + { + float iou = intersectionOverUnion(next_candidate.box, selected[j].box); + next_candidate.score *= func(iou); + + if (iou >= iou_threshold) + { + should_hard_suppress = true + break; + } + + if (next_candidate.score <= score_threshold) + { + break; + } + } + + next_candidate.suppress_begin_index = selected.size(); + + if (!should_hard_suppress) { + if (next_candidate.score == original_score) { + // // Suppression has not occurred, so select next_candidate + // selected.push_back(next_candidate.box_index); + // selected_scores.push_back(next_candidate.score); + // continue; + } + if (next_candidate.score > score_threshold) { + // Soft suppression has occurred and current score is still greater than + // score_threshold; add next_candidate back onto priority queue. + candidate_priority_queue.push(next_candidate); + } + } + } + + } } } From 8287813f4f2b5ccfe929652ec8b668c31b5e428d Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 1 Oct 2020 16:35:29 +0300 Subject: [PATCH 024/173] Small fixes. --- .../runtime/reference/non_max_suppression.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index aa982c804986c9..17a8a66a216ce4 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -171,10 +171,10 @@ namespace ngraph for (size_t batch = 0; batch < num_batches; batch++) { - float* boxesPtr = boxes + batch * boxesStrides; + float* boxesPtr = boxes_data + batch * boxesStrides; for (size_t class_idx = 0; class_idx < num_classes; class_idx++) { - float* scoresPtr = scores + batch * scoresStrides[0] + + float* scoresPtr = scores_data + batch * scoresStrides[0] + class_idx * scoresStrides[1]; std::vector candidate_boxes; candidate_boxes.reserve(num_boxes); @@ -212,7 +212,7 @@ namespace ngraph if (iou >= iou_threshold) { - should_hard_suppress = true + should_hard_suppress = true; break; } @@ -224,17 +224,20 @@ namespace ngraph next_candidate.suppress_begin_index = selected.size(); - if (!should_hard_suppress) { - if (next_candidate.score == original_score) { + if (!should_hard_suppress) + { + if (next_candidate.score == original_score) + { // // Suppression has not occurred, so select next_candidate // selected.push_back(next_candidate.box_index); // selected_scores.push_back(next_candidate.score); // continue; } - if (next_candidate.score > score_threshold) { + if (next_candidate.score > score_threshold) + { // Soft suppression has occurred and current score is still greater than // score_threshold; add next_candidate back onto priority queue. - candidate_priority_queue.push(next_candidate); + sorted_boxes.push(next_candidate); } } } From 8097591307cf0bc646abbbe12bc95c296409ac8c Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 1 Oct 2020 16:44:23 +0300 Subject: [PATCH 025/173] Small fixes. --- .../src/runtime/reference/non_max_suppression.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index 17a8a66a216ce4..0197bee22eb152 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -171,11 +171,12 @@ namespace ngraph for (size_t batch = 0; batch < num_batches; batch++) { - float* boxesPtr = boxes_data + batch * boxesStrides; + const float* boxesPtr = boxes_data + batch * boxesStrides; for (size_t class_idx = 0; class_idx < num_classes; class_idx++) { - float* scoresPtr = scores_data + batch * scoresStrides[0] + - class_idx * scoresStrides[1]; + const float* scoresPtr = scores_data + batch * scoresStrides[0] + + class_idx * scoresStrides[1]; + std::vector candidate_boxes; candidate_boxes.reserve(num_boxes); From 3a1b8ea3db0b14e35994a3fde0463f29db674f75 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 1 Oct 2020 17:02:46 +0300 Subject: [PATCH 026/173] Some changes. --- .../runtime/reference/non_max_suppression.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index 0197bee22eb152..133d03cbc9c9ac 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -103,10 +103,9 @@ struct BoxInfo BoxInfo(const Rectangle& r, int64_t idx, float sc, int64_t suppress_idx) : box{r} , index{idx} - , score{sc} , suppress_begin_index{suppress_idx} + , score{sc} { - area = (box.y2 - box.y1) * (box.x2 - box.x1); } BoxInfo() = default; @@ -118,9 +117,8 @@ struct BoxInfo Rectangle box; int64_t index = 0; - float score = 0.0f; int64_t suppress_begin_index = 0; - float area = 0.0f; + float score = 0.0f; }; namespace ngraph @@ -171,7 +169,9 @@ namespace ngraph for (size_t batch = 0; batch < num_batches; batch++) { - const float* boxesPtr = boxes_data + batch * boxesStrides; + const float* boxesPtr = boxes_data + batch * boxesStrides; + Rectangle* r = reinterpret_cast(const_cast(boxesPtr)); + for (size_t class_idx = 0; class_idx < num_classes; class_idx++) { const float* scoresPtr = scores_data + batch * scoresStrides[0] + @@ -184,8 +184,7 @@ namespace ngraph { if (scoresPtr[box_idx] > score_threshold) { - Rectangle* r = reinterpret_cast(boxesPtr) + box_idx; - candidate_boxes.emplace_back(*r, box_idx, scoresPtr[box_idx], 0); + candidate_boxes.emplace_back(r[box_idx], box_idx, scoresPtr[box_idx], 0); } } @@ -229,8 +228,8 @@ namespace ngraph { if (next_candidate.score == original_score) { - // // Suppression has not occurred, so select next_candidate - // selected.push_back(next_candidate.box_index); + // Suppression has not occurred, so select next_candidate + selected.push_back(next_candidate.box_index); // selected_scores.push_back(next_candidate.score); // continue; } From 49f775dae415d4f929680a724aca37c3d1a013e4 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 1 Oct 2020 17:41:28 +0300 Subject: [PATCH 027/173] Some additions. --- .../runtime/reference/non_max_suppression.hpp | 3 +- .../runtime/reference/non_max_suppression.cpp | 28 ++++++++++++++++--- ngraph/core/src/op/non_max_suppression.cpp | 3 +- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp b/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp index 9f76239f08753c..aa5725650f5da2 100644 --- a/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp +++ b/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp @@ -45,7 +45,8 @@ namespace ngraph const Shape& selected_indices_shape, float* selected_scores, const Shape& selected_scores_shape, - int64_t* valid_outputs); + int64_t* valid_outputs, + const bool sort_result_descending); } } } diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index 133d03cbc9c9ac..d63a82548a655e 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -139,7 +139,8 @@ namespace ngraph const Shape& selected_indices_shape, float* selected_scores, const Shape& selected_scores_shape, - int64_t* valid_outputs) + int64_t* valid_outputs, + const bool sort_result_descending) { float scale = 0.0f; if (soft_nms_sigma > 0.0f) { @@ -167,6 +168,8 @@ namespace ngraph size_t boxes_per_class = static_cast(max_output_boxes_per_class); + int64_t num_of_valid_boxes = 0; + for (size_t batch = 0; batch < num_batches; batch++) { const float* boxesPtr = boxes_data + batch * boxesStrides; @@ -229,9 +232,8 @@ namespace ngraph if (next_candidate.score == original_score) { // Suppression has not occurred, so select next_candidate - selected.push_back(next_candidate.box_index); - // selected_scores.push_back(next_candidate.score); - // continue; + selected.push_back(next_candidate.index); + continue; } if (next_candidate.score > score_threshold) { @@ -242,9 +244,27 @@ namespace ngraph } } + for (const auto& box_info : selected) + { + SelectedIndex selected_index{batch, class_idx, box_info.index}; + SelectedScore selected_score{batch, class_idx, box_info.score}; + + selected_indices_ptr[num_of_valid_boxes] = selected_index; + selected_scores_ptr[num_of_valid_boxes] = selected_score; + num_of_valid_boxes++; + } } } + + + if (sort_result_descending) + { + } + else + { + } + } } } diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index b1363822e69cb1..41814afdf16480 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -1131,7 +1131,8 @@ bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, out_shape, selected_scores.data(), out_shape, - &valid_outputs); + &valid_outputs, + m_sort_result_descending); evaluate_postprocessing(outputs, m_output_type, From 4ff9f60ecd0c40625d70d6b88036608906fc9b8d Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 2 Oct 2020 09:32:27 +0300 Subject: [PATCH 028/173] Some replacements size_t by int64_t. --- .../runtime/reference/non_max_suppression.cpp | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index d63a82548a655e..dfccbbfd29b08f 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -16,7 +16,6 @@ #include -#include #include #include #include @@ -154,31 +153,28 @@ namespace ngraph // boxes shape: {num_batches, num_boxes, 4} // scores shape: {num_batches, num_classes, num_boxes} - size_t num_batches = scores_data_shape[0]; - size_t num_classes = scores_data_shape[1]; - size_t num_boxes = boxes_data_shape[1]; + int64_t num_batches = static_cast(scores_data_shape[0]); + int64_t num_classes = static_cast(scores_data_shape[1]); + int64_t num_boxes = static_cast(boxes_data_shape[1]); SelectedIndex* selected_indices_ptr = reinterpret_cast(selected_indices); SelectedScore* selected_scores_ptr = reinterpret_cast(selected_scores); - size_t boxesStrides = num_boxes * 4; - std::array scoresStrides = {num_classes * num_boxes, num_boxes}; - size_t boxes_per_class = static_cast(max_output_boxes_per_class); int64_t num_of_valid_boxes = 0; - for (size_t batch = 0; batch < num_batches; batch++) + for (int64_t batch = 0; batch < num_batches; batch++) { - const float* boxesPtr = boxes_data + batch * boxesStrides; + const float* boxesPtr = boxes_data + batch * num_boxes * 4; Rectangle* r = reinterpret_cast(const_cast(boxesPtr)); - for (size_t class_idx = 0; class_idx < num_classes; class_idx++) + for (int64_t class_idx = 0; class_idx < num_classes; class_idx++) { - const float* scoresPtr = scores_data + batch * scoresStrides[0] + - class_idx * scoresStrides[1]; + const float* scoresPtr = scores_data + batch * (num_classes * num_boxes) + + class_idx * num_boxes; std::vector candidate_boxes; candidate_boxes.reserve(num_boxes); @@ -232,7 +228,7 @@ namespace ngraph if (next_candidate.score == original_score) { // Suppression has not occurred, so select next_candidate - selected.push_back(next_candidate.index); + selected.push_back(next_candidate); continue; } if (next_candidate.score > score_threshold) From a6872da15da56a9857e0e153a014d82f5d9abc5c Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 2 Oct 2020 09:48:35 +0300 Subject: [PATCH 029/173] Added casts to float in the construction of selected_score variable. --- .../reference/src/runtime/reference/non_max_suppression.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index dfccbbfd29b08f..9b53edaa21a75a 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -243,7 +243,8 @@ namespace ngraph for (const auto& box_info : selected) { SelectedIndex selected_index{batch, class_idx, box_info.index}; - SelectedScore selected_score{batch, class_idx, box_info.score}; + SelectedScore selected_score{static_cast(batch), + static_cast(class_idx), box_info.score}; selected_indices_ptr[num_of_valid_boxes] = selected_index; selected_scores_ptr[num_of_valid_boxes] = selected_score; From f64fcede93d14591f5b6843c12865b3815030bc7 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 2 Oct 2020 10:11:51 +0300 Subject: [PATCH 030/173] Code style fixes. --- .../runtime/reference/non_max_suppression.cpp | 80 ++++++++++--------- ngraph/core/src/op/non_max_suppression.cpp | 16 ++-- 2 files changed, 47 insertions(+), 49 deletions(-) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index 9b53edaa21a75a..7f530d655b59eb 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -15,11 +15,11 @@ //***************************************************************************** +#include "ngraph/op/non_max_suppression.hpp" #include #include -#include #include -#include "ngraph/op/non_max_suppression.hpp" +#include #include "ngraph/runtime/reference/non_max_suppression.hpp" #include "ngraph/shape.hpp" @@ -33,8 +33,8 @@ struct Rectangle , x1{x_left} , y2{y_right} , x2{x_right} - { - } + { + } Rectangle() = default; @@ -60,41 +60,41 @@ static float intersectionOverUnion(const Rectangle& boxI, const Rectangle& boxJ) float intersection_xmax = std::min(boxI.x2, boxJ.x2); float intersection_area = std::max(intersection_ymax - intersection_ymin, 0.0f) * - std::max(intersection_xmax - intersection_xmin, 0.0f); + std::max(intersection_xmax - intersection_xmin, 0.0f); return intersection_area / (areaI + areaJ - intersection_area); } struct SelectedIndex { - SelectedIndex(int64_t batch_idx, int64_t class_idx, int64_t box_idx) - : batch_index(batch_idx) - , class_index(class_idx) - , box_index(box_idx) - { - } - - SelectedIndex() = default; - - int64_t batch_index = 0; - int64_t class_index = 0; - int64_t box_index = 0; + SelectedIndex(int64_t batch_idx, int64_t class_idx, int64_t box_idx) + : batch_index(batch_idx) + , class_index(class_idx) + , box_index(box_idx) + { + } + + SelectedIndex() = default; + + int64_t batch_index = 0; + int64_t class_index = 0; + int64_t box_index = 0; }; struct SelectedScore { - SelectedScore(float batch_idx, float class_idx, float score) - : batch_index{batch_idx} - , class_index{class_idx} - , box_score{score} - { - } - - SelectedScore() = default; - - float batch_index = 0.0f; - float class_index = 0.0f; - float box_score = 0.0f; + SelectedScore(float batch_idx, float class_idx, float score) + : batch_index{batch_idx} + , class_index{class_idx} + , box_score{score} + { + } + + SelectedScore() = default; + + float batch_index = 0.0f; + float class_index = 0.0f; + float box_score = 0.0f; }; struct BoxInfo @@ -142,7 +142,8 @@ namespace ngraph const bool sort_result_descending) { float scale = 0.0f; - if (soft_nms_sigma > 0.0f) { + if (soft_nms_sigma > 0.0f) + { scale = - 0.5f / soft_nms_sigma; } @@ -173,8 +174,8 @@ namespace ngraph for (int64_t class_idx = 0; class_idx < num_classes; class_idx++) { - const float* scoresPtr = scores_data + batch * (num_classes * num_boxes) + - class_idx * num_boxes; + const float* scoresPtr = + scores_data + batch * (num_classes * num_boxes) + class_idx * num_boxes; std::vector candidate_boxes; candidate_boxes.reserve(num_boxes); @@ -183,7 +184,8 @@ namespace ngraph { if (scoresPtr[box_idx] > score_threshold) { - candidate_boxes.emplace_back(r[box_idx], box_idx, scoresPtr[box_idx], 0); + candidate_boxes.emplace_back( + r[box_idx], box_idx, scoresPtr[box_idx], 0); } } @@ -204,9 +206,11 @@ namespace ngraph bool should_hard_suppress = false; for (int64_t j = static_cast(selected.size()) - 1; - j >= next_candidate.suppress_begin_index; --j) + j >= next_candidate.suppress_begin_index; + --j) { - float iou = intersectionOverUnion(next_candidate.box, selected[j].box); + float iou = + intersectionOverUnion(next_candidate.box, selected[j].box); next_candidate.score *= func(iou); if (iou >= iou_threshold) @@ -227,14 +231,11 @@ namespace ngraph { if (next_candidate.score == original_score) { - // Suppression has not occurred, so select next_candidate selected.push_back(next_candidate); continue; } if (next_candidate.score > score_threshold) { - // Soft suppression has occurred and current score is still greater than - // score_threshold; add next_candidate back onto priority queue. sorted_boxes.push(next_candidate); } } @@ -244,7 +245,8 @@ namespace ngraph { SelectedIndex selected_index{batch, class_idx, box_info.index}; SelectedScore selected_score{static_cast(batch), - static_cast(class_idx), box_info.score}; + static_cast(class_idx), + box_info.score}; selected_indices_ptr[num_of_valid_boxes] = selected_index; selected_scores_ptr[num_of_valid_boxes] = selected_score; diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 41814afdf16480..fcc7b697779848 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -14,8 +14,8 @@ // limitations under the License. //***************************************************************************** -#include #include "ngraph/op/non_max_suppression.hpp" +#include #include "ngraph/attribute_visitor.hpp" #include "ngraph/op/constant.hpp" #include "ngraph/op/util/op_types.hpp" @@ -952,7 +952,7 @@ static void normalize_center(float* boxes, const Shape& boxes_shape) float y1 = y_center - height / 2.0; float x1 = x_center - width / 2.0; float y2 = y_center + height / 2.0; - float x2 = x_center + width / 2.0;; + float x2 = x_center + width / 2.0; box_ptr[0] = y1; box_ptr[1] = x1; @@ -964,9 +964,8 @@ static void normalize_center(float* boxes, const Shape& boxes_shape) } } -static void normalize_box_encoding(float* boxes, - const Shape& boxes_shape, - const V5BoxEncoding box_encoding) +static void + normalize_box_encoding(float* boxes, const Shape& boxes_shape, const V5BoxEncoding box_encoding) { if (box_encoding == V5BoxEncoding::CORNER) { @@ -1134,11 +1133,8 @@ bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, &valid_outputs, m_sort_result_descending); - evaluate_postprocessing(outputs, - m_output_type, - selected_indices, - selected_scores, - valid_outputs); + evaluate_postprocessing( + outputs, m_output_type, selected_indices, selected_scores, valid_outputs); return true; } From 2a292533acba776e52e10ca80f61e595eb43ef1c Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 2 Oct 2020 11:04:09 +0300 Subject: [PATCH 031/173] Written draft of NMS-5 nGraph reference implementation. --- .../runtime/reference/non_max_suppression.cpp | 57 ++++++++++++++----- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index 7f530d655b59eb..e33aacb45180c3 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -99,10 +99,17 @@ struct SelectedScore struct BoxInfo { - BoxInfo(const Rectangle& r, int64_t idx, float sc, int64_t suppress_idx) + BoxInfo(const Rectangle& r, + int64_t idx, + float sc, + int64_t suppress_idx, + int64_t batch_idx, + int64_t class_idx) : box{r} , index{idx} , suppress_begin_index{suppress_idx} + , batch_index{batch_idx} + , class_index{class_idx} , score{sc} { } @@ -117,6 +124,8 @@ struct BoxInfo Rectangle box; int64_t index = 0; int64_t suppress_begin_index = 0; + int64_t batch_index = 0; + int64_t class_index = 0; float score = 0.0f; }; @@ -144,7 +153,7 @@ namespace ngraph float scale = 0.0f; if (soft_nms_sigma > 0.0f) { - scale = - 0.5f / soft_nms_sigma; + scale = -0.5f / soft_nms_sigma; } auto func = [iou_threshold, scale](float iou) { @@ -167,6 +176,8 @@ namespace ngraph int64_t num_of_valid_boxes = 0; + std::vector filteredBoxes; + for (int64_t batch = 0; batch < num_batches; batch++) { const float* boxesPtr = boxes_data + batch * num_boxes * 4; @@ -185,7 +196,7 @@ namespace ngraph if (scoresPtr[box_idx] > score_threshold) { candidate_boxes.emplace_back( - r[box_idx], box_idx, scoresPtr[box_idx], 0); + r[box_idx], box_idx, scoresPtr[box_idx], 0, batch, class_idx); } } @@ -243,27 +254,45 @@ namespace ngraph for (const auto& box_info : selected) { - SelectedIndex selected_index{batch, class_idx, box_info.index}; - SelectedScore selected_score{static_cast(batch), - static_cast(class_idx), - box_info.score}; - - selected_indices_ptr[num_of_valid_boxes] = selected_index; - selected_scores_ptr[num_of_valid_boxes] = selected_score; - - num_of_valid_boxes++; + filteredBoxes.push_back(box_info); } } } - if (sort_result_descending) { + std::sort(filteredBoxes.begin(), + filteredBoxes.end(), + [](const BoxInfo& l, const BoxInfo& r) { return l.score > r.score; }); } - else + + size_t max_num_of_selected_indices = selected_indices_shape[0] + size_t output_size = std::min(filteredBoxes.size(), max_num_of_selected_indices); + + *valid_outputs = output_size; + + size_t idx; + for (idx = 0; idx < output_size; idx++) { + const auto& box_info = filteredBoxes[idx]; + SelectedIndex selected_index{box_info.batch_index, + box_info.class_index, + box_info.index}; + SelectedScore selected_score{static_cast(box_info.batch_index), + static_cast(box_info.class_index), + box_info.score}; + + selected_indices_ptr[idx] = selected_index; + selected_scores_ptr[idx] = selected_score; } + SelectedIndex selected_index_filler{0, 0, 0}; + SelectedScore selected_score_filler{0.0f, 0.0f, 0.0f}; + for (; idx < max_num_of_selected_indices; idx++) + { + selected_indices_ptr[idx] = selected_index_filler; + selected_scores_ptr[idx] = selected_score_filler; + } } } } From 71a08889838841da623523b888044ad318830d2d Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 2 Oct 2020 11:11:57 +0300 Subject: [PATCH 032/173] Code style fixes. --- .../src/runtime/reference/non_max_suppression.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index e33aacb45180c3..2f7e81b4ba543e 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -14,7 +14,6 @@ // limitations under the License. //***************************************************************************** - #include "ngraph/op/non_max_suppression.hpp" #include #include @@ -266,7 +265,7 @@ namespace ngraph [](const BoxInfo& l, const BoxInfo& r) { return l.score > r.score; }); } - size_t max_num_of_selected_indices = selected_indices_shape[0] + size_t max_num_of_selected_indices = selected_indices_shape[0]; size_t output_size = std::min(filteredBoxes.size(), max_num_of_selected_indices); *valid_outputs = output_size; @@ -275,9 +274,8 @@ namespace ngraph for (idx = 0; idx < output_size; idx++) { const auto& box_info = filteredBoxes[idx]; - SelectedIndex selected_index{box_info.batch_index, - box_info.class_index, - box_info.index}; + SelectedIndex selected_index{ + box_info.batch_index, box_info.class_index, box_info.index}; SelectedScore selected_score{static_cast(box_info.batch_index), static_cast(box_info.class_index), box_info.score}; From 5caeef76cfd8f4998491ba7fcedca2ef0a1bd018 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 2 Oct 2020 13:21:05 +0300 Subject: [PATCH 033/173] Started to write tests for void op::v5::NonMaxSuppression::validate_and_infer_types(). --- ngraph/test/type_prop/non_max_suppression.cpp | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/ngraph/test/type_prop/non_max_suppression.cpp b/ngraph/test/type_prop/non_max_suppression.cpp index 405a3845049094..74f4ba5a448d6d 100644 --- a/ngraph/test/type_prop/non_max_suppression.cpp +++ b/ngraph/test/type_prop/non_max_suppression.cpp @@ -547,3 +547,35 @@ TEST(type_prop, nms_v4_dynamic_boxes_and_scores) ASSERT_TRUE( nms->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); } + +// ------------------------------ V5 ------------------------------ + +TEST(type_prop, nms_v5_incorrect_boxes_rank) +{ + try + { + const auto boxes = make_shared(element::f32, Shape{1, 2, 3, 4}); + const auto scores = make_shared(element::f32, Shape{1, 2, 3}); + + make_shared(boxes, scores); + } + catch (const NodeValidationFailure& error) + { + EXPECT_HAS_SUBSTRING(error.what(), "Expected a 3D tensor for the 'boxes' input"); + } +} + +TEST(type_prop, nms_v5_incorrect_scores_rank) +{ + try + { + const auto boxes = make_shared(element::f32, Shape{1, 2, 3}); + const auto scores = make_shared(element::f32, Shape{1, 2}); + + make_shared(boxes, scores); + } + catch (const NodeValidationFailure& error) + { + EXPECT_HAS_SUBSTRING(error.what(), "Expected a 3D tensor for the 'scores' input"); + } +} From 01200aaef42e616f11fc9b4f325dce1959d5009f Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 2 Oct 2020 16:38:48 +0300 Subject: [PATCH 034/173] Added tests for scalars/nonscalars. --- ngraph/test/type_prop/non_max_suppression.cpp | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/ngraph/test/type_prop/non_max_suppression.cpp b/ngraph/test/type_prop/non_max_suppression.cpp index 74f4ba5a448d6d..01462a0659697a 100644 --- a/ngraph/test/type_prop/non_max_suppression.cpp +++ b/ngraph/test/type_prop/non_max_suppression.cpp @@ -579,3 +579,95 @@ TEST(type_prop, nms_v5_incorrect_scores_rank) EXPECT_HAS_SUBSTRING(error.what(), "Expected a 3D tensor for the 'scores' input"); } } + +TEST(type_prop, nms_v5_incorrect_scheme_num_batches) +{ + try + { + const auto boxes = make_shared(element::f32, Shape{1, 2, 3}); + const auto scores = make_shared(element::f32, Shape{2, 2, 3}); + + make_shared(boxes, scores); + } + catch (const NodeValidationFailure& error) + { + EXPECT_HAS_SUBSTRING(error.what(), + "The first dimension of both 'boxes' and 'scores' must match"); + } +} + +TEST(type_prop, nms_v5_incorrect_scheme_num_boxes) +{ + try + { + const auto boxes = make_shared(element::f32, Shape{1, 2, 3}); + const auto scores = make_shared(element::f32, Shape{1, 2, 3}); + + make_shared(boxes, scores); + } + catch (const NodeValidationFailure& error) + { + EXPECT_HAS_SUBSTRING(error.what(), + "'boxes' and 'scores' input shapes must match at the second and third " + "dimension respectively"); + } +} + +TEST(type_prop, nms_v5_scalar_inputs_check) +{ + const auto boxes = make_shared(element::f32, Shape{1, 2, 4}); + const auto scores = make_shared(element::f32, Shape{1, 2, 2}); + + const auto scalar = make_shared(element::f32, Shape{}); + const auto non_scalar = make_shared(element::f32, Shape{1}); + + try + { + make_shared(boxes, scores, non_scalar, scalar, scalar); + } + catch (const NodeValidationFailure& error) + { + EXPECT_HAS_SUBSTRING(error.what(), + "Expected a scalar for the 'max_output_boxes_per_class' input"); + } + + try + { + make_shared(boxes, scores, scalar, non_scalar, scalar); + } + catch (const NodeValidationFailure& error) + { + EXPECT_HAS_SUBSTRING(error.what(), "Expected a scalar for the 'iou_threshold' input"); + } + + try + { + make_shared(boxes, scores, scalar, scalar, non_scalar); + } + catch (const NodeValidationFailure& error) + { + EXPECT_HAS_SUBSTRING(error.what(), "Expected a scalar for the 'score_threshold' input"); + } + + try + { + make_shared(boxes, scores, scalar, scalar, scalar, non_scalar); + } + catch (const NodeValidationFailure& error) + { + EXPECT_HAS_SUBSTRING(error.what(), "Expected a scalar for the 'soft_nms_sigma' input"); + } +} + +TEST(type_prop, nms_v5_output_shape) +{ + const auto boxes = make_shared(element::f32, Shape{5, 2, 4}); + const auto scores = make_shared(element::f32, Shape{5, 3, 2}); + + const auto nms = make_shared(boxes, scores); + const auto nms_indices_out_ps = nms->get_output_partial_shape(0); + + EXPECT_TRUE(nms_indices_out_ps.rank().is_static()); + EXPECT_EQ(nms_indices_out_ps.rank().get_length(), 2); + EXPECT_EQ(nms->get_shape(), (Shape{0, 3})); +} From 0eb5f40ab57ca119cabbe06c2169d461e6272da1 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 2 Oct 2020 17:11:32 +0300 Subject: [PATCH 035/173] Fixes in the test type_prop.nms_v5_output_shape. --- ngraph/test/type_prop/non_max_suppression.cpp | 76 ++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/ngraph/test/type_prop/non_max_suppression.cpp b/ngraph/test/type_prop/non_max_suppression.cpp index 01462a0659697a..6ae27470c7232f 100644 --- a/ngraph/test/type_prop/non_max_suppression.cpp +++ b/ngraph/test/type_prop/non_max_suppression.cpp @@ -669,5 +669,79 @@ TEST(type_prop, nms_v5_output_shape) EXPECT_TRUE(nms_indices_out_ps.rank().is_static()); EXPECT_EQ(nms_indices_out_ps.rank().get_length(), 2); - EXPECT_EQ(nms->get_shape(), (Shape{0, 3})); + EXPECT_EQ(nms->get_output_shape(0), (Shape{0, 3})); + + const auto nms_scores_out_ps = nms->get_output_partial_shape(1); + + EXPECT_TRUE(nms_scores_out_ps.rank().is_static()); + EXPECT_EQ(nms_scores_out_ps.rank().get_length(), 2); + EXPECT_EQ(nms->get_output_shape(1), (Shape{0, 3})); } + +// TEST(type_prop, nms_v5_output_shape_2) +// { +// const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); +// const auto scores = make_shared(element::f32, Shape{2, 5, 7}); +// const auto max_output_boxes_per_class = op::Constant::create(element::i32, Shape{}, {3}); +// const auto iou_threshold = make_shared(element::f32, Shape{}); +// const auto score_threshold = make_shared(element::f32, Shape{}); +// +// const auto nms = make_shared( +// boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); +// +// ASSERT_EQ(nms->get_element_type(), element::i64); +// ASSERT_EQ(nms->get_shape(), (Shape{2 * 5 * 3, 3})); +// } +// +// TEST(type_prop, nms_v5_output_shape_3) +// { +// const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); +// const auto scores = make_shared(element::f32, Shape{2, 5, 7}); +// const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {1000}); +// const auto iou_threshold = make_shared(element::f32, Shape{}); +// const auto score_threshold = make_shared(element::f32, Shape{}); +// +// const auto nms = make_shared( +// boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); +// +// ASSERT_EQ(nms->get_element_type(), element::i64); +// ASSERT_EQ(nms->get_shape(), (Shape{2 * 5 * 7, 3})); +// } +// +// TEST(type_prop, nms_v5_output_shape_i32) +// { +// const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); +// const auto scores = make_shared(element::f32, Shape{2, 5, 7}); +// const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); +// const auto iou_threshold = make_shared(element::f32, Shape{}); +// const auto score_threshold = make_shared(element::f32, Shape{}); +// +// const auto nms = +// make_shared(boxes, +// scores, +// max_output_boxes_per_class, +// iou_threshold, +// score_threshold, +// op::v5::NonMaxSuppression::BoxEncodingType::CORNER, +// true, +// element::i32); +// +// ASSERT_EQ(nms->get_element_type(), element::i32); +// ASSERT_EQ(nms->get_shape(), (Shape{30, 3})); +// } +// +// TEST(type_prop, nms_v5_dynamic_boxes_and_scores) +// { +// const auto boxes = make_shared(element::f32, PartialShape::dynamic()); +// const auto scores = make_shared(element::f32, PartialShape::dynamic()); +// const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); +// const auto iou_threshold = make_shared(element::f32, Shape{}); +// const auto score_threshold = make_shared(element::f32, Shape{}); +// +// const auto nms = make_shared( +// boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); +// +// ASSERT_EQ(nms->get_element_type(), element::i64); +// ASSERT_TRUE( +// nms->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); +// } From b98be7fac0e8aab066a28bdd6529499e4c88b4d8 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 2 Oct 2020 17:57:05 +0300 Subject: [PATCH 036/173] Fixes in tests nms_v5_output_shape_2 and nms_v5_output_shape. --- ngraph/core/src/op/non_max_suppression.cpp | 2 +- ngraph/test/type_prop/non_max_suppression.cpp | 36 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index fcc7b697779848..6950ec5f957781 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -848,7 +848,7 @@ void op::v5::NonMaxSuppression::validate_and_infer_types() } set_output_type(0, m_output_type, out_shape); set_output_type(1, element::f32, out_shape); - set_output_type(2, element::f32, Shape{}); + set_output_type(2, m_output_type, Shape{}); } namespace ngraph diff --git a/ngraph/test/type_prop/non_max_suppression.cpp b/ngraph/test/type_prop/non_max_suppression.cpp index 6ae27470c7232f..551b0728b62202 100644 --- a/ngraph/test/type_prop/non_max_suppression.cpp +++ b/ngraph/test/type_prop/non_max_suppression.cpp @@ -676,23 +676,29 @@ TEST(type_prop, nms_v5_output_shape) EXPECT_TRUE(nms_scores_out_ps.rank().is_static()); EXPECT_EQ(nms_scores_out_ps.rank().get_length(), 2); EXPECT_EQ(nms->get_output_shape(1), (Shape{0, 3})); + + EXPECT_EQ(nms->get_output_shape(2), (Shape{})); +} + +TEST(type_prop, nms_v5_output_shape_2) +{ + const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); + const auto scores = make_shared(element::f32, Shape{2, 5, 7}); + const auto max_output_boxes_per_class = op::Constant::create(element::i32, Shape{}, {3}); + const auto iou_threshold = make_shared(element::f32, Shape{}); + const auto score_threshold = make_shared(element::f32, Shape{}); + + const auto nms = make_shared( + boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); + + ASSERT_EQ(nms->get_output_element_type(0), element::i64); + ASSERT_EQ(nms->get_output_element_type(1), element::f32); + ASSERT_EQ(nms->get_output_shape(0), (Shape{2 * 5 * 3, 3})); + ASSERT_EQ(nms->get_output_shape(1), (Shape{2 * 5 * 3, 3})); + + EXPECT_EQ(nms->get_output_shape(2), (Shape{})); } -// TEST(type_prop, nms_v5_output_shape_2) -// { -// const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); -// const auto scores = make_shared(element::f32, Shape{2, 5, 7}); -// const auto max_output_boxes_per_class = op::Constant::create(element::i32, Shape{}, {3}); -// const auto iou_threshold = make_shared(element::f32, Shape{}); -// const auto score_threshold = make_shared(element::f32, Shape{}); -// -// const auto nms = make_shared( -// boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); -// -// ASSERT_EQ(nms->get_element_type(), element::i64); -// ASSERT_EQ(nms->get_shape(), (Shape{2 * 5 * 3, 3})); -// } -// // TEST(type_prop, nms_v5_output_shape_3) // { // const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); From 1eda5aba460d2a9fdcac9802c87242be53ed8175 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Mon, 5 Oct 2020 12:18:11 +0300 Subject: [PATCH 037/173] Written tests for validate_and_infer_types() of NMS-5. --- ngraph/test/type_prop/non_max_suppression.cpp | 121 ++++++++++-------- 1 file changed, 69 insertions(+), 52 deletions(-) diff --git a/ngraph/test/type_prop/non_max_suppression.cpp b/ngraph/test/type_prop/non_max_suppression.cpp index 551b0728b62202..5bdc6a90856129 100644 --- a/ngraph/test/type_prop/non_max_suppression.cpp +++ b/ngraph/test/type_prop/non_max_suppression.cpp @@ -693,61 +693,78 @@ TEST(type_prop, nms_v5_output_shape_2) ASSERT_EQ(nms->get_output_element_type(0), element::i64); ASSERT_EQ(nms->get_output_element_type(1), element::f32); + ASSERT_EQ(nms->get_output_element_type(2), element::i64); ASSERT_EQ(nms->get_output_shape(0), (Shape{2 * 5 * 3, 3})); ASSERT_EQ(nms->get_output_shape(1), (Shape{2 * 5 * 3, 3})); EXPECT_EQ(nms->get_output_shape(2), (Shape{})); } -// TEST(type_prop, nms_v5_output_shape_3) -// { -// const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); -// const auto scores = make_shared(element::f32, Shape{2, 5, 7}); -// const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {1000}); -// const auto iou_threshold = make_shared(element::f32, Shape{}); -// const auto score_threshold = make_shared(element::f32, Shape{}); -// -// const auto nms = make_shared( -// boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); -// -// ASSERT_EQ(nms->get_element_type(), element::i64); -// ASSERT_EQ(nms->get_shape(), (Shape{2 * 5 * 7, 3})); -// } -// -// TEST(type_prop, nms_v5_output_shape_i32) -// { -// const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); -// const auto scores = make_shared(element::f32, Shape{2, 5, 7}); -// const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); -// const auto iou_threshold = make_shared(element::f32, Shape{}); -// const auto score_threshold = make_shared(element::f32, Shape{}); -// -// const auto nms = -// make_shared(boxes, -// scores, -// max_output_boxes_per_class, -// iou_threshold, -// score_threshold, -// op::v5::NonMaxSuppression::BoxEncodingType::CORNER, -// true, -// element::i32); -// -// ASSERT_EQ(nms->get_element_type(), element::i32); -// ASSERT_EQ(nms->get_shape(), (Shape{30, 3})); -// } -// -// TEST(type_prop, nms_v5_dynamic_boxes_and_scores) -// { -// const auto boxes = make_shared(element::f32, PartialShape::dynamic()); -// const auto scores = make_shared(element::f32, PartialShape::dynamic()); -// const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); -// const auto iou_threshold = make_shared(element::f32, Shape{}); -// const auto score_threshold = make_shared(element::f32, Shape{}); -// -// const auto nms = make_shared( -// boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); -// -// ASSERT_EQ(nms->get_element_type(), element::i64); -// ASSERT_TRUE( -// nms->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); -// } + TEST(type_prop, nms_v5_output_shape_3) + { + const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); + const auto scores = make_shared(element::f32, Shape{2, 5, 7}); + const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {1000}); + const auto iou_threshold = make_shared(element::f32, Shape{}); + const auto score_threshold = make_shared(element::f32, Shape{}); + + const auto nms = make_shared( + boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); + + ASSERT_EQ(nms->get_output_element_type(0), element::i64); + ASSERT_EQ(nms->get_output_element_type(1), element::f32); + ASSERT_EQ(nms->get_output_element_type(2), element::i64); + ASSERT_EQ(nms->get_output_shape(0), (Shape{2 * 5 * 7, 3})); + ASSERT_EQ(nms->get_output_shape(1), (Shape{2 * 5 * 7, 3})); + + EXPECT_EQ(nms->get_output_shape(2), (Shape{})); + } + + TEST(type_prop, nms_v5_output_shape_i32) + { + const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); + const auto scores = make_shared(element::f32, Shape{2, 5, 7}); + const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); + const auto iou_threshold = make_shared(element::f32, Shape{}); + const auto score_threshold = make_shared(element::f32, Shape{}); + + const auto nms = + make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + op::v5::NonMaxSuppression::BoxEncodingType::CORNER, + true, + element::i32); + + ASSERT_EQ(nms->get_output_element_type(0), element::i32); + ASSERT_EQ(nms->get_output_element_type(1), element::f32); + ASSERT_EQ(nms->get_output_element_type(2), element::i32); + ASSERT_EQ(nms->get_output_shape(0), (Shape{30, 3})); + ASSERT_EQ(nms->get_output_shape(1), (Shape{30, 3})); + + EXPECT_EQ(nms->get_output_shape(2), (Shape{})); +} + + TEST(type_prop, nms_v5_dynamic_boxes_and_scores) + { + const auto boxes = make_shared(element::f32, PartialShape::dynamic()); + const auto scores = make_shared(element::f32, PartialShape::dynamic()); + const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); + const auto iou_threshold = make_shared(element::f32, Shape{}); + const auto score_threshold = make_shared(element::f32, Shape{}); + + const auto nms = make_shared( + boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); + + ASSERT_EQ(nms->get_output_element_type(0), element::i64); + ASSERT_EQ(nms->get_output_element_type(1), element::f32); + ASSERT_EQ(nms->get_output_element_type(2), element::i64); + ASSERT_TRUE( + nms->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE( + nms->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + + EXPECT_EQ(nms->get_output_shape(2), (Shape{})); + } From b3a64931a571d1ea83cca52daa06830b1767c831 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Mon, 5 Oct 2020 14:06:15 +0300 Subject: [PATCH 038/173] Code style fixes. --- ngraph/test/type_prop/non_max_suppression.cpp | 136 +++++++++--------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/ngraph/test/type_prop/non_max_suppression.cpp b/ngraph/test/type_prop/non_max_suppression.cpp index 5bdc6a90856129..1c9c682131fe20 100644 --- a/ngraph/test/type_prop/non_max_suppression.cpp +++ b/ngraph/test/type_prop/non_max_suppression.cpp @@ -700,71 +700,71 @@ TEST(type_prop, nms_v5_output_shape_2) EXPECT_EQ(nms->get_output_shape(2), (Shape{})); } - TEST(type_prop, nms_v5_output_shape_3) - { - const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); - const auto scores = make_shared(element::f32, Shape{2, 5, 7}); - const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {1000}); - const auto iou_threshold = make_shared(element::f32, Shape{}); - const auto score_threshold = make_shared(element::f32, Shape{}); - - const auto nms = make_shared( - boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); - - ASSERT_EQ(nms->get_output_element_type(0), element::i64); - ASSERT_EQ(nms->get_output_element_type(1), element::f32); - ASSERT_EQ(nms->get_output_element_type(2), element::i64); - ASSERT_EQ(nms->get_output_shape(0), (Shape{2 * 5 * 7, 3})); - ASSERT_EQ(nms->get_output_shape(1), (Shape{2 * 5 * 7, 3})); - - EXPECT_EQ(nms->get_output_shape(2), (Shape{})); - } - - TEST(type_prop, nms_v5_output_shape_i32) - { - const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); - const auto scores = make_shared(element::f32, Shape{2, 5, 7}); - const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); - const auto iou_threshold = make_shared(element::f32, Shape{}); - const auto score_threshold = make_shared(element::f32, Shape{}); - - const auto nms = - make_shared(boxes, - scores, - max_output_boxes_per_class, - iou_threshold, - score_threshold, - op::v5::NonMaxSuppression::BoxEncodingType::CORNER, - true, - element::i32); - - ASSERT_EQ(nms->get_output_element_type(0), element::i32); - ASSERT_EQ(nms->get_output_element_type(1), element::f32); - ASSERT_EQ(nms->get_output_element_type(2), element::i32); - ASSERT_EQ(nms->get_output_shape(0), (Shape{30, 3})); - ASSERT_EQ(nms->get_output_shape(1), (Shape{30, 3})); - - EXPECT_EQ(nms->get_output_shape(2), (Shape{})); -} - - TEST(type_prop, nms_v5_dynamic_boxes_and_scores) - { - const auto boxes = make_shared(element::f32, PartialShape::dynamic()); - const auto scores = make_shared(element::f32, PartialShape::dynamic()); - const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); - const auto iou_threshold = make_shared(element::f32, Shape{}); - const auto score_threshold = make_shared(element::f32, Shape{}); - - const auto nms = make_shared( - boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); - - ASSERT_EQ(nms->get_output_element_type(0), element::i64); - ASSERT_EQ(nms->get_output_element_type(1), element::f32); - ASSERT_EQ(nms->get_output_element_type(2), element::i64); - ASSERT_TRUE( - nms->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); - ASSERT_TRUE( - nms->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); - - EXPECT_EQ(nms->get_output_shape(2), (Shape{})); - } +TEST(type_prop, nms_v5_output_shape_3) +{ + const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); + const auto scores = make_shared(element::f32, Shape{2, 5, 7}); + const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {1000}); + const auto iou_threshold = make_shared(element::f32, Shape{}); + const auto score_threshold = make_shared(element::f32, Shape{}); + + const auto nms = make_shared( + boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); + + ASSERT_EQ(nms->get_output_element_type(0), element::i64); + ASSERT_EQ(nms->get_output_element_type(1), element::f32); + ASSERT_EQ(nms->get_output_element_type(2), element::i64); + ASSERT_EQ(nms->get_output_shape(0), (Shape{2 * 5 * 7, 3})); + ASSERT_EQ(nms->get_output_shape(1), (Shape{2 * 5 * 7, 3})); + + EXPECT_EQ(nms->get_output_shape(2), (Shape{})); +} + +TEST(type_prop, nms_v5_output_shape_i32) +{ + const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); + const auto scores = make_shared(element::f32, Shape{2, 5, 7}); + const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); + const auto iou_threshold = make_shared(element::f32, Shape{}); + const auto score_threshold = make_shared(element::f32, Shape{}); + + const auto nms = + make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + op::v5::NonMaxSuppression::BoxEncodingType::CORNER, + true, + element::i32); + + ASSERT_EQ(nms->get_output_element_type(0), element::i32); + ASSERT_EQ(nms->get_output_element_type(1), element::f32); + ASSERT_EQ(nms->get_output_element_type(2), element::i32); + ASSERT_EQ(nms->get_output_shape(0), (Shape{30, 3})); + ASSERT_EQ(nms->get_output_shape(1), (Shape{30, 3})); + + EXPECT_EQ(nms->get_output_shape(2), (Shape{})); + + +TEST(type_prop, nms_v5_dynamic_boxes_and_scores) +{ + const auto boxes = make_shared(element::f32, PartialShape::dynamic()); + const auto scores = make_shared(element::f32, PartialShape::dynamic()); + const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); + const auto iou_threshold = make_shared(element::f32, Shape{}); + const auto score_threshold = make_shared(element::f32, Shape{}); + + const auto nms = make_shared( + boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); + + ASSERT_EQ(nms->get_output_element_type(0), element::i64); + ASSERT_EQ(nms->get_output_element_type(1), element::f32); + ASSERT_EQ(nms->get_output_element_type(2), element::i64); + ASSERT_TRUE( + nms->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE( + nms->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + + EXPECT_EQ(nms->get_output_shape(2), (Shape{})); +} From 2ad006645776e893947173cb9478c3ccb06e7a6e Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Mon, 5 Oct 2020 14:42:37 +0300 Subject: [PATCH 039/173] Now NMS-5 evaluate() can have outputs with calculated shapes. --- ngraph/core/src/op/non_max_suppression.cpp | 59 ++--- ngraph/test/type_prop/non_max_suppression.cpp | 218 +++++++++--------- 2 files changed, 131 insertions(+), 146 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 6950ec5f957781..69d59a24723f96 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -831,21 +831,6 @@ void op::v5::NonMaxSuppression::validate_and_infer_types() validate(); - if (boxes_ps.rank().is_static() && scores_ps.rank().is_static()) - { - const auto num_boxes_boxes = boxes_ps[1]; - const auto max_output_boxes_per_class_node = input_value(2).get_node_shared_ptr(); - if (num_boxes_boxes.is_static() && scores_ps[0].is_static() && scores_ps[1].is_static() && - op::is_constant(max_output_boxes_per_class_node)) - { - const auto num_boxes = num_boxes_boxes.get_length(); - const auto num_classes = scores_ps[1].get_length(); - const auto max_output_boxes_per_class = max_boxes_output_from_input(); - - out_shape[0] = std::min(num_boxes, max_output_boxes_per_class) * num_classes * - scores_ps[0].get_length(); - } - } set_output_type(0, m_output_type, out_shape); set_output_type(1, element::f32, out_shape); set_output_type(2, m_output_type, Shape{}); @@ -1037,19 +1022,19 @@ static void evaluate_postprocessing(const HostTensorVector& outputs, int64_t valid_outputs) { size_t num_of_outputs = outputs.size(); + size_t selected_size = valid_outputs * 3; if (output_type == ngraph::element::i64) { int64_t* indices_ptr = outputs[0]->get_data_ptr(); - memcpy(indices_ptr, selected_indices.data(), selected_indices.size() * sizeof(int64_t)); + memcpy(indices_ptr, selected_indices.data(), selected_size * sizeof(int64_t)); } else { int32_t* indices_ptr = outputs[0]->get_data_ptr(); - for (int64_t idx : selected_indices) + for (size_t i = 0; i < selected_size; ++i) { - *indices_ptr = static_cast(idx); - indices_ptr++; + indices_ptr[i] = static_cast(selected_indices[i]); } } @@ -1059,7 +1044,7 @@ static void evaluate_postprocessing(const HostTensorVector& outputs, } float* scores_ptr = outputs[1]->get_data_ptr(); - memcpy(scores_ptr, selected_scores.data(), selected_scores.size() * sizeof(float)); + memcpy(scores_ptr, selected_scores.data(), selected_size * sizeof(float)); if (num_of_outputs < 3) { @@ -1089,23 +1074,6 @@ bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, auto selected_indices_shape = infer_selected_indices_shape(inputs, max_output_boxes_per_class); Shape out_shape = selected_indices_shape.to_shape(); - outputs[0]->set_element_type(m_output_type); - outputs[0]->set_shape(out_shape); - - size_t num_of_outputs = outputs.size(); - - if (num_of_outputs >= 2) - { - outputs[1]->set_element_type(element::f32); - outputs[1]->set_shape(out_shape); - } - - if (num_of_outputs >= 3) - { - outputs[2]->set_element_type(element::f32); - outputs[2]->set_shape(Shape{}); - } - Shape boxes_shape = inputs[boxes_port]->get_shape(); Shape scores_shape = inputs[scores_port]->get_shape(); @@ -1133,6 +1101,23 @@ bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, &valid_outputs, m_sort_result_descending); + outputs[0]->set_element_type(m_output_type); + outputs[0]->set_shape(Shape{valid_outputs, 3}); + + size_t num_of_outputs = outputs.size(); + + if (num_of_outputs >= 2) + { + outputs[1]->set_element_type(element::f32); + outputs[1]->set_shape(Shape{valid_outputs, 3}); + } + + if (num_of_outputs >= 3) + { + outputs[2]->set_element_type(m_output_type); + outputs[2]->set_shape(Shape{}); + } + evaluate_postprocessing( outputs, m_output_type, selected_indices, selected_scores, valid_outputs); diff --git a/ngraph/test/type_prop/non_max_suppression.cpp b/ngraph/test/type_prop/non_max_suppression.cpp index 1c9c682131fe20..6a355d133a7628 100644 --- a/ngraph/test/type_prop/non_max_suppression.cpp +++ b/ngraph/test/type_prop/non_max_suppression.cpp @@ -659,112 +659,112 @@ TEST(type_prop, nms_v5_scalar_inputs_check) } } -TEST(type_prop, nms_v5_output_shape) -{ - const auto boxes = make_shared(element::f32, Shape{5, 2, 4}); - const auto scores = make_shared(element::f32, Shape{5, 3, 2}); - - const auto nms = make_shared(boxes, scores); - const auto nms_indices_out_ps = nms->get_output_partial_shape(0); - - EXPECT_TRUE(nms_indices_out_ps.rank().is_static()); - EXPECT_EQ(nms_indices_out_ps.rank().get_length(), 2); - EXPECT_EQ(nms->get_output_shape(0), (Shape{0, 3})); - - const auto nms_scores_out_ps = nms->get_output_partial_shape(1); - - EXPECT_TRUE(nms_scores_out_ps.rank().is_static()); - EXPECT_EQ(nms_scores_out_ps.rank().get_length(), 2); - EXPECT_EQ(nms->get_output_shape(1), (Shape{0, 3})); - - EXPECT_EQ(nms->get_output_shape(2), (Shape{})); -} - -TEST(type_prop, nms_v5_output_shape_2) -{ - const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); - const auto scores = make_shared(element::f32, Shape{2, 5, 7}); - const auto max_output_boxes_per_class = op::Constant::create(element::i32, Shape{}, {3}); - const auto iou_threshold = make_shared(element::f32, Shape{}); - const auto score_threshold = make_shared(element::f32, Shape{}); - - const auto nms = make_shared( - boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); - - ASSERT_EQ(nms->get_output_element_type(0), element::i64); - ASSERT_EQ(nms->get_output_element_type(1), element::f32); - ASSERT_EQ(nms->get_output_element_type(2), element::i64); - ASSERT_EQ(nms->get_output_shape(0), (Shape{2 * 5 * 3, 3})); - ASSERT_EQ(nms->get_output_shape(1), (Shape{2 * 5 * 3, 3})); - - EXPECT_EQ(nms->get_output_shape(2), (Shape{})); -} - -TEST(type_prop, nms_v5_output_shape_3) -{ - const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); - const auto scores = make_shared(element::f32, Shape{2, 5, 7}); - const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {1000}); - const auto iou_threshold = make_shared(element::f32, Shape{}); - const auto score_threshold = make_shared(element::f32, Shape{}); - - const auto nms = make_shared( - boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); - - ASSERT_EQ(nms->get_output_element_type(0), element::i64); - ASSERT_EQ(nms->get_output_element_type(1), element::f32); - ASSERT_EQ(nms->get_output_element_type(2), element::i64); - ASSERT_EQ(nms->get_output_shape(0), (Shape{2 * 5 * 7, 3})); - ASSERT_EQ(nms->get_output_shape(1), (Shape{2 * 5 * 7, 3})); - - EXPECT_EQ(nms->get_output_shape(2), (Shape{})); -} - -TEST(type_prop, nms_v5_output_shape_i32) -{ - const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); - const auto scores = make_shared(element::f32, Shape{2, 5, 7}); - const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); - const auto iou_threshold = make_shared(element::f32, Shape{}); - const auto score_threshold = make_shared(element::f32, Shape{}); - - const auto nms = - make_shared(boxes, - scores, - max_output_boxes_per_class, - iou_threshold, - score_threshold, - op::v5::NonMaxSuppression::BoxEncodingType::CORNER, - true, - element::i32); - - ASSERT_EQ(nms->get_output_element_type(0), element::i32); - ASSERT_EQ(nms->get_output_element_type(1), element::f32); - ASSERT_EQ(nms->get_output_element_type(2), element::i32); - ASSERT_EQ(nms->get_output_shape(0), (Shape{30, 3})); - ASSERT_EQ(nms->get_output_shape(1), (Shape{30, 3})); - - EXPECT_EQ(nms->get_output_shape(2), (Shape{})); - - -TEST(type_prop, nms_v5_dynamic_boxes_and_scores) -{ - const auto boxes = make_shared(element::f32, PartialShape::dynamic()); - const auto scores = make_shared(element::f32, PartialShape::dynamic()); - const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); - const auto iou_threshold = make_shared(element::f32, Shape{}); - const auto score_threshold = make_shared(element::f32, Shape{}); - - const auto nms = make_shared( - boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); - - ASSERT_EQ(nms->get_output_element_type(0), element::i64); - ASSERT_EQ(nms->get_output_element_type(1), element::f32); - ASSERT_EQ(nms->get_output_element_type(2), element::i64); - ASSERT_TRUE( - nms->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); - ASSERT_TRUE( - nms->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); - - EXPECT_EQ(nms->get_output_shape(2), (Shape{})); -} +// TEST(type_prop, nms_v5_output_shape) +// { +// const auto boxes = make_shared(element::f32, Shape{5, 2, 4}); +// const auto scores = make_shared(element::f32, Shape{5, 3, 2}); +// +// const auto nms = make_shared(boxes, scores); +// const auto nms_indices_out_ps = nms->get_output_partial_shape(0); +// +// EXPECT_TRUE(nms_indices_out_ps.rank().is_static()); +// EXPECT_EQ(nms_indices_out_ps.rank().get_length(), 2); +// EXPECT_EQ(nms->get_output_shape(0), (Shape{0, 3})); +// +// const auto nms_scores_out_ps = nms->get_output_partial_shape(1); +// +// EXPECT_TRUE(nms_scores_out_ps.rank().is_static()); +// EXPECT_EQ(nms_scores_out_ps.rank().get_length(), 2); +// EXPECT_EQ(nms->get_output_shape(1), (Shape{0, 3})); +// +// EXPECT_EQ(nms->get_output_shape(2), (Shape{})); +// } +// +// TEST(type_prop, nms_v5_output_shape_2) +// { +// const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); +// const auto scores = make_shared(element::f32, Shape{2, 5, 7}); +// const auto max_output_boxes_per_class = op::Constant::create(element::i32, Shape{}, {3}); +// const auto iou_threshold = make_shared(element::f32, Shape{}); +// const auto score_threshold = make_shared(element::f32, Shape{}); +// +// const auto nms = make_shared( +// boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); +// +// ASSERT_EQ(nms->get_output_element_type(0), element::i64); +// ASSERT_EQ(nms->get_output_element_type(1), element::f32); +// ASSERT_EQ(nms->get_output_element_type(2), element::i64); +// ASSERT_EQ(nms->get_output_shape(0), (Shape{2 * 5 * 3, 3})); +// ASSERT_EQ(nms->get_output_shape(1), (Shape{2 * 5 * 3, 3})); +// +// EXPECT_EQ(nms->get_output_shape(2), (Shape{})); +// } +// +// TEST(type_prop, nms_v5_output_shape_3) +// { +// const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); +// const auto scores = make_shared(element::f32, Shape{2, 5, 7}); +// const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {1000}); +// const auto iou_threshold = make_shared(element::f32, Shape{}); +// const auto score_threshold = make_shared(element::f32, Shape{}); +// +// const auto nms = make_shared( +// boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); +// +// ASSERT_EQ(nms->get_output_element_type(0), element::i64); +// ASSERT_EQ(nms->get_output_element_type(1), element::f32); +// ASSERT_EQ(nms->get_output_element_type(2), element::i64); +// ASSERT_EQ(nms->get_output_shape(0), (Shape{2 * 5 * 7, 3})); +// ASSERT_EQ(nms->get_output_shape(1), (Shape{2 * 5 * 7, 3})); +// +// EXPECT_EQ(nms->get_output_shape(2), (Shape{})); +// } +// +// TEST(type_prop, nms_v5_output_shape_i32) +// { +// const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); +// const auto scores = make_shared(element::f32, Shape{2, 5, 7}); +// const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); +// const auto iou_threshold = make_shared(element::f32, Shape{}); +// const auto score_threshold = make_shared(element::f32, Shape{}); +// +// const auto nms = +// make_shared(boxes, +// scores, +// max_output_boxes_per_class, +// iou_threshold, +// score_threshold, +// op::v5::NonMaxSuppression::BoxEncodingType::CORNER, +// true, +// element::i32); +// +// ASSERT_EQ(nms->get_output_element_type(0), element::i32); +// ASSERT_EQ(nms->get_output_element_type(1), element::f32); +// ASSERT_EQ(nms->get_output_element_type(2), element::i32); +// ASSERT_EQ(nms->get_output_shape(0), (Shape{30, 3})); +// ASSERT_EQ(nms->get_output_shape(1), (Shape{30, 3})); +// +// EXPECT_EQ(nms->get_output_shape(2), (Shape{})); +// +// +// TEST(type_prop, nms_v5_dynamic_boxes_and_scores) +// { +// const auto boxes = make_shared(element::f32, PartialShape::dynamic()); +// const auto scores = make_shared(element::f32, PartialShape::dynamic()); +// const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); +// const auto iou_threshold = make_shared(element::f32, Shape{}); +// const auto score_threshold = make_shared(element::f32, Shape{}); +// +// const auto nms = make_shared( +// boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); +// +// ASSERT_EQ(nms->get_output_element_type(0), element::i64); +// ASSERT_EQ(nms->get_output_element_type(1), element::f32); +// ASSERT_EQ(nms->get_output_element_type(2), element::i64); +// ASSERT_TRUE( +// nms->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); +// ASSERT_TRUE( +// nms->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); +// +// EXPECT_EQ(nms->get_output_shape(2), (Shape{})); +// } From 136b6b770175e208707f12e76cbc3df30beb4821 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Mon, 5 Oct 2020 14:55:32 +0300 Subject: [PATCH 040/173] Small fixes. --- ngraph/core/src/op/non_max_suppression.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 69d59a24723f96..4efcf87798878f 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -1102,14 +1102,14 @@ bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, m_sort_result_descending); outputs[0]->set_element_type(m_output_type); - outputs[0]->set_shape(Shape{valid_outputs, 3}); + outputs[0]->set_shape(Shape{static_cast(valid_outputs), 3}); size_t num_of_outputs = outputs.size(); if (num_of_outputs >= 2) { outputs[1]->set_element_type(element::f32); - outputs[1]->set_shape(Shape{valid_outputs, 3}); + outputs[1]->set_shape(Shape{static_cast(valid_outputs), 3}); } if (num_of_outputs >= 3) From fe5a2546907d3b383e0ec9abb12f7e9344574919 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Mon, 5 Oct 2020 17:48:05 +0300 Subject: [PATCH 041/173] Corrected tests for NMS-5 validate_and_infer_type(). --- ngraph/test/type_prop/non_max_suppression.cpp | 218 +++++++++--------- 1 file changed, 109 insertions(+), 109 deletions(-) diff --git a/ngraph/test/type_prop/non_max_suppression.cpp b/ngraph/test/type_prop/non_max_suppression.cpp index 6a355d133a7628..a0db6b606efc43 100644 --- a/ngraph/test/type_prop/non_max_suppression.cpp +++ b/ngraph/test/type_prop/non_max_suppression.cpp @@ -659,112 +659,112 @@ TEST(type_prop, nms_v5_scalar_inputs_check) } } -// TEST(type_prop, nms_v5_output_shape) -// { -// const auto boxes = make_shared(element::f32, Shape{5, 2, 4}); -// const auto scores = make_shared(element::f32, Shape{5, 3, 2}); -// -// const auto nms = make_shared(boxes, scores); -// const auto nms_indices_out_ps = nms->get_output_partial_shape(0); -// -// EXPECT_TRUE(nms_indices_out_ps.rank().is_static()); -// EXPECT_EQ(nms_indices_out_ps.rank().get_length(), 2); -// EXPECT_EQ(nms->get_output_shape(0), (Shape{0, 3})); -// -// const auto nms_scores_out_ps = nms->get_output_partial_shape(1); -// -// EXPECT_TRUE(nms_scores_out_ps.rank().is_static()); -// EXPECT_EQ(nms_scores_out_ps.rank().get_length(), 2); -// EXPECT_EQ(nms->get_output_shape(1), (Shape{0, 3})); -// -// EXPECT_EQ(nms->get_output_shape(2), (Shape{})); -// } -// -// TEST(type_prop, nms_v5_output_shape_2) -// { -// const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); -// const auto scores = make_shared(element::f32, Shape{2, 5, 7}); -// const auto max_output_boxes_per_class = op::Constant::create(element::i32, Shape{}, {3}); -// const auto iou_threshold = make_shared(element::f32, Shape{}); -// const auto score_threshold = make_shared(element::f32, Shape{}); -// -// const auto nms = make_shared( -// boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); -// -// ASSERT_EQ(nms->get_output_element_type(0), element::i64); -// ASSERT_EQ(nms->get_output_element_type(1), element::f32); -// ASSERT_EQ(nms->get_output_element_type(2), element::i64); -// ASSERT_EQ(nms->get_output_shape(0), (Shape{2 * 5 * 3, 3})); -// ASSERT_EQ(nms->get_output_shape(1), (Shape{2 * 5 * 3, 3})); -// -// EXPECT_EQ(nms->get_output_shape(2), (Shape{})); -// } -// -// TEST(type_prop, nms_v5_output_shape_3) -// { -// const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); -// const auto scores = make_shared(element::f32, Shape{2, 5, 7}); -// const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {1000}); -// const auto iou_threshold = make_shared(element::f32, Shape{}); -// const auto score_threshold = make_shared(element::f32, Shape{}); -// -// const auto nms = make_shared( -// boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); -// -// ASSERT_EQ(nms->get_output_element_type(0), element::i64); -// ASSERT_EQ(nms->get_output_element_type(1), element::f32); -// ASSERT_EQ(nms->get_output_element_type(2), element::i64); -// ASSERT_EQ(nms->get_output_shape(0), (Shape{2 * 5 * 7, 3})); -// ASSERT_EQ(nms->get_output_shape(1), (Shape{2 * 5 * 7, 3})); -// -// EXPECT_EQ(nms->get_output_shape(2), (Shape{})); -// } -// -// TEST(type_prop, nms_v5_output_shape_i32) -// { -// const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); -// const auto scores = make_shared(element::f32, Shape{2, 5, 7}); -// const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); -// const auto iou_threshold = make_shared(element::f32, Shape{}); -// const auto score_threshold = make_shared(element::f32, Shape{}); -// -// const auto nms = -// make_shared(boxes, -// scores, -// max_output_boxes_per_class, -// iou_threshold, -// score_threshold, -// op::v5::NonMaxSuppression::BoxEncodingType::CORNER, -// true, -// element::i32); -// -// ASSERT_EQ(nms->get_output_element_type(0), element::i32); -// ASSERT_EQ(nms->get_output_element_type(1), element::f32); -// ASSERT_EQ(nms->get_output_element_type(2), element::i32); -// ASSERT_EQ(nms->get_output_shape(0), (Shape{30, 3})); -// ASSERT_EQ(nms->get_output_shape(1), (Shape{30, 3})); -// -// EXPECT_EQ(nms->get_output_shape(2), (Shape{})); -// -// -// TEST(type_prop, nms_v5_dynamic_boxes_and_scores) -// { -// const auto boxes = make_shared(element::f32, PartialShape::dynamic()); -// const auto scores = make_shared(element::f32, PartialShape::dynamic()); -// const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); -// const auto iou_threshold = make_shared(element::f32, Shape{}); -// const auto score_threshold = make_shared(element::f32, Shape{}); -// -// const auto nms = make_shared( -// boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); -// -// ASSERT_EQ(nms->get_output_element_type(0), element::i64); -// ASSERT_EQ(nms->get_output_element_type(1), element::f32); -// ASSERT_EQ(nms->get_output_element_type(2), element::i64); -// ASSERT_TRUE( -// nms->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); -// ASSERT_TRUE( -// nms->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); -// -// EXPECT_EQ(nms->get_output_shape(2), (Shape{})); -// } +TEST(type_prop, nms_v5_output_shape) +{ + const auto boxes = make_shared(element::f32, Shape{5, 2, 4}); + const auto scores = make_shared(element::f32, Shape{5, 3, 2}); + + const auto nms = make_shared(boxes, scores); + + ASSERT_TRUE( + nms->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE( + nms->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + + EXPECT_EQ(nms->get_output_shape(2), (Shape{})); +} + +TEST(type_prop, nms_v5_output_shape_2) +{ + const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); + const auto scores = make_shared(element::f32, Shape{2, 5, 7}); + const auto max_output_boxes_per_class = op::Constant::create(element::i32, Shape{}, {3}); + const auto iou_threshold = make_shared(element::f32, Shape{}); + const auto score_threshold = make_shared(element::f32, Shape{}); + + const auto nms = make_shared( + boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); + + ASSERT_EQ(nms->get_output_element_type(0), element::i64); + ASSERT_EQ(nms->get_output_element_type(1), element::f32); + ASSERT_EQ(nms->get_output_element_type(2), element::i64); + ASSERT_TRUE( + nms->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE( + nms->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + + EXPECT_EQ(nms->get_output_shape(2), (Shape{})); +} + +TEST(type_prop, nms_v5_output_shape_3) +{ + const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); + const auto scores = make_shared(element::f32, Shape{2, 5, 7}); + const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {1000}); + const auto iou_threshold = make_shared(element::f32, Shape{}); + const auto score_threshold = make_shared(element::f32, Shape{}); + + const auto nms = make_shared( + boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); + + ASSERT_EQ(nms->get_output_element_type(0), element::i64); + ASSERT_EQ(nms->get_output_element_type(1), element::f32); + ASSERT_EQ(nms->get_output_element_type(2), element::i64); + ASSERT_TRUE( + nms->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE( + nms->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + + EXPECT_EQ(nms->get_output_shape(2), (Shape{})); +} + +TEST(type_prop, nms_v5_output_shape_i32) +{ + const auto boxes = make_shared(element::f32, Shape{2, 7, 4}); + const auto scores = make_shared(element::f32, Shape{2, 5, 7}); + const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); + const auto iou_threshold = make_shared(element::f32, Shape{}); + const auto score_threshold = make_shared(element::f32, Shape{}); + + const auto nms = + make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + op::v5::NonMaxSuppression::BoxEncodingType::CORNER, + true, + element::i32); + + ASSERT_EQ(nms->get_output_element_type(0), element::i32); + ASSERT_EQ(nms->get_output_element_type(1), element::f32); + ASSERT_EQ(nms->get_output_element_type(2), element::i32); + ASSERT_TRUE( + nms->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE( + nms->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + + EXPECT_EQ(nms->get_output_shape(2), (Shape{})); +} + +TEST(type_prop, nms_v5_dynamic_boxes_and_scores) +{ + const auto boxes = make_shared(element::f32, PartialShape::dynamic()); + const auto scores = make_shared(element::f32, PartialShape::dynamic()); + const auto max_output_boxes_per_class = op::Constant::create(element::i16, Shape{}, {3}); + const auto iou_threshold = make_shared(element::f32, Shape{}); + const auto score_threshold = make_shared(element::f32, Shape{}); + + const auto nms = make_shared( + boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); + + ASSERT_EQ(nms->get_output_element_type(0), element::i64); + ASSERT_EQ(nms->get_output_element_type(1), element::f32); + ASSERT_EQ(nms->get_output_element_type(2), element::i64); + ASSERT_TRUE( + nms->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE( + nms->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + + EXPECT_EQ(nms->get_output_shape(2), (Shape{})); +} From 1d065669d53452e7b7b84d916503fff467074e9e Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 6 Oct 2020 09:36:06 +0300 Subject: [PATCH 042/173] Code style fixes. --- ngraph/test/type_prop/non_max_suppression.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngraph/test/type_prop/non_max_suppression.cpp b/ngraph/test/type_prop/non_max_suppression.cpp index a0db6b606efc43..5bcb86de3ca57f 100644 --- a/ngraph/test/type_prop/non_max_suppression.cpp +++ b/ngraph/test/type_prop/non_max_suppression.cpp @@ -665,7 +665,7 @@ TEST(type_prop, nms_v5_output_shape) const auto scores = make_shared(element::f32, Shape{5, 3, 2}); const auto nms = make_shared(boxes, scores); - + ASSERT_TRUE( nms->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); ASSERT_TRUE( From fed50cc9cb68fb011a10c9fa189321841bdfea45 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 6 Oct 2020 12:32:23 +0300 Subject: [PATCH 043/173] Started to write inner version of NMS-5 with static output shapes. --- .../include/ngraph_ops/nms_ie.hpp | 26 +++++++++++++++++++ .../transformations/src/ngraph_ops/nms_ie.cpp | 23 ++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/inference-engine/src/transformations/include/ngraph_ops/nms_ie.hpp b/inference-engine/src/transformations/include/ngraph_ops/nms_ie.hpp index 79a31dedcc1a83..130b21c14772bc 100644 --- a/inference-engine/src/transformations/include/ngraph_ops/nms_ie.hpp +++ b/inference-engine/src/transformations/include/ngraph_ops/nms_ie.hpp @@ -58,5 +58,31 @@ class TRANSFORMATIONS_API NonMaxSuppressionIE2 : public NonMaxSuppressionIE { std::shared_ptr clone_with_new_inputs(const OutputVector & new_args) const override; }; +class TRANSFORMATIONS_API NonMaxSuppressionIE3 : public Op { +public: + static constexpr NodeTypeInfo type_info{"NonMaxSuppressionIE", 3}; + const NodeTypeInfo& get_type_info() const override { return type_info; } + + NonMaxSuppressionIE3(const Output& boxes, + const Output& scores, + const Output& max_output_boxes_per_class, + const Output& iou_threshold, + const Output& score_threshold, + const Output& soft_nms_sigma, + int center_point_box, + bool sort_result_descending, + const ngraph::element::Type& output_type = ngraph::element::i64); + + void validate_and_infer_types() override; + + bool visit_attributes(AttributeVisitor& visitor) override; + + std::shared_ptr clone_with_new_inputs(const OutputVector & new_args) const override; + + int m_center_point_box; + bool m_sort_result_descending = true; + element::Type m_output_type; +}; + } // namespace op } // namespace ngraph diff --git a/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp b/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp index 02473ad09131b4..7c65aad9cec9e0 100644 --- a/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp +++ b/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp @@ -101,3 +101,26 @@ void op::NonMaxSuppressionIE2::validate_and_infer_types() { m_output_type); set_output_type(0, nms->output(0).get_element_type(), nms->output(0).get_partial_shape()); } + +constexpr NodeTypeInfo op::NonMaxSuppressionIE3::type_info; + +op::NonMaxSuppressionIE3::NonMaxSuppressionIE3(const Output& boxes, + const Output& scores, + const Output& max_output_boxes_per_class, + const Output& iou_threshold, + const Output& score_threshold, + const Output& soft_nms_sigma, + int center_point_box, + bool sort_result_descending, + const ngraph::element::Type& output_type) + : Op({boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, soft_nms_sigma}), + m_center_point_box(center_point_box), m_sort_result_descending(sort_result_descending), m_output_type(output_type) { + constructor_validate_and_infer_types(); +} + +std::shared_ptr op::NonMaxSuppressionIE3::clone_with_new_inputs(const ngraph::OutputVector &new_args) const { + check_new_args_count(this, new_args); + return make_shared(new_args.at(0), new_args.at(1), new_args.at(2), new_args.at(3), + new_args.at(4), new_args.at(5), m_center_point_box, m_sort_result_descending, + m_output_type); +} From 924061b37c20786d4fb21696fe0e1dc45b5efd0e Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 6 Oct 2020 13:58:55 +0300 Subject: [PATCH 044/173] Written draft of the inner operation NonMaxSuppressionIE3. --- .../include/ngraph_ops/nms_ie.hpp | 2 + .../transformations/src/ngraph_ops/nms_ie.cpp | 51 +++++++++++++++++++ ngraph/core/src/op/non_max_suppression.cpp | 2 +- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/inference-engine/src/transformations/include/ngraph_ops/nms_ie.hpp b/inference-engine/src/transformations/include/ngraph_ops/nms_ie.hpp index 130b21c14772bc..9f37d1c083235d 100644 --- a/inference-engine/src/transformations/include/ngraph_ops/nms_ie.hpp +++ b/inference-engine/src/transformations/include/ngraph_ops/nms_ie.hpp @@ -82,6 +82,8 @@ class TRANSFORMATIONS_API NonMaxSuppressionIE3 : public Op { int m_center_point_box; bool m_sort_result_descending = true; element::Type m_output_type; +private: + int64_t max_boxes_output_from_input() const; }; } // namespace op diff --git a/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp b/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp index 7c65aad9cec9e0..d54f46d50335f9 100644 --- a/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp +++ b/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp @@ -124,3 +124,54 @@ std::shared_ptr op::NonMaxSuppressionIE3::clone_with_new_inputs(const ngra new_args.at(4), new_args.at(5), m_center_point_box, m_sort_result_descending, m_output_type); } + +bool op::NonMaxSuppressionIE3::visit_attributes(AttributeVisitor& visitor) { + visitor.on_attribute("center_point_box", m_center_point_box); + visitor.on_attribute("sort_result_descending", m_sort_result_descending); + visitor.on_attribute("output_type", m_output_type); + return true; +} + +static constexpr size_t boxes_port = 0; +static constexpr size_t scores_port = 1; +static constexpr size_t max_output_boxes_per_class_port = 2; + +int64_t op::NonMaxSuppressionIE3::max_boxes_output_from_input() const +{ + int64_t max_output_boxes{0}; + + const auto max_output_boxes_input = + as_type_ptr(input_value(2).get_node_shared_ptr()); + max_output_boxes = max_output_boxes_input->cast_vector().at(0); + + return max_output_boxes; +} + +void op::NonMaxSuppressionIE3::validate_and_infer_types() { + const auto boxes_ps = get_input_partial_shape(boxes_port); + const auto scores_ps = get_input_partial_shape(scores_port); + + // NonMaxSuppression produces triplets + // that have the following format: [batch_index, class_index, box_index] + PartialShape out_shape = {Dimension::dynamic(), 3}; + + if (boxes_ps.rank().is_static() && scores_ps.rank().is_static()) + { + const auto num_boxes_boxes = boxes_ps[1]; + const auto max_output_boxes_per_class_node = input_value(max_output_boxes_per_class_port).get_node_shared_ptr(); + if (num_boxes_boxes.is_static() && scores_ps[0].is_static() && scores_ps[1].is_static() && + op::is_constant(max_output_boxes_per_class_node)) + { + const auto num_boxes = num_boxes_boxes.get_length(); + const auto num_classes = scores_ps[1].get_length(); + const auto max_output_boxes_per_class = max_boxes_output_from_input(); + + out_shape[0] = std::min(num_boxes, max_output_boxes_per_class) * num_classes * + scores_ps[0].get_length(); + } + } + + set_output_type(0, m_output_type, out_shape); + set_output_type(1, element::f32, out_shape); + set_output_type(2, m_output_type, Shape{}); +} diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 4efcf87798878f..3c4c406780d054 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -863,7 +863,7 @@ static PartialShape infer_selected_indices_shape(const HostTensorVector& inputs, int64_t max_output_boxes_per_class) { const auto boxes_ps = inputs[boxes_port]->get_partial_shape(); - const auto scores_ps = inputs[1]->get_partial_shape(); + const auto scores_ps = inputs[scores_port]->get_partial_shape(); // NonMaxSuppression produces triplets // that have the following format: [batch_index, class_index, box_index] From 8a57122d1f51ac4a69cd6072552d270c246ad2f7 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 6 Oct 2020 16:28:54 +0300 Subject: [PATCH 045/173] Started to write conversion of op::v5::NonMaxSuppression into NonMaxSuppressionIE3. --- .../convert_nms_5_to_legacy.hpp | 33 +++++ .../convert_nms_5_to_legacy.cpp | 113 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 inference-engine/src/transformations/include/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp create mode 100644 inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp diff --git a/inference-engine/src/transformations/include/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp b/inference-engine/src/transformations/include/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp new file mode 100644 index 00000000000000..11fd03444f126c --- /dev/null +++ b/inference-engine/src/transformations/include/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp @@ -0,0 +1,33 @@ +// Copyright (C) 2018-2020 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include + +#include + +#include + +namespace ngraph { +namespace pass { + + class TRANSFORMATIONS_API ConvertNMS4ToLegacyMatcher; + +} // namespace pass +} // namespace ngraph + +/* + * Description: + * Convert NMS-5 directly to inner NMS. + */ + + +class ngraph::pass::ConvertNMS5ToLegacyMatcher: public ngraph::pass::MatcherPass { +public: + NGRAPH_RTTI_DECLARATION; + ConvertNMS4ToLegacyMatcher(); +}; + diff --git a/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp b/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp new file mode 100644 index 00000000000000..b6ecb00fb7eac8 --- /dev/null +++ b/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp @@ -0,0 +1,113 @@ +// Copyright (C) 2018-2020 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp" + +NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS5ToLegacyMatcher, "ConvertNMS4ToLegacyMatcher", 0); + +ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { +// auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); +// auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); +// auto max_output_boxes_per_class = ngraph::opset4::Constant::create(element::i64, Shape{}, {10}); +// auto iou_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.75}); +// auto score_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.7}); +// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, +// iou_threshold, score_threshold); +// +// ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { +// auto nms_4 = std::dynamic_pointer_cast(m.get_match_root()); +// if (!nms_4) { +// return false; +// } +// +// const auto new_args = nms_4->input_values(); +// const auto& arg2 = new_args.size() > 2 ? new_args.at(2) : ngraph::opset4::Constant::create(element::i32, Shape{}, {0}); +// const auto& arg3 = new_args.size() > 3 ? new_args.at(3) : ngraph::opset4::Constant::create(element::f32, Shape{}, {.0f}); +// const auto& arg4 = new_args.size() > 4 ? new_args.at(4) : ngraph::opset4::Constant::create(element::f32, Shape{}, {.0f}); +// +// const auto max_output_boxes_per_class_rank = arg2.get_partial_shape().rank(); +// const auto iou_threshold_rank = arg3.get_partial_shape().rank(); +// const auto score_threshold_rank = arg4.get_partial_shape().rank(); +// +// // Check that required ranks are not dynamic +// if (max_output_boxes_per_class_rank.is_dynamic() || +// iou_threshold_rank.is_dynamic() || +// score_threshold_rank.is_dynamic()) { +// return false; +// } +// +// if (max_output_boxes_per_class_rank.get_length() == 1 && +// iou_threshold_rank.get_length() == 1 && +// score_threshold_rank.get_length() == 1) { +// return false; +// } +// +// // vector of new nGraph operations +// NodeVector new_ops; +// +// auto new_max_per_class = arg2; +// if (max_output_boxes_per_class_rank.get_length() == 0) { +// // WA: we need to create Constant manually because it requires by NMS shape inference +// // otherwise we will get dynamic shape until first CF is executed. It can be resolved +// // if CF will be executed right after transformation and before Validate pass. +// if (auto new_max_per_class_const = std::dynamic_pointer_cast(new_max_per_class.get_node_shared_ptr())) { +// new_max_per_class = opset1::Constant::create(element::i64, Shape{1}, new_max_per_class_const->cast_vector()); +// } else { +// new_max_per_class = std::make_shared(arg2, opset1::Constant::create(element::i64, Shape{1}, {0})); +// new_ops.push_back(new_max_per_class.get_node_shared_ptr()); +// } +// } +// auto new_iou_threshold = arg3; +// if (iou_threshold_rank.get_length() == 0) { +// new_iou_threshold = std::make_shared(arg3, opset1::Constant::create(element::i64, Shape{1}, {0})); +// new_ops.push_back(new_iou_threshold.get_node_shared_ptr()); +// } +// auto new_score_threshold = arg4; +// if (score_threshold_rank.get_length() == 0) { +// new_score_threshold = std::make_shared(arg4, opset1::Constant::create(element::i64, Shape{1}, {0})); +// new_ops.push_back(new_score_threshold.get_node_shared_ptr()); +// } +// +// int center_point_box = 0; +// switch (nms_4->get_box_encoding()) { +// case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CENTER: +// center_point_box = 1; +// break; +// case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CORNER: +// center_point_box = 0; +// break; +// default: +// throw ngraph_error("NonMaxSuppression layer " + nms_4->get_friendly_name() + +// " has unsupported box encoding"); +// } +// const auto nms_legacy = std::make_shared( +// new_args.at(0), +// new_args.at(1), +// new_max_per_class, +// new_iou_threshold, +// new_score_threshold, +// center_point_box, +// nms_4->get_sort_result_descending(), +// nms_4->get_output_type()); +// new_ops.push_back(nms_legacy); +// +// nms_legacy->set_friendly_name(nms_4->get_friendly_name()); +// ngraph::copy_runtime_info(nms_4, new_ops); +// ngraph::replace_node(nms_4, nms_legacy); +// return true; +// }; +// +// auto m = std::make_shared(nms, "ConvertNMS4ToNMSLegacy"); +// this->register_matcher(m, callback); +} From 44da98e6b7336b7d1fb12558a9e546dd09ae501d Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 6 Oct 2020 16:32:22 +0300 Subject: [PATCH 046/173] Small changes. --- .../convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp b/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp index b6ecb00fb7eac8..61f8ebd659d2ac 100644 --- a/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp +++ b/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp @@ -14,11 +14,11 @@ #include "transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp" -NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS5ToLegacyMatcher, "ConvertNMS4ToLegacyMatcher", 0); +NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS5ToLegacyMatcher, "ConvertNMS5ToLegacyMatcher", 0); ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { -// auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); -// auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); // auto max_output_boxes_per_class = ngraph::opset4::Constant::create(element::i64, Shape{}, {10}); // auto iou_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.75}); // auto score_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.7}); From b112a2fa3892c4bf961055dd047e9c0d899eb847 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 6 Oct 2020 17:00:54 +0300 Subject: [PATCH 047/173] Some additions. --- .../transformations/src/ngraph_ops/nms_ie.cpp | 9 ++--- .../convert_nms_5_to_legacy.cpp | 37 ++++++++++--------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp b/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp index d54f46d50335f9..2cf573304f7385 100644 --- a/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp +++ b/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp @@ -136,8 +136,7 @@ static constexpr size_t boxes_port = 0; static constexpr size_t scores_port = 1; static constexpr size_t max_output_boxes_per_class_port = 2; -int64_t op::NonMaxSuppressionIE3::max_boxes_output_from_input() const -{ +int64_t op::NonMaxSuppressionIE3::max_boxes_output_from_input() const { int64_t max_output_boxes{0}; const auto max_output_boxes_input = @@ -155,13 +154,11 @@ void op::NonMaxSuppressionIE3::validate_and_infer_types() { // that have the following format: [batch_index, class_index, box_index] PartialShape out_shape = {Dimension::dynamic(), 3}; - if (boxes_ps.rank().is_static() && scores_ps.rank().is_static()) - { + if (boxes_ps.rank().is_static() && scores_ps.rank().is_static()) { const auto num_boxes_boxes = boxes_ps[1]; const auto max_output_boxes_per_class_node = input_value(max_output_boxes_per_class_port).get_node_shared_ptr(); if (num_boxes_boxes.is_static() && scores_ps[0].is_static() && scores_ps[1].is_static() && - op::is_constant(max_output_boxes_per_class_node)) - { + op::is_constant(max_output_boxes_per_class_node)) { const auto num_boxes = num_boxes_boxes.get_length(); const auto num_classes = scores_ps[1].get_length(); const auto max_output_boxes_per_class = max_boxes_output_from_input(); diff --git a/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp b/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp index 61f8ebd659d2ac..bee33707a10427 100644 --- a/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp +++ b/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp @@ -19,19 +19,20 @@ NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS5ToLegacyMatcher, "ConvertNMS5ToL ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); -// auto max_output_boxes_per_class = ngraph::opset4::Constant::create(element::i64, Shape{}, {10}); -// auto iou_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.75}); -// auto score_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.7}); -// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, -// iou_threshold, score_threshold); -// -// ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { -// auto nms_4 = std::dynamic_pointer_cast(m.get_match_root()); -// if (!nms_4) { -// return false; -// } -// -// const auto new_args = nms_4->input_values(); + auto max_output_boxes_per_class = ngraph::opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, + iou_threshold, score_threshold, soft_nms_sigma); + + ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { + auto nms_5 = std::dynamic_pointer_cast(m.get_match_root()); + if (!nms_5) { + return false; + } + + const auto new_args = nms_5->input_values(); // const auto& arg2 = new_args.size() > 2 ? new_args.at(2) : ngraph::opset4::Constant::create(element::i32, Shape{}, {0}); // const auto& arg3 = new_args.size() > 3 ? new_args.at(3) : ngraph::opset4::Constant::create(element::f32, Shape{}, {.0f}); // const auto& arg4 = new_args.size() > 4 ? new_args.at(4) : ngraph::opset4::Constant::create(element::f32, Shape{}, {.0f}); @@ -105,9 +106,9 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { // nms_legacy->set_friendly_name(nms_4->get_friendly_name()); // ngraph::copy_runtime_info(nms_4, new_ops); // ngraph::replace_node(nms_4, nms_legacy); -// return true; -// }; -// -// auto m = std::make_shared(nms, "ConvertNMS4ToNMSLegacy"); -// this->register_matcher(m, callback); + return true; + }; + + auto m = std::make_shared(nms, "ConvertNMS5ToNMSLegacy"); + this->register_matcher(m, callback); } From 285ce509417ad80a8a9ce965c7caa6e324fb0489 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 6 Oct 2020 17:33:57 +0300 Subject: [PATCH 048/173] Small fixes. --- .../include/ngraph_ops/nms_ie.hpp | 1 + .../convert_nms_5_to_legacy.cpp | 54 ++++++++++--------- .../convert_opset1_to_legacy.cpp | 2 + 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/inference-engine/src/transformations/include/ngraph_ops/nms_ie.hpp b/inference-engine/src/transformations/include/ngraph_ops/nms_ie.hpp index 9f37d1c083235d..721392f8a42902 100644 --- a/inference-engine/src/transformations/include/ngraph_ops/nms_ie.hpp +++ b/inference-engine/src/transformations/include/ngraph_ops/nms_ie.hpp @@ -82,6 +82,7 @@ class TRANSFORMATIONS_API NonMaxSuppressionIE3 : public Op { int m_center_point_box; bool m_sort_result_descending = true; element::Type m_output_type; + private: int64_t max_boxes_output_from_input() const; }; diff --git a/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp b/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp index bee33707a10427..26cde5f87fe788 100644 --- a/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp +++ b/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp @@ -33,31 +33,35 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { } const auto new_args = nms_5->input_values(); -// const auto& arg2 = new_args.size() > 2 ? new_args.at(2) : ngraph::opset4::Constant::create(element::i32, Shape{}, {0}); -// const auto& arg3 = new_args.size() > 3 ? new_args.at(3) : ngraph::opset4::Constant::create(element::f32, Shape{}, {.0f}); -// const auto& arg4 = new_args.size() > 4 ? new_args.at(4) : ngraph::opset4::Constant::create(element::f32, Shape{}, {.0f}); -// -// const auto max_output_boxes_per_class_rank = arg2.get_partial_shape().rank(); -// const auto iou_threshold_rank = arg3.get_partial_shape().rank(); -// const auto score_threshold_rank = arg4.get_partial_shape().rank(); -// -// // Check that required ranks are not dynamic -// if (max_output_boxes_per_class_rank.is_dynamic() || -// iou_threshold_rank.is_dynamic() || -// score_threshold_rank.is_dynamic()) { -// return false; -// } -// -// if (max_output_boxes_per_class_rank.get_length() == 1 && -// iou_threshold_rank.get_length() == 1 && -// score_threshold_rank.get_length() == 1) { -// return false; -// } -// -// // vector of new nGraph operations -// NodeVector new_ops; -// -// auto new_max_per_class = arg2; + const auto& arg2 = new_args.size() > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i32, Shape{}, {0}); + const auto& arg3 = new_args.size() > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + const auto& arg4 = new_args.size() > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + const auto& arg5 = new_args.size() > 5 ? new_args.at(5) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + + const auto max_output_boxes_per_class_rank = arg2.get_partial_shape().rank(); + const auto iou_threshold_rank = arg3.get_partial_shape().rank(); + const auto score_threshold_rank = arg4.get_partial_shape().rank(); + const auto soft_nms_sigma_rank = arg5.get_partial_shape().rank(); + + // Check that required ranks are not dynamic + if (max_output_boxes_per_class_rank.is_dynamic() || + iou_threshold_rank.is_dynamic() || + score_threshold_rank.is_dynamic() || + soft_nms_sigma_rank.is_dynamic()) { + return false; + } + + if (max_output_boxes_per_class_rank.get_length() == 1 && + iou_threshold_rank.get_length() == 1 && + score_threshold_rank.get_length() == 1 && + soft_nms_sigma_rank.get_length() == 1) { + return false; + } + + // vector of new nGraph operations + NodeVector new_ops; + + auto new_max_per_class = arg2; // if (max_output_boxes_per_class_rank.get_length() == 0) { // // WA: we need to create Constant manually because it requires by NMS shape inference // // otherwise we will get dynamic shape until first CF is executed. It can be resolved diff --git a/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_opset1_to_legacy.cpp b/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_opset1_to_legacy.cpp index b6784268879769..204c8c017fe1b0 100644 --- a/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_opset1_to_legacy.cpp +++ b/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_opset1_to_legacy.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -140,6 +141,7 @@ bool ngraph::pass::ConvertOpSet1ToLegacy::run_on_function(std::shared_ptradd_matcher(); anchor->add_matcher(); anchor->add_matcher(); + anchor->add_matcher(); anchor->add_matcher(); anchor->add_matcher(); anchor->add_matcher(); From 8063e01d6019fc20a70bdfc8b84d3b4663c7b179 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 7 Oct 2020 09:21:16 +0300 Subject: [PATCH 049/173] Fixed typo. --- inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp b/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp index 2cf573304f7385..ce039843d9d701 100644 --- a/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp +++ b/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp @@ -120,7 +120,7 @@ op::NonMaxSuppressionIE3::NonMaxSuppressionIE3(const Output& boxes, std::shared_ptr op::NonMaxSuppressionIE3::clone_with_new_inputs(const ngraph::OutputVector &new_args) const { check_new_args_count(this, new_args); - return make_shared(new_args.at(0), new_args.at(1), new_args.at(2), new_args.at(3), + return make_shared(new_args.at(0), new_args.at(1), new_args.at(2), new_args.at(3), new_args.at(4), new_args.at(5), m_center_point_box, m_sort_result_descending, m_output_type); } From c2db7479d7a9ecce7961180b415713c7021b4526 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 7 Oct 2020 09:44:54 +0300 Subject: [PATCH 050/173] Fixed typos. --- .../convert_nms_5_to_legacy.hpp | 2 +- .../convert_nms_5_to_legacy.cpp | 72 ++++++++++--------- 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/inference-engine/src/transformations/include/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp b/inference-engine/src/transformations/include/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp index 11fd03444f126c..5fd6b3ade95f60 100644 --- a/inference-engine/src/transformations/include/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp +++ b/inference-engine/src/transformations/include/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp @@ -14,7 +14,7 @@ namespace ngraph { namespace pass { - class TRANSFORMATIONS_API ConvertNMS4ToLegacyMatcher; + class TRANSFORMATIONS_API ConvertNMS5ToLegacyMatcher; } // namespace pass } // namespace ngraph diff --git a/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp b/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp index 26cde5f87fe788..01c139fff0d41c 100644 --- a/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp +++ b/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp @@ -62,40 +62,44 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { NodeVector new_ops; auto new_max_per_class = arg2; -// if (max_output_boxes_per_class_rank.get_length() == 0) { -// // WA: we need to create Constant manually because it requires by NMS shape inference -// // otherwise we will get dynamic shape until first CF is executed. It can be resolved -// // if CF will be executed right after transformation and before Validate pass. -// if (auto new_max_per_class_const = std::dynamic_pointer_cast(new_max_per_class.get_node_shared_ptr())) { -// new_max_per_class = opset1::Constant::create(element::i64, Shape{1}, new_max_per_class_const->cast_vector()); -// } else { -// new_max_per_class = std::make_shared(arg2, opset1::Constant::create(element::i64, Shape{1}, {0})); -// new_ops.push_back(new_max_per_class.get_node_shared_ptr()); -// } -// } -// auto new_iou_threshold = arg3; -// if (iou_threshold_rank.get_length() == 0) { -// new_iou_threshold = std::make_shared(arg3, opset1::Constant::create(element::i64, Shape{1}, {0})); -// new_ops.push_back(new_iou_threshold.get_node_shared_ptr()); -// } -// auto new_score_threshold = arg4; -// if (score_threshold_rank.get_length() == 0) { -// new_score_threshold = std::make_shared(arg4, opset1::Constant::create(element::i64, Shape{1}, {0})); -// new_ops.push_back(new_score_threshold.get_node_shared_ptr()); -// } -// -// int center_point_box = 0; -// switch (nms_4->get_box_encoding()) { -// case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CENTER: -// center_point_box = 1; -// break; -// case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CORNER: -// center_point_box = 0; -// break; -// default: -// throw ngraph_error("NonMaxSuppression layer " + nms_4->get_friendly_name() + -// " has unsupported box encoding"); -// } + if (max_output_boxes_per_class_rank.get_length() == 0) { + // WA: we need to create Constant manually because it requires by NMS shape inference + // otherwise we will get dynamic shape until first CF is executed. It can be resolved + // if CF will be executed right after transformation and before Validate pass. + if (auto new_max_per_class_const = std::dynamic_pointer_cast(new_max_per_class.get_node_shared_ptr())) { + new_max_per_class = opset1::Constant::create(element::i64, Shape{1}, new_max_per_class_const->cast_vector()); + } else { + new_max_per_class = std::make_shared(arg2, opset1::Constant::create(element::i64, Shape{1}, {0})); + new_ops.push_back(new_max_per_class.get_node_shared_ptr()); + } + } + auto new_iou_threshold = arg3; + if (iou_threshold_rank.get_length() == 0) { + new_iou_threshold = std::make_shared(arg3, opset1::Constant::create(element::f32, Shape{1}, {0.0f})); + new_ops.push_back(new_iou_threshold.get_node_shared_ptr()); + } + auto new_score_threshold = arg4; + if (score_threshold_rank.get_length() == 0) { + new_score_threshold = std::make_shared(arg4, opset1::Constant::create(element::f32, Shape{1}, {0.0f})); + new_ops.push_back(new_score_threshold.get_node_shared_ptr()); + } + auto new_soft_nms_sigma = arg5; + if (soft_nms_sigma_rank.get_length() == 0) { + new_soft_nms_sigma = std::make_shared(arg5, opset1::Constant::create(element::f32, Shape{1}, {0.0f})); + new_ops.push_back(new_soft_nms_sigma.get_node_shared_ptr()); + } + int center_point_box = 0; + switch (nms_5->get_box_encoding()) { + case ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER: + center_point_box = 1; + break; + case ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER: + center_point_box = 0; + break; + default: + throw ngraph_error("NonMaxSuppression layer " + nms_5->get_friendly_name() + + " has unsupported box encoding"); + } // const auto nms_legacy = std::make_shared( // new_args.at(0), // new_args.at(1), From eb861a9e9c0ac7236ca604b092687d77714b5204 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 7 Oct 2020 11:00:18 +0300 Subject: [PATCH 051/173] Written draft of the transformation ConvertNMS5ToLegacyMatcher that converts ngraph::opset5::NonMaxSuppression into op::NonMaxSuppressionIE3. --- .../convert_nms_5_to_legacy.hpp | 2 +- .../convert_nms_5_to_legacy.cpp | 29 ++++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/inference-engine/src/transformations/include/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp b/inference-engine/src/transformations/include/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp index 5fd6b3ade95f60..1dcfcbe1e2c4a8 100644 --- a/inference-engine/src/transformations/include/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp +++ b/inference-engine/src/transformations/include/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp @@ -28,6 +28,6 @@ namespace pass { class ngraph::pass::ConvertNMS5ToLegacyMatcher: public ngraph::pass::MatcherPass { public: NGRAPH_RTTI_DECLARATION; - ConvertNMS4ToLegacyMatcher(); + ConvertNMS5ToLegacyMatcher(); }; diff --git a/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp b/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp index 01c139fff0d41c..a633b103501639 100644 --- a/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp +++ b/inference-engine/src/transformations/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp @@ -100,20 +100,21 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { throw ngraph_error("NonMaxSuppression layer " + nms_5->get_friendly_name() + " has unsupported box encoding"); } -// const auto nms_legacy = std::make_shared( -// new_args.at(0), -// new_args.at(1), -// new_max_per_class, -// new_iou_threshold, -// new_score_threshold, -// center_point_box, -// nms_4->get_sort_result_descending(), -// nms_4->get_output_type()); -// new_ops.push_back(nms_legacy); -// -// nms_legacy->set_friendly_name(nms_4->get_friendly_name()); -// ngraph::copy_runtime_info(nms_4, new_ops); -// ngraph::replace_node(nms_4, nms_legacy); + const auto nms_legacy = std::make_shared( + new_args.at(0), + new_args.at(1), + new_max_per_class, + new_iou_threshold, + new_score_threshold, + new_soft_nms_sigma, + center_point_box, + nms_5->get_sort_result_descending(), + nms_5->get_output_type()); + new_ops.push_back(nms_legacy); + + nms_legacy->set_friendly_name(nms_5->get_friendly_name()); + ngraph::copy_runtime_info(nms_5, new_ops); + ngraph::replace_node(nms_5, nms_legacy); return true; }; From 88e17964a1bff052ebf991c65d1766e777119888 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 7 Oct 2020 11:46:49 +0300 Subject: [PATCH 052/173] Written header file for the transformations from NMS-1, NMS-3, NMS-4 to NMS-5. --- .../convert_previous_nms_to_nms_5.hpp | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 inference-engine/src/transformations/include/transformations/convert_previous_nms_to_nms_5.hpp diff --git a/inference-engine/src/transformations/include/transformations/convert_previous_nms_to_nms_5.hpp b/inference-engine/src/transformations/include/transformations/convert_previous_nms_to_nms_5.hpp new file mode 100644 index 00000000000000..9b7d610dc73d07 --- /dev/null +++ b/inference-engine/src/transformations/include/transformations/convert_previous_nms_to_nms_5.hpp @@ -0,0 +1,39 @@ +// Copyright (C) 2018-2020 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#pragma once + +#include +#include + +#include +#include + +namespace ngraph { +namespace pass { + +class TRANSFORMATIONS_API ConvertNMS1ToNMS5; +class TRANSFORMATIONS_API ConvertNMS3ToNMS5; +class TRANSFORMATIONS_API ConvertNMS4ToNMS5; + +} // namespace pass +} // namespace ngraph + +class ngraph::pass::ConvertNMS1ToNMS5: public ngraph::pass::MatcherPass { +public: + NGRAPH_RTTI_DECLARATION; + ConvertNMS1ToNMS5(); +}; + +class ngraph::pass::ConvertNMS3ToNMS5: public ngraph::pass::MatcherPass { +public: + NGRAPH_RTTI_DECLARATION; + ConvertNMS3ToNMS5(); +}; + +class ngraph::pass::ConvertNMS4ToNMS5: public ngraph::pass::MatcherPass { +public: + NGRAPH_RTTI_DECLARATION; + ConvertNMS4ToNMS5(); +}; From e662ec4efef1da387ccbeec52eb68be06d7f44e5 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 7 Oct 2020 12:54:13 +0300 Subject: [PATCH 053/173] Started to write conversion of NMS-4 to NMS-5. --- .../convert_previous_nms_to_nms_5.cpp | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp diff --git a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp new file mode 100644 index 00000000000000..4bbb1e492d42f2 --- /dev/null +++ b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp @@ -0,0 +1,112 @@ +// Copyright (C) 2018-2020 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include "transformations/convert_previous_nms_to_nms_5.hpp" + +#include +#include + +#include +#include +#include +#include +#include + +NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS4ToNMS5, "ConvertNMS4ToNMS5", 0); + +ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = ngraph::opset4::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.7}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, + iou_threshold, score_threshold); + + ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { + auto nms_4 = std::dynamic_pointer_cast(m.get_match_root()); + if (!nms_4) { + return false; + } + +// const auto new_args = nms_4->input_values(); +// const auto& arg2 = new_args.size() > 2 ? new_args.at(2) : ngraph::opset4::Constant::create(element::i32, Shape{}, {0}); +// const auto& arg3 = new_args.size() > 3 ? new_args.at(3) : ngraph::opset4::Constant::create(element::f32, Shape{}, {.0f}); +// const auto& arg4 = new_args.size() > 4 ? new_args.at(4) : ngraph::opset4::Constant::create(element::f32, Shape{}, {.0f}); +// +// const auto max_output_boxes_per_class_rank = arg2.get_partial_shape().rank(); +// const auto iou_threshold_rank = arg3.get_partial_shape().rank(); +// const auto score_threshold_rank = arg4.get_partial_shape().rank(); +// +// // Check that required ranks are not dynamic +// if (max_output_boxes_per_class_rank.is_dynamic() || +// iou_threshold_rank.is_dynamic() || +// score_threshold_rank.is_dynamic()) { +// return false; +// } +// +// if (max_output_boxes_per_class_rank.get_length() == 1 && +// iou_threshold_rank.get_length() == 1 && +// score_threshold_rank.get_length() == 1) { +// return false; +// } +// +// // vector of new nGraph operations +// NodeVector new_ops; +// +// auto new_max_per_class = arg2; +// if (max_output_boxes_per_class_rank.get_length() == 0) { +// // WA: we need to create Constant manually because it requires by NMS shape inference +// // otherwise we will get dynamic shape until first CF is executed. It can be resolved +// // if CF will be executed right after transformation and before Validate pass. +// if (auto new_max_per_class_const = std::dynamic_pointer_cast(new_max_per_class.get_node_shared_ptr())) { +// new_max_per_class = opset1::Constant::create(element::i64, Shape{1}, new_max_per_class_const->cast_vector()); +// } else { +// new_max_per_class = std::make_shared(arg2, opset1::Constant::create(element::i64, Shape{1}, {0})); +// new_ops.push_back(new_max_per_class.get_node_shared_ptr()); +// } +// } +// auto new_iou_threshold = arg3; +// if (iou_threshold_rank.get_length() == 0) { +// new_iou_threshold = std::make_shared(arg3, opset1::Constant::create(element::i64, Shape{1}, {0})); +// new_ops.push_back(new_iou_threshold.get_node_shared_ptr()); +// } +// auto new_score_threshold = arg4; +// if (score_threshold_rank.get_length() == 0) { +// new_score_threshold = std::make_shared(arg4, opset1::Constant::create(element::i64, Shape{1}, {0})); +// new_ops.push_back(new_score_threshold.get_node_shared_ptr()); +// } +// +// int center_point_box = 0; +// switch (nms_4->get_box_encoding()) { +// case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CENTER: +// center_point_box = 1; +// break; +// case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CORNER: +// center_point_box = 0; +// break; +// default: +// throw ngraph_error("NonMaxSuppression layer " + nms_4->get_friendly_name() + +// " has unsupported box encoding"); +// } +// const auto nms_legacy = std::make_shared( +// new_args.at(0), +// new_args.at(1), +// new_max_per_class, +// new_iou_threshold, +// new_score_threshold, +// center_point_box, +// nms_4->get_sort_result_descending(), +// nms_4->get_output_type()); +// new_ops.push_back(nms_legacy); +// +// nms_legacy->set_friendly_name(nms_4->get_friendly_name()); +// ngraph::copy_runtime_info(nms_4, new_ops); +// ngraph::replace_node(nms_4, nms_legacy); + return true; + }; + +// auto m = std::make_shared(nms, "ConvertNMS4ToNMSLegacy"); +// this->register_matcher(m, callback); +} From d8d0a191b81ab3b50c67b18e6a0fe71b1a32262f Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 7 Oct 2020 13:18:40 +0300 Subject: [PATCH 054/173] Added include for ngraph/opsets/opset4.hpp. --- .../src/transformations/convert_previous_nms_to_nms_5.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp index 4bbb1e492d42f2..01d0a0434fd26a 100644 --- a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include From f6d5ea08945966d77ca278bfd87c44c31011bdd4 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 7 Oct 2020 14:48:49 +0300 Subject: [PATCH 055/173] Started to write conversion of NMS-3 to NMS-5. --- .../convert_previous_nms_to_nms_5.cpp | 147 +++++++++--------- .../include/ngraph/op/non_max_suppression.hpp | 2 +- 2 files changed, 72 insertions(+), 77 deletions(-) diff --git a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp index 01d0a0434fd26a..6e7d2e91bcbd8c 100644 --- a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp @@ -4,6 +4,7 @@ #include "transformations/convert_previous_nms_to_nms_5.hpp" +#include #include #include @@ -31,83 +32,77 @@ ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { return false; } -// const auto new_args = nms_4->input_values(); -// const auto& arg2 = new_args.size() > 2 ? new_args.at(2) : ngraph::opset4::Constant::create(element::i32, Shape{}, {0}); -// const auto& arg3 = new_args.size() > 3 ? new_args.at(3) : ngraph::opset4::Constant::create(element::f32, Shape{}, {.0f}); -// const auto& arg4 = new_args.size() > 4 ? new_args.at(4) : ngraph::opset4::Constant::create(element::f32, Shape{}, {.0f}); -// -// const auto max_output_boxes_per_class_rank = arg2.get_partial_shape().rank(); -// const auto iou_threshold_rank = arg3.get_partial_shape().rank(); -// const auto score_threshold_rank = arg4.get_partial_shape().rank(); -// -// // Check that required ranks are not dynamic -// if (max_output_boxes_per_class_rank.is_dynamic() || -// iou_threshold_rank.is_dynamic() || -// score_threshold_rank.is_dynamic()) { -// return false; -// } -// -// if (max_output_boxes_per_class_rank.get_length() == 1 && -// iou_threshold_rank.get_length() == 1 && -// score_threshold_rank.get_length() == 1) { -// return false; -// } -// -// // vector of new nGraph operations -// NodeVector new_ops; -// -// auto new_max_per_class = arg2; -// if (max_output_boxes_per_class_rank.get_length() == 0) { -// // WA: we need to create Constant manually because it requires by NMS shape inference -// // otherwise we will get dynamic shape until first CF is executed. It can be resolved -// // if CF will be executed right after transformation and before Validate pass. -// if (auto new_max_per_class_const = std::dynamic_pointer_cast(new_max_per_class.get_node_shared_ptr())) { -// new_max_per_class = opset1::Constant::create(element::i64, Shape{1}, new_max_per_class_const->cast_vector()); -// } else { -// new_max_per_class = std::make_shared(arg2, opset1::Constant::create(element::i64, Shape{1}, {0})); -// new_ops.push_back(new_max_per_class.get_node_shared_ptr()); -// } -// } -// auto new_iou_threshold = arg3; -// if (iou_threshold_rank.get_length() == 0) { -// new_iou_threshold = std::make_shared(arg3, opset1::Constant::create(element::i64, Shape{1}, {0})); -// new_ops.push_back(new_iou_threshold.get_node_shared_ptr()); -// } -// auto new_score_threshold = arg4; -// if (score_threshold_rank.get_length() == 0) { -// new_score_threshold = std::make_shared(arg4, opset1::Constant::create(element::i64, Shape{1}, {0})); -// new_ops.push_back(new_score_threshold.get_node_shared_ptr()); -// } -// -// int center_point_box = 0; -// switch (nms_4->get_box_encoding()) { -// case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CENTER: -// center_point_box = 1; -// break; -// case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CORNER: -// center_point_box = 0; -// break; -// default: -// throw ngraph_error("NonMaxSuppression layer " + nms_4->get_friendly_name() + -// " has unsupported box encoding"); -// } -// const auto nms_legacy = std::make_shared( -// new_args.at(0), -// new_args.at(1), -// new_max_per_class, -// new_iou_threshold, -// new_score_threshold, -// center_point_box, -// nms_4->get_sort_result_descending(), -// nms_4->get_output_type()); -// new_ops.push_back(nms_legacy); -// -// nms_legacy->set_friendly_name(nms_4->get_friendly_name()); -// ngraph::copy_runtime_info(nms_4, new_ops); -// ngraph::replace_node(nms_4, nms_legacy); + const auto new_args = nms_4->input_values(); + + size_t num_of_args = new_args.size(); + + const auto& arg2 = num_of_args > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i32, Shape{}, {0}); + const auto& arg3 = num_of_args > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + const auto& arg4 = num_of_args > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + const auto& arg5 = ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + + auto box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; + switch (nms_4->get_box_encoding()) { + case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CENTER: + center_point_box = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER;; + break; + case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CORNER: + center_point_box = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; + break; + default: + throw ngraph_error("NonMaxSuppression layer " + nms_4->get_friendly_name() + + " has unsupported box encoding"); + } + + // list of new nGraph operations + std::list> new_ops_list; + + new_ops_list.push_front(arg5); + if (num_of_args <= 4) { + new_ops_list.push_front(arg4); + } + if (num_of_args <= 3) { + new_ops_list.push_front(arg3); + } + if (num_of_args <= 2) { + new_ops_list.push_front(arg2); + } + + const auto nms_5 = std::make_shared( + new_args.at(0), + new_args.at(1), + arg2, + arg3, + arg4, + arg5, + box_encoding, + nms_4->get_sort_result_descending(), + nms_4->get_output_type()); + + new_ops_list.push_back(nms_5); + + // vector of new nGraph operations + NodeVector new_ops(new_ops_list.begin(), new_ops_list.end()); + + nms_5->set_friendly_name(nms_4->get_friendly_name()); + ngraph::copy_runtime_info(nms_4, new_ops); + ngraph::replace_node(nms_4, nms_5); return true; }; -// auto m = std::make_shared(nms, "ConvertNMS4ToNMSLegacy"); -// this->register_matcher(m, callback); + auto m = std::make_shared(nms, "ConvertNMS4ToNMS5"); + this->register_matcher(m, callback); +} + + +NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS3ToNMS5, "ConvertNMS3ToNMS5", 0); + +ngraph::pass::ConvertNMS3ToNMS5::ConvertNMS3ToNMS5() { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = ngraph::opset3::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = ngraph::opset3::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = ngraph::opset3::Constant::create(element::f32, Shape{}, {0.7}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, + iou_threshold, score_threshold); } diff --git a/ngraph/core/include/ngraph/op/non_max_suppression.hpp b/ngraph/core/include/ngraph/op/non_max_suppression.hpp index 5ba5b63b52a8a1..a3828e9f65f704 100644 --- a/ngraph/core/include/ngraph/op/non_max_suppression.hpp +++ b/ngraph/core/include/ngraph/op/non_max_suppression.hpp @@ -253,7 +253,7 @@ namespace ngraph const NodeTypeInfo& get_type_info() const override { return type_info; } NonMaxSuppression() = default; - /// \brief Constructs a NonMaxSuppression operation with default values in the last. + /// \brief Constructs a NonMaxSuppression operation with default values in the last /// 4 inputs. /// /// \param boxes Node producing the box coordinates From f2adb311e397c0c3220d4c85fd25d00ade65a41c Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 7 Oct 2020 15:19:07 +0300 Subject: [PATCH 056/173] Small fixes. --- .../convert_previous_nms_to_nms_5.cpp | 77 +++++++++++++++++-- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp index 6e7d2e91bcbd8c..145436e8874f23 100644 --- a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp @@ -44,10 +44,10 @@ ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { auto box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; switch (nms_4->get_box_encoding()) { case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CENTER: - center_point_box = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER;; + box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER;; break; case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CORNER: - center_point_box = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; + box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; break; default: throw ngraph_error("NonMaxSuppression layer " + nms_4->get_friendly_name() + @@ -59,13 +59,13 @@ ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { new_ops_list.push_front(arg5); if (num_of_args <= 4) { - new_ops_list.push_front(arg4); + new_ops_list.push_front(arg4.get_node_shared_ptr()); } if (num_of_args <= 3) { - new_ops_list.push_front(arg3); + new_ops_list.push_front(arg3.get_node_shared_ptr()); } if (num_of_args <= 2) { - new_ops_list.push_front(arg2); + new_ops_list.push_front(arg2.get_node_shared_ptr()); } const auto nms_5 = std::make_shared( @@ -105,4 +105,71 @@ ngraph::pass::ConvertNMS3ToNMS5::ConvertNMS3ToNMS5() { auto score_threshold = ngraph::opset3::Constant::create(element::f32, Shape{}, {0.7}); auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); + + ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { + auto nms_3 = std::dynamic_pointer_cast(m.get_match_root()); + if (!nms_3) { + return false; + } + + const auto new_args = nms_3->input_values(); + + size_t num_of_args = new_args.size(); + + const auto& arg2 = num_of_args > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i32, Shape{}, {0}); + const auto& arg3 = num_of_args > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + const auto& arg4 = num_of_args > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + const auto& arg5 = ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + +// auto box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; +// switch (nms_4->get_box_encoding()) { +// case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CENTER: +// center_point_box = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER;; +// break; +// case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CORNER: +// center_point_box = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; +// break; +// default: +// throw ngraph_error("NonMaxSuppression layer " + nms_4->get_friendly_name() + +// " has unsupported box encoding"); +// } +// +// // list of new nGraph operations +// std::list> new_ops_list; +// +// new_ops_list.push_front(arg5); +// if (num_of_args <= 4) { +// new_ops_list.push_front(arg4); +// } +// if (num_of_args <= 3) { +// new_ops_list.push_front(arg3); +// } +// if (num_of_args <= 2) { +// new_ops_list.push_front(arg2); +// } +// +// const auto nms_5 = std::make_shared( +// new_args.at(0), +// new_args.at(1), +// arg2, +// arg3, +// arg4, +// arg5, +// box_encoding, +// nms_4->get_sort_result_descending(), +// nms_4->get_output_type()); +// +// new_ops_list.push_back(nms_5); +// +// // vector of new nGraph operations +// NodeVector new_ops(new_ops_list.begin(), new_ops_list.end()); +// +// nms_5->set_friendly_name(nms_4->get_friendly_name()); +// ngraph::copy_runtime_info(nms_4, new_ops); +// ngraph::replace_node(nms_4, nms_5); +// return true; +// }; +// +// auto m = std::make_shared(nms, "ConvertNMS4ToNMS5"); +// this->register_matcher(m, callback); } From 8fa5661b424fe461e26bfa8773ca6b92271061f3 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 7 Oct 2020 15:48:26 +0300 Subject: [PATCH 057/173] Written draft of the conversion of NMS-3 into NMS-5. --- .../convert_previous_nms_to_nms_5.cpp | 102 +++++++++--------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp index 145436e8874f23..8975aeca432a5e 100644 --- a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp @@ -121,55 +121,55 @@ ngraph::pass::ConvertNMS3ToNMS5::ConvertNMS3ToNMS5() { const auto& arg4 = num_of_args > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); const auto& arg5 = ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); -// auto box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; -// switch (nms_4->get_box_encoding()) { -// case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CENTER: -// center_point_box = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER;; -// break; -// case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CORNER: -// center_point_box = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; -// break; -// default: -// throw ngraph_error("NonMaxSuppression layer " + nms_4->get_friendly_name() + -// " has unsupported box encoding"); -// } -// -// // list of new nGraph operations -// std::list> new_ops_list; -// -// new_ops_list.push_front(arg5); -// if (num_of_args <= 4) { -// new_ops_list.push_front(arg4); -// } -// if (num_of_args <= 3) { -// new_ops_list.push_front(arg3); -// } -// if (num_of_args <= 2) { -// new_ops_list.push_front(arg2); -// } -// -// const auto nms_5 = std::make_shared( -// new_args.at(0), -// new_args.at(1), -// arg2, -// arg3, -// arg4, -// arg5, -// box_encoding, -// nms_4->get_sort_result_descending(), -// nms_4->get_output_type()); -// -// new_ops_list.push_back(nms_5); -// -// // vector of new nGraph operations -// NodeVector new_ops(new_ops_list.begin(), new_ops_list.end()); -// -// nms_5->set_friendly_name(nms_4->get_friendly_name()); -// ngraph::copy_runtime_info(nms_4, new_ops); -// ngraph::replace_node(nms_4, nms_5); -// return true; -// }; -// -// auto m = std::make_shared(nms, "ConvertNMS4ToNMS5"); -// this->register_matcher(m, callback); + auto box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; + switch (nms_3->get_box_encoding()) { + case ::ngraph::opset3::NonMaxSuppression::BoxEncodingType::CENTER: + center_point_box = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER;; + break; + case ::ngraph::opset3::NonMaxSuppression::BoxEncodingType::CORNER: + center_point_box = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; + break; + default: + throw ngraph_error("NonMaxSuppression layer " + nms_4->get_friendly_name() + + " has unsupported box encoding"); + } + + // list of new nGraph operations + std::list> new_ops_list; + + new_ops_list.push_front(arg5); + if (num_of_args <= 4) { + new_ops_list.push_front(arg4.get_node_shared_ptr()); + } + if (num_of_args <= 3) { + new_ops_list.push_front(arg3.get_node_shared_ptr()); + } + if (num_of_args <= 2) { + new_ops_list.push_front(arg2.get_node_shared_ptr()); + } + + const auto nms_5 = std::make_shared( + new_args.at(0), + new_args.at(1), + arg2, + arg3, + arg4, + arg5, + box_encoding, + nms_3->get_sort_result_descending(), + nms_3->get_output_type()); + + new_ops_list.push_back(nms_5); + + // vector of new nGraph operations + NodeVector new_ops(new_ops_list.begin(), new_ops_list.end()); + + nms_5->set_friendly_name(nms_3->get_friendly_name()); + ngraph::copy_runtime_info(nms_3, new_ops); + ngraph::replace_node(nms_3, nms_5); + return true; + }; + + auto m = std::make_shared(nms, "ConvertNMS3ToNMS5"); + this->register_matcher(m, callback); } From 57e4042f962b0f574835ab6043bea1226b111a01 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 7 Oct 2020 15:49:53 +0300 Subject: [PATCH 058/173] Fixed typo. --- .../src/transformations/convert_previous_nms_to_nms_5.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp index 8975aeca432a5e..dfee87a053be49 100644 --- a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp @@ -130,7 +130,7 @@ ngraph::pass::ConvertNMS3ToNMS5::ConvertNMS3ToNMS5() { center_point_box = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; break; default: - throw ngraph_error("NonMaxSuppression layer " + nms_4->get_friendly_name() + + throw ngraph_error("NonMaxSuppression layer " + nms_3->get_friendly_name() + " has unsupported box encoding"); } From 515dbb7149ddbad89758aeda96b401cc9ccdd76a Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 7 Oct 2020 16:15:45 +0300 Subject: [PATCH 059/173] Started to write conversion of NMS-1 to NMS-5. --- .../convert_previous_nms_to_nms_5.cpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp index dfee87a053be49..123269ea3d3a14 100644 --- a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp @@ -44,7 +44,7 @@ ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { auto box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; switch (nms_4->get_box_encoding()) { case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CENTER: - box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER;; + box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; break; case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CORNER: box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; @@ -124,10 +124,10 @@ ngraph::pass::ConvertNMS3ToNMS5::ConvertNMS3ToNMS5() { auto box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; switch (nms_3->get_box_encoding()) { case ::ngraph::opset3::NonMaxSuppression::BoxEncodingType::CENTER: - center_point_box = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER;; + box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; break; case ::ngraph::opset3::NonMaxSuppression::BoxEncodingType::CORNER: - center_point_box = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; + box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; break; default: throw ngraph_error("NonMaxSuppression layer " + nms_3->get_friendly_name() + @@ -173,3 +173,15 @@ ngraph::pass::ConvertNMS3ToNMS5::ConvertNMS3ToNMS5() { auto m = std::make_shared(nms, "ConvertNMS3ToNMS5"); this->register_matcher(m, callback); } + +NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS1ToNMS5, "ConvertNMS1ToNMS5", 0); + +ngraph::pass::ConvertNMS1ToNMS5::ConvertNMS1ToNMS5() { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = ngraph::opset1::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = ngraph::opset1::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = ngraph::opset1::Constant::create(element::f32, Shape{}, {0.7}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, + iou_threshold, score_threshold); +} From a0b0a3fd2af4cef547429b44cfe06ca88195074c Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 7 Oct 2020 16:50:49 +0300 Subject: [PATCH 060/173] Written draft of the conversion NMS-1 to NMS-5. --- .../convert_previous_nms_to_nms_5.cpp | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp index 123269ea3d3a14..5c7dbd8dfb7c24 100644 --- a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp @@ -184,4 +184,71 @@ ngraph::pass::ConvertNMS1ToNMS5::ConvertNMS1ToNMS5() { auto score_threshold = ngraph::opset1::Constant::create(element::f32, Shape{}, {0.7}); auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold); + + ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { + auto nms_1 = std::dynamic_pointer_cast(m.get_match_root()); + if (!nms_1) { + return false; + } + + const auto new_args = nms_1->input_values(); + + size_t num_of_args = new_args.size(); + + const auto& arg2 = num_of_args > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i32, Shape{}, {0}); + const auto& arg3 = num_of_args > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + const auto& arg4 = num_of_args > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + const auto& arg5 = ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + + auto box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; + switch (nms_1->get_box_encoding()) { + case ::ngraph::opset1::NonMaxSuppression::BoxEncodingType::CENTER: + box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; + break; + case ::ngraph::opset1::NonMaxSuppression::BoxEncodingType::CORNER: + box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; + break; + default: + throw ngraph_error("NonMaxSuppression layer " + nms_1->get_friendly_name() + + " has unsupported box encoding"); + } + + // list of new nGraph operations + std::list> new_ops_list; + + new_ops_list.push_front(arg5); + if (num_of_args <= 4) { + new_ops_list.push_front(arg4.get_node_shared_ptr()); + } + if (num_of_args <= 3) { + new_ops_list.push_front(arg3.get_node_shared_ptr()); + } + if (num_of_args <= 2) { + new_ops_list.push_front(arg2.get_node_shared_ptr()); + } + + const auto nms_5 = std::make_shared( + new_args.at(0), + new_args.at(1), + arg2, + arg3, + arg4, + arg5, + box_encoding, + nms_1->get_sort_result_descending(), + ::ngraph::element::i64); + + new_ops_list.push_back(nms_5); + + // vector of new nGraph operations + NodeVector new_ops(new_ops_list.begin(), new_ops_list.end()); + + nms_5->set_friendly_name(nms_1->get_friendly_name()); + ngraph::copy_runtime_info(nms_1, new_ops); + ngraph::replace_node(nms_1, nms_5); + return true; + }; + + auto m = std::make_shared(nms, "ConvertNMS1ToNMS5"); + this->register_matcher(m, callback); } From 56b52ddb0c7554ed48f3dae7fe6b2126329e5517 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 7 Oct 2020 17:37:35 +0300 Subject: [PATCH 061/173] Started to write tests for the conversion nGraph NMS-5 to inner NMS. --- .../common_optimizations.cpp | 5 + .../transformations/convert_nms5_test.cpp | 148 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp diff --git a/inference-engine/src/transformations/src/transformations/common_optimizations/common_optimizations.cpp b/inference-engine/src/transformations/src/transformations/common_optimizations/common_optimizations.cpp index fae8de5cfcbae4..2c2b85c8f15bf8 100644 --- a/inference-engine/src/transformations/src/transformations/common_optimizations/common_optimizations.cpp +++ b/inference-engine/src/transformations/src/transformations/common_optimizations/common_optimizations.cpp @@ -41,6 +41,7 @@ #include "transformations/remove_filtering_boxes_by_size.hpp" #include "transformations/hswish_decomposition.hpp" #include "transformations/hswish_fusion.hpp" +#include "transformations/convert_previous_nms_to_nms_5.hpp" #include #include @@ -105,6 +106,10 @@ bool ngraph::pass::CommonOptimizations::run_on_function(std::shared_ptr(); manager.register_pass(); + manager.register_pass(); + manager.register_pass(); + manager.register_pass(); + auto fq_fusions = manager.register_pass(); fq_fusions->add_matcher(); fq_fusions->add_matcher(); diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp new file mode 100644 index 00000000000000..73d519d7e2f2e9 --- /dev/null +++ b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp @@ -0,0 +1,148 @@ +// Copyright (C) 2020 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "common_test_utils/ngraph_test_utils.hpp" + +using namespace testing; +using namespace ngraph; + +TEST(TransformationTests, ConvertNMS5ToNMSIEStatic) { + std::shared_ptr f(nullptr), f_ref(nullptr); +// { +// auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); +// auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); +// auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{}, {10}); +// auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); +// auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); +// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, +// opset4::NonMaxSuppression::BoxEncodingType::CORNER, true); +// +// f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); +// +// const auto &orig_shape = f->get_output_partial_shape(0); +// pass::Manager manager; +// manager.register_pass(); +// manager.register_pass(); +// manager.run_passes(f); +// ASSERT_NO_THROW(check_rt_info(f)); +// ASSERT_TRUE(f->get_output_partial_shape(0).is_static()) << "Shape " << f->get_output_partial_shape(0) << " should be static"; +// } +// +// { +// auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); +// auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); +// auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{1}, {10}); +// auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); +// auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); +// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, +// std::make_shared(iou_threshold, +// opset4::Constant::create(element::i64, Shape{1}, {0})), +// std::make_shared(score_threshold, +// opset4::Constant::create(element::i64, Shape{1}, {0})), +// 0, true); +// nms->set_friendly_name("nms"); +// +// f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); +// ASSERT_TRUE(f_ref->get_output_partial_shape(0).is_static()) << "Shape " << f_ref->get_output_partial_shape(0) << " should be static"; +// } +// +// auto res = compare_functions(f, f_ref); +// ASSERT_TRUE(res.first) << res.second; +} + +// TEST(TransformationTests, ConvertNMS4ToNMSIEDynamic1) { +// std::shared_ptr f(nullptr), f_ref(nullptr); +// { +// auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); +// auto scores = std::make_shared(element::f32, PartialShape::dynamic()); +// auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{}, {10}); +// auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); +// auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); +// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, +// opset4::NonMaxSuppression::BoxEncodingType::CORNER, true, element::i32); +// +// f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); +// +// pass::Manager manager; +// manager.register_pass(); +// manager.register_pass(); +// manager.run_passes(f); +// f->validate_nodes_and_infer_types(); +// ASSERT_NO_THROW(check_rt_info(f)); +// } +// +// { +// auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); +// auto scores = std::make_shared(element::f32, PartialShape::dynamic()); +// auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{1}, {10}); +// auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); +// auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); +// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, +// std::make_shared(iou_threshold, +// opset4::Constant::create(element::i64, Shape{1}, {0})), +// std::make_shared(score_threshold, +// opset4::Constant::create(element::i64, Shape{1}, {0})), +// 0, true); +// +// f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); +// } +// +// auto res = compare_functions(f, f_ref); +// ASSERT_TRUE(res.first) << res.second; +// } +// +// TEST(TransformationTests, ConvertNMS4ToNMSIEDynamic2) { +// std::shared_ptr f(nullptr), f_ref(nullptr); +// { +// auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); +// auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); +// auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{}, {10}); +// auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); +// auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); +// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, +// opset4::NonMaxSuppression::BoxEncodingType::CORNER, true, element::i32); +// +// f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); +// +// pass::Manager manager; +// manager.register_pass(); +// manager.register_pass(); +// manager.run_passes(f); +// +// f->validate_nodes_and_infer_types(); +// ASSERT_NO_THROW(check_rt_info(f)); +// } +// +// { +// auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); +// auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); +// auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{1}, {10}); +// auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); +// auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); +// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, +// std::make_shared(iou_threshold, +// opset4::Constant::create(element::i64, Shape{1}, {0})), +// std::make_shared(score_threshold, +// opset4::Constant::create(element::i64, Shape{1}, {0})), +// 0, true); +// +// f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); +// } +// +// auto res = compare_functions(f, f_ref); +// ASSERT_TRUE(res.first) << res.second; +// } From 00e5c1707dd74af5ca8d9bd9e4a87eaeb233dd2f Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 8 Oct 2020 09:16:01 +0300 Subject: [PATCH 062/173] Started to write the test ConvertNMS5ToNMSIEStatic. --- .../transformations/convert_nms5_test.cpp | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp index 73d519d7e2f2e9..073e4d4240aabe 100644 --- a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp +++ b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include #include #include @@ -22,26 +22,28 @@ using namespace ngraph; TEST(TransformationTests, ConvertNMS5ToNMSIEStatic) { std::shared_ptr f(nullptr), f_ref(nullptr); -// { -// auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); -// auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); -// auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{}, {10}); -// auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); -// auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); -// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, -// opset4::NonMaxSuppression::BoxEncodingType::CORNER, true); -// -// f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); -// -// const auto &orig_shape = f->get_output_partial_shape(0); -// pass::Manager manager; -// manager.register_pass(); -// manager.register_pass(); -// manager.run_passes(f); -// ASSERT_NO_THROW(check_rt_info(f)); + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + + const auto &orig_selected_indices_shape = f->get_output_partial_shape(0); + const auto &orig_selected_scores_shape = f->get_output_partial_shape(1); + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + ASSERT_NO_THROW(check_rt_info(f)); // ASSERT_TRUE(f->get_output_partial_shape(0).is_static()) << "Shape " << f->get_output_partial_shape(0) << " should be static"; -// } -// + } + // { // auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); // auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); @@ -62,6 +64,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStatic) { // // auto res = compare_functions(f, f_ref); // ASSERT_TRUE(res.first) << res.second; + ASSERT_TRUE(true) << ""; } // TEST(TransformationTests, ConvertNMS4ToNMSIEDynamic1) { From 97e6809e8b1d2f8ef2a7ceefa708b6370a802a77 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 8 Oct 2020 15:26:33 +0300 Subject: [PATCH 063/173] Written tests for conversion of nGraph NMS-5 to inner NMSIE3. --- .../transformations/convert_nms5_test.cpp | 219 +++++++++--------- 1 file changed, 114 insertions(+), 105 deletions(-) diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp index 073e4d4240aabe..e9adb14669b1d5 100644 --- a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp +++ b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp @@ -35,117 +35,126 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStatic) { f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); const auto &orig_selected_indices_shape = f->get_output_partial_shape(0); - const auto &orig_selected_scores_shape = f->get_output_partial_shape(1); pass::Manager manager; manager.register_pass(); manager.register_pass(); manager.run_passes(f); ASSERT_NO_THROW(check_rt_info(f)); -// ASSERT_TRUE(f->get_output_partial_shape(0).is_static()) << "Shape " << f->get_output_partial_shape(0) << " should be static"; + ASSERT_TRUE(f->get_output_partial_shape(0).is_static()) << "Shape " << f->get_output_partial_shape(0) << " should be static"; } -// { -// auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); -// auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); -// auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{1}, {10}); -// auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); -// auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); -// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, -// std::make_shared(iou_threshold, -// opset4::Constant::create(element::i64, Shape{1}, {0})), -// std::make_shared(score_threshold, -// opset4::Constant::create(element::i64, Shape{1}, {0})), -// 0, true); -// nms->set_friendly_name("nms"); -// -// f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); -// ASSERT_TRUE(f_ref->get_output_partial_shape(0).is_static()) << "Shape " << f_ref->get_output_partial_shape(0) << " should be static"; -// } -// -// auto res = compare_functions(f, f_ref); -// ASSERT_TRUE(res.first) << res.second; - ASSERT_TRUE(true) << ""; + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{1}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, + std::make_shared(iou_threshold, + opset5::Constant::create(element::i64, Shape{1}, {0})), + std::make_shared(score_threshold, + opset5::Constant::create(element::i64, Shape{1}, {0})), + std::make_shared(soft_nms_sigma, + opset5::Constant::create(element::i64, Shape{1}, {0})), + 0, true); + nms->set_friendly_name("nms"); + + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + ASSERT_TRUE(f_ref->get_output_partial_shape(0).is_static()) << "Shape " << f_ref->get_output_partial_shape(0) << " should be static"; + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; } -// TEST(TransformationTests, ConvertNMS4ToNMSIEDynamic1) { -// std::shared_ptr f(nullptr), f_ref(nullptr); -// { -// auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); -// auto scores = std::make_shared(element::f32, PartialShape::dynamic()); -// auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{}, {10}); -// auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); -// auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); -// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, -// opset4::NonMaxSuppression::BoxEncodingType::CORNER, true, element::i32); -// -// f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); -// -// pass::Manager manager; -// manager.register_pass(); -// manager.register_pass(); -// manager.run_passes(f); -// f->validate_nodes_and_infer_types(); -// ASSERT_NO_THROW(check_rt_info(f)); -// } -// -// { -// auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); -// auto scores = std::make_shared(element::f32, PartialShape::dynamic()); -// auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{1}, {10}); -// auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); -// auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); -// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, -// std::make_shared(iou_threshold, -// opset4::Constant::create(element::i64, Shape{1}, {0})), -// std::make_shared(score_threshold, -// opset4::Constant::create(element::i64, Shape{1}, {0})), -// 0, true); -// -// f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); -// } -// -// auto res = compare_functions(f, f_ref); -// ASSERT_TRUE(res.first) << res.second; -// } -// -// TEST(TransformationTests, ConvertNMS4ToNMSIEDynamic2) { -// std::shared_ptr f(nullptr), f_ref(nullptr); -// { -// auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); -// auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); -// auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{}, {10}); -// auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); -// auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); -// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, -// opset4::NonMaxSuppression::BoxEncodingType::CORNER, true, element::i32); -// -// f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); -// -// pass::Manager manager; -// manager.register_pass(); -// manager.register_pass(); -// manager.run_passes(f); -// -// f->validate_nodes_and_infer_types(); -// ASSERT_NO_THROW(check_rt_info(f)); -// } -// -// { -// auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); -// auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); -// auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{1}, {10}); -// auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); -// auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); -// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, -// std::make_shared(iou_threshold, -// opset4::Constant::create(element::i64, Shape{1}, {0})), -// std::make_shared(score_threshold, -// opset4::Constant::create(element::i64, Shape{1}, {0})), -// 0, true); -// -// f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); -// } -// -// auto res = compare_functions(f, f_ref); -// ASSERT_TRUE(res.first) << res.second; -// } +TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); + auto scores = std::make_shared(element::f32, PartialShape::dynamic()); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true, element::i32); + + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + f->validate_nodes_and_infer_types(); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); + auto scores = std::make_shared(element::f32, PartialShape::dynamic()); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{1}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, + std::make_shared(iou_threshold, + opset5::Constant::create(element::i64, Shape{1}, {0})), + std::make_shared(score_threshold, + opset5::Constant::create(element::i64, Shape{1}, {0})), + std::make_shared(soft_nms_sigma, + opset5::Constant::create(element::i64, Shape{1}, {0})), + 0, true, element::i32); + + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); + auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true, element::i32); + + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + + f->validate_nodes_and_infer_types(); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); + auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{1}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, + std::make_shared(iou_threshold, + opset5::Constant::create(element::i64, Shape{1}, {0})), + std::make_shared(score_threshold, + opset5::Constant::create(element::i64, Shape{1}, {0})), + std::make_shared(soft_nms_sigma, + opset5::Constant::create(element::i64, Shape{1}, {0})), + 0, true, element::i32); + + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} From ca757963ab4a005c7b1eb81427ea9a8703faf41d Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 8 Oct 2020 16:15:39 +0300 Subject: [PATCH 064/173] Started to write tests for conversion of previous NMS to nGraph NMS-5. --- .../convert_previous_nms_to_nms_5.cpp | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 inference-engine/tests/functional/inference_engine/transformations/convert_previous_nms_to_nms_5.cpp diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_previous_nms_to_nms_5.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_previous_nms_to_nms_5.cpp new file mode 100644 index 00000000000000..fded694e396142 --- /dev/null +++ b/inference-engine/tests/functional/inference_engine/transformations/convert_previous_nms_to_nms_5.cpp @@ -0,0 +1,72 @@ +// Copyright (C) 2020 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 +// + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common_test_utils/ngraph_test_utils.hpp" + +using namespace testing; +using namespace ngraph; + +TEST(TransformationTests, ConvertNMS4ToNMS5) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + opset4::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + + pass::Manager m; + m.register_pass(); + m.register_pass(); + m.run_passes(); + ASSERT_NO_THROW(check_rt_info(f)); + } +// { +// auto data = std::make_shared(ngraph::element::f32, ngraph::Shape{3, 1, 2}); +// auto divide_constant = ngraph::opset1::Constant::create(ngraph::element::f32, ngraph::Shape{1}, {1.5}); +// auto divide = std::make_shared(data, divide_constant); +// +// f = std::make_shared(ngraph::NodeVector{divide}, ngraph::ParameterVector{data}); +// +// ngraph::pass::Manager m; +// m.register_pass(); +// m.register_pass(); +// m.run_passes(f); +// ASSERT_NO_THROW(check_rt_info(f)); +// } +// +// { +// auto data = std::make_shared(ngraph::element::f32, ngraph::Shape{3, 1, 2}); +// auto divide_constant = ngraph::opset1::Constant::create(ngraph::element::f32, ngraph::Shape{1}, {1.5}); +// auto pow = std::make_shared(divide_constant, +// ngraph::opset1::Constant::create(ngraph::element::f32, ngraph::Shape{1}, {-1})); +// auto mul = std::make_shared(data, pow); +// +// f_ref = std::make_shared(ngraph::NodeVector{mul}, ngraph::ParameterVector{data}); +// } +// +// auto res = compare_functions(f, f_ref); +// ASSERT_TRUE(res.first) << res.second; + ASSERT_TRUE(true) << ""; +} From deb6ff04914e4f6c90042a0cb59f2492eea1a08c Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 9 Oct 2020 11:25:42 +0300 Subject: [PATCH 065/173] Written tests for conversion of old nGraph NMS to NMS-5. --- .../convert_previous_nms_to_nms_5.cpp | 10 +- .../convert_previous_nms_to_nms_5.cpp | 220 +++++++++++++++--- 2 files changed, 196 insertions(+), 34 deletions(-) diff --git a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp index 5c7dbd8dfb7c24..ee4ab6a6f8dc99 100644 --- a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp @@ -36,7 +36,7 @@ ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { size_t num_of_args = new_args.size(); - const auto& arg2 = num_of_args > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i32, Shape{}, {0}); + const auto& arg2 = num_of_args > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i64, Shape{}, {0}); const auto& arg3 = num_of_args > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); const auto& arg4 = num_of_args > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); const auto& arg5 = ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); @@ -86,7 +86,7 @@ ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { nms_5->set_friendly_name(nms_4->get_friendly_name()); ngraph::copy_runtime_info(nms_4, new_ops); - ngraph::replace_node(nms_4, nms_5); + nms_4->output(0).replace(nms_5->output(0)); return true; }; @@ -116,7 +116,7 @@ ngraph::pass::ConvertNMS3ToNMS5::ConvertNMS3ToNMS5() { size_t num_of_args = new_args.size(); - const auto& arg2 = num_of_args > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i32, Shape{}, {0}); + const auto& arg2 = num_of_args > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i64, Shape{}, {0}); const auto& arg3 = num_of_args > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); const auto& arg4 = num_of_args > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); const auto& arg5 = ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); @@ -166,7 +166,7 @@ ngraph::pass::ConvertNMS3ToNMS5::ConvertNMS3ToNMS5() { nms_5->set_friendly_name(nms_3->get_friendly_name()); ngraph::copy_runtime_info(nms_3, new_ops); - ngraph::replace_node(nms_3, nms_5); + nms_3->output(0).replace(nms_5->output(0)); return true; }; @@ -245,7 +245,7 @@ ngraph::pass::ConvertNMS1ToNMS5::ConvertNMS1ToNMS5() { nms_5->set_friendly_name(nms_1->get_friendly_name()); ngraph::copy_runtime_info(nms_1, new_ops); - ngraph::replace_node(nms_1, nms_5); + nms_1->output(0).replace(nms_5->output(0)); return true; }; diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_previous_nms_to_nms_5.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_previous_nms_to_nms_5.cpp index fded694e396142..e965d8d3d23e4f 100644 --- a/inference-engine/tests/functional/inference_engine/transformations/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/tests/functional/inference_engine/transformations/convert_previous_nms_to_nms_5.cpp @@ -23,7 +23,7 @@ using namespace testing; using namespace ngraph; -TEST(TransformationTests, ConvertNMS4ToNMS5) { +TEST(TransformationTests, ConvertNMS4FiveInputsToNMS5) { std::shared_ptr f(nullptr), f_ref(nullptr); { auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); @@ -39,34 +39,196 @@ TEST(TransformationTests, ConvertNMS4ToNMS5) { pass::Manager m; m.register_pass(); m.register_pass(); - m.run_passes(); + m.run_passes(f); ASSERT_NO_THROW(check_rt_info(f)); } -// { -// auto data = std::make_shared(ngraph::element::f32, ngraph::Shape{3, 1, 2}); -// auto divide_constant = ngraph::opset1::Constant::create(ngraph::element::f32, ngraph::Shape{1}, {1.5}); -// auto divide = std::make_shared(data, divide_constant); -// -// f = std::make_shared(ngraph::NodeVector{divide}, ngraph::ParameterVector{data}); -// -// ngraph::pass::Manager m; -// m.register_pass(); -// m.register_pass(); -// m.run_passes(f); -// ASSERT_NO_THROW(check_rt_info(f)); -// } -// -// { -// auto data = std::make_shared(ngraph::element::f32, ngraph::Shape{3, 1, 2}); -// auto divide_constant = ngraph::opset1::Constant::create(ngraph::element::f32, ngraph::Shape{1}, {1.5}); -// auto pow = std::make_shared(divide_constant, -// ngraph::opset1::Constant::create(ngraph::element::f32, ngraph::Shape{1}, {-1})); -// auto mul = std::make_shared(data, pow); -// -// f_ref = std::make_shared(ngraph::NodeVector{mul}, ngraph::ParameterVector{data}); -// } -// -// auto res = compare_functions(f, f_ref); -// ASSERT_TRUE(res.first) << res.second; - ASSERT_TRUE(true) << ""; + + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = opset5::Constant::create(element::f32, Shape{}, {.0f}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS4TwoInputsToNMS5) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto nms = std::make_shared(boxes, scores, opset4::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + + pass::Manager m; + m.register_pass(); + m.register_pass(); + m.run_passes(f); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {0}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto soft_nms_sigma = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS3FiveInputsToNMS5) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset3::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset3::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset3::Constant::create(element::f32, Shape{}, {0.7}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + opset3::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + + pass::Manager m; + m.register_pass(); + m.register_pass(); + m.run_passes(f); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = opset5::Constant::create(element::f32, Shape{}, {.0f}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS3TwoInputsToNMS5) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto nms = std::make_shared(boxes, scores, opset3::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + + pass::Manager m; + m.register_pass(); + m.register_pass(); + m.run_passes(f); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {0}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto soft_nms_sigma = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS1FiveInputsToNMS5) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset1::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset1::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset1::Constant::create(element::f32, Shape{}, {0.7}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + opset1::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + + pass::Manager m; + m.register_pass(); + m.register_pass(); + m.run_passes(f); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = opset5::Constant::create(element::f32, Shape{}, {.0f}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS1TwoInputsToNMS5) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto nms = std::make_shared(boxes, scores, opset1::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + + pass::Manager m; + m.register_pass(); + m.register_pass(); + m.run_passes(f); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {0}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto soft_nms_sigma = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; } From 2a41688a9e1a3523f4c3c37b9988715205cb1958 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 9 Oct 2020 12:51:04 +0300 Subject: [PATCH 066/173] Started to write tests for opset5::NonMaxSuppression::evaluate(). --- ngraph/test/CMakeLists.txt | 1 + ngraph/test/op_eval/non_max_suppression_5.cpp | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 ngraph/test/op_eval/non_max_suppression_5.cpp diff --git a/ngraph/test/CMakeLists.txt b/ngraph/test/CMakeLists.txt index 253c1e9737058a..bfe837a47e174f 100644 --- a/ngraph/test/CMakeLists.txt +++ b/ngraph/test/CMakeLists.txt @@ -74,6 +74,7 @@ set(SRC op_eval/interpolate.cpp op_eval/matmul.cpp op_eval/mish.cpp + op_eval/non_max_suppression_5.cpp op_eval/non_zero.cpp op_eval/reduce_l1.cpp op_eval/reduce_l2.cpp diff --git a/ngraph/test/op_eval/non_max_suppression_5.cpp b/ngraph/test/op_eval/non_max_suppression_5.cpp new file mode 100644 index 00000000000000..73f431ac5fa443 --- /dev/null +++ b/ngraph/test/op_eval/non_max_suppression_5.cpp @@ -0,0 +1,33 @@ +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//***************************************************************************** + +#include +#include + +#include "gtest/gtest.h" + +#include "ngraph/op/non_max_suppression.hpp" +#include "ngraph/runtime/host_tensor.hpp" +#include "ngraph/validation_util.hpp" +#include "runtime/backend.hpp" +#include "util/test_tools.hpp" +#include "util/type_prop.hpp" + +using namespace std; +using namespace ngraph; + +TEST(op_eval, nonmaxsuppression_center_point_box_format) +{ + std::vector boxes = {0.5, 0.5, 1.0, 1.0, 0.5, 0.6, 1.0, 1.0, 0.5, 0.4, 1.0, 1.0, 0.5, + 10.5, 1.0, 1.0, 0.5, 10.6, 1.0, 1.0, 0.5, 100.5, 1.0, 1.0}; + std::vector scores = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; + const int64_t max_output_boxes_per_class = 3; + const float iou_threshold = 0.0f; + const float score_threshold = 0.0f; + ASSERT_TRUE(true); +} \ No newline at end of file From c0aea2d26fad5a34893c498b31fd29e4f10a9abb Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 9 Oct 2020 13:06:41 +0300 Subject: [PATCH 067/173] Some additions. --- ngraph/test/op_eval/non_max_suppression_5.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ngraph/test/op_eval/non_max_suppression_5.cpp b/ngraph/test/op_eval/non_max_suppression_5.cpp index 73f431ac5fa443..c7d36e7bd80ce5 100644 --- a/ngraph/test/op_eval/non_max_suppression_5.cpp +++ b/ngraph/test/op_eval/non_max_suppression_5.cpp @@ -29,5 +29,9 @@ TEST(op_eval, nonmaxsuppression_center_point_box_format) const int64_t max_output_boxes_per_class = 3; const float iou_threshold = 0.0f; const float score_threshold = 0.0f; + const auto box_encoding = op::v5::BoxEncodingType::CENTER; + const auto boxes_shape = Shape{1, 6, 4}; + const auto scores_shape = Shape{1, 1, 6}; + ASSERT_TRUE(true); } \ No newline at end of file From 6944c1a500a5636c6b8ada79083fddacfe2bd4a6 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 9 Oct 2020 13:08:50 +0300 Subject: [PATCH 068/173] Small fix. --- ngraph/test/op_eval/non_max_suppression_5.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngraph/test/op_eval/non_max_suppression_5.cpp b/ngraph/test/op_eval/non_max_suppression_5.cpp index c7d36e7bd80ce5..fbcc3dc00455cf 100644 --- a/ngraph/test/op_eval/non_max_suppression_5.cpp +++ b/ngraph/test/op_eval/non_max_suppression_5.cpp @@ -29,7 +29,7 @@ TEST(op_eval, nonmaxsuppression_center_point_box_format) const int64_t max_output_boxes_per_class = 3; const float iou_threshold = 0.0f; const float score_threshold = 0.0f; - const auto box_encoding = op::v5::BoxEncodingType::CENTER; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CENTER; const auto boxes_shape = Shape{1, 6, 4}; const auto scores_shape = Shape{1, 1, 6}; From 9494d73665d6cc1c67f1fd75ea07102191081276 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 13 Oct 2020 15:48:46 +0300 Subject: [PATCH 069/173] Written tests for op::v5::NonMaxSuppression::evaluate(). --- ngraph/core/src/op/non_max_suppression.cpp | 87 ++- ngraph/test/op_eval/non_max_suppression_5.cpp | 526 +++++++++++++++++- 2 files changed, 554 insertions(+), 59 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 3c4c406780d054..791d1e7c969606 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -889,63 +889,49 @@ using V5BoxEncoding = op::v5::NonMaxSuppression::BoxEncodingType; static void normalize_corner(float* boxes, const Shape& boxes_shape) { - size_t num_batches = boxes_shape[0]; - size_t num_boxes = boxes_shape[1]; - - float* box_ptr = boxes; - - for (size_t batch = 0; batch < num_batches; ++batch) + size_t total_num_of_boxes = shape_size(boxes_shape) / 4; + for (size_t i = 0; i < total_num_of_boxes; ++i) { - for (size_t box = 0; box < num_boxes; ++num_boxes) - { - float y1 = box_ptr[0]; - float x1 = box_ptr[1]; - float y2 = box_ptr[2]; - float x2 = box_ptr[3]; - - float ymin = std::min(y1, y2); - float ymax = std::max(y1, y2); - float xmin = std::min(x1, x2); - float xmax = std::max(x1, x2); - - box_ptr[0] = ymin; - box_ptr[1] = xmin; - box_ptr[2] = ymax; - box_ptr[3] = xmax; - - box_ptr += 4; - } + float* current_box = boxes + 4 * i; + + float y1 = current_box[0]; + float x1 = current_box[1]; + float y2 = current_box[2]; + float x2 = current_box[3]; + + float ymin = std::min(y1, y2); + float ymax = std::max(y1, y2); + float xmin = std::min(x1, x2); + float xmax = std::max(x1, x2); + + current_box[0] = ymin; + current_box[1] = xmin; + current_box[2] = ymax; + current_box[3] = xmax; } } static void normalize_center(float* boxes, const Shape& boxes_shape) { - size_t num_batches = boxes_shape[0]; - size_t num_boxes = boxes_shape[1]; - - float* box_ptr = boxes; - - for (size_t batch = 0; batch < num_batches; ++batch) + size_t total_num_of_boxes = shape_size(boxes_shape) / 4; + for (size_t i = 0; i < total_num_of_boxes; ++i) { - for (size_t box = 0; box < num_boxes; ++num_boxes) - { - float x_center = box_ptr[0]; - float y_center = box_ptr[1]; - float width = box_ptr[2]; - float height = box_ptr[3]; - - float y1 = y_center - height / 2.0; - float x1 = x_center - width / 2.0; - float y2 = y_center + height / 2.0; - float x2 = x_center + width / 2.0; - - box_ptr[0] = y1; - box_ptr[1] = x1; - box_ptr[2] = y2; - box_ptr[3] = x2; - - box_ptr += 4; - } + float* current_box = boxes + 4 * i; + + float x_center = current_box[0]; + float y_center = current_box[1]; + float width = current_box[2]; + float height = current_box[3]; + + float y1 = y_center - height / 2.0; + float x1 = x_center - width / 2.0; + float y2 = y_center + height / 2.0; + float x2 = x_center + width / 2.0; + + current_box[0] = y1; + current_box[1] = x1; + current_box[2] = y2; + current_box[3] = x2; } } @@ -986,7 +972,6 @@ static std::vector prepare_boxes_data(const HostTensorPtr& boxes, } normalize_box_encoding(result.data(), boxes_shape, box_encoding); - return result; } diff --git a/ngraph/test/op_eval/non_max_suppression_5.cpp b/ngraph/test/op_eval/non_max_suppression_5.cpp index fbcc3dc00455cf..cd8e496ae7fc44 100644 --- a/ngraph/test/op_eval/non_max_suppression_5.cpp +++ b/ngraph/test/op_eval/non_max_suppression_5.cpp @@ -11,6 +11,7 @@ #include "gtest/gtest.h" +#include "ngraph/op/constant.hpp" #include "ngraph/op/non_max_suppression.hpp" #include "ngraph/runtime/host_tensor.hpp" #include "ngraph/validation_util.hpp" @@ -23,15 +24,524 @@ using namespace ngraph; TEST(op_eval, nonmaxsuppression_center_point_box_format) { - std::vector boxes = {0.5, 0.5, 1.0, 1.0, 0.5, 0.6, 1.0, 1.0, 0.5, 0.4, 1.0, 1.0, 0.5, - 10.5, 1.0, 1.0, 0.5, 10.6, 1.0, 1.0, 0.5, 100.5, 1.0, 1.0}; - std::vector scores = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; - const int64_t max_output_boxes_per_class = 3; - const float iou_threshold = 0.0f; - const float score_threshold = 0.0f; + std::vector boxes_data = { + 0.5, 0.5, 1.0, 1.0, 0.5, 0.6, 1.0, 1.0, 0.5, 0.4, 1.0, 1.0, 0.5, 10.5, 1.0, 1.0, 0.5, 10.6, + 1.0, 1.0, 0.5, 100.5, 1.0, 1.0}; + + std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; + + const int64_t max_output_boxes_per_class_data = 3; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.0f; const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CENTER; const auto boxes_shape = Shape{1, 6, 4}; const auto scores_shape = Shape{1, 1, 6}; - ASSERT_TRUE(true); -} \ No newline at end of file + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + HostTensorVector results(3); + for (auto& result : results) + { + result = make_shared(); + } + ASSERT_TRUE( + f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); + + std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 0, 5}; + std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 0.0, 0.3}; + std::vector expected_valid_outputs = {3}; + + EXPECT_EQ(results[0]->get_element_type(), element::i64); + EXPECT_EQ(results[1]->get_element_type(), element::f32); + EXPECT_EQ(results[2]->get_element_type(), element::i64); + EXPECT_EQ(results[0]->get_shape(), (Shape{3, 3})); + EXPECT_EQ(results[1]->get_shape(), (Shape{3, 3})); + EXPECT_EQ(results[2]->get_shape(), (Shape{})); + EXPECT_EQ(read_vector(results[0]), expected_selected_indices); + EXPECT_EQ(read_vector(results[1]), expected_selected_scores); + EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); +} + +TEST(op_eval, nonmaxsuppression_flipped_coordinates) +{ + std::vector boxes_data = { + 1.0, 1.0, 0.0, 0.0, 0.0, 0.1, 1.0, 1.1, 0.0, 0.9, 1.0, -0.1, 0.0, 10.0, 1.0, 11.0, + 1.0, 10.1, 0.0, 11.1, 1.0, 101.0, 0.0, 100.0}; + + std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; + + const int64_t max_output_boxes_per_class_data = 3; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.0f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; + const auto boxes_shape = Shape{1, 6, 4}; + const auto scores_shape = Shape{1, 1, 6}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + HostTensorVector results(3); + for (auto& result : results) + { + result = make_shared(); + } + ASSERT_TRUE( + f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); + + std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 0, 5}; + std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 0.0, 0.3}; + std::vector expected_valid_outputs = {3}; + + EXPECT_EQ(results[0]->get_element_type(), element::i64); + EXPECT_EQ(results[1]->get_element_type(), element::f32); + EXPECT_EQ(results[2]->get_element_type(), element::i64); + EXPECT_EQ(results[0]->get_shape(), (Shape{3, 3})); + EXPECT_EQ(results[1]->get_shape(), (Shape{3, 3})); + EXPECT_EQ(results[2]->get_shape(), (Shape{})); + EXPECT_EQ(read_vector(results[0]), expected_selected_indices); + EXPECT_EQ(read_vector(results[1]), expected_selected_scores); + EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); +} + +TEST(op_eval, nonmaxsuppression_identical_boxes) +{ + std::vector boxes_data = { + 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, + 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, + 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0}; + + std::vector scores_data = {0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9}; + + const int64_t max_output_boxes_per_class_data = 3; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.0f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; + const auto boxes_shape = Shape{1, 10, 4}; + const auto scores_shape = Shape{1, 1, 10}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + HostTensorVector results(3); + for (auto& result : results) + { + result = make_shared(); + } + ASSERT_TRUE( + f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); + + std::vector expected_selected_indices = {0, 0, 0}; + std::vector expected_selected_scores = {0.0, 0.0, 0.9}; + std::vector expected_valid_outputs = {1}; + + EXPECT_EQ(results[0]->get_element_type(), element::i64); + EXPECT_EQ(results[1]->get_element_type(), element::f32); + EXPECT_EQ(results[2]->get_element_type(), element::i64); + EXPECT_EQ(results[0]->get_shape(), (Shape{1, 3})); + EXPECT_EQ(results[1]->get_shape(), (Shape{1, 3})); + EXPECT_EQ(results[2]->get_shape(), (Shape{})); + EXPECT_EQ(read_vector(results[0]), expected_selected_indices); + EXPECT_EQ(read_vector(results[1]), expected_selected_scores); + EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); +} + +TEST(op_eval, nonmaxsuppression_limit_output_size) +{ + std::vector boxes_data = { + 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, + 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; + + std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; + + const int64_t max_output_boxes_per_class_data = 2; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.0f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; + const auto boxes_shape = Shape{1, 6, 4}; + const auto scores_shape = Shape{1, 1, 6}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + HostTensorVector results(3); + for (auto& result : results) + { + result = make_shared(); + } + ASSERT_TRUE( + f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); + + std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0}; + std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9}; + std::vector expected_valid_outputs = {2}; + + EXPECT_EQ(results[0]->get_element_type(), element::i64); + EXPECT_EQ(results[1]->get_element_type(), element::f32); + EXPECT_EQ(results[2]->get_element_type(), element::i64); + EXPECT_EQ(results[0]->get_shape(), (Shape{2, 3})); + EXPECT_EQ(results[1]->get_shape(), (Shape{2, 3})); + EXPECT_EQ(results[2]->get_shape(), (Shape{})); + EXPECT_EQ(read_vector(results[0]), expected_selected_indices); + EXPECT_EQ(read_vector(results[1]), expected_selected_scores); + EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); +} + +TEST(op_eval, nonmaxsuppression_single_box) +{ + std::vector boxes_data = {0.0, 0.0, 1.0, 1.0}; + + std::vector scores_data = {0.9}; + + const int64_t max_output_boxes_per_class_data = 3; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.0f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; + const auto boxes_shape = Shape{1, 1, 4}; + const auto scores_shape = Shape{1, 1, 1}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + HostTensorVector results(3); + for (auto& result : results) + { + result = make_shared(); + } + ASSERT_TRUE( + f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); + + std::vector expected_selected_indices = {0, 0, 0}; + std::vector expected_selected_scores = {0.0, 0.0, 0.9}; + std::vector expected_valid_outputs = {1}; + + EXPECT_EQ(results[0]->get_element_type(), element::i64); + EXPECT_EQ(results[1]->get_element_type(), element::f32); + EXPECT_EQ(results[2]->get_element_type(), element::i64); + EXPECT_EQ(results[0]->get_shape(), (Shape{1, 3})); + EXPECT_EQ(results[1]->get_shape(), (Shape{1, 3})); + EXPECT_EQ(results[2]->get_shape(), (Shape{})); + EXPECT_EQ(read_vector(results[0]), expected_selected_indices); + EXPECT_EQ(read_vector(results[1]), expected_selected_scores); + EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); +} + +TEST(op_eval, nonmaxsuppression_suppress_by_IOU) +{ + std::vector boxes_data = { + 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, + 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; + + std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; + + const int64_t max_output_boxes_per_class_data = 3; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.0f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; + const auto boxes_shape = Shape{1, 6, 4}; + const auto scores_shape = Shape{1, 1, 6}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + HostTensorVector results(3); + for (auto& result : results) + { + result = make_shared(); + } + ASSERT_TRUE( + f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); + + std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 0, 5}; + std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 0.0, 0.3}; + std::vector expected_valid_outputs = {3}; + + EXPECT_EQ(results[0]->get_element_type(), element::i64); + EXPECT_EQ(results[1]->get_element_type(), element::f32); + EXPECT_EQ(results[2]->get_element_type(), element::i64); + EXPECT_EQ(results[0]->get_shape(), (Shape{3, 3})); + EXPECT_EQ(results[1]->get_shape(), (Shape{3, 3})); + EXPECT_EQ(results[2]->get_shape(), (Shape{})); + EXPECT_EQ(read_vector(results[0]), expected_selected_indices); + EXPECT_EQ(read_vector(results[1]), expected_selected_scores); + EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); +} + +TEST(op_eval, nonmaxsuppression_suppress_by_IOU_and_scores) +{ + std::vector boxes_data = { + 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, + 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; + + std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; + + const int64_t max_output_boxes_per_class_data = 3; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.4f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; + const auto boxes_shape = Shape{1, 6, 4}; + const auto scores_shape = Shape{1, 1, 6}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + HostTensorVector results(3); + for (auto& result : results) + { + result = make_shared(); + } + ASSERT_TRUE( + f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); + + std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0}; + std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9}; + std::vector expected_valid_outputs = {2}; + + EXPECT_EQ(results[0]->get_element_type(), element::i64); + EXPECT_EQ(results[1]->get_element_type(), element::f32); + EXPECT_EQ(results[2]->get_element_type(), element::i64); + EXPECT_EQ(results[0]->get_shape(), (Shape{2, 3})); + EXPECT_EQ(results[1]->get_shape(), (Shape{2, 3})); + EXPECT_EQ(results[2]->get_shape(), (Shape{})); + EXPECT_EQ(read_vector(results[0]), expected_selected_indices); + EXPECT_EQ(read_vector(results[1]), expected_selected_scores); + EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); +} + +TEST(op_eval, nonmaxsuppression_two_batches) +{ + std::vector boxes_data = { + 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, + 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, + 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; + + std::vector scores_data = { + 0.9, 0.75, 0.6, 0.95, 0.5, 0.3, 0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; + + const int64_t max_output_boxes_per_class_data = 2; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.0f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; + const auto boxes_shape = Shape{2, 6, 4}; + const auto scores_shape = Shape{2, 1, 6}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + HostTensorVector results(3); + for (auto& result : results) + { + result = make_shared(); + } + ASSERT_TRUE( + f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); + + std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 1, 0, 3, 1, 0, 0}; + std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 1.0, 0.0, 0.95, 1.0, 0.0, 0.9}; + std::vector expected_valid_outputs = {4}; + + EXPECT_EQ(results[0]->get_element_type(), element::i64); + EXPECT_EQ(results[1]->get_element_type(), element::f32); + EXPECT_EQ(results[2]->get_element_type(), element::i64); + EXPECT_EQ(results[0]->get_shape(), (Shape{4, 3})); + EXPECT_EQ(results[1]->get_shape(), (Shape{4, 3})); + EXPECT_EQ(results[2]->get_shape(), (Shape{})); + EXPECT_EQ(read_vector(results[0]), expected_selected_indices); + EXPECT_EQ(read_vector(results[1]), expected_selected_scores); + EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); +} + +TEST(op_eval, nonmaxsuppression_two_classes) +{ + std::vector boxes_data = { + 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, + 0.0, 10.0, 1.0, 11.0, 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; + + std::vector scores_data = { + 0.9, 0.75, 0.6, 0.95, 0.5, 0.3, 0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; + + const int64_t max_output_boxes_per_class_data = 2; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.0f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; + const auto boxes_shape = Shape{1, 6, 4}; + const auto scores_shape = Shape{1, 2, 6}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + HostTensorVector results(3); + for (auto& result : results) + { + result = make_shared(); + } + ASSERT_TRUE( + f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); + + std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 1, 3, 0, 1, 0}; + std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 1.0, 0.95, 0.0, 1.0, 0.9}; + std::vector expected_valid_outputs = {4}; + + EXPECT_EQ(results[0]->get_element_type(), element::i64); + EXPECT_EQ(results[1]->get_element_type(), element::f32); + EXPECT_EQ(results[2]->get_element_type(), element::i64); + EXPECT_EQ(results[0]->get_shape(), (Shape{4, 3})); + EXPECT_EQ(results[1]->get_shape(), (Shape{4, 3})); + EXPECT_EQ(results[2]->get_shape(), (Shape{})); + EXPECT_EQ(read_vector(results[0]), expected_selected_indices); + EXPECT_EQ(read_vector(results[1]), expected_selected_scores); + EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); +} From 54e89f51618734953304880652d626b76c2a7fc5 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 14 Oct 2020 10:18:44 +0300 Subject: [PATCH 070/173] Used NGRAPH_RTTI_DECLARATION for NonMaxSuppressionIE3. --- .../src/transformations/include/ngraph_ops/nms_ie.hpp | 3 +-- inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/inference-engine/src/transformations/include/ngraph_ops/nms_ie.hpp b/inference-engine/src/transformations/include/ngraph_ops/nms_ie.hpp index 721392f8a42902..aace940cf0a87e 100644 --- a/inference-engine/src/transformations/include/ngraph_ops/nms_ie.hpp +++ b/inference-engine/src/transformations/include/ngraph_ops/nms_ie.hpp @@ -60,8 +60,7 @@ class TRANSFORMATIONS_API NonMaxSuppressionIE2 : public NonMaxSuppressionIE { class TRANSFORMATIONS_API NonMaxSuppressionIE3 : public Op { public: - static constexpr NodeTypeInfo type_info{"NonMaxSuppressionIE", 3}; - const NodeTypeInfo& get_type_info() const override { return type_info; } + NGRAPH_RTTI_DECLARATION; NonMaxSuppressionIE3(const Output& boxes, const Output& scores, diff --git a/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp b/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp index ce039843d9d701..965ecafb00c56b 100644 --- a/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp +++ b/inference-engine/src/transformations/src/ngraph_ops/nms_ie.cpp @@ -102,7 +102,7 @@ void op::NonMaxSuppressionIE2::validate_and_infer_types() { set_output_type(0, nms->output(0).get_element_type(), nms->output(0).get_partial_shape()); } -constexpr NodeTypeInfo op::NonMaxSuppressionIE3::type_info; +NGRAPH_RTTI_DEFINITION(op::NonMaxSuppressionIE3, "NonMaxSuppressionIE", 3); op::NonMaxSuppressionIE3::NonMaxSuppressionIE3(const Output& boxes, const Output& scores, From 61051a7b83c374604926ce748fed6895ed866368 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 14 Oct 2020 10:25:33 +0300 Subject: [PATCH 071/173] Used NGRAPH_RTTI_DECLARATION for NMS-5. --- ngraph/core/include/ngraph/op/non_max_suppression.hpp | 3 +-- ngraph/core/src/op/non_max_suppression.cpp | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ngraph/core/include/ngraph/op/non_max_suppression.hpp b/ngraph/core/include/ngraph/op/non_max_suppression.hpp index a3828e9f65f704..817e8128242c63 100644 --- a/ngraph/core/include/ngraph/op/non_max_suppression.hpp +++ b/ngraph/core/include/ngraph/op/non_max_suppression.hpp @@ -243,14 +243,13 @@ namespace ngraph class NGRAPH_API NonMaxSuppression : public Op { public: + NGRAPH_RTTI_DECLARATION; enum class BoxEncodingType { CORNER, CENTER }; - static constexpr NodeTypeInfo type_info{"NonMaxSuppression", 5}; - const NodeTypeInfo& get_type_info() const override { return type_info; } NonMaxSuppression() = default; /// \brief Constructs a NonMaxSuppression operation with default values in the last diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 791d1e7c969606..cd6bebe2f84df2 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -536,7 +536,7 @@ void op::v4::NonMaxSuppression::validate_and_infer_types() // ------------------------------ V5 ------------------------------ -constexpr NodeTypeInfo op::v5::NonMaxSuppression::type_info; +NGRAPH_RTTI_DEFINITION(op::v5::NonMaxSuppression, "NonMaxSuppression", 5); op::v5::NonMaxSuppression::NonMaxSuppression( const Output& boxes, From 3e97da488b5564d294c05bfcd2f91afa228577f6 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 14 Oct 2020 10:42:05 +0300 Subject: [PATCH 072/173] All static local constants and functions for NMS-5 were moved into non-named namespace. --- ngraph/core/src/op/non_max_suppression.cpp | 391 +++++++++++---------- 1 file changed, 197 insertions(+), 194 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index cd6bebe2f84df2..318d98f0dda858 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -773,11 +773,203 @@ int64_t op::v5::NonMaxSuppression::max_boxes_output_from_input() const return max_output_boxes; } -static constexpr size_t boxes_port = 0; -static constexpr size_t scores_port = 1; -static constexpr size_t iou_threshold_port = 3; -static constexpr size_t score_threshold_port = 4; -static constexpr size_t soft_nms_sigma_port = 5; +using V5BoxEncoding = op::v5::NonMaxSuppression::BoxEncodingType; + +namespace { + constexpr size_t boxes_port = 0; + constexpr size_t scores_port = 1; + constexpr size_t iou_threshold_port = 3; + constexpr size_t score_threshold_port = 4; + constexpr size_t soft_nms_sigma_port = 5; + + PartialShape infer_selected_indices_shape(const HostTensorVector& inputs, + int64_t max_output_boxes_per_class) + { + const auto boxes_ps = inputs[boxes_port]->get_partial_shape(); + const auto scores_ps = inputs[scores_port]->get_partial_shape(); + + // NonMaxSuppression produces triplets + // that have the following format: [batch_index, class_index, box_index] + PartialShape result = {Dimension::dynamic(), 3}; + + if (boxes_ps.rank().is_static() && scores_ps.rank().is_static()) + { + const auto num_boxes_boxes = boxes_ps[1]; + if (num_boxes_boxes.is_static() && scores_ps[0].is_static() && scores_ps[1].is_static()) + { + const auto num_boxes = num_boxes_boxes.get_length(); + const auto num_classes = scores_ps[1].get_length(); + + result[0] = std::min(num_boxes, max_output_boxes_per_class) * num_classes * + scores_ps[0].get_length(); + } + } + + return result; + } + + void normalize_corner(float* boxes, const Shape& boxes_shape) + { + size_t total_num_of_boxes = shape_size(boxes_shape) / 4; + for (size_t i = 0; i < total_num_of_boxes; ++i) + { + float* current_box = boxes + 4 * i; + + float y1 = current_box[0]; + float x1 = current_box[1]; + float y2 = current_box[2]; + float x2 = current_box[3]; + + float ymin = std::min(y1, y2); + float ymax = std::max(y1, y2); + float xmin = std::min(x1, x2); + float xmax = std::max(x1, x2); + + current_box[0] = ymin; + current_box[1] = xmin; + current_box[2] = ymax; + current_box[3] = xmax; + } + } + + void normalize_center(float* boxes, const Shape& boxes_shape) + { + size_t total_num_of_boxes = shape_size(boxes_shape) / 4; + for (size_t i = 0; i < total_num_of_boxes; ++i) + { + float* current_box = boxes + 4 * i; + + float x_center = current_box[0]; + float y_center = current_box[1]; + float width = current_box[2]; + float height = current_box[3]; + + float y1 = y_center - height / 2.0; + float x1 = x_center - width / 2.0; + float y2 = y_center + height / 2.0; + float x2 = x_center + width / 2.0; + + current_box[0] = y1; + current_box[1] = x1; + current_box[2] = y2; + current_box[3] = x2; + } + } + + void normalize_box_encoding(float* boxes, + const Shape& boxes_shape, + const V5BoxEncoding box_encoding) + { + if (box_encoding == V5BoxEncoding::CORNER) + { + normalize_corner(boxes, boxes_shape); + } + else + { + normalize_center(boxes, boxes_shape); + } + } + + std::vector prepare_boxes_data(const HostTensorPtr& boxes, + const Shape& boxes_shape, + const V5BoxEncoding box_encoding) + { + element::Type boxes_input_et = boxes->get_element_type(); + + size_t boxes_size = shape_size(boxes_shape); + std::vector result(boxes_size); + + if (boxes_input_et == ngraph::element::f32) + { + float* boxes_ptr = boxes->get_data_ptr(); + memcpy(result.data(), boxes_ptr, boxes_size * sizeof(float)); + } + else + { + float16* boxes_ptr = boxes->get_data_ptr(); + for (size_t i = 0; i < boxes_size; ++i) + { + result[i] = float(boxes_ptr[i]); + } + } + + normalize_box_encoding(result.data(), boxes_shape, box_encoding); + return result; + } + + std::vector prepare_scores_data(const HostTensorPtr& scores, + const Shape& scores_shape) + { + element::Type scores_input_et = scores->get_element_type(); + + size_t scores_size = shape_size(scores_shape); + std::vector result(scores_size); + + if (scores_input_et == ngraph::element::f32) + { + float* scores_ptr = scores->get_data_ptr(); + memcpy(result.data(), scores_ptr, scores_size * sizeof(float)); + } + else + { + float16* scores_ptr = scores->get_data_ptr(); + for (size_t i = 0; i < scores_size; ++i) + { + result[i] = float(scores_ptr[i]); + } + } + + return result; + } + + void evaluate_postprocessing(const HostTensorVector& outputs, + const ngraph::element::Type output_type, + const std::vector& selected_indices, + const std::vector& selected_scores, + int64_t valid_outputs) + { + size_t num_of_outputs = outputs.size(); + size_t selected_size = valid_outputs * 3; + + if (output_type == ngraph::element::i64) + { + int64_t* indices_ptr = outputs[0]->get_data_ptr(); + memcpy(indices_ptr, selected_indices.data(), selected_size * sizeof(int64_t)); + } + else + { + int32_t* indices_ptr = outputs[0]->get_data_ptr(); + for (size_t i = 0; i < selected_size; ++i) + { + indices_ptr[i] = static_cast(selected_indices[i]); + } + } + + if (num_of_outputs < 2) + { + return; + } + + float* scores_ptr = outputs[1]->get_data_ptr(); + memcpy(scores_ptr, selected_scores.data(), selected_size * sizeof(float)); + + if (num_of_outputs < 3) + { + return; + } + + if (output_type == ngraph::element::i64) + { + int64_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); + *valid_outputs_ptr = valid_outputs; + } + else + { + int32_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); + *valid_outputs_ptr = static_cast(valid_outputs); + } + } +} float op::v5::NonMaxSuppression::iou_threshold_from_input() const { @@ -859,195 +1051,6 @@ namespace ngraph } } // namespace ngraph -static PartialShape infer_selected_indices_shape(const HostTensorVector& inputs, - int64_t max_output_boxes_per_class) -{ - const auto boxes_ps = inputs[boxes_port]->get_partial_shape(); - const auto scores_ps = inputs[scores_port]->get_partial_shape(); - - // NonMaxSuppression produces triplets - // that have the following format: [batch_index, class_index, box_index] - PartialShape result = {Dimension::dynamic(), 3}; - - if (boxes_ps.rank().is_static() && scores_ps.rank().is_static()) - { - const auto num_boxes_boxes = boxes_ps[1]; - if (num_boxes_boxes.is_static() && scores_ps[0].is_static() && scores_ps[1].is_static()) - { - const auto num_boxes = num_boxes_boxes.get_length(); - const auto num_classes = scores_ps[1].get_length(); - - result[0] = std::min(num_boxes, max_output_boxes_per_class) * num_classes * - scores_ps[0].get_length(); - } - } - - return result; -} - -using V5BoxEncoding = op::v5::NonMaxSuppression::BoxEncodingType; - -static void normalize_corner(float* boxes, const Shape& boxes_shape) -{ - size_t total_num_of_boxes = shape_size(boxes_shape) / 4; - for (size_t i = 0; i < total_num_of_boxes; ++i) - { - float* current_box = boxes + 4 * i; - - float y1 = current_box[0]; - float x1 = current_box[1]; - float y2 = current_box[2]; - float x2 = current_box[3]; - - float ymin = std::min(y1, y2); - float ymax = std::max(y1, y2); - float xmin = std::min(x1, x2); - float xmax = std::max(x1, x2); - - current_box[0] = ymin; - current_box[1] = xmin; - current_box[2] = ymax; - current_box[3] = xmax; - } -} - -static void normalize_center(float* boxes, const Shape& boxes_shape) -{ - size_t total_num_of_boxes = shape_size(boxes_shape) / 4; - for (size_t i = 0; i < total_num_of_boxes; ++i) - { - float* current_box = boxes + 4 * i; - - float x_center = current_box[0]; - float y_center = current_box[1]; - float width = current_box[2]; - float height = current_box[3]; - - float y1 = y_center - height / 2.0; - float x1 = x_center - width / 2.0; - float y2 = y_center + height / 2.0; - float x2 = x_center + width / 2.0; - - current_box[0] = y1; - current_box[1] = x1; - current_box[2] = y2; - current_box[3] = x2; - } -} - -static void - normalize_box_encoding(float* boxes, const Shape& boxes_shape, const V5BoxEncoding box_encoding) -{ - if (box_encoding == V5BoxEncoding::CORNER) - { - normalize_corner(boxes, boxes_shape); - } - else - { - normalize_center(boxes, boxes_shape); - } -} - -static std::vector prepare_boxes_data(const HostTensorPtr& boxes, - const Shape& boxes_shape, - const V5BoxEncoding box_encoding) -{ - element::Type boxes_input_et = boxes->get_element_type(); - - size_t boxes_size = shape_size(boxes_shape); - std::vector result(boxes_size); - - if (boxes_input_et == ngraph::element::f32) - { - float* boxes_ptr = boxes->get_data_ptr(); - memcpy(result.data(), boxes_ptr, boxes_size * sizeof(float)); - } - else - { - float16* boxes_ptr = boxes->get_data_ptr(); - for (size_t i = 0; i < boxes_size; ++i) - { - result[i] = float(boxes_ptr[i]); - } - } - - normalize_box_encoding(result.data(), boxes_shape, box_encoding); - return result; -} - -static std::vector prepare_scores_data(const HostTensorPtr& scores, - const Shape& scores_shape) -{ - element::Type scores_input_et = scores->get_element_type(); - - size_t scores_size = shape_size(scores_shape); - std::vector result(scores_size); - - if (scores_input_et == ngraph::element::f32) - { - float* scores_ptr = scores->get_data_ptr(); - memcpy(result.data(), scores_ptr, scores_size * sizeof(float)); - } - else - { - float16* scores_ptr = scores->get_data_ptr(); - for (size_t i = 0; i < scores_size; ++i) - { - result[i] = float(scores_ptr[i]); - } - } - - return result; -} - -static void evaluate_postprocessing(const HostTensorVector& outputs, - const ngraph::element::Type output_type, - const std::vector& selected_indices, - const std::vector& selected_scores, - int64_t valid_outputs) -{ - size_t num_of_outputs = outputs.size(); - size_t selected_size = valid_outputs * 3; - - if (output_type == ngraph::element::i64) - { - int64_t* indices_ptr = outputs[0]->get_data_ptr(); - memcpy(indices_ptr, selected_indices.data(), selected_size * sizeof(int64_t)); - } - else - { - int32_t* indices_ptr = outputs[0]->get_data_ptr(); - for (size_t i = 0; i < selected_size; ++i) - { - indices_ptr[i] = static_cast(selected_indices[i]); - } - } - - if (num_of_outputs < 2) - { - return; - } - - float* scores_ptr = outputs[1]->get_data_ptr(); - memcpy(scores_ptr, selected_scores.data(), selected_size * sizeof(float)); - - if (num_of_outputs < 3) - { - return; - } - - if (output_type == ngraph::element::i64) - { - int64_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); - *valid_outputs_ptr = valid_outputs; - } - else - { - int32_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); - *valid_outputs_ptr = static_cast(valid_outputs); - } -} - bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, const HostTensorVector& inputs) const { From 898eaf56634ad8f0d44198a2649777844319d5cf Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 14 Oct 2020 11:15:10 +0300 Subject: [PATCH 073/173] Some code style fixes. --- ngraph/core/src/op/non_max_suppression.cpp | 6 +- ngraph/test/op_eval/non_max_suppression_5.cpp | 146 +++++++++--------- 2 files changed, 77 insertions(+), 75 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 318d98f0dda858..f6f96f85d013d4 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -775,7 +775,8 @@ int64_t op::v5::NonMaxSuppression::max_boxes_output_from_input() const using V5BoxEncoding = op::v5::NonMaxSuppression::BoxEncodingType; -namespace { +namespace +{ constexpr size_t boxes_port = 0; constexpr size_t scores_port = 1; constexpr size_t iou_threshold_port = 3; @@ -897,8 +898,7 @@ namespace { return result; } - std::vector prepare_scores_data(const HostTensorPtr& scores, - const Shape& scores_shape) + std::vector prepare_scores_data(const HostTensorPtr& scores, const Shape& scores_shape) { element::Type scores_input_et = scores->get_element_type(); diff --git a/ngraph/test/op_eval/non_max_suppression_5.cpp b/ngraph/test/op_eval/non_max_suppression_5.cpp index cd8e496ae7fc44..fd70e33525386b 100644 --- a/ngraph/test/op_eval/non_max_suppression_5.cpp +++ b/ngraph/test/op_eval/non_max_suppression_5.cpp @@ -24,9 +24,9 @@ using namespace ngraph; TEST(op_eval, nonmaxsuppression_center_point_box_format) { - std::vector boxes_data = { - 0.5, 0.5, 1.0, 1.0, 0.5, 0.6, 1.0, 1.0, 0.5, 0.4, 1.0, 1.0, 0.5, 10.5, 1.0, 1.0, 0.5, 10.6, - 1.0, 1.0, 0.5, 100.5, 1.0, 1.0}; + std::vector boxes_data = {0.5, 0.5, 1.0, 1.0, 0.5, 0.6, 1.0, 1.0, + 0.5, 0.4, 1.0, 1.0, 0.5, 10.5, 1.0, 1.0, + 0.5, 10.6, 1.0, 1.0, 0.5, 100.5, 1.0, 1.0}; std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; @@ -42,7 +42,8 @@ TEST(op_eval, nonmaxsuppression_center_point_box_format) auto max_output_boxes_per_class = op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); auto nms = make_shared(boxes, scores, @@ -60,10 +61,9 @@ TEST(op_eval, nonmaxsuppression_center_point_box_format) { result = make_shared(); } - ASSERT_TRUE( - f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); + ASSERT_TRUE(f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 0, 5}; std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 0.0, 0.3}; @@ -82,9 +82,9 @@ TEST(op_eval, nonmaxsuppression_center_point_box_format) TEST(op_eval, nonmaxsuppression_flipped_coordinates) { - std::vector boxes_data = { - 1.0, 1.0, 0.0, 0.0, 0.0, 0.1, 1.0, 1.1, 0.0, 0.9, 1.0, -0.1, 0.0, 10.0, 1.0, 11.0, - 1.0, 10.1, 0.0, 11.1, 1.0, 101.0, 0.0, 100.0}; + std::vector boxes_data = {1.0, 1.0, 0.0, 0.0, 0.0, 0.1, 1.0, 1.1, + 0.0, 0.9, 1.0, -0.1, 0.0, 10.0, 1.0, 11.0, + 1.0, 10.1, 0.0, 11.1, 1.0, 101.0, 0.0, 100.0}; std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; @@ -100,7 +100,8 @@ TEST(op_eval, nonmaxsuppression_flipped_coordinates) auto max_output_boxes_per_class = op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); auto nms = make_shared(boxes, scores, @@ -118,10 +119,9 @@ TEST(op_eval, nonmaxsuppression_flipped_coordinates) { result = make_shared(); } - ASSERT_TRUE( - f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); + ASSERT_TRUE(f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 0, 5}; std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 0.0, 0.3}; @@ -140,10 +140,10 @@ TEST(op_eval, nonmaxsuppression_flipped_coordinates) TEST(op_eval, nonmaxsuppression_identical_boxes) { - std::vector boxes_data = { - 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, - 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, - 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0}; + std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, + 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, + 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, + 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0}; std::vector scores_data = {0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9}; @@ -159,7 +159,8 @@ TEST(op_eval, nonmaxsuppression_identical_boxes) auto max_output_boxes_per_class = op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); auto nms = make_shared(boxes, scores, @@ -177,10 +178,9 @@ TEST(op_eval, nonmaxsuppression_identical_boxes) { result = make_shared(); } - ASSERT_TRUE( - f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); + ASSERT_TRUE(f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); std::vector expected_selected_indices = {0, 0, 0}; std::vector expected_selected_scores = {0.0, 0.0, 0.9}; @@ -199,9 +199,9 @@ TEST(op_eval, nonmaxsuppression_identical_boxes) TEST(op_eval, nonmaxsuppression_limit_output_size) { - std::vector boxes_data = { - 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, - 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; + std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, + 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, + 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; @@ -217,7 +217,8 @@ TEST(op_eval, nonmaxsuppression_limit_output_size) auto max_output_boxes_per_class = op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); auto nms = make_shared(boxes, scores, @@ -235,10 +236,9 @@ TEST(op_eval, nonmaxsuppression_limit_output_size) { result = make_shared(); } - ASSERT_TRUE( - f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); + ASSERT_TRUE(f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0}; std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9}; @@ -273,7 +273,8 @@ TEST(op_eval, nonmaxsuppression_single_box) auto max_output_boxes_per_class = op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); auto nms = make_shared(boxes, scores, @@ -291,10 +292,9 @@ TEST(op_eval, nonmaxsuppression_single_box) { result = make_shared(); } - ASSERT_TRUE( - f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); + ASSERT_TRUE(f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); std::vector expected_selected_indices = {0, 0, 0}; std::vector expected_selected_scores = {0.0, 0.0, 0.9}; @@ -313,9 +313,9 @@ TEST(op_eval, nonmaxsuppression_single_box) TEST(op_eval, nonmaxsuppression_suppress_by_IOU) { - std::vector boxes_data = { - 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, - 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; + std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, + 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, + 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; @@ -331,7 +331,8 @@ TEST(op_eval, nonmaxsuppression_suppress_by_IOU) auto max_output_boxes_per_class = op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); auto nms = make_shared(boxes, scores, @@ -349,10 +350,9 @@ TEST(op_eval, nonmaxsuppression_suppress_by_IOU) { result = make_shared(); } - ASSERT_TRUE( - f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); + ASSERT_TRUE(f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 0, 5}; std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 0.0, 0.3}; @@ -371,9 +371,9 @@ TEST(op_eval, nonmaxsuppression_suppress_by_IOU) TEST(op_eval, nonmaxsuppression_suppress_by_IOU_and_scores) { - std::vector boxes_data = { - 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, - 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; + std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, + 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, + 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; @@ -389,7 +389,8 @@ TEST(op_eval, nonmaxsuppression_suppress_by_IOU_and_scores) auto max_output_boxes_per_class = op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); auto nms = make_shared(boxes, scores, @@ -407,10 +408,9 @@ TEST(op_eval, nonmaxsuppression_suppress_by_IOU_and_scores) { result = make_shared(); } - ASSERT_TRUE( - f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); + ASSERT_TRUE(f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0}; std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9}; @@ -430,9 +430,9 @@ TEST(op_eval, nonmaxsuppression_suppress_by_IOU_and_scores) TEST(op_eval, nonmaxsuppression_two_batches) { std::vector boxes_data = { - 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, - 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, - 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; + 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, + 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, + 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; std::vector scores_data = { 0.9, 0.75, 0.6, 0.95, 0.5, 0.3, 0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; @@ -449,7 +449,8 @@ TEST(op_eval, nonmaxsuppression_two_batches) auto max_output_boxes_per_class = op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); auto nms = make_shared(boxes, scores, @@ -467,13 +468,13 @@ TEST(op_eval, nonmaxsuppression_two_batches) { result = make_shared(); } - ASSERT_TRUE( - f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); + ASSERT_TRUE(f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 1, 0, 3, 1, 0, 0}; - std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 1.0, 0.0, 0.95, 1.0, 0.0, 0.9}; + std::vector expected_selected_scores = { + 0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 1.0, 0.0, 0.95, 1.0, 0.0, 0.9}; std::vector expected_valid_outputs = {4}; EXPECT_EQ(results[0]->get_element_type(), element::i64); @@ -489,9 +490,9 @@ TEST(op_eval, nonmaxsuppression_two_batches) TEST(op_eval, nonmaxsuppression_two_classes) { - std::vector boxes_data = { - 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, - 0.0, 10.0, 1.0, 11.0, 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; + std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, + 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, + 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; std::vector scores_data = { 0.9, 0.75, 0.6, 0.95, 0.5, 0.3, 0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; @@ -508,7 +509,8 @@ TEST(op_eval, nonmaxsuppression_two_classes) auto max_output_boxes_per_class = op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); auto nms = make_shared(boxes, scores, @@ -526,13 +528,13 @@ TEST(op_eval, nonmaxsuppression_two_classes) { result = make_shared(); } - ASSERT_TRUE( - f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); + ASSERT_TRUE(f->evaluate(results, + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 1, 3, 0, 1, 0}; - std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 1.0, 0.95, 0.0, 1.0, 0.9}; + std::vector expected_selected_scores = { + 0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 1.0, 0.95, 0.0, 1.0, 0.9}; std::vector expected_valid_outputs = {4}; EXPECT_EQ(results[0]->get_element_type(), element::i64); From 5530ad03ce916e929622bdb8c1c7ad1ca66f75d7 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 14 Oct 2020 12:04:18 +0300 Subject: [PATCH 074/173] Moved some file. --- .../convert_opset1_to_legacy}/convert_nms_5_to_legacy.cpp | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename inference-engine/src/{transformations/src/transformations/op_conversions => legacy_api/src/transformations/convert_opset1_to_legacy}/convert_nms_5_to_legacy.cpp (100%) diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_nms_5_to_legacy.cpp b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp similarity index 100% rename from inference-engine/src/transformations/src/transformations/op_conversions/convert_nms_5_to_legacy.cpp rename to inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp From 5e412e558755591c4d3d71c16ead407a91ea2fa1 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 14 Oct 2020 13:57:00 +0300 Subject: [PATCH 075/173] Small fix. --- .../src/legacy_api/include/legacy/ngraph_ops/nms_ie.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inference-engine/src/legacy_api/include/legacy/ngraph_ops/nms_ie.hpp b/inference-engine/src/legacy_api/include/legacy/ngraph_ops/nms_ie.hpp index 7912993200ffc2..49e689e9bd4727 100644 --- a/inference-engine/src/legacy_api/include/legacy/ngraph_ops/nms_ie.hpp +++ b/inference-engine/src/legacy_api/include/legacy/ngraph_ops/nms_ie.hpp @@ -58,7 +58,7 @@ class INFERENCE_ENGINE_API_CLASS(NonMaxSuppressionIE2) : public NonMaxSuppressio std::shared_ptr clone_with_new_inputs(const OutputVector & new_args) const override; }; -class TRANSFORMATIONS_API NonMaxSuppressionIE3 : public Op { +class INFERENCE_ENGINE_API_CLASS(NonMaxSuppressionIE3) : public Op { public: NGRAPH_RTTI_DECLARATION; From 8ec8adbf58cd03ed715b7c6007375b4f9b59de52 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 14 Oct 2020 14:11:26 +0300 Subject: [PATCH 076/173] Code style fix. --- ngraph/test/op_eval/non_max_suppression_5.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ngraph/test/op_eval/non_max_suppression_5.cpp b/ngraph/test/op_eval/non_max_suppression_5.cpp index fd70e33525386b..78ee61ad2f14ff 100644 --- a/ngraph/test/op_eval/non_max_suppression_5.cpp +++ b/ngraph/test/op_eval/non_max_suppression_5.cpp @@ -62,8 +62,8 @@ TEST(op_eval, nonmaxsuppression_center_point_box_format) result = make_shared(); } ASSERT_TRUE(f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); + {make_host_tensor(boxes_shape, boxes_data), + make_host_tensor(scores_shape, scores_data)})); std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 0, 5}; std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 0.0, 0.3}; From d9d761dce2e6c9027d6fe8867368f5cf066ff48b Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 14 Oct 2020 14:43:33 +0300 Subject: [PATCH 077/173] Now NMS-5 supports all floating types in inputs 0 and 1. --- ngraph/core/src/op/non_max_suppression.cpp | 117 +++++++++++++++------ 1 file changed, 82 insertions(+), 35 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index f6f96f85d013d4..c146c264bf80ef 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -20,6 +20,7 @@ #include "ngraph/op/constant.hpp" #include "ngraph/op/util/op_types.hpp" #include "ngraph/runtime/reference/non_max_suppression.hpp" +#include "ngraph/type/bfloat16.hpp" #include "ngraph/type/float16.hpp" using namespace std; @@ -871,54 +872,100 @@ namespace } } - std::vector prepare_boxes_data(const HostTensorPtr& boxes, - const Shape& boxes_shape, - const V5BoxEncoding box_encoding) + std::vector get_floats(const HostTensorPtr& input, const Shape& shape) { - element::Type boxes_input_et = boxes->get_element_type(); - - size_t boxes_size = shape_size(boxes_shape); - std::vector result(boxes_size); + size_t input_size = shape_size(shape); + std::vector result(input_size); - if (boxes_input_et == ngraph::element::f32) + switch(input->get_element_type()) { - float* boxes_ptr = boxes->get_data_ptr(); - memcpy(result.data(), boxes_ptr, boxes_size * sizeof(float)); - } - else - { - float16* boxes_ptr = boxes->get_data_ptr(); - for (size_t i = 0; i < boxes_size; ++i) + case element::Type_t::bf16: { - result[i] = float(boxes_ptr[i]); + bfloat16* p = input->get_data_ptr(); + for (size_t i = 0; i < input_size; ++i) + { + result[i] = float(p[i]); + } } + break; + case element::Type_t::f16: + { + float16* p = input->get_data_ptr(); + for (size_t i = 0; i < input_size; ++i) + { + result[i] = float(p[i]); + } + } + break; + case element::Type_t::f32: + { + float* p = input->get_data_ptr(); + memcpy(result.data(), p, input_size * sizeof(float)); + } + break; + case element::Type_t::f64: + { + double* p = input->get_data_ptr(); + for (size_t i = 0; i < input_size; ++i) + { + result[i] = float(p[i]); + } + } + break; + default:; } + return result; + } + + std::vector prepare_boxes_data(const HostTensorPtr& boxes, + const Shape& boxes_shape, + const V5BoxEncoding box_encoding) + { + // element::Type boxes_input_et = boxes->get_element_type(); + // + // size_t boxes_size = shape_size(boxes_shape); + // std::vector result(boxes_size); + // + // if (boxes_input_et == ngraph::element::f32) + // { + // float* boxes_ptr = boxes->get_data_ptr(); + // memcpy(result.data(), boxes_ptr, boxes_size * sizeof(float)); + // } + // else + // { + // float16* boxes_ptr = boxes->get_data_ptr(); + // for (size_t i = 0; i < boxes_size; ++i) + // { + // result[i] = float(boxes_ptr[i]); + // } + // } + auto result = get_floats(boxes, boxes_shape); normalize_box_encoding(result.data(), boxes_shape, box_encoding); return result; } std::vector prepare_scores_data(const HostTensorPtr& scores, const Shape& scores_shape) { - element::Type scores_input_et = scores->get_element_type(); - - size_t scores_size = shape_size(scores_shape); - std::vector result(scores_size); - - if (scores_input_et == ngraph::element::f32) - { - float* scores_ptr = scores->get_data_ptr(); - memcpy(result.data(), scores_ptr, scores_size * sizeof(float)); - } - else - { - float16* scores_ptr = scores->get_data_ptr(); - for (size_t i = 0; i < scores_size; ++i) - { - result[i] = float(scores_ptr[i]); - } - } - + // element::Type scores_input_et = scores->get_element_type(); + // + // size_t scores_size = shape_size(scores_shape); + // std::vector result(scores_size); + // + // if (scores_input_et == ngraph::element::f32) + // { + // float* scores_ptr = scores->get_data_ptr(); + // memcpy(result.data(), scores_ptr, scores_size * sizeof(float)); + // } + // else + // { + // float16* scores_ptr = scores->get_data_ptr(); + // for (size_t i = 0; i < scores_size; ++i) + // { + // result[i] = float(scores_ptr[i]); + // } + // } + auto result = get_floats(scores, scores_shape); return result; } From 83a0eea1970e3936456e7299c1116db8f418eaa6 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 14 Oct 2020 15:13:29 +0300 Subject: [PATCH 078/173] Moved some files. --- .../convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp | 2 +- .../{ => op_conversions}/convert_previous_nms_to_nms_5.hpp | 0 .../common_optimizations/common_optimizations.cpp | 1 + .../{ => op_conversions}/convert_previous_nms_to_nms_5.cpp | 2 +- 4 files changed, 3 insertions(+), 2 deletions(-) rename inference-engine/src/transformations/include/transformations/{ => op_conversions}/convert_previous_nms_to_nms_5.hpp (100%) rename inference-engine/src/transformations/src/transformations/{ => op_conversions}/convert_previous_nms_to_nms_5.cpp (99%) diff --git a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp index a633b103501639..9d15a172b6d03c 100644 --- a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp +++ b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include #include diff --git a/inference-engine/src/transformations/include/transformations/convert_previous_nms_to_nms_5.hpp b/inference-engine/src/transformations/include/transformations/op_conversions/convert_previous_nms_to_nms_5.hpp similarity index 100% rename from inference-engine/src/transformations/include/transformations/convert_previous_nms_to_nms_5.hpp rename to inference-engine/src/transformations/include/transformations/op_conversions/convert_previous_nms_to_nms_5.hpp diff --git a/inference-engine/src/transformations/src/transformations/common_optimizations/common_optimizations.cpp b/inference-engine/src/transformations/src/transformations/common_optimizations/common_optimizations.cpp index 23f121fa97f4db..6e09435a61650e 100644 --- a/inference-engine/src/transformations/src/transformations/common_optimizations/common_optimizations.cpp +++ b/inference-engine/src/transformations/src/transformations/common_optimizations/common_optimizations.cpp @@ -41,6 +41,7 @@ #include "transformations/op_conversions/reduce_l1_decomposition.hpp" #include "transformations/op_conversions/reduce_l2_decomposition.hpp" #include "transformations/op_conversions/hswish_decomposition.hpp" +#include "transformations/op_conversions/convert_previous_nms_to_nms_5.hpp" #include #include diff --git a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp similarity index 99% rename from inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp rename to inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp index ee4ab6a6f8dc99..6e0b78ae190f43 100644 --- a/inference-engine/src/transformations/src/transformations/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // -#include "transformations/convert_previous_nms_to_nms_5.hpp" +#include "transformations/op_conversions/convert_previous_nms_to_nms_5.hpp" #include #include From 8428e209e9cf4e06f08ddfe43393ac314efeaab8 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 14 Oct 2020 15:40:43 +0300 Subject: [PATCH 079/173] Fixed include directive in the file convert_nms_5_to_legacy.cpp with transformations NMS-1, 3, 4 -> NMS-5. --- .../convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp index 9d15a172b6d03c..aceecea837ff94 100644 --- a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp +++ b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp @@ -12,7 +12,7 @@ #include #include -#include "transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp" +#include "legacy/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp" NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS5ToLegacyMatcher, "ConvertNMS5ToLegacyMatcher", 0); From 571a7fde913762c9feeb140e881883441ad8c23c Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 14 Oct 2020 16:30:42 +0300 Subject: [PATCH 080/173] Small changes. --- .../convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp | 2 +- .../inference_engine/transformations/convert_nms5_test.cpp | 4 ++-- .../transformations/convert_previous_nms_to_nms_5.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/inference-engine/src/legacy_api/include/legacy/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp b/inference-engine/src/legacy_api/include/legacy/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp index 1dcfcbe1e2c4a8..630ef3f027bf99 100644 --- a/inference-engine/src/legacy_api/include/legacy/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp +++ b/inference-engine/src/legacy_api/include/legacy/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp @@ -14,7 +14,7 @@ namespace ngraph { namespace pass { - class TRANSFORMATIONS_API ConvertNMS5ToLegacyMatcher; + class INFERENCE_ENGINE_API_CLASS(ConvertNMS5ToLegacyMatcher); } // namespace pass } // namespace ngraph diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp index e9adb14669b1d5..6567330464f31c 100644 --- a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp +++ b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp @@ -9,8 +9,8 @@ #include #include -#include -#include +#include +#include #include #include #include diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_previous_nms_to_nms_5.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_previous_nms_to_nms_5.cpp index e965d8d3d23e4f..0f96d32b690c49 100644 --- a/inference-engine/tests/functional/inference_engine/transformations/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/tests/functional/inference_engine/transformations/convert_previous_nms_to_nms_5.cpp @@ -13,7 +13,7 @@ #include #include #include -#include +#include #include #include #include From 730cd6df057b4487b91a7ff60535952febe42ae0 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 14 Oct 2020 17:39:50 +0300 Subject: [PATCH 081/173] Deleted conversion NMS-3 -> legacy. --- .../op_conversions/convert_nms3.hpp | 31 ---------- .../op_conversions/convert_nms3.cpp | 45 -------------- .../convert_opset3_to_opset2.cpp | 1 - .../transformations/convert_nms3_test.cpp | 62 ------------------- 4 files changed, 139 deletions(-) delete mode 100644 inference-engine/src/transformations/include/transformations/op_conversions/convert_nms3.hpp delete mode 100644 inference-engine/src/transformations/src/transformations/op_conversions/convert_nms3.cpp delete mode 100644 inference-engine/tests/functional/inference_engine/transformations/convert_nms3_test.cpp diff --git a/inference-engine/src/transformations/include/transformations/op_conversions/convert_nms3.hpp b/inference-engine/src/transformations/include/transformations/op_conversions/convert_nms3.hpp deleted file mode 100644 index 724566fdd2391d..00000000000000 --- a/inference-engine/src/transformations/include/transformations/op_conversions/convert_nms3.hpp +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (C) 2018-2020 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#pragma once - -#include -#include - -#include - -#include - -namespace ngraph { -namespace pass { - -class TRANSFORMATIONS_API ConvertNMS1ToNMS3; - -} // namespace pass -} // namespace ngraph - -class ngraph::pass::ConvertNMS1ToNMS3: public ngraph::pass::GraphRewrite { -public: - NGRAPH_RTTI_DECLARATION; - ConvertNMS1ToNMS3() : GraphRewrite() { - convert_nms1_to_nms3(); - } - -private: - void convert_nms1_to_nms3(); -}; diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_nms3.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_nms3.cpp deleted file mode 100644 index fa90efa9c58c81..00000000000000 --- a/inference-engine/src/transformations/src/transformations/op_conversions/convert_nms3.cpp +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (C) 2018-2020 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#include "transformations/op_conversions/convert_nms3.hpp" - -#include -#include - -#include -#include -#include -#include - -NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS1ToNMS3, "ConvertNMS1ToNMS3", 0); - -void ngraph::pass::ConvertNMS1ToNMS3::convert_nms1_to_nms3() { - auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); - auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); - auto max_output_boxes_per_class = ngraph::opset3::Constant::create(element::i64, Shape{}, {10}); - auto iou_threshold = ngraph::opset3::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = ngraph::opset3::Constant::create(element::f32, Shape{}, {0.7}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - iou_threshold, score_threshold); - - ngraph::graph_rewrite_callback callback = [](pattern::Matcher &m) { - auto nms = std::dynamic_pointer_cast(m.get_match_root()); - if (!nms) { - return false; - } - - auto nms3 = std::make_shared(nms->input_value(0), nms->input_value(1), - nms->input_value(2), nms->input_value(3), nms->input_value(4), - static_cast(nms->get_box_encoding()), - nms->get_sort_result_descending()); - - nms3->set_friendly_name(nms->get_friendly_name()); - ngraph::copy_runtime_info(nms, nms3); - ngraph::replace_node(nms, nms3); - return true; - }; - - auto m = std::make_shared(nms, "ConvertNMS1ToNMS3"); - this->add_matcher(m, callback, PassProperty::CHANGE_DYNAMIC_STATE); -} diff --git a/inference-engine/src/transformations/src/transformations/opset_conversions/convert_opset3_to_opset2.cpp b/inference-engine/src/transformations/src/transformations/opset_conversions/convert_opset3_to_opset2.cpp index 02c45bd0ee4a3d..c9fbecc4f13cbe 100644 --- a/inference-engine/src/transformations/src/transformations/opset_conversions/convert_opset3_to_opset2.cpp +++ b/inference-engine/src/transformations/src/transformations/opset_conversions/convert_opset3_to_opset2.cpp @@ -5,7 +5,6 @@ #include "transformations/opset_conversions/convert_opset3_to_opset2.hpp" #include "transformations/op_conversions/convert_broadcast3.hpp" -#include "transformations/op_conversions/convert_nms3.hpp" #include "transformations/op_conversions/convert_shapeof3.hpp" #include "transformations/op_conversions/convert_shuffle_channels3.hpp" #include "transformations/op_conversions/convert_topk3.hpp" diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_nms3_test.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_nms3_test.cpp deleted file mode 100644 index ae4ee04c26b7c6..00000000000000 --- a/inference-engine/tests/functional/inference_engine/transformations/convert_nms3_test.cpp +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (C) 2020 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#include - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "common_test_utils/ngraph_test_utils.hpp" - -using namespace testing; -using namespace ngraph; - -TEST(TransformationTests, ConvertNMS3I32Output) { - std::shared_ptr f(nullptr), f_ref(nullptr); - { - auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); - auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); - auto max_output_boxes_per_class = opset3::Constant::create(element::i64, Shape{}, {10}); - auto iou_threshold = opset3::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = opset3::Constant::create(element::f32, Shape{}, {0.7}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - iou_threshold, score_threshold, opset1::NonMaxSuppression::BoxEncodingType::CORNER, true); - nms->set_friendly_name("nms"); - - f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); - - pass::InitNodeInfo().run_on_function(f); - pass::ConvertNMS1ToNMS3().run_on_function(f); - ASSERT_NO_THROW(check_rt_info(f)); - } - - { - auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); - auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); - auto max_output_boxes_per_class = opset3::Constant::create(element::i64, Shape{}, {10}); - auto iou_threshold = opset3::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = opset3::Constant::create(element::f32, Shape{}, {0.7}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - iou_threshold, score_threshold, opset3::NonMaxSuppression::BoxEncodingType::CORNER, true); - nms->set_friendly_name("nms"); - - f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); - } - - auto res = compare_functions(f, f_ref); - ASSERT_TRUE(res.first) << res.second; - - auto result_node_of_converted_f = f->get_output_op(0); - auto nms_node = result_node_of_converted_f->input(0).get_source_output().get_node_shared_ptr(); - ASSERT_TRUE(nms_node->get_friendly_name() == "nms") << "Transformation ConvertNMS1ToNMS3 should keep output names.\n"; -} From 32337fc6966acafcc0af1c40a10510e5e60db80f Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 14 Oct 2020 17:48:27 +0300 Subject: [PATCH 082/173] Small changes. --- .../opset_conversions/convert_opset3_to_opset2.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/inference-engine/src/transformations/src/transformations/opset_conversions/convert_opset3_to_opset2.cpp b/inference-engine/src/transformations/src/transformations/opset_conversions/convert_opset3_to_opset2.cpp index c9fbecc4f13cbe..666d6e80d3eb05 100644 --- a/inference-engine/src/transformations/src/transformations/opset_conversions/convert_opset3_to_opset2.cpp +++ b/inference-engine/src/transformations/src/transformations/opset_conversions/convert_opset3_to_opset2.cpp @@ -25,7 +25,6 @@ bool ngraph::pass::ConvertOpSet3ToOpSet2::run_on_function(std::shared_ptr(); - manager.register_pass(); manager.register_pass(); manager.register_pass(); manager.register_pass(); From 24a6a2f3dcdcec3b936d8e12bab286b540c5826e Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 14 Oct 2020 17:54:20 +0300 Subject: [PATCH 083/173] Fix in op::v5::NonMaxSuppression::evaluate: output shape [1] instead of [] in the output port 2. --- ngraph/core/src/op/non_max_suppression.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 81e8efcbf78419..e9755309f89b36 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -1114,7 +1114,7 @@ bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, if (num_of_outputs >= 3) { outputs[2]->set_element_type(m_output_type); - outputs[2]->set_shape(Shape{}); + outputs[2]->set_shape(Shape{1}); } evaluate_postprocessing( From c0f9697e59c188796ed9524798968806c71df1b8 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 15 Oct 2020 09:25:36 +0300 Subject: [PATCH 084/173] Code style fixes. --- ngraph/core/src/op/non_max_suppression.cpp | 48 +++++++++++----------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index e9755309f89b36..26f9f7878b6817 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -877,41 +877,41 @@ namespace size_t input_size = shape_size(shape); std::vector result(input_size); - switch(input->get_element_type()) + switch (input->get_element_type()) { case element::Type_t::bf16: + { + bfloat16* p = input->get_data_ptr(); + for (size_t i = 0; i < input_size; ++i) { - bfloat16* p = input->get_data_ptr(); - for (size_t i = 0; i < input_size; ++i) - { - result[i] = float(p[i]); - } + result[i] = float(p[i]); } - break; + } + break; case element::Type_t::f16: + { + float16* p = input->get_data_ptr(); + for (size_t i = 0; i < input_size; ++i) { - float16* p = input->get_data_ptr(); - for (size_t i = 0; i < input_size; ++i) - { - result[i] = float(p[i]); - } + result[i] = float(p[i]); } - break; + } + break; case element::Type_t::f32: - { - float* p = input->get_data_ptr(); - memcpy(result.data(), p, input_size * sizeof(float)); - } - break; + { + float* p = input->get_data_ptr(); + memcpy(result.data(), p, input_size * sizeof(float)); + } + break; case element::Type_t::f64: + { + double* p = input->get_data_ptr(); + for (size_t i = 0; i < input_size; ++i) { - double* p = input->get_data_ptr(); - for (size_t i = 0; i < input_size; ++i) - { - result[i] = float(p[i]); - } + result[i] = float(p[i]); } - break; + } + break; default:; } From ad6ea78b96fd301623f61d28e87e8690565ae898 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 15 Oct 2020 10:26:37 +0300 Subject: [PATCH 085/173] Deleted conversion of NMS-4 into legacy NMS. --- .../convert_nms_4_to_legacy.hpp | 33 ---- .../convert_nms_4_to_legacy.cpp | 113 ------------- .../convert_opset1_to_legacy.cpp | 2 - .../transformations/convert_nms4_test.cpp | 148 ------------------ 4 files changed, 296 deletions(-) delete mode 100644 inference-engine/src/legacy_api/include/legacy/transformations/convert_opset1_to_legacy/convert_nms_4_to_legacy.hpp delete mode 100644 inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_4_to_legacy.cpp delete mode 100644 inference-engine/tests/functional/inference_engine/transformations/convert_nms4_test.cpp diff --git a/inference-engine/src/legacy_api/include/legacy/transformations/convert_opset1_to_legacy/convert_nms_4_to_legacy.hpp b/inference-engine/src/legacy_api/include/legacy/transformations/convert_opset1_to_legacy/convert_nms_4_to_legacy.hpp deleted file mode 100644 index affd092ce56b0a..00000000000000 --- a/inference-engine/src/legacy_api/include/legacy/transformations/convert_opset1_to_legacy/convert_nms_4_to_legacy.hpp +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2018-2020 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#pragma once - -#include -#include - -#include - -#include - -namespace ngraph { -namespace pass { - - class INFERENCE_ENGINE_API_CLASS(ConvertNMS4ToLegacyMatcher); - -} // namespace pass -} // namespace ngraph - -/* - * Description: - * Convert NMS-4 directly to legacy NMS because NMS-3 and NMS-1 have different shape infer function - */ - - -class ngraph::pass::ConvertNMS4ToLegacyMatcher: public ngraph::pass::MatcherPass { -public: - NGRAPH_RTTI_DECLARATION; - ConvertNMS4ToLegacyMatcher(); -}; - diff --git a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_4_to_legacy.cpp b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_4_to_legacy.cpp deleted file mode 100644 index ca6a7459b63952..00000000000000 --- a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_4_to_legacy.cpp +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (C) 2018-2020 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "legacy/transformations/convert_opset1_to_legacy/convert_nms_4_to_legacy.hpp" - -NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS4ToLegacyMatcher, "ConvertNMS4ToLegacyMatcher", 0); - -ngraph::pass::ConvertNMS4ToLegacyMatcher::ConvertNMS4ToLegacyMatcher() { - auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); - auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); - auto max_output_boxes_per_class = ngraph::opset4::Constant::create(element::i64, Shape{}, {10}); - auto iou_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.7}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - iou_threshold, score_threshold); - - ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { - auto nms_4 = std::dynamic_pointer_cast(m.get_match_root()); - if (!nms_4) { - return false; - } - - const auto new_args = nms_4->input_values(); - const auto& arg2 = new_args.size() > 2 ? new_args.at(2) : ngraph::opset4::Constant::create(element::i32, Shape{}, {0}); - const auto& arg3 = new_args.size() > 3 ? new_args.at(3) : ngraph::opset4::Constant::create(element::f32, Shape{}, {.0f}); - const auto& arg4 = new_args.size() > 4 ? new_args.at(4) : ngraph::opset4::Constant::create(element::f32, Shape{}, {.0f}); - - const auto max_output_boxes_per_class_rank = arg2.get_partial_shape().rank(); - const auto iou_threshold_rank = arg3.get_partial_shape().rank(); - const auto score_threshold_rank = arg4.get_partial_shape().rank(); - - // Check that required ranks are not dynamic - if (max_output_boxes_per_class_rank.is_dynamic() || - iou_threshold_rank.is_dynamic() || - score_threshold_rank.is_dynamic()) { - return false; - } - - if (max_output_boxes_per_class_rank.get_length() == 1 && - iou_threshold_rank.get_length() == 1 && - score_threshold_rank.get_length() == 1) { - return false; - } - - // vector of new nGraph operations - NodeVector new_ops; - - auto new_max_per_class = arg2; - if (max_output_boxes_per_class_rank.get_length() == 0) { - // WA: we need to create Constant manually because it requires by NMS shape inference - // otherwise we will get dynamic shape until first CF is executed. It can be resolved - // if CF will be executed right after transformation and before Validate pass. - if (auto new_max_per_class_const = std::dynamic_pointer_cast(new_max_per_class.get_node_shared_ptr())) { - new_max_per_class = opset1::Constant::create(element::i64, Shape{1}, new_max_per_class_const->cast_vector()); - } else { - new_max_per_class = std::make_shared(arg2, opset1::Constant::create(element::i64, Shape{1}, {0})); - new_ops.push_back(new_max_per_class.get_node_shared_ptr()); - } - } - auto new_iou_threshold = arg3; - if (iou_threshold_rank.get_length() == 0) { - new_iou_threshold = std::make_shared(arg3, opset1::Constant::create(element::i64, Shape{1}, {0})); - new_ops.push_back(new_iou_threshold.get_node_shared_ptr()); - } - auto new_score_threshold = arg4; - if (score_threshold_rank.get_length() == 0) { - new_score_threshold = std::make_shared(arg4, opset1::Constant::create(element::i64, Shape{1}, {0})); - new_ops.push_back(new_score_threshold.get_node_shared_ptr()); - } - - int center_point_box = 0; - switch (nms_4->get_box_encoding()) { - case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CENTER: - center_point_box = 1; - break; - case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CORNER: - center_point_box = 0; - break; - default: - throw ngraph_error("NonMaxSuppression layer " + nms_4->get_friendly_name() + - " has unsupported box encoding"); - } - const auto nms_legacy = std::make_shared( - new_args.at(0), - new_args.at(1), - new_max_per_class, - new_iou_threshold, - new_score_threshold, - center_point_box, - nms_4->get_sort_result_descending(), - nms_4->get_output_type()); - new_ops.push_back(nms_legacy); - - nms_legacy->set_friendly_name(nms_4->get_friendly_name()); - ngraph::copy_runtime_info(nms_4, new_ops); - ngraph::replace_node(nms_4, nms_legacy); - return true; - }; - - auto m = std::make_shared(nms, "ConvertNMS4ToNMSLegacy"); - this->register_matcher(m, callback); -} diff --git a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_opset1_to_legacy.cpp b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_opset1_to_legacy.cpp index cef550019877d6..5f9a657c06be5c 100644 --- a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_opset1_to_legacy.cpp +++ b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_opset1_to_legacy.cpp @@ -13,7 +13,6 @@ #include "legacy/transformations/convert_opset1_to_legacy/convert_mul_add_to_scaleshift_or_power.hpp" #include "legacy/transformations/convert_opset1_to_legacy/convert_mul_or_add_finally.hpp" #include "legacy/transformations/convert_opset1_to_legacy/convert_nms_to_nms_ie.hpp" -#include "legacy/transformations/convert_opset1_to_legacy/convert_nms_4_to_legacy.hpp" #include "legacy/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp" #include "legacy/transformations/convert_opset1_to_legacy/convert_normalizel2_to_normalize_ie.hpp" #include "legacy/transformations/convert_opset1_to_legacy/convert_one_hot_to_one_hot_ie.hpp" @@ -134,7 +133,6 @@ bool ngraph::pass::ConvertOpSet1ToLegacy::run_on_function(std::shared_ptradd_matcher(); anchor->add_matcher(); anchor->add_matcher(); - anchor->add_matcher(); anchor->add_matcher(); anchor->add_matcher(); anchor->add_matcher(); diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_nms4_test.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_nms4_test.cpp deleted file mode 100644 index 6b62756b247b84..00000000000000 --- a/inference-engine/tests/functional/inference_engine/transformations/convert_nms4_test.cpp +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (C) 2020 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 -// - -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "common_test_utils/ngraph_test_utils.hpp" - -using namespace testing; -using namespace ngraph; - -TEST(TransformationTests, ConvertNMS4ToNMSIEStatic) { - std::shared_ptr f(nullptr), f_ref(nullptr); - { - auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); - auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); - auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{}, {10}); - auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, - opset4::NonMaxSuppression::BoxEncodingType::CORNER, true); - - f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); - - const auto &orig_shape = f->get_output_partial_shape(0); - pass::Manager manager; - manager.register_pass(); - manager.register_pass(); - manager.run_passes(f); - ASSERT_NO_THROW(check_rt_info(f)); - ASSERT_TRUE(f->get_output_partial_shape(0).is_static()) << "Shape " << f->get_output_partial_shape(0) << " should be static"; - } - - { - auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); - auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); - auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{1}, {10}); - auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - std::make_shared(iou_threshold, - opset4::Constant::create(element::i64, Shape{1}, {0})), - std::make_shared(score_threshold, - opset4::Constant::create(element::i64, Shape{1}, {0})), - 0, true); - nms->set_friendly_name("nms"); - - f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); - ASSERT_TRUE(f_ref->get_output_partial_shape(0).is_static()) << "Shape " << f_ref->get_output_partial_shape(0) << " should be static"; - } - - auto res = compare_functions(f, f_ref); - ASSERT_TRUE(res.first) << res.second; -} - -TEST(TransformationTests, ConvertNMS4ToNMSIEDynamic1) { - std::shared_ptr f(nullptr), f_ref(nullptr); - { - auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); - auto scores = std::make_shared(element::f32, PartialShape::dynamic()); - auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{}, {10}); - auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, - opset4::NonMaxSuppression::BoxEncodingType::CORNER, true, element::i32); - - f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); - - pass::Manager manager; - manager.register_pass(); - manager.register_pass(); - manager.run_passes(f); - f->validate_nodes_and_infer_types(); - ASSERT_NO_THROW(check_rt_info(f)); - } - - { - auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); - auto scores = std::make_shared(element::f32, PartialShape::dynamic()); - auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{1}, {10}); - auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - std::make_shared(iou_threshold, - opset4::Constant::create(element::i64, Shape{1}, {0})), - std::make_shared(score_threshold, - opset4::Constant::create(element::i64, Shape{1}, {0})), - 0, true); - - f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); - } - - auto res = compare_functions(f, f_ref); - ASSERT_TRUE(res.first) << res.second; -} - -TEST(TransformationTests, ConvertNMS4ToNMSIEDynamic2) { - std::shared_ptr f(nullptr), f_ref(nullptr); - { - auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); - auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); - auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{}, {10}); - auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, - opset4::NonMaxSuppression::BoxEncodingType::CORNER, true, element::i32); - - f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); - - pass::Manager manager; - manager.register_pass(); - manager.register_pass(); - manager.run_passes(f); - - f->validate_nodes_and_infer_types(); - ASSERT_NO_THROW(check_rt_info(f)); - } - - { - auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); - auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); - auto max_output_boxes_per_class = opset4::Constant::create(element::i64, Shape{1}, {10}); - auto iou_threshold = opset4::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = opset4::Constant::create(element::f32, Shape{}, {0.7}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - std::make_shared(iou_threshold, - opset4::Constant::create(element::i64, Shape{1}, {0})), - std::make_shared(score_threshold, - opset4::Constant::create(element::i64, Shape{1}, {0})), - 0, true); - - f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); - } - - auto res = compare_functions(f, f_ref); - ASSERT_TRUE(res.first) << res.second; -} From 0952f96e96edc947c19dc09b6d31cf86faa4e57c Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 15 Oct 2020 10:55:18 +0300 Subject: [PATCH 086/173] Deleted redundant ifs. --- ngraph/core/src/op/non_max_suppression.cpp | 56 +++++++++------------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 26f9f7878b6817..c27f75d270ebf1 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -700,43 +700,31 @@ void op::v5::NonMaxSuppression::validate() "Expected a 3D tensor for the 'scores' input. Got: ", scores_ps); - if (inputs().size() >= 3) - { - const auto max_boxes_ps = get_input_partial_shape(2); - NODE_VALIDATION_CHECK(this, - max_boxes_ps.is_dynamic() || is_scalar(max_boxes_ps.to_shape()), - "Expected a scalar for the 'max_output_boxes_per_class' input. Got: ", - max_boxes_ps); - } + const auto max_boxes_ps = get_input_partial_shape(2); + NODE_VALIDATION_CHECK(this, + max_boxes_ps.is_dynamic() || is_scalar(max_boxes_ps.to_shape()), + "Expected a scalar for the 'max_output_boxes_per_class' input. Got: ", + max_boxes_ps); - if (inputs().size() >= 4) - { - const auto iou_threshold_ps = get_input_partial_shape(3); - NODE_VALIDATION_CHECK(this, - iou_threshold_ps.is_dynamic() || - is_scalar(iou_threshold_ps.to_shape()), - "Expected a scalar for the 'iou_threshold' input. Got: ", - iou_threshold_ps); - } + const auto iou_threshold_ps = get_input_partial_shape(3); + NODE_VALIDATION_CHECK(this, + iou_threshold_ps.is_dynamic() || + is_scalar(iou_threshold_ps.to_shape()), + "Expected a scalar for the 'iou_threshold' input. Got: ", + iou_threshold_ps); - if (inputs().size() >= 5) - { - const auto score_threshold_ps = get_input_partial_shape(4); - NODE_VALIDATION_CHECK(this, - score_threshold_ps.is_dynamic() || - is_scalar(score_threshold_ps.to_shape()), - "Expected a scalar for the 'score_threshold' input. Got: ", - score_threshold_ps); - } + const auto score_threshold_ps = get_input_partial_shape(4); + NODE_VALIDATION_CHECK(this, + score_threshold_ps.is_dynamic() || + is_scalar(score_threshold_ps.to_shape()), + "Expected a scalar for the 'score_threshold' input. Got: ", + score_threshold_ps); - if (inputs().size() >= 6) - { - const auto soft_nms_sigma = get_input_partial_shape(5); - NODE_VALIDATION_CHECK(this, - soft_nms_sigma.is_dynamic() || is_scalar(soft_nms_sigma.to_shape()), - "Expected a scalar for the 'soft_nms_sigma' input. Got: ", - soft_nms_sigma); - } + const auto soft_nms_sigma = get_input_partial_shape(5); + NODE_VALIDATION_CHECK(this, + soft_nms_sigma.is_dynamic() || is_scalar(soft_nms_sigma.to_shape()), + "Expected a scalar for the 'soft_nms_sigma' input. Got: ", + soft_nms_sigma); const auto num_batches_boxes = boxes_ps[0]; const auto num_batches_scores = scores_ps[0]; From 44b6d09d707b11cd47829fbe05a8a2cf3486ced5 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 15 Oct 2020 13:16:51 +0300 Subject: [PATCH 087/173] Added NMS-5 to Python API. --- ngraph/python/src/ngraph/opset5/__init__.py | 2 +- ngraph/python/src/ngraph/opset5/ops.py | 47 +++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/ngraph/python/src/ngraph/opset5/__init__.py b/ngraph/python/src/ngraph/opset5/__init__.py index e2b4a83c488ec3..16663ef7521d0f 100644 --- a/ngraph/python/src/ngraph/opset5/__init__.py +++ b/ngraph/python/src/ngraph/opset5/__init__.py @@ -95,7 +95,7 @@ from ngraph.opset1.ops import multiply from ngraph.opset2.ops import mvn from ngraph.opset1.ops import negative -from ngraph.opset4.ops import non_max_suppression +from ngraph.opset5.ops import non_max_suppression from ngraph.opset3.ops import non_zero from ngraph.opset1.ops import normalize_l2 from ngraph.opset1.ops import not_equal diff --git a/ngraph/python/src/ngraph/opset5/ops.py b/ngraph/python/src/ngraph/opset5/ops.py index 8c1950e93bede2..193133963f5008 100644 --- a/ngraph/python/src/ngraph/opset5/ops.py +++ b/ngraph/python/src/ngraph/opset5/ops.py @@ -90,3 +90,50 @@ def log_softmax(data: NodeInput, axis: int, name: Optional[str] = None) -> Node: :return: The new node with LogSoftmax operation applied on each element. """ return _get_node_factory_opset5().create("LogSoftmax", [as_node(data)], {"axis": axis}) + + +@nameable_op +def non_max_suppression( + boxes: NodeInput, + scores: NodeInput, + max_output_boxes_per_class: Optional[NodeInput] = None, + iou_threshold: Optional[NodeInput] = None, + score_threshold: Optional[NodeInput] = None, + soft_nms_sigma: Optional[NodeInput] = None, + box_encoding: str = "corner", + sort_result_descending: bool = True, + output_type: str = "i64", + name: Optional[str] = None, +) -> Node: + """Return a node which performs NonMaxSuppression. + + :param boxes: Tensor with box coordinates. + :param scores: Tensor with box scores. + :param max_output_boxes_per_class: Tensor Specifying maximum number of boxes + to be selected per class. + :param iou_threshold: Tensor specifying intersection over union threshold + :param score_threshold: Tensor specifying minimum score to consider box for the processing. + :param soft_nms_sigma: Tensor specifying the sigma parameter for Soft-NMS. + :param box_encoding: Format of boxes data encoding. + :param sort_result_descending: Flag that specifies whenever it is necessary to sort selected + boxes across batches or not. + :param output_type: Output element type. + :return: The new node which performs NonMaxSuppression + """ + if max_output_boxes_per_class is None: + max_output_boxes_per_class = make_constant_node(0, np.int64) + if iou_threshold is None: + iou_threshold = make_constant_node(0, np.float32) + if score_threshold is None: + score_threshold = make_constant_node(0, np.float32) + if soft_nms_sigma is None: + soft_nms_sigma = make_constant_node(0, np.float32) + + inputs = as_nodes(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, soft_nms_sigma) + attributes = { + "box_encoding": box_encoding, + "sort_result_descending": sort_result_descending, + "output_type": output_type, + } + + return _get_node_factory_opset5().create("NonMaxSuppression", inputs, attributes) From cca2675b68a408e6caa36ec60dcc999340276bbe Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 15 Oct 2020 15:39:04 +0300 Subject: [PATCH 088/173] Code style fix. --- ngraph/core/src/op/non_max_suppression.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index c27f75d270ebf1..909f1c5e9216b9 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -708,8 +708,7 @@ void op::v5::NonMaxSuppression::validate() const auto iou_threshold_ps = get_input_partial_shape(3); NODE_VALIDATION_CHECK(this, - iou_threshold_ps.is_dynamic() || - is_scalar(iou_threshold_ps.to_shape()), + iou_threshold_ps.is_dynamic() || is_scalar(iou_threshold_ps.to_shape()), "Expected a scalar for the 'iou_threshold' input. Got: ", iou_threshold_ps); From c2e181ac02037c4a068b021a769b702dcaddbf27 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 15 Oct 2020 17:18:54 +0300 Subject: [PATCH 089/173] Small change. --- ngraph/python/src/ngraph/opset5/ops.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ngraph/python/src/ngraph/opset5/ops.py b/ngraph/python/src/ngraph/opset5/ops.py index 193133963f5008..aa49942988143b 100644 --- a/ngraph/python/src/ngraph/opset5/ops.py +++ b/ngraph/python/src/ngraph/opset5/ops.py @@ -129,7 +129,9 @@ def non_max_suppression( if soft_nms_sigma is None: soft_nms_sigma = make_constant_node(0, np.float32) - inputs = as_nodes(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, soft_nms_sigma) + inputs = as_nodes( + boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, soft_nms_sigma + ) attributes = { "box_encoding": box_encoding, "sort_result_descending": sort_result_descending, From 7597f3095699680f9c638cc8ca63b9e4dc6f767d Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 09:44:49 +0300 Subject: [PATCH 090/173] Fixed element type for constants in the conversion of NMS-5 to NMSIE3. --- .../convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp index aceecea837ff94..7d89d43327fe05 100644 --- a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp +++ b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp @@ -75,17 +75,17 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { } auto new_iou_threshold = arg3; if (iou_threshold_rank.get_length() == 0) { - new_iou_threshold = std::make_shared(arg3, opset1::Constant::create(element::f32, Shape{1}, {0.0f})); + new_iou_threshold = std::make_shared(arg3, opset1::Constant::create(element::i64, Shape{1}, {0})); new_ops.push_back(new_iou_threshold.get_node_shared_ptr()); } auto new_score_threshold = arg4; if (score_threshold_rank.get_length() == 0) { - new_score_threshold = std::make_shared(arg4, opset1::Constant::create(element::f32, Shape{1}, {0.0f})); + new_score_threshold = std::make_shared(arg4, opset1::Constant::create(element::i64, Shape{1}, {0})); new_ops.push_back(new_score_threshold.get_node_shared_ptr()); } auto new_soft_nms_sigma = arg5; if (soft_nms_sigma_rank.get_length() == 0) { - new_soft_nms_sigma = std::make_shared(arg5, opset1::Constant::create(element::f32, Shape{1}, {0.0f})); + new_soft_nms_sigma = std::make_shared(arg5, opset1::Constant::create(element::i64, Shape{1}, {0})); new_ops.push_back(new_soft_nms_sigma.get_node_shared_ptr()); } int center_point_box = 0; From debdf367b4166167cb7d024606d6293c156190f8 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 09:50:47 +0300 Subject: [PATCH 091/173] Deleted support of f64 in NMS-5. --- ngraph/core/src/op/non_max_suppression.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 909f1c5e9216b9..4415565761e72b 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -890,15 +890,6 @@ namespace memcpy(result.data(), p, input_size * sizeof(float)); } break; - case element::Type_t::f64: - { - double* p = input->get_data_ptr(); - for (size_t i = 0; i < input_size; ++i) - { - result[i] = float(p[i]); - } - } - break; default:; } From 3d9d2e4ae33d9f95fe19d8da970374bc4c47060f Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 10:46:01 +0300 Subject: [PATCH 092/173] Added checks for input element types for inputs #0, #1, #3, #4, #5. --- ngraph/core/src/op/non_max_suppression.cpp | 208 ++++++++++++--------- 1 file changed, 123 insertions(+), 85 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 4415565761e72b..c64598a63fad9b 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -676,91 +676,6 @@ shared_ptr m_output_type); } -void op::v5::NonMaxSuppression::validate() -{ - const auto boxes_ps = get_input_partial_shape(0); - const auto scores_ps = get_input_partial_shape(1); - - NODE_VALIDATION_CHECK(this, - m_output_type == element::i64 || m_output_type == element::i32, - "Output type must be i32 or i64"); - - if (boxes_ps.is_dynamic() || scores_ps.is_dynamic()) - { - return; - } - - NODE_VALIDATION_CHECK(this, - boxes_ps.rank().is_static() && boxes_ps.rank().get_length() == 3, - "Expected a 3D tensor for the 'boxes' input. Got: ", - boxes_ps); - - NODE_VALIDATION_CHECK(this, - scores_ps.rank().is_static() && scores_ps.rank().get_length() == 3, - "Expected a 3D tensor for the 'scores' input. Got: ", - scores_ps); - - const auto max_boxes_ps = get_input_partial_shape(2); - NODE_VALIDATION_CHECK(this, - max_boxes_ps.is_dynamic() || is_scalar(max_boxes_ps.to_shape()), - "Expected a scalar for the 'max_output_boxes_per_class' input. Got: ", - max_boxes_ps); - - const auto iou_threshold_ps = get_input_partial_shape(3); - NODE_VALIDATION_CHECK(this, - iou_threshold_ps.is_dynamic() || is_scalar(iou_threshold_ps.to_shape()), - "Expected a scalar for the 'iou_threshold' input. Got: ", - iou_threshold_ps); - - const auto score_threshold_ps = get_input_partial_shape(4); - NODE_VALIDATION_CHECK(this, - score_threshold_ps.is_dynamic() || - is_scalar(score_threshold_ps.to_shape()), - "Expected a scalar for the 'score_threshold' input. Got: ", - score_threshold_ps); - - const auto soft_nms_sigma = get_input_partial_shape(5); - NODE_VALIDATION_CHECK(this, - soft_nms_sigma.is_dynamic() || is_scalar(soft_nms_sigma.to_shape()), - "Expected a scalar for the 'soft_nms_sigma' input. Got: ", - soft_nms_sigma); - - const auto num_batches_boxes = boxes_ps[0]; - const auto num_batches_scores = scores_ps[0]; - NODE_VALIDATION_CHECK(this, - num_batches_boxes.same_scheme(num_batches_scores), - "The first dimension of both 'boxes' and 'scores' must match. Boxes: ", - num_batches_boxes, - "; Scores: ", - num_batches_scores); - - const auto num_boxes_boxes = boxes_ps[1]; - const auto num_boxes_scores = scores_ps[2]; - NODE_VALIDATION_CHECK(this, - num_boxes_boxes.same_scheme(num_boxes_scores), - "'boxes' and 'scores' input shapes must match at the second and third " - "dimension respectively. Boxes: ", - num_boxes_boxes, - "; Scores: ", - num_boxes_scores); - - NODE_VALIDATION_CHECK(this, - boxes_ps[2].is_static() && boxes_ps[2].get_length() == 4u, - "The last dimension of the 'boxes' input must be equal to 4. Got:", - boxes_ps[2]); -} - -int64_t op::v5::NonMaxSuppression::max_boxes_output_from_input() const -{ - int64_t max_output_boxes{0}; - - const auto max_output_boxes_input = - as_type_ptr(input_value(2).get_node_shared_ptr()); - max_output_boxes = max_output_boxes_input->cast_vector().at(0); - - return max_output_boxes; -} - using V5BoxEncoding = op::v5::NonMaxSuppression::BoxEncodingType; namespace @@ -958,6 +873,129 @@ namespace *valid_outputs_ptr = static_cast(valid_outputs); } } + + inline bool is_float_type_admissible(const element::Type& t) + { + return t == element::f32 || t == element::f16 || t == element::bf16; + } +} + +void op::v5::NonMaxSuppression::validate() +{ + const auto boxes_ps = get_input_partial_shape(0); + const auto scores_ps = get_input_partial_shape(1); + + NODE_VALIDATION_CHECK(this, + m_output_type == element::i64 || m_output_type == element::i32, + "Output type must be i32 or i64"); + + if (boxes_ps.is_dynamic() || scores_ps.is_dynamic()) + { + return; + } + + NODE_VALIDATION_CHECK(this, + is_float_type_admissible(get_input_element_type(0)), + "Expected bf16, fp16 or fp32 as element type for the 'boxes' input."); + + NODE_VALIDATION_CHECK(this, + is_float_type_admissible(get_input_element_type(1)), + "Expected bf16, fp16 or fp32 as element type for the 'scores' input."); + + NODE_VALIDATION_CHECK(this, + boxes_ps.rank().is_static() && boxes_ps.rank().get_length() == 3, + "Expected a 3D tensor for the 'boxes' input. Got: ", + boxes_ps); + + NODE_VALIDATION_CHECK(this, + scores_ps.rank().is_static() && scores_ps.rank().get_length() == 3, + "Expected a 3D tensor for the 'scores' input. Got: ", + scores_ps); + + if (inputs().size() >= 3) + { + const auto max_boxes_ps = get_input_partial_shape(2); + NODE_VALIDATION_CHECK(this, + max_boxes_ps.is_dynamic() || is_scalar(max_boxes_ps.to_shape()), + "Expected a scalar for the 'max_output_boxes_per_class' input. Got: ", + max_boxes_ps); + } + + if (inputs().size() >= 4) + { + const auto iou_threshold_ps = get_input_partial_shape(3); + NODE_VALIDATION_CHECK(this, + is_float_type_admissible(get_input_element_type(3)), + "Expected bf16, fp16 or fp32 as element type for the " + "'iou_threshold' input."); + NODE_VALIDATION_CHECK(this, + iou_threshold_ps.is_dynamic() || + is_scalar(iou_threshold_ps.to_shape()), + "Expected a scalar for the 'iou_threshold' input. Got: ", + iou_threshold_ps); + } + + if (inputs().size() >= 5) + { + const auto score_threshold_ps = get_input_partial_shape(4); + NODE_VALIDATION_CHECK(this, + is_float_type_admissible(get_input_element_type(4)), + "Expected bf16, fp16 or fp32 as element type for the " + "'score_threshold_ps' input."); + NODE_VALIDATION_CHECK(this, + score_threshold_ps.is_dynamic() || + is_scalar(score_threshold_ps.to_shape()), + "Expected a scalar for the 'score_threshold' input. Got: ", + score_threshold_ps); + } + + if (inputs().size() >= 6) + { + const auto soft_nms_sigma = get_input_partial_shape(5); + NODE_VALIDATION_CHECK(this, + is_float_type_admissible(get_input_element_type(5)), + "Expected bf16, fp16 or fp32 as element type for the " + "'soft_nms_sigma' input."); + NODE_VALIDATION_CHECK(this, + soft_nms_sigma.is_dynamic() || is_scalar(soft_nms_sigma.to_shape()), + "Expected a scalar for the 'soft_nms_sigma' input. Got: ", + soft_nms_sigma); + } + + const auto num_batches_boxes = boxes_ps[0]; + const auto num_batches_scores = scores_ps[0]; + NODE_VALIDATION_CHECK(this, + num_batches_boxes.same_scheme(num_batches_scores), + "The first dimension of both 'boxes' and 'scores' must match. Boxes: ", + num_batches_boxes, + "; Scores: ", + num_batches_scores); + + const auto num_boxes_boxes = boxes_ps[1]; + const auto num_boxes_scores = scores_ps[2]; + NODE_VALIDATION_CHECK(this, + num_boxes_boxes.same_scheme(num_boxes_scores), + "'boxes' and 'scores' input shapes must match at the second and third " + "dimension respectively. Boxes: ", + num_boxes_boxes, + "; Scores: ", + num_boxes_scores); + + NODE_VALIDATION_CHECK(this, + boxes_ps[2].is_static() && boxes_ps[2].get_length() == 4u, + "The last dimension of the 'boxes' input must be equal to 4. Got:", + boxes_ps[2]); +} + +int64_t op::v5::NonMaxSuppression::max_boxes_output_from_input() const +{ + int64_t max_output_boxes{0}; + + const auto max_output_boxes_input = + as_type_ptr(input_value(2).get_node_shared_ptr()); + max_output_boxes = max_output_boxes_input->cast_vector().at(0); + + return max_output_boxes; } float op::v5::NonMaxSuppression::iou_threshold_from_input() const From e6cbf479011a0265ed64a1e8853520e1bb7f47bd Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 11:02:37 +0300 Subject: [PATCH 093/173] Small change. --- .../convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inference-engine/src/legacy_api/include/legacy/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp b/inference-engine/src/legacy_api/include/legacy/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp index 630ef3f027bf99..375ec6cdc71ada 100644 --- a/inference-engine/src/legacy_api/include/legacy/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp +++ b/inference-engine/src/legacy_api/include/legacy/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp @@ -14,7 +14,7 @@ namespace ngraph { namespace pass { - class INFERENCE_ENGINE_API_CLASS(ConvertNMS5ToLegacyMatcher); +class INFERENCE_ENGINE_API_CLASS(ConvertNMS5ToLegacyMatcher); } // namespace pass } // namespace ngraph From e004a33fb54e8535d8988ac13c48870226064c56 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 11:17:54 +0300 Subject: [PATCH 094/173] Now get_floats throws an exception for unsupported types. --- ngraph/core/src/op/non_max_suppression.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index c64598a63fad9b..2d1ab63bfbc521 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -22,6 +22,7 @@ #include "ngraph/runtime/reference/non_max_suppression.hpp" #include "ngraph/type/bfloat16.hpp" #include "ngraph/type/float16.hpp" +#include "ngraph/util.hpp" using namespace std; using namespace ngraph; @@ -805,7 +806,7 @@ namespace memcpy(result.data(), p, input_size * sizeof(float)); } break; - default:; + default: throw std::runtime_error("Unsupported data type in op NonMaxSuppression-5"); break; } return result; From ddea6e26eee10272646d06d507b790b3e1d5cf6d Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 11:37:25 +0300 Subject: [PATCH 095/173] Now nGraph NMS-5 supports 0D and 1D tensors in inputs #2, #3, #4, #5. --- ngraph/core/src/op/non_max_suppression.cpp | 31 ++++++-- ngraph/test/type_prop/non_max_suppression.cpp | 73 ++++++++++--------- 2 files changed, 60 insertions(+), 44 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 2d1ab63bfbc521..16dc12c55560e0 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -879,6 +879,18 @@ namespace { return t == element::f32 || t == element::f16 || t == element::bf16; } + + inline bool is_scalar_or_1d_tensor_with_1_element(const PartialShape& p) + { + if (p.is_dynamic()) + { + return false; + } + + Shape shape = p.to_shape(); + + return is_scalar(shape) || is_vector(shape) && (shape[0] == 1); + } } void op::v5::NonMaxSuppression::validate() @@ -917,8 +929,10 @@ void op::v5::NonMaxSuppression::validate() { const auto max_boxes_ps = get_input_partial_shape(2); NODE_VALIDATION_CHECK(this, - max_boxes_ps.is_dynamic() || is_scalar(max_boxes_ps.to_shape()), - "Expected a scalar for the 'max_output_boxes_per_class' input. Got: ", + max_boxes_ps.is_dynamic() || + is_scalar_or_1d_tensor_with_1_element(max_boxes_ps), + "Expected 0D or 1D tensor for the 'max_output_boxes_per_class' input. " + "Got: ", max_boxes_ps); } @@ -931,8 +945,8 @@ void op::v5::NonMaxSuppression::validate() "'iou_threshold' input."); NODE_VALIDATION_CHECK(this, iou_threshold_ps.is_dynamic() || - is_scalar(iou_threshold_ps.to_shape()), - "Expected a scalar for the 'iou_threshold' input. Got: ", + is_scalar_or_1d_tensor_with_1_element(iou_threshold_ps), + "Expected 0D or 1D tensor for the 'iou_threshold' input. Got: ", iou_threshold_ps); } @@ -945,8 +959,8 @@ void op::v5::NonMaxSuppression::validate() "'score_threshold_ps' input."); NODE_VALIDATION_CHECK(this, score_threshold_ps.is_dynamic() || - is_scalar(score_threshold_ps.to_shape()), - "Expected a scalar for the 'score_threshold' input. Got: ", + is_scalar_or_1d_tensor_with_1_element(score_threshold_ps), + "Expected 0D or 1D tensor for the 'score_threshold' input. Got: ", score_threshold_ps); } @@ -958,8 +972,9 @@ void op::v5::NonMaxSuppression::validate() "Expected bf16, fp16 or fp32 as element type for the " "'soft_nms_sigma' input."); NODE_VALIDATION_CHECK(this, - soft_nms_sigma.is_dynamic() || is_scalar(soft_nms_sigma.to_shape()), - "Expected a scalar for the 'soft_nms_sigma' input. Got: ", + soft_nms_sigma.is_dynamic() || + is_scalar_or_1d_tensor_with_1_element(soft_nms_sigma), + "Expected 0D or 1D tensor for the 'soft_nms_sigma' input. Got: ", soft_nms_sigma); } diff --git a/ngraph/test/type_prop/non_max_suppression.cpp b/ngraph/test/type_prop/non_max_suppression.cpp index d1ced839867ad3..7c6531629eca09 100644 --- a/ngraph/test/type_prop/non_max_suppression.cpp +++ b/ngraph/test/type_prop/non_max_suppression.cpp @@ -621,42 +621,43 @@ TEST(type_prop, nms_v5_scalar_inputs_check) const auto scalar = make_shared(element::f32, Shape{}); const auto non_scalar = make_shared(element::f32, Shape{1}); - try - { - make_shared(boxes, scores, non_scalar, scalar, scalar); - } - catch (const NodeValidationFailure& error) - { - EXPECT_HAS_SUBSTRING(error.what(), - "Expected a scalar for the 'max_output_boxes_per_class' input"); - } - - try - { - make_shared(boxes, scores, scalar, non_scalar, scalar); - } - catch (const NodeValidationFailure& error) - { - EXPECT_HAS_SUBSTRING(error.what(), "Expected a scalar for the 'iou_threshold' input"); - } - - try - { - make_shared(boxes, scores, scalar, scalar, non_scalar); - } - catch (const NodeValidationFailure& error) - { - EXPECT_HAS_SUBSTRING(error.what(), "Expected a scalar for the 'score_threshold' input"); - } - - try - { - make_shared(boxes, scores, scalar, scalar, scalar, non_scalar); - } - catch (const NodeValidationFailure& error) - { - EXPECT_HAS_SUBSTRING(error.what(), "Expected a scalar for the 'soft_nms_sigma' input"); - } +// try +// { +// make_shared(boxes, scores, non_scalar, scalar, scalar); +// } +// catch (const NodeValidationFailure& error) +// { +// EXPECT_HAS_SUBSTRING(error.what(), +// "Expected 0D or 1D tensor for the 'max_output_boxes_per_class' input"); +// } +// +// try +// { +// make_shared(boxes, scores, scalar, non_scalar, scalar); +// } +// catch (const NodeValidationFailure& error) +// { +// EXPECT_HAS_SUBSTRING(error.what(), "Expected a scalar for the 'iou_threshold' input"); +// } +// +// try +// { +// make_shared(boxes, scores, scalar, scalar, non_scalar); +// } +// catch (const NodeValidationFailure& error) +// { +// EXPECT_HAS_SUBSTRING(error.what(), "Expected a scalar for the 'score_threshold' input"); +// } +// +// try +// { +// make_shared(boxes, scores, scalar, scalar, scalar, non_scalar); +// } +// catch (const NodeValidationFailure& error) +// { +// EXPECT_HAS_SUBSTRING(error.what(), "Expected a scalar for the 'soft_nms_sigma' input"); +// } + ASSERT_TRUE(true); } TEST(type_prop, nms_v5_output_shape) From c09b791270be247d7f531c46daf080769d65a39b Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 11:48:30 +0300 Subject: [PATCH 096/173] Small fix in test_non_max_suppression. --- ngraph/python/tests/test_ngraph/test_reduction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngraph/python/tests/test_ngraph/test_reduction.py b/ngraph/python/tests/test_ngraph/test_reduction.py index 6aa94a7c52fa4b..b23e8b242b3720 100644 --- a/ngraph/python/tests/test_ngraph/test_reduction.py +++ b/ngraph/python/tests/test_ngraph/test_reduction.py @@ -112,7 +112,7 @@ def test_non_max_suppression(): node = ng.non_max_suppression(boxes_parameter, scores_parameter) assert node.get_type_name() == "NonMaxSuppression" - assert node.get_output_size() == 1 + assert node.get_output_size() == 3 assert list(node.get_output_shape(0)) == expected_shape From ab571576b39549172a64c85f3dfbf34f1ac111e7 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 12:20:26 +0300 Subject: [PATCH 097/173] Deleted using namespace std --- ngraph/core/src/op/non_max_suppression.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 16dc12c55560e0..445917f5be4310 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -24,7 +24,6 @@ #include "ngraph/type/float16.hpp" #include "ngraph/util.hpp" -using namespace std; using namespace ngraph; // ------------------------------ V1 ------------------------------ From 53190d908384150e54876ed448a5e47f329c5dd0 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 12:29:22 +0300 Subject: [PATCH 098/173] Fixes in test_non_max_suppression(). --- ngraph/python/tests/test_ngraph/test_reduction.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ngraph/python/tests/test_ngraph/test_reduction.py b/ngraph/python/tests/test_ngraph/test_reduction.py index b23e8b242b3720..6cf5f4a81e71ee 100644 --- a/ngraph/python/tests/test_ngraph/test_reduction.py +++ b/ngraph/python/tests/test_ngraph/test_reduction.py @@ -105,7 +105,6 @@ def test_non_max_suppression(): boxes_shape = [1, 1000, 4] scores_shape = [1, 1, 1000] - expected_shape = [0, 3] boxes_parameter = ng.parameter(boxes_shape, name="Boxes", dtype=np.float32) scores_parameter = ng.parameter(scores_shape, name="Scores", dtype=np.float32) @@ -113,7 +112,9 @@ def test_non_max_suppression(): assert node.get_type_name() == "NonMaxSuppression" assert node.get_output_size() == 3 - assert list(node.get_output_shape(0)) == expected_shape + assert node.get_output_partial_shape(0).same_scheme(PartialShape([-1, 3])) + assert node.get_output_partial_shape(1).same_scheme(PartialShape([-1, 3])) + assert list(node.get_output_shape(2)) == [1] def test_non_zero(): From 2e23b0aeadb9e74f37be4bd0ba3c013e3397826c Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 12:33:33 +0300 Subject: [PATCH 099/173] Small fixes. --- ngraph/core/src/op/non_max_suppression.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 445917f5be4310..c115ad08e3ef96 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -61,7 +61,7 @@ op::v1::NonMaxSuppression::NonMaxSuppression( constructor_validate_and_infer_types(); } -shared_ptr +std::shared_ptr op::v1::NonMaxSuppression::clone_with_new_inputs(const OutputVector& new_args) const { check_new_args_count(this, new_args); @@ -259,7 +259,7 @@ op::v3::NonMaxSuppression::NonMaxSuppression( constructor_validate_and_infer_types(); } -shared_ptr +std::shared_ptr op::v3::NonMaxSuppression::clone_with_new_inputs(const OutputVector& new_args) const { check_new_args_count(this, new_args); @@ -478,7 +478,7 @@ op::v4::NonMaxSuppression::NonMaxSuppression( constructor_validate_and_infer_types(); } -shared_ptr +std::shared_ptr op::v4::NonMaxSuppression::clone_with_new_inputs(const OutputVector& new_args) const { check_new_args_count(this, new_args); @@ -644,7 +644,7 @@ op::v5::NonMaxSuppression::NonMaxSuppression( constructor_validate_and_infer_types(); } -shared_ptr +std::shared_ptr op::v5::NonMaxSuppression::clone_with_new_inputs(const OutputVector& new_args) const { check_new_args_count(this, new_args); From facc5083f6ab4c2ed1cc55c3a0e112b57812db6a Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 13:03:16 +0300 Subject: [PATCH 100/173] Added 'import PartialShape' in test_reduction.py. --- ngraph/python/tests/test_ngraph/test_reduction.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ngraph/python/tests/test_ngraph/test_reduction.py b/ngraph/python/tests/test_ngraph/test_reduction.py index 6cf5f4a81e71ee..4ca00f59dce8fb 100644 --- a/ngraph/python/tests/test_ngraph/test_reduction.py +++ b/ngraph/python/tests/test_ngraph/test_reduction.py @@ -15,6 +15,7 @@ # ****************************************************************************** import numpy as np import pytest +from _pyngraph import PartialShape import ngraph as ng from tests.runtime import get_runtime From 4e27e3589b81a3010e0410c09c221646b462ab3b Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 13:13:36 +0300 Subject: [PATCH 101/173] Deleted creating fake inputs in the ctor of op::v5::NonMaxSuppression. --- ngraph/core/src/op/non_max_suppression.cpp | 31 ++++------------------ 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index c115ad08e3ef96..f5f19fa5b7bd74 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -545,12 +545,7 @@ op::v5::NonMaxSuppression::NonMaxSuppression( const op::v5::NonMaxSuppression::BoxEncodingType box_encoding, const bool sort_result_descending, const element::Type& output_type) - : Op({boxes, - scores, - op::Constant::create(element::i64, Shape{}, {0}), - op::Constant::create(element::f32, Shape{}, {.0f}), - op::Constant::create(element::f32, Shape{}, {.0f}), - op::Constant::create(element::f32, Shape{}, {.0f})}) + : Op({boxes, scores}) , m_box_encoding{box_encoding} , m_sort_result_descending{sort_result_descending} , m_output_type{output_type} @@ -565,12 +560,7 @@ op::v5::NonMaxSuppression::NonMaxSuppression( const op::v5::NonMaxSuppression::BoxEncodingType box_encoding, const bool sort_result_descending, const element::Type& output_type) - : Op({boxes, - scores, - max_output_boxes_per_class, - op::Constant::create(element::f32, Shape{}, {.0f}), - op::Constant::create(element::f32, Shape{}, {.0f}), - op::Constant::create(element::f32, Shape{}, {.0f})}) + : Op({boxes, scores, max_output_boxes_per_class}) , m_box_encoding{box_encoding} , m_sort_result_descending{sort_result_descending} , m_output_type{output_type} @@ -586,12 +576,7 @@ op::v5::NonMaxSuppression::NonMaxSuppression( const op::v5::NonMaxSuppression::BoxEncodingType box_encoding, const bool sort_result_descending, const element::Type& output_type) - : Op({boxes, - scores, - max_output_boxes_per_class, - iou_threshold, - op::Constant::create(element::f32, Shape{}, {.0f}), - op::Constant::create(element::f32, Shape{}, {.0f})}) + : Op({boxes, scores, max_output_boxes_per_class, iou_threshold}) , m_box_encoding{box_encoding} , m_sort_result_descending{sort_result_descending} , m_output_type{output_type} @@ -608,12 +593,7 @@ op::v5::NonMaxSuppression::NonMaxSuppression( const op::v5::NonMaxSuppression::BoxEncodingType box_encoding, const bool sort_result_descending, const element::Type& output_type) - : Op({boxes, - scores, - max_output_boxes_per_class, - iou_threshold, - score_threshold, - op::Constant::create(element::f32, Shape{}, {.0f})}) + : Op({boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold}) , m_box_encoding{box_encoding} , m_sort_result_descending{sort_result_descending} , m_output_type{output_type} @@ -631,8 +611,7 @@ op::v5::NonMaxSuppression::NonMaxSuppression( const op::v5::NonMaxSuppression::BoxEncodingType box_encoding, const bool sort_result_descending, const element::Type& output_type) - : Op({boxes, - scores, + : Op({boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, From 8249034a53d69b3e4b340dd2157ec2fe31d19acb Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 13:27:59 +0300 Subject: [PATCH 102/173] Deleted creating fake inputs in op::v5::NonMaxSuppression::clone_with_new_inputs. --- ngraph/core/src/op/non_max_suppression.cpp | 70 +++++++++++++++------- 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index f5f19fa5b7bd74..acad31c1a930a4 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -631,28 +631,54 @@ std::shared_ptr new_args.size() >= 2 && new_args.size() <= 6, "Number of inputs must be 2, 3, 4, 5 or 6"); - const auto& arg2 = new_args.size() > 2 - ? new_args.at(2) - : ngraph::op::Constant::create(element::i64, Shape{}, {0}); - const auto& arg3 = new_args.size() > 3 - ? new_args.at(3) - : ngraph::op::Constant::create(element::f32, Shape{}, {.0f}); - const auto& arg4 = new_args.size() > 4 - ? new_args.at(4) - : ngraph::op::Constant::create(element::f32, Shape{}, {.0f}); - const auto& arg5 = new_args.size() > 5 - ? new_args.at(5) - : ngraph::op::Constant::create(element::f32, Shape{}, {.0f}); - - return std::make_shared(new_args.at(0), - new_args.at(1), - arg2, - arg3, - arg4, - arg5, - m_box_encoding, - m_sort_result_descending, - m_output_type); + switch (new_args.size()) + { + case 2: + return std::make_shared(new_args.at(0), + new_args.at(1), + m_box_encoding, + m_sort_result_descending, + m_output_type) + break; + case 3: + return std::make_shared(new_args.at(0), + new_args.at(1), + new_args.at(2), + m_box_encoding, + m_sort_result_descending, + m_output_type) + break; + case 4: + return std::make_shared(new_args.at(0), + new_args.at(1), + new_args.at(2), + new_args.at(3), + m_box_encoding, + m_sort_result_descending, + m_output_type) + break; + case 5: + return std::make_shared(new_args.at(0), + new_args.at(1), + new_args.at(2), + new_args.at(3), + new_args.at(4), + m_box_encoding, + m_sort_result_descending, + m_output_type) + break; + default: + return std::make_shared(new_args.at(0), + new_args.at(1), + new_args.at(2), + new_args.at(3), + new_args.at(4), + new_args.at(5), + m_box_encoding, + m_sort_result_descending, + m_output_type) + break; + } } using V5BoxEncoding = op::v5::NonMaxSuppression::BoxEncodingType; From fcd2d00ede2df686b12d0bd0632c5cdb243f1694 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 13:35:02 +0300 Subject: [PATCH 103/173] Corrections in int64_t op::v5::NonMaxSuppression::max_boxes_output_from_input() const. --- ngraph/core/src/op/non_max_suppression.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index acad31c1a930a4..b16565a5f738ac 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -687,6 +687,7 @@ namespace { constexpr size_t boxes_port = 0; constexpr size_t scores_port = 1; + constexpr size_t max_output_boxes_port = 2; constexpr size_t iou_threshold_port = 3; constexpr size_t score_threshold_port = 4; constexpr size_t soft_nms_sigma_port = 5; @@ -1011,8 +1012,14 @@ int64_t op::v5::NonMaxSuppression::max_boxes_output_from_input() const { int64_t max_output_boxes{0}; + size_t num_of_inputs = inputs().size(); + if (num_of_inputs < 3) + { + return 0; + } + const auto max_output_boxes_input = - as_type_ptr(input_value(2).get_node_shared_ptr()); + as_type_ptr(input_value(max_output_boxes_port).get_node_shared_ptr()); max_output_boxes = max_output_boxes_input->cast_vector().at(0); return max_output_boxes; From 635babdb46c11c658341a27fd97393be24cefd26 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 14:13:05 +0300 Subject: [PATCH 104/173] Corrected functions float op::v5::NonMaxSuppression::soft_nms_sigma_from_input() const, float op::v5::NonMaxSuppression::score_threshold_from_input() const, float op::v5::NonMaxSuppression::iou_threshold_from_input() const. --- ngraph/core/src/op/non_max_suppression.cpp | 50 ++++++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index b16565a5f738ac..53e1ca2db8951b 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -896,6 +896,26 @@ namespace return is_scalar(shape) || is_vector(shape) && (shape[0] == 1); } + + float float_from_constant_node(const std::shared_ptr& constant, + const element::Type& et) + { + float result = 0.0f; + switch (et) + { + case element::Type_t::bf16: + result = static_cast(constant->cast_vector().at(0)); + break; + case element::Type_t::f16: + result = static_cast(constant->cast_vector().at(0)); + break; + case element::Type_t::f32: + result = constant->cast_vector().at(0); + break; + default: throw std::runtime_error("Unsupported data type in op NonMaxSuppression-5"); break; + } + return result; + } } void op::v5::NonMaxSuppression::validate() @@ -1029,9 +1049,17 @@ float op::v5::NonMaxSuppression::iou_threshold_from_input() const { float iou_threshold = 0.0f; + size_t num_of_inputs = inputs().size(); + if (num_of_inputs < 4) + { + return iou_threshold; + } + const auto iou_threshold_input = as_type_ptr(input_value(iou_threshold_port).get_node_shared_ptr()); - iou_threshold = iou_threshold_input->cast_vector().at(0); + iou_threshold = float_from_constant_node(iou_threshold_input, + get_input_element_type(iou_threshold_port)); + // iou_threshold = iou_threshold_input->cast_vector().at(0); return iou_threshold; } @@ -1040,9 +1068,17 @@ float op::v5::NonMaxSuppression::score_threshold_from_input() const { float score_threshold = 0.0f; + size_t num_of_inputs = inputs().size(); + if (num_of_inputs < 5) + { + return score_threshold; + } + const auto score_threshold_input = as_type_ptr(input_value(score_threshold_port).get_node_shared_ptr()); - score_threshold = score_threshold_input->cast_vector().at(0); + score_threshold = float_from_constant_node(score_threshold_input, + get_input_element_type(score_threshold_port)); + // score_threshold = score_threshold_input->cast_vector().at(0); return score_threshold; } @@ -1051,9 +1087,17 @@ float op::v5::NonMaxSuppression::soft_nms_sigma_from_input() const { float soft_nms_sigma = 0.0f; + size_t num_of_inputs = inputs().size(); + if (num_of_inputs < 6) + { + return soft_nms_sigma; + } + const auto soft_nms_sigma_input = as_type_ptr(input_value(soft_nms_sigma_port).get_node_shared_ptr()); - soft_nms_sigma = soft_nms_sigma_input->cast_vector().at(0); + soft_nms_sigma = float_from_constant_node(soft_nms_sigma_input, + get_input_element_type(soft_nms_sigma_port)); + // soft_nms_sigma = soft_nms_sigma_input->cast_vector().at(0); return soft_nms_sigma; } From e1b4ee05785d5ef3b7a3ac73354b331799fccd2e Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 14:40:29 +0300 Subject: [PATCH 105/173] Small fixes. --- ngraph/core/src/op/non_max_suppression.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 53e1ca2db8951b..09f98d5dfa8c34 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -638,7 +638,7 @@ std::shared_ptr new_args.at(1), m_box_encoding, m_sort_result_descending, - m_output_type) + m_output_type); break; case 3: return std::make_shared(new_args.at(0), @@ -646,7 +646,7 @@ std::shared_ptr new_args.at(2), m_box_encoding, m_sort_result_descending, - m_output_type) + m_output_type); break; case 4: return std::make_shared(new_args.at(0), @@ -655,7 +655,7 @@ std::shared_ptr new_args.at(3), m_box_encoding, m_sort_result_descending, - m_output_type) + m_output_type); break; case 5: return std::make_shared(new_args.at(0), @@ -665,7 +665,7 @@ std::shared_ptr new_args.at(4), m_box_encoding, m_sort_result_descending, - m_output_type) + m_output_type); break; default: return std::make_shared(new_args.at(0), @@ -676,7 +676,7 @@ std::shared_ptr new_args.at(5), m_box_encoding, m_sort_result_descending, - m_output_type) + m_output_type); break; } } From 3f14ed9c3b17d38d121bc792016eebb4651762c6 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 15:38:19 +0300 Subject: [PATCH 106/173] Deleted commented code. --- ngraph/core/src/op/non_max_suppression.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 09f98d5dfa8c34..235781c2a63683 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -1059,7 +1059,6 @@ float op::v5::NonMaxSuppression::iou_threshold_from_input() const as_type_ptr(input_value(iou_threshold_port).get_node_shared_ptr()); iou_threshold = float_from_constant_node(iou_threshold_input, get_input_element_type(iou_threshold_port)); - // iou_threshold = iou_threshold_input->cast_vector().at(0); return iou_threshold; } @@ -1078,7 +1077,6 @@ float op::v5::NonMaxSuppression::score_threshold_from_input() const as_type_ptr(input_value(score_threshold_port).get_node_shared_ptr()); score_threshold = float_from_constant_node(score_threshold_input, get_input_element_type(score_threshold_port)); - // score_threshold = score_threshold_input->cast_vector().at(0); return score_threshold; } @@ -1097,7 +1095,6 @@ float op::v5::NonMaxSuppression::soft_nms_sigma_from_input() const as_type_ptr(input_value(soft_nms_sigma_port).get_node_shared_ptr()); soft_nms_sigma = float_from_constant_node(soft_nms_sigma_input, get_input_element_type(soft_nms_sigma_port)); - // soft_nms_sigma = soft_nms_sigma_input->cast_vector().at(0); return soft_nms_sigma; } From 6cc4fc9f18f30a62d48fe85b41c01409fa2491fb Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 16 Oct 2020 16:19:04 +0300 Subject: [PATCH 107/173] Fixes in nms_v5_scalar_inputs_check. --- ngraph/test/type_prop/non_max_suppression.cpp | 78 ++++++++++--------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/ngraph/test/type_prop/non_max_suppression.cpp b/ngraph/test/type_prop/non_max_suppression.cpp index 7c6531629eca09..ab70f7cb45709b 100644 --- a/ngraph/test/type_prop/non_max_suppression.cpp +++ b/ngraph/test/type_prop/non_max_suppression.cpp @@ -619,45 +619,47 @@ TEST(type_prop, nms_v5_scalar_inputs_check) const auto scores = make_shared(element::f32, Shape{1, 2, 2}); const auto scalar = make_shared(element::f32, Shape{}); - const auto non_scalar = make_shared(element::f32, Shape{1}); + const auto non_0d_or_1d = make_shared(element::f32, Shape{2}); -// try -// { -// make_shared(boxes, scores, non_scalar, scalar, scalar); -// } -// catch (const NodeValidationFailure& error) -// { -// EXPECT_HAS_SUBSTRING(error.what(), -// "Expected 0D or 1D tensor for the 'max_output_boxes_per_class' input"); -// } -// -// try -// { -// make_shared(boxes, scores, scalar, non_scalar, scalar); -// } -// catch (const NodeValidationFailure& error) -// { -// EXPECT_HAS_SUBSTRING(error.what(), "Expected a scalar for the 'iou_threshold' input"); -// } -// -// try -// { -// make_shared(boxes, scores, scalar, scalar, non_scalar); -// } -// catch (const NodeValidationFailure& error) -// { -// EXPECT_HAS_SUBSTRING(error.what(), "Expected a scalar for the 'score_threshold' input"); -// } -// -// try -// { -// make_shared(boxes, scores, scalar, scalar, scalar, non_scalar); -// } -// catch (const NodeValidationFailure& error) -// { -// EXPECT_HAS_SUBSTRING(error.what(), "Expected a scalar for the 'soft_nms_sigma' input"); -// } - ASSERT_TRUE(true); + try + { + make_shared(boxes, scores, non_0d_or_1d, scalar, scalar); + } + catch (const NodeValidationFailure& error) + { + EXPECT_HAS_SUBSTRING(error.what(), + "Expected 0D or 1D tensor for the 'max_output_boxes_per_class' input"); + } + + try + { + make_shared(boxes, scores, scalar, non_0d_or_1d, scalar); + } + catch (const NodeValidationFailure& error) + { + EXPECT_HAS_SUBSTRING(error.what(), + "Expected 0D or 1D tensor for the 'iou_threshold' input"); + } + + try + { + make_shared(boxes, scores, scalar, scalar, non_0d_or_1d); + } + catch (const NodeValidationFailure& error) + { + EXPECT_HAS_SUBSTRING(error.what(), + "Expected 0D or 1D tensor for the 'score_threshold' input"); + } + + try + { + make_shared(boxes, scores, scalar, scalar, scalar, non_0d_or_1d); + } + catch (const NodeValidationFailure& error) + { + EXPECT_HAS_SUBSTRING(error.what(), + "Expected 0D or 1D tensor for the 'soft_nms_sigma' input"); + } } TEST(type_prop, nms_v5_output_shape) From 7da307f4f6e5400b20842424efc4266bb8b4f59d Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Mon, 19 Oct 2020 10:25:18 +0300 Subject: [PATCH 108/173] Some changes. --- .../convert_previous_nms_to_nms_5.hpp | 7 + .../convert_previous_nms_to_nms_5.cpp | 257 ++++++++++++++++-- ngraph/core/src/op/non_max_suppression.cpp | 2 +- 3 files changed, 244 insertions(+), 22 deletions(-) diff --git a/inference-engine/src/transformations/include/transformations/op_conversions/convert_previous_nms_to_nms_5.hpp b/inference-engine/src/transformations/include/transformations/op_conversions/convert_previous_nms_to_nms_5.hpp index 9b7d610dc73d07..c3f5e1a9cfd6d0 100644 --- a/inference-engine/src/transformations/include/transformations/op_conversions/convert_previous_nms_to_nms_5.hpp +++ b/inference-engine/src/transformations/include/transformations/op_conversions/convert_previous_nms_to_nms_5.hpp @@ -16,6 +16,7 @@ namespace pass { class TRANSFORMATIONS_API ConvertNMS1ToNMS5; class TRANSFORMATIONS_API ConvertNMS3ToNMS5; class TRANSFORMATIONS_API ConvertNMS4ToNMS5; +class TRANSFORMATIONS_API ConvertPreviousNMSToNMS5; } // namespace pass } // namespace ngraph @@ -37,3 +38,9 @@ class ngraph::pass::ConvertNMS4ToNMS5: public ngraph::pass::MatcherPass { NGRAPH_RTTI_DECLARATION; ConvertNMS4ToNMS5(); }; + +class ngraph::pass::ConvertPreviousNMSToNMS5: public ngraph::pass::MatcherPass { +public: + NGRAPH_RTTI_DECLARATION; + ConvertPreviousNMSToNMS5(); +}; diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp index 6e0b78ae190f43..6a4d3182e298f9 100644 --- a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp @@ -15,16 +15,229 @@ #include #include +namespace +{ + std::shared_ptr nms4_pattern() + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = ngraph::opset4::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.7}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, + iou_threshold, score_threshold); + return nms; + } + + std::shared_ptr nms3_pattern() + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = ngraph::opset3::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = ngraph::opset3::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = ngraph::opset3::Constant::create(element::f32, Shape{}, {0.7}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, + iou_threshold, score_threshold); + return nms; + } + + std::shared_ptr nms1_pattern() + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = ngraph::opset1::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = ngraph::opset1::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = ngraph::opset1::Constant::create(element::f32, Shape{}, {0.7}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, + iou_threshold, score_threshold); + return nms; + } + + using NMS5BoxEncoding = ngraph::opset5::NonMaxSuppression::BoxEncodingType; + + struct NMSAttributes + { + ngraph::element::Type output_type; + NMS5BoxEncoding box_encoding; + bool sort_result_descending; + bool is_supported_nms; + }; + + NMSAttributes get_nms4_attrs(const std::shared_ptr& nms4) + { + NMSAttributes attrs; + + attrs.box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; + attrs.is_supported_nms = true; + attrs.sort_result_descending = true; + attrs.output_type = ::ngraph::element::i64; + + switch (nms4->get_box_encoding()) { + case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CENTER: + attrs.box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; + break; + case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CORNER: + attrs.box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; + break; + default: + throw ngraph_error("NonMaxSuppression layer " + nms4->get_friendly_name() + + " has unsupported box encoding"); + } + + attrs.sort_result_descending = nms4->get_sort_result_descending(), + attrs.output_type = nms4->get_output_type(); + + return attrs; + } + + NMSAttributes get_nms3_attrs(const std::shared_ptr& nms3) + { + NMSAttributes attrs; + + attrs.box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; + attrs.is_supported_nms = true; + attrs.sort_result_descending = true; + attrs.output_type = ::ngraph::element::i64; + + switch (nms3->get_box_encoding()) { + case ::ngraph::opset3::NonMaxSuppression::BoxEncodingType::CENTER: + attrs.box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; + break; + case ::ngraph::opset3::NonMaxSuppression::BoxEncodingType::CORNER: + attrs.box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; + break; + default: + throw ngraph_error("NonMaxSuppression layer " + nms3->get_friendly_name() + + " has unsupported box encoding"); + } + + attrs.sort_result_descending = nms3->get_sort_result_descending(), + attrs.output_type = nms3->get_output_type(); + + return attrs; + } + + NMSAttributes get_nms1_attrs(const std::shared_ptr& nms1) + { + NMSAttributes attrs; + + attrs.box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; + attrs.is_supported_nms = true; + attrs.sort_result_descending = true; + attrs.output_type = ::ngraph::element::i64; + + switch (nms1->get_box_encoding()) { + case ::ngraph::opset1::NonMaxSuppression::BoxEncodingType::CENTER: + attrs.box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; + break; + case ::ngraph::opset1::NonMaxSuppression::BoxEncodingType::CORNER: + attrs.box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; + break; + default: + throw ngraph_error("NonMaxSuppression layer " + nms1->get_friendly_name() + + " has unsupported box encoding"); + } + + attrs.sort_result_descending = nms1->get_sort_result_descending(), + + return attrs; + } + + NMSAttributes get_nms_attrs(const std::shared_ptr& root) + { + NMSAttributes attrs; + attrs.output_type = ::ngraph::element::i64; + attrs.box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; + attrs.sort_result_descending = false; + attrs.is_supported_nms = false; + + auto nms_4 = std::dynamic_pointer_cast(m.get_match_root()); + if (nms_4) { + return get_nms4_attrs(nms_4); + } + auto nms_3 = std::dynamic_pointer_cast(m.get_match_root()); + if (nms_4) { + return get_nms3_attrs(nms_3); + } + auto nms_1 = std::dynamic_pointer_cast(m.get_match_root()); + if (nms_1) { + return get_nms1_attrs(nms_3); + } + + return attrs; + } + + bool callback_func(pattern::Matcher &m) + { + auto root = m.get_match_root(); + + auto attrs = get_nms_attrs(root); + if (!attrs.is_supported_nms) { + return false; + } + + const auto new_args = root->input_values(); + + size_t num_of_args = new_args.size(); + + const auto& arg2 = num_of_args > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i64, Shape{}, {0}); + const auto& arg3 = num_of_args > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + const auto& arg4 = num_of_args > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + const auto& arg5 = ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + + // list of new nGraph operations + std::list> new_ops_list; + + new_ops_list.push_front(arg5); + if (num_of_args <= 4) { + new_ops_list.push_front(arg4.get_node_shared_ptr()); + } + if (num_of_args <= 3) { + new_ops_list.push_front(arg3.get_node_shared_ptr()); + } + if (num_of_args <= 2) { + new_ops_list.push_front(arg2.get_node_shared_ptr()); + } + + const auto nms_5 = std::make_shared( + new_args.at(0), + new_args.at(1), + arg2, + arg3, + arg4, + arg5, + box_encoding, + attrs.sort_result_descending, + attrs.output_type); + + new_ops_list.push_back(nms_5); + + // vector of new nGraph operations + NodeVector new_ops(new_ops_list.begin(), new_ops_list.end()); + + nms_5->set_friendly_name(root->get_friendly_name()); + ngraph::copy_runtime_info(root, new_ops); + root->output(0).replace(nms_5->output(0)); + return true; + } +} + +NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertPreviousNMSToNMS5, "ConvertPreviousNMSToNMS5", 0); + +ngraph::pass::ConvertPreviousNMSToNMS5::ConvertPreviousNMSToNMS5() { +} + NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS4ToNMS5, "ConvertNMS4ToNMS5", 0); ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { - auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); - auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); - auto max_output_boxes_per_class = ngraph::opset4::Constant::create(element::i64, Shape{}, {10}); - auto iou_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.7}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - iou_threshold, score_threshold); + auto nms = nms4_pattern(); +// auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); +// auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); +// auto max_output_boxes_per_class = ngraph::opset4::Constant::create(element::i64, Shape{}, {10}); +// auto iou_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.75}); +// auto score_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.7}); +// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, +// iou_threshold, score_threshold); ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { auto nms_4 = std::dynamic_pointer_cast(m.get_match_root()); @@ -98,13 +311,14 @@ ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS3ToNMS5, "ConvertNMS3ToNMS5", 0); ngraph::pass::ConvertNMS3ToNMS5::ConvertNMS3ToNMS5() { - auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); - auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); - auto max_output_boxes_per_class = ngraph::opset3::Constant::create(element::i64, Shape{}, {10}); - auto iou_threshold = ngraph::opset3::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = ngraph::opset3::Constant::create(element::f32, Shape{}, {0.7}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - iou_threshold, score_threshold); + auto nms = nms3_pattern(); +// auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); +// auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); +// auto max_output_boxes_per_class = ngraph::opset3::Constant::create(element::i64, Shape{}, {10}); +// auto iou_threshold = ngraph::opset3::Constant::create(element::f32, Shape{}, {0.75}); +// auto score_threshold = ngraph::opset3::Constant::create(element::f32, Shape{}, {0.7}); +// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, +// iou_threshold, score_threshold); ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { auto nms_3 = std::dynamic_pointer_cast(m.get_match_root()); @@ -177,13 +391,14 @@ ngraph::pass::ConvertNMS3ToNMS5::ConvertNMS3ToNMS5() { NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS1ToNMS5, "ConvertNMS1ToNMS5", 0); ngraph::pass::ConvertNMS1ToNMS5::ConvertNMS1ToNMS5() { - auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); - auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); - auto max_output_boxes_per_class = ngraph::opset1::Constant::create(element::i64, Shape{}, {10}); - auto iou_threshold = ngraph::opset1::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = ngraph::opset1::Constant::create(element::f32, Shape{}, {0.7}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - iou_threshold, score_threshold); + auto nms = nms1_pattern(); +// auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); +// auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); +// auto max_output_boxes_per_class = ngraph::opset1::Constant::create(element::i64, Shape{}, {10}); +// auto iou_threshold = ngraph::opset1::Constant::create(element::f32, Shape{}, {0.75}); +// auto score_threshold = ngraph::opset1::Constant::create(element::f32, Shape{}, {0.7}); +// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, +// iou_threshold, score_threshold); ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { auto nms_1 = std::dynamic_pointer_cast(m.get_match_root()); diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 235781c2a63683..4259e2d4514482 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -894,7 +894,7 @@ namespace Shape shape = p.to_shape(); - return is_scalar(shape) || is_vector(shape) && (shape[0] == 1); + return is_scalar(shape) || (is_vector(shape) && (shape[0] == 1)); } float float_from_constant_node(const std::shared_ptr& constant, From 40adcbb0ff98b3af2ab5ca0bf147b7d7b39f87cc Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Mon, 19 Oct 2020 10:57:33 +0300 Subject: [PATCH 109/173] Small fixes. --- .../convert_previous_nms_to_nms_5.cpp | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp index 6a4d3182e298f9..de2c905bbb0359 100644 --- a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp @@ -15,10 +15,8 @@ #include #include -namespace -{ - std::shared_ptr nms4_pattern() - { +namespace { + std::shared_ptr nms4_pattern() { auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); auto max_output_boxes_per_class = ngraph::opset4::Constant::create(element::i64, Shape{}, {10}); @@ -29,8 +27,7 @@ namespace return nms; } - std::shared_ptr nms3_pattern() - { + std::shared_ptr nms3_pattern() { auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); auto max_output_boxes_per_class = ngraph::opset3::Constant::create(element::i64, Shape{}, {10}); @@ -41,8 +38,7 @@ namespace return nms; } - std::shared_ptr nms1_pattern() - { + std::shared_ptr nms1_pattern() { auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); auto max_output_boxes_per_class = ngraph::opset1::Constant::create(element::i64, Shape{}, {10}); @@ -55,16 +51,14 @@ namespace using NMS5BoxEncoding = ngraph::opset5::NonMaxSuppression::BoxEncodingType; - struct NMSAttributes - { + struct NMSAttributes { ngraph::element::Type output_type; NMS5BoxEncoding box_encoding; bool sort_result_descending; bool is_supported_nms; }; - NMSAttributes get_nms4_attrs(const std::shared_ptr& nms4) - { + NMSAttributes get_nms4_attrs(const std::shared_ptr& nms4) { NMSAttributes attrs; attrs.box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; @@ -90,8 +84,7 @@ namespace return attrs; } - NMSAttributes get_nms3_attrs(const std::shared_ptr& nms3) - { + NMSAttributes get_nms3_attrs(const std::shared_ptr& nms3) { NMSAttributes attrs; attrs.box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; @@ -117,8 +110,7 @@ namespace return attrs; } - NMSAttributes get_nms1_attrs(const std::shared_ptr& nms1) - { + NMSAttributes get_nms1_attrs(const std::shared_ptr& nms1) { NMSAttributes attrs; attrs.box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; @@ -143,8 +135,7 @@ namespace return attrs; } - NMSAttributes get_nms_attrs(const std::shared_ptr& root) - { + NMSAttributes get_nms_attrs(const std::shared_ptr& root) { NMSAttributes attrs; attrs.output_type = ::ngraph::element::i64; attrs.box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; @@ -167,8 +158,7 @@ namespace return attrs; } - bool callback_func(pattern::Matcher &m) - { + bool callback_func(pattern::Matcher &m) { auto root = m.get_match_root(); auto attrs = get_nms_attrs(root); @@ -220,7 +210,7 @@ namespace root->output(0).replace(nms_5->output(0)); return true; } -} +} // namespace NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertPreviousNMSToNMS5, "ConvertPreviousNMSToNMS5", 0); From 83b14a8c3882fc82b7dc1b7fd3d1210cd45fa199 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Mon, 19 Oct 2020 11:36:07 +0300 Subject: [PATCH 110/173] Code style fixes. --- .../convert_previous_nms_to_nms_5.cpp | 18 ++++++------ ngraph/core/src/op/non_max_suppression.cpp | 29 +++++++++---------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp index de2c905bbb0359..46f40ab9bd1f46 100644 --- a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp @@ -15,6 +15,15 @@ #include #include +using NMS5BoxEncoding = ngraph::opset5::NonMaxSuppression::BoxEncodingType; + +struct NMSAttributes { + ngraph::element::Type output_type; + NMS5BoxEncoding box_encoding; + bool sort_result_descending; + bool is_supported_nms; +}; + namespace { std::shared_ptr nms4_pattern() { auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); @@ -49,15 +58,6 @@ namespace { return nms; } - using NMS5BoxEncoding = ngraph::opset5::NonMaxSuppression::BoxEncodingType; - - struct NMSAttributes { - ngraph::element::Type output_type; - NMS5BoxEncoding box_encoding; - bool sort_result_descending; - bool is_supported_nms; - }; - NMSAttributes get_nms4_attrs(const std::shared_ptr& nms4) { NMSAttributes attrs; diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 4259e2d4514482..2d581b2f2c5f46 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -611,7 +611,8 @@ op::v5::NonMaxSuppression::NonMaxSuppression( const op::v5::NonMaxSuppression::BoxEncodingType box_encoding, const bool sort_result_descending, const element::Type& output_type) - : Op({boxes, scores, + : Op({boxes, + scores, max_output_boxes_per_class, iou_threshold, score_threshold, @@ -909,9 +910,7 @@ namespace case element::Type_t::f16: result = static_cast(constant->cast_vector().at(0)); break; - case element::Type_t::f32: - result = constant->cast_vector().at(0); - break; + case element::Type_t::f32: result = constant->cast_vector().at(0); break; default: throw std::runtime_error("Unsupported data type in op NonMaxSuppression-5"); break; } return result; @@ -953,12 +952,12 @@ void op::v5::NonMaxSuppression::validate() if (inputs().size() >= 3) { const auto max_boxes_ps = get_input_partial_shape(2); - NODE_VALIDATION_CHECK(this, - max_boxes_ps.is_dynamic() || - is_scalar_or_1d_tensor_with_1_element(max_boxes_ps), - "Expected 0D or 1D tensor for the 'max_output_boxes_per_class' input. " - "Got: ", - max_boxes_ps); + NODE_VALIDATION_CHECK( + this, + max_boxes_ps.is_dynamic() || is_scalar_or_1d_tensor_with_1_element(max_boxes_ps), + "Expected 0D or 1D tensor for the 'max_output_boxes_per_class' input. " + "Got: ", + max_boxes_ps); } if (inputs().size() >= 4) @@ -998,7 +997,7 @@ void op::v5::NonMaxSuppression::validate() "'soft_nms_sigma' input."); NODE_VALIDATION_CHECK(this, soft_nms_sigma.is_dynamic() || - is_scalar_or_1d_tensor_with_1_element(soft_nms_sigma), + is_scalar_or_1d_tensor_with_1_element(soft_nms_sigma), "Expected 0D or 1D tensor for the 'soft_nms_sigma' input. Got: ", soft_nms_sigma); } @@ -1057,8 +1056,8 @@ float op::v5::NonMaxSuppression::iou_threshold_from_input() const const auto iou_threshold_input = as_type_ptr(input_value(iou_threshold_port).get_node_shared_ptr()); - iou_threshold = float_from_constant_node(iou_threshold_input, - get_input_element_type(iou_threshold_port)); + iou_threshold = + float_from_constant_node(iou_threshold_input, get_input_element_type(iou_threshold_port)); return iou_threshold; } @@ -1093,8 +1092,8 @@ float op::v5::NonMaxSuppression::soft_nms_sigma_from_input() const const auto soft_nms_sigma_input = as_type_ptr(input_value(soft_nms_sigma_port).get_node_shared_ptr()); - soft_nms_sigma = float_from_constant_node(soft_nms_sigma_input, - get_input_element_type(soft_nms_sigma_port)); + soft_nms_sigma = + float_from_constant_node(soft_nms_sigma_input, get_input_element_type(soft_nms_sigma_port)); return soft_nms_sigma; } From f7da96dc6d11dcb37c13da1c01912a79e2751b81 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Mon, 19 Oct 2020 12:00:50 +0300 Subject: [PATCH 111/173] Small changes. --- .../op_conversions/convert_previous_nms_to_nms_5.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp index 46f40ab9bd1f46..a1474079700065 100644 --- a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp @@ -24,6 +24,8 @@ struct NMSAttributes { bool is_supported_nms; }; +using namespace ngraph; + namespace { std::shared_ptr nms4_pattern() { auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); From 743e87bc8d7c05ccb288571dd932dca17fa64b70 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Mon, 19 Oct 2020 12:26:52 +0300 Subject: [PATCH 112/173] Small changes. --- .../convert_previous_nms_to_nms_5.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp index a1474079700065..fc9c968f2ea686 100644 --- a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp @@ -80,7 +80,7 @@ namespace { " has unsupported box encoding"); } - attrs.sort_result_descending = nms4->get_sort_result_descending(), + attrs.sort_result_descending = nms4->get_sort_result_descending(); attrs.output_type = nms4->get_output_type(); return attrs; @@ -106,7 +106,7 @@ namespace { " has unsupported box encoding"); } - attrs.sort_result_descending = nms3->get_sort_result_descending(), + attrs.sort_result_descending = nms3->get_sort_result_descending(); attrs.output_type = nms3->get_output_type(); return attrs; @@ -132,7 +132,7 @@ namespace { " has unsupported box encoding"); } - attrs.sort_result_descending = nms1->get_sort_result_descending(), + attrs.sort_result_descending = nms1->get_sort_result_descending(); return attrs; } @@ -144,15 +144,15 @@ namespace { attrs.sort_result_descending = false; attrs.is_supported_nms = false; - auto nms_4 = std::dynamic_pointer_cast(m.get_match_root()); + auto nms_4 = std::dynamic_pointer_cast(root); if (nms_4) { return get_nms4_attrs(nms_4); } - auto nms_3 = std::dynamic_pointer_cast(m.get_match_root()); + auto nms_3 = std::dynamic_pointer_cast(root); if (nms_4) { return get_nms3_attrs(nms_3); } - auto nms_1 = std::dynamic_pointer_cast(m.get_match_root()); + auto nms_1 = std::dynamic_pointer_cast(root); if (nms_1) { return get_nms1_attrs(nms_3); } @@ -198,7 +198,7 @@ namespace { arg3, arg4, arg5, - box_encoding, + attrs.box_encoding, attrs.sort_result_descending, attrs.output_type); From db12b8ad6973281aa0980d715a720c00234f2e1a Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Mon, 19 Oct 2020 12:49:15 +0300 Subject: [PATCH 113/173] Small fix. --- .../op_conversions/convert_previous_nms_to_nms_5.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp index fc9c968f2ea686..3ae8672519e80e 100644 --- a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp @@ -154,7 +154,7 @@ namespace { } auto nms_1 = std::dynamic_pointer_cast(root); if (nms_1) { - return get_nms1_attrs(nms_3); + return get_nms1_attrs(nms_1); } return attrs; From b38e91657320ad4ddb8a538e4d8fb911fc353989 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Mon, 19 Oct 2020 13:17:33 +0300 Subject: [PATCH 114/173] Deleted commented code. --- .../convert_previous_nms_to_nms_5.cpp | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp index 3ae8672519e80e..ffb05fcce69b62 100644 --- a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp @@ -223,13 +223,6 @@ NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS4ToNMS5, "ConvertNMS4ToNMS5", 0); ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { auto nms = nms4_pattern(); -// auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); -// auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); -// auto max_output_boxes_per_class = ngraph::opset4::Constant::create(element::i64, Shape{}, {10}); -// auto iou_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.75}); -// auto score_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.7}); -// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, -// iou_threshold, score_threshold); ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { auto nms_4 = std::dynamic_pointer_cast(m.get_match_root()); @@ -304,13 +297,6 @@ NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS3ToNMS5, "ConvertNMS3ToNMS5", 0); ngraph::pass::ConvertNMS3ToNMS5::ConvertNMS3ToNMS5() { auto nms = nms3_pattern(); -// auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); -// auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); -// auto max_output_boxes_per_class = ngraph::opset3::Constant::create(element::i64, Shape{}, {10}); -// auto iou_threshold = ngraph::opset3::Constant::create(element::f32, Shape{}, {0.75}); -// auto score_threshold = ngraph::opset3::Constant::create(element::f32, Shape{}, {0.7}); -// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, -// iou_threshold, score_threshold); ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { auto nms_3 = std::dynamic_pointer_cast(m.get_match_root()); @@ -384,13 +370,6 @@ NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS1ToNMS5, "ConvertNMS1ToNMS5", 0); ngraph::pass::ConvertNMS1ToNMS5::ConvertNMS1ToNMS5() { auto nms = nms1_pattern(); -// auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); -// auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); -// auto max_output_boxes_per_class = ngraph::opset1::Constant::create(element::i64, Shape{}, {10}); -// auto iou_threshold = ngraph::opset1::Constant::create(element::f32, Shape{}, {0.75}); -// auto score_threshold = ngraph::opset1::Constant::create(element::f32, Shape{}, {0.7}); -// auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, -// iou_threshold, score_threshold); ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { auto nms_1 = std::dynamic_pointer_cast(m.get_match_root()); From fdbf03c02fb8a433a5b6f5aaa5c7d33b61f94420 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Mon, 19 Oct 2020 13:22:26 +0300 Subject: [PATCH 115/173] Some refactoring in ConvertNMS4ToNMS5 ctor. --- .../convert_previous_nms_to_nms_5.cpp | 127 +++++++++--------- 1 file changed, 64 insertions(+), 63 deletions(-) diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp index ffb05fcce69b62..0d18e152677311 100644 --- a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp @@ -224,69 +224,70 @@ NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS4ToNMS5, "ConvertNMS4ToNMS5", 0); ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { auto nms = nms4_pattern(); - ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { - auto nms_4 = std::dynamic_pointer_cast(m.get_match_root()); - if (!nms_4) { - return false; - } - - const auto new_args = nms_4->input_values(); - - size_t num_of_args = new_args.size(); - - const auto& arg2 = num_of_args > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i64, Shape{}, {0}); - const auto& arg3 = num_of_args > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); - const auto& arg4 = num_of_args > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); - const auto& arg5 = ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); - - auto box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; - switch (nms_4->get_box_encoding()) { - case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CENTER: - box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; - break; - case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CORNER: - box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; - break; - default: - throw ngraph_error("NonMaxSuppression layer " + nms_4->get_friendly_name() + - " has unsupported box encoding"); - } - - // list of new nGraph operations - std::list> new_ops_list; - - new_ops_list.push_front(arg5); - if (num_of_args <= 4) { - new_ops_list.push_front(arg4.get_node_shared_ptr()); - } - if (num_of_args <= 3) { - new_ops_list.push_front(arg3.get_node_shared_ptr()); - } - if (num_of_args <= 2) { - new_ops_list.push_front(arg2.get_node_shared_ptr()); - } - - const auto nms_5 = std::make_shared( - new_args.at(0), - new_args.at(1), - arg2, - arg3, - arg4, - arg5, - box_encoding, - nms_4->get_sort_result_descending(), - nms_4->get_output_type()); - - new_ops_list.push_back(nms_5); - - // vector of new nGraph operations - NodeVector new_ops(new_ops_list.begin(), new_ops_list.end()); - - nms_5->set_friendly_name(nms_4->get_friendly_name()); - ngraph::copy_runtime_info(nms_4, new_ops); - nms_4->output(0).replace(nms_5->output(0)); - return true; - }; + ngraph::matcher_pass_callback = callback_func; +// ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { +// auto nms_4 = std::dynamic_pointer_cast(m.get_match_root()); +// if (!nms_4) { +// return false; +// } +// +// const auto new_args = nms_4->input_values(); +// +// size_t num_of_args = new_args.size(); +// +// const auto& arg2 = num_of_args > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i64, Shape{}, {0}); +// const auto& arg3 = num_of_args > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); +// const auto& arg4 = num_of_args > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); +// const auto& arg5 = ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); +// +// auto box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; +// switch (nms_4->get_box_encoding()) { +// case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CENTER: +// box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; +// break; +// case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CORNER: +// box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; +// break; +// default: +// throw ngraph_error("NonMaxSuppression layer " + nms_4->get_friendly_name() + +// " has unsupported box encoding"); +// } +// +// // list of new nGraph operations +// std::list> new_ops_list; +// +// new_ops_list.push_front(arg5); +// if (num_of_args <= 4) { +// new_ops_list.push_front(arg4.get_node_shared_ptr()); +// } +// if (num_of_args <= 3) { +// new_ops_list.push_front(arg3.get_node_shared_ptr()); +// } +// if (num_of_args <= 2) { +// new_ops_list.push_front(arg2.get_node_shared_ptr()); +// } +// +// const auto nms_5 = std::make_shared( +// new_args.at(0), +// new_args.at(1), +// arg2, +// arg3, +// arg4, +// arg5, +// box_encoding, +// nms_4->get_sort_result_descending(), +// nms_4->get_output_type()); +// +// new_ops_list.push_back(nms_5); +// +// // vector of new nGraph operations +// NodeVector new_ops(new_ops_list.begin(), new_ops_list.end()); +// +// nms_5->set_friendly_name(nms_4->get_friendly_name()); +// ngraph::copy_runtime_info(nms_4, new_ops); +// nms_4->output(0).replace(nms_5->output(0)); +// return true; +// }; auto m = std::make_shared(nms, "ConvertNMS4ToNMS5"); this->register_matcher(m, callback); From 032e98bc1c500909a7705eae7d2f155ecf00c914 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Mon, 19 Oct 2020 13:59:45 +0300 Subject: [PATCH 116/173] Small fix. --- .../op_conversions/convert_previous_nms_to_nms_5.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp index 0d18e152677311..c7bb62215060ab 100644 --- a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp @@ -224,7 +224,7 @@ NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS4ToNMS5, "ConvertNMS4ToNMS5", 0); ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { auto nms = nms4_pattern(); - ngraph::matcher_pass_callback = callback_func; + ngraph::matcher_pass_callback callback = callback_func; // ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { // auto nms_4 = std::dynamic_pointer_cast(m.get_match_root()); // if (!nms_4) { From 0d739a95d56e016e26590b05dec68a16e28159ac Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Mon, 19 Oct 2020 15:27:03 +0300 Subject: [PATCH 117/173] Common part of conversions NMS-1 -> NMS-5, NMS-3 -> NMS-5, NMS-4 -> NMS-5 was moved into the separate function. --- .../convert_previous_nms_to_nms_5.cpp | 191 +----------------- 1 file changed, 2 insertions(+), 189 deletions(-) diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp index c7bb62215060ab..8eff19f4672b68 100644 --- a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp @@ -225,69 +225,6 @@ ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { auto nms = nms4_pattern(); ngraph::matcher_pass_callback callback = callback_func; -// ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { -// auto nms_4 = std::dynamic_pointer_cast(m.get_match_root()); -// if (!nms_4) { -// return false; -// } -// -// const auto new_args = nms_4->input_values(); -// -// size_t num_of_args = new_args.size(); -// -// const auto& arg2 = num_of_args > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i64, Shape{}, {0}); -// const auto& arg3 = num_of_args > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); -// const auto& arg4 = num_of_args > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); -// const auto& arg5 = ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); -// -// auto box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; -// switch (nms_4->get_box_encoding()) { -// case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CENTER: -// box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; -// break; -// case ::ngraph::opset4::NonMaxSuppression::BoxEncodingType::CORNER: -// box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; -// break; -// default: -// throw ngraph_error("NonMaxSuppression layer " + nms_4->get_friendly_name() + -// " has unsupported box encoding"); -// } -// -// // list of new nGraph operations -// std::list> new_ops_list; -// -// new_ops_list.push_front(arg5); -// if (num_of_args <= 4) { -// new_ops_list.push_front(arg4.get_node_shared_ptr()); -// } -// if (num_of_args <= 3) { -// new_ops_list.push_front(arg3.get_node_shared_ptr()); -// } -// if (num_of_args <= 2) { -// new_ops_list.push_front(arg2.get_node_shared_ptr()); -// } -// -// const auto nms_5 = std::make_shared( -// new_args.at(0), -// new_args.at(1), -// arg2, -// arg3, -// arg4, -// arg5, -// box_encoding, -// nms_4->get_sort_result_descending(), -// nms_4->get_output_type()); -// -// new_ops_list.push_back(nms_5); -// -// // vector of new nGraph operations -// NodeVector new_ops(new_ops_list.begin(), new_ops_list.end()); -// -// nms_5->set_friendly_name(nms_4->get_friendly_name()); -// ngraph::copy_runtime_info(nms_4, new_ops); -// nms_4->output(0).replace(nms_5->output(0)); -// return true; -// }; auto m = std::make_shared(nms, "ConvertNMS4ToNMS5"); this->register_matcher(m, callback); @@ -299,69 +236,7 @@ NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS3ToNMS5, "ConvertNMS3ToNMS5", 0); ngraph::pass::ConvertNMS3ToNMS5::ConvertNMS3ToNMS5() { auto nms = nms3_pattern(); - ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { - auto nms_3 = std::dynamic_pointer_cast(m.get_match_root()); - if (!nms_3) { - return false; - } - - const auto new_args = nms_3->input_values(); - - size_t num_of_args = new_args.size(); - - const auto& arg2 = num_of_args > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i64, Shape{}, {0}); - const auto& arg3 = num_of_args > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); - const auto& arg4 = num_of_args > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); - const auto& arg5 = ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); - - auto box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; - switch (nms_3->get_box_encoding()) { - case ::ngraph::opset3::NonMaxSuppression::BoxEncodingType::CENTER: - box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; - break; - case ::ngraph::opset3::NonMaxSuppression::BoxEncodingType::CORNER: - box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; - break; - default: - throw ngraph_error("NonMaxSuppression layer " + nms_3->get_friendly_name() + - " has unsupported box encoding"); - } - - // list of new nGraph operations - std::list> new_ops_list; - - new_ops_list.push_front(arg5); - if (num_of_args <= 4) { - new_ops_list.push_front(arg4.get_node_shared_ptr()); - } - if (num_of_args <= 3) { - new_ops_list.push_front(arg3.get_node_shared_ptr()); - } - if (num_of_args <= 2) { - new_ops_list.push_front(arg2.get_node_shared_ptr()); - } - - const auto nms_5 = std::make_shared( - new_args.at(0), - new_args.at(1), - arg2, - arg3, - arg4, - arg5, - box_encoding, - nms_3->get_sort_result_descending(), - nms_3->get_output_type()); - - new_ops_list.push_back(nms_5); - - // vector of new nGraph operations - NodeVector new_ops(new_ops_list.begin(), new_ops_list.end()); - - nms_5->set_friendly_name(nms_3->get_friendly_name()); - ngraph::copy_runtime_info(nms_3, new_ops); - nms_3->output(0).replace(nms_5->output(0)); - return true; - }; + ngraph::matcher_pass_callback callback = callback_func; auto m = std::make_shared(nms, "ConvertNMS3ToNMS5"); this->register_matcher(m, callback); @@ -372,69 +247,7 @@ NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS1ToNMS5, "ConvertNMS1ToNMS5", 0); ngraph::pass::ConvertNMS1ToNMS5::ConvertNMS1ToNMS5() { auto nms = nms1_pattern(); - ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { - auto nms_1 = std::dynamic_pointer_cast(m.get_match_root()); - if (!nms_1) { - return false; - } - - const auto new_args = nms_1->input_values(); - - size_t num_of_args = new_args.size(); - - const auto& arg2 = num_of_args > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i32, Shape{}, {0}); - const auto& arg3 = num_of_args > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); - const auto& arg4 = num_of_args > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); - const auto& arg5 = ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); - - auto box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; - switch (nms_1->get_box_encoding()) { - case ::ngraph::opset1::NonMaxSuppression::BoxEncodingType::CENTER: - box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER; - break; - case ::ngraph::opset1::NonMaxSuppression::BoxEncodingType::CORNER: - box_encoding = ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CORNER; - break; - default: - throw ngraph_error("NonMaxSuppression layer " + nms_1->get_friendly_name() + - " has unsupported box encoding"); - } - - // list of new nGraph operations - std::list> new_ops_list; - - new_ops_list.push_front(arg5); - if (num_of_args <= 4) { - new_ops_list.push_front(arg4.get_node_shared_ptr()); - } - if (num_of_args <= 3) { - new_ops_list.push_front(arg3.get_node_shared_ptr()); - } - if (num_of_args <= 2) { - new_ops_list.push_front(arg2.get_node_shared_ptr()); - } - - const auto nms_5 = std::make_shared( - new_args.at(0), - new_args.at(1), - arg2, - arg3, - arg4, - arg5, - box_encoding, - nms_1->get_sort_result_descending(), - ::ngraph::element::i64); - - new_ops_list.push_back(nms_5); - - // vector of new nGraph operations - NodeVector new_ops(new_ops_list.begin(), new_ops_list.end()); - - nms_5->set_friendly_name(nms_1->get_friendly_name()); - ngraph::copy_runtime_info(nms_1, new_ops); - nms_1->output(0).replace(nms_5->output(0)); - return true; - }; + ngraph::matcher_pass_callback callback = callback_func; auto m = std::make_shared(nms, "ConvertNMS1ToNMS5"); this->register_matcher(m, callback); From 525481ba19d8f3bfc414aefd9017ab60ac5ebd56 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 20 Oct 2020 10:45:43 +0300 Subject: [PATCH 118/173] Now conversions NMS-1 -> NMS-5, NMS-3 -> NMS-5, NMS-4 -> NMS-5 support both 2 inputs, and 5 inputs. --- .../samples/speech_sample/main.cpp | 2 +- .../convert_previous_nms_to_nms_5.cpp | 44 ++----------------- 2 files changed, 5 insertions(+), 41 deletions(-) diff --git a/inference-engine/samples/speech_sample/main.cpp b/inference-engine/samples/speech_sample/main.cpp index 5bae50b1d0bd5b..17ce5904ba61fa 100644 --- a/inference-engine/samples/speech_sample/main.cpp +++ b/inference-engine/samples/speech_sample/main.cpp @@ -702,7 +702,7 @@ int main(int argc, char *argv[]) { outputs.push_back(outBlobName.substr(0, pos_layer)); try { ports.push_back(std::stoi(outBlobName.substr(pos_layer + 1))); - } catch (std::exception) { + } catch (const std::exception&) { throw std::logic_error("Ports should have integer type"); } } diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp index 8eff19f4672b68..5fff024ee860d0 100644 --- a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp @@ -27,39 +27,6 @@ struct NMSAttributes { using namespace ngraph; namespace { - std::shared_ptr nms4_pattern() { - auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); - auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); - auto max_output_boxes_per_class = ngraph::opset4::Constant::create(element::i64, Shape{}, {10}); - auto iou_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = ngraph::opset4::Constant::create(element::f32, Shape{}, {0.7}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - iou_threshold, score_threshold); - return nms; - } - - std::shared_ptr nms3_pattern() { - auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); - auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); - auto max_output_boxes_per_class = ngraph::opset3::Constant::create(element::i64, Shape{}, {10}); - auto iou_threshold = ngraph::opset3::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = ngraph::opset3::Constant::create(element::f32, Shape{}, {0.7}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - iou_threshold, score_threshold); - return nms; - } - - std::shared_ptr nms1_pattern() { - auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); - auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); - auto max_output_boxes_per_class = ngraph::opset1::Constant::create(element::i64, Shape{}, {10}); - auto iou_threshold = ngraph::opset1::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = ngraph::opset1::Constant::create(element::f32, Shape{}, {0.7}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - iou_threshold, score_threshold); - return nms; - } - NMSAttributes get_nms4_attrs(const std::shared_ptr& nms4) { NMSAttributes attrs; @@ -149,7 +116,7 @@ namespace { return get_nms4_attrs(nms_4); } auto nms_3 = std::dynamic_pointer_cast(root); - if (nms_4) { + if (nms_3) { return get_nms3_attrs(nms_3); } auto nms_1 = std::dynamic_pointer_cast(root); @@ -222,8 +189,7 @@ ngraph::pass::ConvertPreviousNMSToNMS5::ConvertPreviousNMSToNMS5() { NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS4ToNMS5, "ConvertNMS4ToNMS5", 0); ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { - auto nms = nms4_pattern(); - + auto nms = ngraph::pattern::wrap_type(); ngraph::matcher_pass_callback callback = callback_func; auto m = std::make_shared(nms, "ConvertNMS4ToNMS5"); @@ -234,8 +200,7 @@ ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS3ToNMS5, "ConvertNMS3ToNMS5", 0); ngraph::pass::ConvertNMS3ToNMS5::ConvertNMS3ToNMS5() { - auto nms = nms3_pattern(); - + auto nms = ngraph::pattern::wrap_type(); ngraph::matcher_pass_callback callback = callback_func; auto m = std::make_shared(nms, "ConvertNMS3ToNMS5"); @@ -245,8 +210,7 @@ ngraph::pass::ConvertNMS3ToNMS5::ConvertNMS3ToNMS5() { NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS1ToNMS5, "ConvertNMS1ToNMS5", 0); ngraph::pass::ConvertNMS1ToNMS5::ConvertNMS1ToNMS5() { - auto nms = nms1_pattern(); - + auto nms = ngraph::pattern::wrap_type(); ngraph::matcher_pass_callback callback = callback_func; auto m = std::make_shared(nms, "ConvertNMS1ToNMS5"); From 398c2e0b562e1474db1a039e7d85c5069fc360ac Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 20 Oct 2020 11:04:28 +0300 Subject: [PATCH 119/173] Now transformations NMS-1 -> NMS-5, NMS-3 -> NMS-5, NMS-4 -> NMS-5 are called from 'umbrella' transformation. --- .../op_conversions/convert_previous_nms_to_nms_5.hpp | 8 ++++++-- .../common_optimizations/common_optimizations.cpp | 4 +--- .../op_conversions/convert_previous_nms_to_nms_5.cpp | 4 ---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/inference-engine/src/transformations/include/transformations/op_conversions/convert_previous_nms_to_nms_5.hpp b/inference-engine/src/transformations/include/transformations/op_conversions/convert_previous_nms_to_nms_5.hpp index c3f5e1a9cfd6d0..f0296717526cae 100644 --- a/inference-engine/src/transformations/include/transformations/op_conversions/convert_previous_nms_to_nms_5.hpp +++ b/inference-engine/src/transformations/include/transformations/op_conversions/convert_previous_nms_to_nms_5.hpp @@ -39,8 +39,12 @@ class ngraph::pass::ConvertNMS4ToNMS5: public ngraph::pass::MatcherPass { ConvertNMS4ToNMS5(); }; -class ngraph::pass::ConvertPreviousNMSToNMS5: public ngraph::pass::MatcherPass { +class ngraph::pass::ConvertPreviousNMSToNMS5: public ngraph::pass::GraphRewrite { public: NGRAPH_RTTI_DECLARATION; - ConvertPreviousNMSToNMS5(); + ConvertPreviousNMSToNMS5() { + add_matcher(); + add_matcher(); + add_matcher(); + } }; diff --git a/inference-engine/src/transformations/src/transformations/common_optimizations/common_optimizations.cpp b/inference-engine/src/transformations/src/transformations/common_optimizations/common_optimizations.cpp index 6e09435a61650e..36ad9e14a4ceb0 100644 --- a/inference-engine/src/transformations/src/transformations/common_optimizations/common_optimizations.cpp +++ b/inference-engine/src/transformations/src/transformations/common_optimizations/common_optimizations.cpp @@ -106,9 +106,7 @@ bool ngraph::pass::CommonOptimizations::run_on_function(std::shared_ptr(); manager.register_pass(); - manager.register_pass(); - manager.register_pass(); - manager.register_pass(); + manager.register_pass(); auto fq_fusions = manager.register_pass(); fq_fusions->add_matcher(); diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp index 5fff024ee860d0..e16b62daf7d22a 100644 --- a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp @@ -183,9 +183,6 @@ namespace { NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertPreviousNMSToNMS5, "ConvertPreviousNMSToNMS5", 0); -ngraph::pass::ConvertPreviousNMSToNMS5::ConvertPreviousNMSToNMS5() { -} - NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS4ToNMS5, "ConvertNMS4ToNMS5", 0); ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { @@ -196,7 +193,6 @@ ngraph::pass::ConvertNMS4ToNMS5::ConvertNMS4ToNMS5() { this->register_matcher(m, callback); } - NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS3ToNMS5, "ConvertNMS3ToNMS5", 0); ngraph::pass::ConvertNMS3ToNMS5::ConvertNMS3ToNMS5() { From d1225554ffe0db2a975f610fc6e46b8f29be69a1 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 20 Oct 2020 13:07:33 +0300 Subject: [PATCH 120/173] Now the transformation ConvertNMS5ToLegacyMatcher supports NMS-5 with 2, 3, 4, 5, or 6 inputs. --- .../convert_nms_5_to_legacy.cpp | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp index 7d89d43327fe05..54ee510421479f 100644 --- a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp +++ b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "legacy/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.hpp" @@ -17,14 +18,7 @@ NGRAPH_RTTI_DEFINITION(ngraph::pass::ConvertNMS5ToLegacyMatcher, "ConvertNMS5ToLegacyMatcher", 0); ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { - auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); - auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); - auto max_output_boxes_per_class = ngraph::opset5::Constant::create(element::i64, Shape{}, {10}); - auto iou_threshold = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.7}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - iou_threshold, score_threshold, soft_nms_sigma); + auto nms = ngraph::pattern::wrap_type(); ngraph::matcher_pass_callback callback = [](pattern::Matcher &m) { auto nms_5 = std::dynamic_pointer_cast(m.get_match_root()); @@ -100,21 +94,21 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { throw ngraph_error("NonMaxSuppression layer " + nms_5->get_friendly_name() + " has unsupported box encoding"); } - const auto nms_legacy = std::make_shared( - new_args.at(0), - new_args.at(1), - new_max_per_class, - new_iou_threshold, - new_score_threshold, - new_soft_nms_sigma, - center_point_box, - nms_5->get_sort_result_descending(), - nms_5->get_output_type()); - new_ops.push_back(nms_legacy); + const auto nms_legacy = std::make_shared( + new_args.at(0), + new_args.at(1), + new_max_per_class, + new_iou_threshold, + new_score_threshold, + new_soft_nms_sigma, + center_point_box, + nms_5->get_sort_result_descending(), + nms_5->get_output_type()); + new_ops.push_back(nms_legacy); - nms_legacy->set_friendly_name(nms_5->get_friendly_name()); - ngraph::copy_runtime_info(nms_5, new_ops); - ngraph::replace_node(nms_5, nms_legacy); + nms_legacy->set_friendly_name(nms_5->get_friendly_name()); + ngraph::copy_runtime_info(nms_5, new_ops); + ngraph::replace_node(nms_5, nms_legacy); return true; }; From a61629e25fc21648d76a7da6183cf0315eea44d2 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 21 Oct 2020 09:38:18 +0300 Subject: [PATCH 121/173] The transformation ConvertNMS5ToLegacyMatcher was rewritten using Reshape instead of Unsqueeze. --- .../convert_nms_5_to_legacy.cpp | 76 +++++++------------ 1 file changed, 26 insertions(+), 50 deletions(-) diff --git a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp index 54ee510421479f..d3344f567244dd 100644 --- a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp +++ b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp @@ -27,61 +27,37 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { } const auto new_args = nms_5->input_values(); - const auto& arg2 = new_args.size() > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i32, Shape{}, {0}); - const auto& arg3 = new_args.size() > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); - const auto& arg4 = new_args.size() > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); - const auto& arg5 = new_args.size() > 5 ? new_args.at(5) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); - - const auto max_output_boxes_per_class_rank = arg2.get_partial_shape().rank(); - const auto iou_threshold_rank = arg3.get_partial_shape().rank(); - const auto score_threshold_rank = arg4.get_partial_shape().rank(); - const auto soft_nms_sigma_rank = arg5.get_partial_shape().rank(); - - // Check that required ranks are not dynamic - if (max_output_boxes_per_class_rank.is_dynamic() || - iou_threshold_rank.is_dynamic() || - score_threshold_rank.is_dynamic() || - soft_nms_sigma_rank.is_dynamic()) { - return false; - } + const std::size_t num_of_inputs = new_args.size(); - if (max_output_boxes_per_class_rank.get_length() == 1 && - iou_threshold_rank.get_length() == 1 && - score_threshold_rank.get_length() == 1 && - soft_nms_sigma_rank.get_length() == 1) { - return false; - } + const auto& arg2 = num_of_inputs > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i32, Shape{}, {0}); + const auto& arg3 = num_of_inputs > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + const auto& arg4 = num_of_inputs > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); + const auto& arg5 = num_of_inputs > 5 ? new_args.at(5) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); // vector of new nGraph operations NodeVector new_ops; - auto new_max_per_class = arg2; - if (max_output_boxes_per_class_rank.get_length() == 0) { - // WA: we need to create Constant manually because it requires by NMS shape inference - // otherwise we will get dynamic shape until first CF is executed. It can be resolved - // if CF will be executed right after transformation and before Validate pass. - if (auto new_max_per_class_const = std::dynamic_pointer_cast(new_max_per_class.get_node_shared_ptr())) { - new_max_per_class = opset1::Constant::create(element::i64, Shape{1}, new_max_per_class_const->cast_vector()); - } else { - new_max_per_class = std::make_shared(arg2, opset1::Constant::create(element::i64, Shape{1}, {0})); - new_ops.push_back(new_max_per_class.get_node_shared_ptr()); - } - } - auto new_iou_threshold = arg3; - if (iou_threshold_rank.get_length() == 0) { - new_iou_threshold = std::make_shared(arg3, opset1::Constant::create(element::i64, Shape{1}, {0})); - new_ops.push_back(new_iou_threshold.get_node_shared_ptr()); - } - auto new_score_threshold = arg4; - if (score_threshold_rank.get_length() == 0) { - new_score_threshold = std::make_shared(arg4, opset1::Constant::create(element::i64, Shape{1}, {0})); - new_ops.push_back(new_score_threshold.get_node_shared_ptr()); - } - auto new_soft_nms_sigma = arg5; - if (soft_nms_sigma_rank.get_length() == 0) { - new_soft_nms_sigma = std::make_shared(arg5, opset1::Constant::create(element::i64, Shape{1}, {0})); - new_ops.push_back(new_soft_nms_sigma.get_node_shared_ptr()); - } + auto one_dim_shape = Shape{1}; + auto new_max_per_class = std::make_shared(arg2, + opset1::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + new_ops.push_back(new_max_per_class.get_node_shared_ptr()); + + auto new_iou_threshold = std::make_shared(arg3, + opset1::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + new_ops.push_back(new_iou_threshold.get_node_shared_ptr()); + + auto new_score_threshold = std::make_shared(arg4, + opset1::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + new_ops.push_back(new_score_threshold.get_node_shared_ptr()); + + auto new_soft_nms_sigma = std::make_shared(arg5, + opset1::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + new_ops.push_back(new_soft_nms_sigma.get_node_shared_ptr()); + int center_point_box = 0; switch (nms_5->get_box_encoding()) { case ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER: From 37b7b11f98e79635e12298180c0aaeabe15f16f7 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 21 Oct 2020 10:23:50 +0300 Subject: [PATCH 122/173] Started to rewrite tests for the transformation ConvertNMS5ToLegacyMatcher. --- .../transformations/convert_nms5_test.cpp | 117 +++--------------- 1 file changed, 17 insertions(+), 100 deletions(-) diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp index 6567330464f31c..391d4953654fa4 100644 --- a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp +++ b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp @@ -46,18 +46,26 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStatic) { { auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); - auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{1}, {10}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - std::make_shared(iou_threshold, - opset5::Constant::create(element::i64, Shape{1}, {0})), - std::make_shared(score_threshold, - opset5::Constant::create(element::i64, Shape{1}, {0})), - std::make_shared(soft_nms_sigma, - opset5::Constant::create(element::i64, Shape{1}, {0})), - 0, true); + + auto one_dim_shape = Shape{1}; + auto new_max_per_class = std::make_shared(max_output_boxes_per_class, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_iou_threshold = std::make_shared(iou_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_score_threshold = std::make_shared(score_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, + new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); @@ -67,94 +75,3 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStatic) { auto res = compare_functions(f, f_ref); ASSERT_TRUE(res.first) << res.second; } - -TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1) { - std::shared_ptr f(nullptr), f_ref(nullptr); - { - auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); - auto scores = std::make_shared(element::f32, PartialShape::dynamic()); - auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); - auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, - soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true, element::i32); - - f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); - - pass::Manager manager; - manager.register_pass(); - manager.register_pass(); - manager.run_passes(f); - f->validate_nodes_and_infer_types(); - ASSERT_NO_THROW(check_rt_info(f)); - } - - { - auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); - auto scores = std::make_shared(element::f32, PartialShape::dynamic()); - auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{1}, {10}); - auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - std::make_shared(iou_threshold, - opset5::Constant::create(element::i64, Shape{1}, {0})), - std::make_shared(score_threshold, - opset5::Constant::create(element::i64, Shape{1}, {0})), - std::make_shared(soft_nms_sigma, - opset5::Constant::create(element::i64, Shape{1}, {0})), - 0, true, element::i32); - - f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); - } - - auto res = compare_functions(f, f_ref); - ASSERT_TRUE(res.first) << res.second; -} - -TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2) { - std::shared_ptr f(nullptr), f_ref(nullptr); - { - auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); - auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); - auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); - auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, - soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true, element::i32); - - f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); - - pass::Manager manager; - manager.register_pass(); - manager.register_pass(); - manager.run_passes(f); - - f->validate_nodes_and_infer_types(); - ASSERT_NO_THROW(check_rt_info(f)); - } - - { - auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); - auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); - auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{1}, {10}); - auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); - auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); - auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, - std::make_shared(iou_threshold, - opset5::Constant::create(element::i64, Shape{1}, {0})), - std::make_shared(score_threshold, - opset5::Constant::create(element::i64, Shape{1}, {0})), - std::make_shared(soft_nms_sigma, - opset5::Constant::create(element::i64, Shape{1}, {0})), - 0, true, element::i32); - - f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); - } - - auto res = compare_functions(f, f_ref); - ASSERT_TRUE(res.first) << res.second; -} From 8fcf0469c86d5e34abe1ed2241228bf41d50f770 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 21 Oct 2020 11:02:57 +0300 Subject: [PATCH 123/173] Some fixes. --- .../convert_nms_5_to_legacy.cpp | 30 +++++++++++-------- .../transformations/convert_nms5_test.cpp | 4 +-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp index d3344f567244dd..8efb347abe03e7 100644 --- a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp +++ b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp @@ -38,24 +38,30 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { NodeVector new_ops; auto one_dim_shape = Shape{1}; - auto new_max_per_class = std::make_shared(arg2, - opset1::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); + + auto new_max_per_class = arg2; + auto new_iou_threshold = arg3; + auto new_score_threshold = arg4; + auto new_soft_nms_sigma = arg5; + + new_max_per_class = std::make_shared(arg2, + opset1::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); new_ops.push_back(new_max_per_class.get_node_shared_ptr()); - auto new_iou_threshold = std::make_shared(arg3, - opset1::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); + new_iou_threshold = std::make_shared(arg3, + opset1::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); new_ops.push_back(new_iou_threshold.get_node_shared_ptr()); - auto new_score_threshold = std::make_shared(arg4, - opset1::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); + new_score_threshold = std::make_shared(arg4, + opset1::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); new_ops.push_back(new_score_threshold.get_node_shared_ptr()); - auto new_soft_nms_sigma = std::make_shared(arg5, - opset1::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); + new_soft_nms_sigma = std::make_shared(arg5, + opset1::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); new_ops.push_back(new_soft_nms_sigma.get_node_shared_ptr()); int center_point_box = 0; diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp index 391d4953654fa4..b6b8883313546a 100644 --- a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp +++ b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp @@ -53,8 +53,8 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStatic) { auto one_dim_shape = Shape{1}; auto new_max_per_class = std::make_shared(max_output_boxes_per_class, - opset5::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); auto new_iou_threshold = std::make_shared(iou_threshold, opset5::Constant::create(ngraph::element::i64, one_dim_shape, one_dim_shape), true); From 33fe2f0d4ed06c6ca9fd3164eb5839ec7a61d28f Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 21 Oct 2020 12:02:14 +0300 Subject: [PATCH 124/173] Small fixes. --- .../convert_nms_5_to_legacy.cpp | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp index 8efb347abe03e7..dae4adaf0510ba 100644 --- a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp +++ b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp @@ -39,29 +39,26 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { auto one_dim_shape = Shape{1}; - auto new_max_per_class = arg2; - auto new_iou_threshold = arg3; - auto new_score_threshold = arg4; - auto new_soft_nms_sigma = arg5; - - new_max_per_class = std::make_shared(arg2, - opset1::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); + Output new_max_per_class; + Output new_iou_threshold; + Output new_score_threshold; + Output new_soft_nms_sigma; + + Output new_shape_for_max_per_class = opset1::Constant::create(ngraph::element::i64, Shape{1}, {1}); + Output new_shape_for_iou_threshold = opset1::Constant::create(ngraph::element::i64, Shape{1}, {1}); + Output new_shape_for_score_threshold = opset1::Constant::create(ngraph::element::i64, Shape{1}, {1}); + Output new_shape_for_soft_nms_sigma = opset1::Constant::create(ngraph::element::i64, Shape{1}, {1}); + + new_max_per_class = std::make_shared(arg2, new_shape_for_max_per_class, true); new_ops.push_back(new_max_per_class.get_node_shared_ptr()); - new_iou_threshold = std::make_shared(arg3, - opset1::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); + new_iou_threshold = std::make_shared(arg3, new_shape_for_iou_threshold, true); new_ops.push_back(new_iou_threshold.get_node_shared_ptr()); - new_score_threshold = std::make_shared(arg4, - opset1::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); + new_score_threshold = std::make_shared(arg4, new_shape_for_score_threshold, true); new_ops.push_back(new_score_threshold.get_node_shared_ptr()); - new_soft_nms_sigma = std::make_shared(arg5, - opset1::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); + new_soft_nms_sigma = std::make_shared(arg5, new_shape_for_soft_nms_sigma, true); new_ops.push_back(new_soft_nms_sigma.get_node_shared_ptr()); int center_point_box = 0; From 3fbcb28b8bca8bc30eb791892312c6b7189d2d6d Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 21 Oct 2020 16:50:21 +0300 Subject: [PATCH 125/173] Corrected tests for the transformation NMS-5 -> NMSIE3. --- .../src/legacy_api/src/ngraph_ops/nms_ie.cpp | 2 +- .../transformations/convert_nms5_test.cpp | 766 +++++++++++++++++- 2 files changed, 761 insertions(+), 7 deletions(-) diff --git a/inference-engine/src/legacy_api/src/ngraph_ops/nms_ie.cpp b/inference-engine/src/legacy_api/src/ngraph_ops/nms_ie.cpp index 4e838696811e9b..347e939505290b 100644 --- a/inference-engine/src/legacy_api/src/ngraph_ops/nms_ie.cpp +++ b/inference-engine/src/legacy_api/src/ngraph_ops/nms_ie.cpp @@ -140,7 +140,7 @@ int64_t op::NonMaxSuppressionIE3::max_boxes_output_from_input() const { int64_t max_output_boxes{0}; const auto max_output_boxes_input = - as_type_ptr(input_value(2).get_node_shared_ptr()); + as_type_ptr(input_value(max_output_boxes_per_class_port).get_node_shared_ptr()); max_output_boxes = max_output_boxes_input->cast_vector().at(0); return max_output_boxes; diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp index b6b8883313546a..29cb46fe0fd28a 100644 --- a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp +++ b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp @@ -20,7 +20,7 @@ using namespace testing; using namespace ngraph; -TEST(TransformationTests, ConvertNMS5ToNMSIEStatic) { +TEST(TransformationTests, ConvertNMS5ToNMSIEStaticSixInputs) { std::shared_ptr f(nullptr), f_ref(nullptr); { auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); @@ -32,20 +32,305 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStatic) { auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); - f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); + f = std::make_shared(nms, ParameterVector{boxes, scores}); + + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + ASSERT_NO_THROW(check_rt_info(f)); + ASSERT_TRUE(f->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE(f->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + EXPECT_EQ(f->get_output_shape(2), (Shape{1})); + } + + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); + + auto one_dim_shape = Shape{1}; + auto new_max_per_class = std::make_shared(max_output_boxes_per_class, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_iou_threshold = std::make_shared(iou_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_score_threshold = std::make_shared(score_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, + new_soft_nms_sigma, 0, true); + nms->set_friendly_name("nms"); + + f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + ASSERT_TRUE(f_ref->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE(f_ref->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + EXPECT_EQ(f_ref->get_output_shape(2), (Shape{1})); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS5ToNMSIEStaticFiveInputs) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(nms, ParameterVector{boxes, scores}); + + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + ASSERT_NO_THROW(check_rt_info(f)); + ASSERT_TRUE(f->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE(f->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + EXPECT_EQ(f->get_output_shape(2), (Shape{1})); + } + + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); + + auto one_dim_shape = Shape{1}; + auto new_max_per_class = std::make_shared(max_output_boxes_per_class, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_iou_threshold = std::make_shared(iou_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_score_threshold = std::make_shared(score_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, + new_soft_nms_sigma, 0, true); + nms->set_friendly_name("nms"); + + f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + ASSERT_TRUE(f_ref->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE(f_ref->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + EXPECT_EQ(f_ref->get_output_shape(2), (Shape{1})); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS5ToNMSIEStaticFourInputs) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(nms, ParameterVector{boxes, scores}); + + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + ASSERT_NO_THROW(check_rt_info(f)); + ASSERT_TRUE(f->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE(f->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + EXPECT_EQ(f->get_output_shape(2), (Shape{1})); + } + + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); + + auto one_dim_shape = Shape{1}; + auto new_max_per_class = std::make_shared(max_output_boxes_per_class, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_iou_threshold = std::make_shared(iou_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_score_threshold = std::make_shared(score_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, + new_soft_nms_sigma, 0, true); + nms->set_friendly_name("nms"); + + f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + ASSERT_TRUE(f_ref->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE(f_ref->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + EXPECT_EQ(f_ref->get_output_shape(2), (Shape{1})); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS5ToNMSIEStaticThreeInputs) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(nms, ParameterVector{boxes, scores}); + + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + ASSERT_NO_THROW(check_rt_info(f)); + ASSERT_TRUE(f->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE(f->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + EXPECT_EQ(f->get_output_shape(2), (Shape{1})); + } + + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); + + auto one_dim_shape = Shape{1}; + auto new_max_per_class = std::make_shared(max_output_boxes_per_class, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_iou_threshold = std::make_shared(iou_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_score_threshold = std::make_shared(score_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, + new_soft_nms_sigma, 0, true); + nms->set_friendly_name("nms"); + + f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + ASSERT_TRUE(f_ref->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE(f_ref->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + EXPECT_EQ(f_ref->get_output_shape(2), (Shape{1})); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS5ToNMSIEStaticTwoInputs) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto nms = std::make_shared(boxes, scores, + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(nms, ParameterVector{boxes, scores}); - const auto &orig_selected_indices_shape = f->get_output_partial_shape(0); pass::Manager manager; manager.register_pass(); manager.register_pass(); manager.run_passes(f); ASSERT_NO_THROW(check_rt_info(f)); - ASSERT_TRUE(f->get_output_partial_shape(0).is_static()) << "Shape " << f->get_output_partial_shape(0) << " should be static"; + ASSERT_TRUE(f->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE(f->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + EXPECT_EQ(f->get_output_shape(2), (Shape{1})); } { auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {0}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); + + auto one_dim_shape = Shape{1}; + auto new_max_per_class = std::make_shared(max_output_boxes_per_class, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_iou_threshold = std::make_shared(iou_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_score_threshold = std::make_shared(score_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, + new_soft_nms_sigma, 0, true); + nms->set_friendly_name("nms"); + + f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + ASSERT_TRUE(f_ref->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); + ASSERT_TRUE(f_ref->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); + EXPECT_EQ(f_ref->get_output_shape(2), (Shape{1})); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1SixInputs) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); + auto scores = std::make_shared(element::f32, PartialShape::dynamic()); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(nms, ParameterVector{boxes, scores}); + + const auto &orig_selected_indices_shape = f->get_output_partial_shape(0); + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + f->validate_nodes_and_infer_types(); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); + auto scores = std::make_shared(element::f32, PartialShape::dynamic()); auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); @@ -68,8 +353,477 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStatic) { new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); - f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); - ASSERT_TRUE(f_ref->get_output_partial_shape(0).is_static()) << "Shape " << f_ref->get_output_partial_shape(0) << " should be static"; + f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1FiveInputs) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); + auto scores = std::make_shared(element::f32, PartialShape::dynamic()); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(nms, ParameterVector{boxes, scores}); + + const auto &orig_selected_indices_shape = f->get_output_partial_shape(0); + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + f->validate_nodes_and_infer_types(); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); + auto scores = std::make_shared(element::f32, PartialShape::dynamic()); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); + + auto one_dim_shape = Shape{1}; + auto new_max_per_class = std::make_shared(max_output_boxes_per_class, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_iou_threshold = std::make_shared(iou_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_score_threshold = std::make_shared(score_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, + new_soft_nms_sigma, 0, true); + nms->set_friendly_name("nms"); + + f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1FourInputs) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); + auto scores = std::make_shared(element::f32, PartialShape::dynamic()); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(nms, ParameterVector{boxes, scores}); + + const auto &orig_selected_indices_shape = f->get_output_partial_shape(0); + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + f->validate_nodes_and_infer_types(); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); + auto scores = std::make_shared(element::f32, PartialShape::dynamic()); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); + + auto one_dim_shape = Shape{1}; + auto new_max_per_class = std::make_shared(max_output_boxes_per_class, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_iou_threshold = std::make_shared(iou_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_score_threshold = std::make_shared(score_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, + new_soft_nms_sigma, 0, true); + nms->set_friendly_name("nms"); + + f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1ThreeInputs) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); + auto scores = std::make_shared(element::f32, PartialShape::dynamic()); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(nms, ParameterVector{boxes, scores}); + + const auto &orig_selected_indices_shape = f->get_output_partial_shape(0); + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + f->validate_nodes_and_infer_types(); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); + auto scores = std::make_shared(element::f32, PartialShape::dynamic()); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); + + auto one_dim_shape = Shape{1}; + auto new_max_per_class = std::make_shared(max_output_boxes_per_class, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_iou_threshold = std::make_shared(iou_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_score_threshold = std::make_shared(score_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, + new_soft_nms_sigma, 0, true); + nms->set_friendly_name("nms"); + + f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1TwoInputs) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); + auto scores = std::make_shared(element::f32, PartialShape::dynamic()); + auto nms = std::make_shared(boxes, scores, + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(nms, ParameterVector{boxes, scores}); + + const auto &orig_selected_indices_shape = f->get_output_partial_shape(0); + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + f->validate_nodes_and_infer_types(); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); + auto scores = std::make_shared(element::f32, PartialShape::dynamic()); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {0}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); + + auto one_dim_shape = Shape{1}; + auto new_max_per_class = std::make_shared(max_output_boxes_per_class, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_iou_threshold = std::make_shared(iou_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_score_threshold = std::make_shared(score_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, + new_soft_nms_sigma, 0, true); + nms->set_friendly_name("nms"); + + f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2SixInputs) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); + auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(nms, ParameterVector{boxes, scores}); + + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + f->validate_nodes_and_infer_types(); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); + auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.25}); + + auto one_dim_shape = Shape{1}; + auto new_max_per_class = std::make_shared(max_output_boxes_per_class, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_iou_threshold = std::make_shared(iou_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_score_threshold = std::make_shared(score_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, + new_soft_nms_sigma, 0, true); + nms->set_friendly_name("nms"); + + f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2FiveInputs) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); + auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(nms, ParameterVector{boxes, scores}); + + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + f->validate_nodes_and_infer_types(); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); + auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); + + auto one_dim_shape = Shape{1}; + auto new_max_per_class = std::make_shared(max_output_boxes_per_class, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_iou_threshold = std::make_shared(iou_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_score_threshold = std::make_shared(score_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, + new_soft_nms_sigma, 0, true); + nms->set_friendly_name("nms"); + + f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2FourInputs) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); + auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(nms, ParameterVector{boxes, scores}); + + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + f->validate_nodes_and_infer_types(); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); + auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); + + auto one_dim_shape = Shape{1}; + auto new_max_per_class = std::make_shared(max_output_boxes_per_class, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_iou_threshold = std::make_shared(iou_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_score_threshold = std::make_shared(score_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, + new_soft_nms_sigma, 0, true); + nms->set_friendly_name("nms"); + + f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2ThreeInputs) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); + auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(nms, ParameterVector{boxes, scores}); + + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + f->validate_nodes_and_infer_types(); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); + auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); + + auto one_dim_shape = Shape{1}; + auto new_max_per_class = std::make_shared(max_output_boxes_per_class, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_iou_threshold = std::make_shared(iou_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_score_threshold = std::make_shared(score_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, + new_soft_nms_sigma, 0, true); + nms->set_friendly_name("nms"); + + f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + } + + auto res = compare_functions(f, f_ref); + ASSERT_TRUE(res.first) << res.second; +} + +TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2TwoInputs) { + std::shared_ptr f(nullptr), f_ref(nullptr); + { + auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); + auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); + auto nms = std::make_shared(boxes, scores, + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + + f = std::make_shared(nms, ParameterVector{boxes, scores}); + + pass::Manager manager; + manager.register_pass(); + manager.register_pass(); + manager.run_passes(f); + f->validate_nodes_and_infer_types(); + ASSERT_NO_THROW(check_rt_info(f)); + } + + { + auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); + auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {0}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); + auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); + + auto one_dim_shape = Shape{1}; + auto new_max_per_class = std::make_shared(max_output_boxes_per_class, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_iou_threshold = std::make_shared(iou_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_score_threshold = std::make_shared(score_threshold, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, + opset5::Constant::create(ngraph::element::i64, one_dim_shape, + one_dim_shape), true); + auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, + new_soft_nms_sigma, 0, true); + nms->set_friendly_name("nms"); + + f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); } auto res = compare_functions(f, f_ref); From 707666afc8b24ab3fe1fdadf972725fe5c3ef044 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 22 Oct 2020 09:51:54 +0300 Subject: [PATCH 126/173] Small formatting fix. --- ngraph/python/src/ngraph/opset5/ops.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ngraph/python/src/ngraph/opset5/ops.py b/ngraph/python/src/ngraph/opset5/ops.py index 62a233755671ca..01a62111ff52f8 100644 --- a/ngraph/python/src/ngraph/opset5/ops.py +++ b/ngraph/python/src/ngraph/opset5/ops.py @@ -140,6 +140,7 @@ def non_max_suppression( return _get_node_factory_opset5().create("NonMaxSuppression", inputs, attributes) + @nameable_op def round(data: NodeInput, mode: str = "half_to_even", name: Optional[str] = None) -> Node: """Apply Round operation on each element of input tensor. From cdb77fa6344c61dc2b2d54fbeab018c8121b1857 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 22 Oct 2020 10:29:01 +0300 Subject: [PATCH 127/173] Now methods max_boxes_output_from_input(), iou_threshold_from_input(), score_threshold_from_input(), soft_nms_sigma_from_input() of op::v5::NunMaxSuppression are public. --- ngraph/core/include/ngraph/op/non_max_suppression.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ngraph/core/include/ngraph/op/non_max_suppression.hpp b/ngraph/core/include/ngraph/op/non_max_suppression.hpp index 817e8128242c63..ffab830ad19bb5 100644 --- a/ngraph/core/include/ngraph/op/non_max_suppression.hpp +++ b/ngraph/core/include/ngraph/op/non_max_suppression.hpp @@ -376,15 +376,15 @@ namespace ngraph } using Node::set_output_type; + int64_t max_boxes_output_from_input() const; + float iou_threshold_from_input() const; + float score_threshold_from_input() const; + float soft_nms_sigma_from_input() const; protected: BoxEncodingType m_box_encoding = BoxEncodingType::CORNER; bool m_sort_result_descending = true; ngraph::element::Type m_output_type = ngraph::element::i64; void validate(); - int64_t max_boxes_output_from_input() const; - float iou_threshold_from_input() const; - float score_threshold_from_input() const; - float soft_nms_sigma_from_input() const; }; } // namespace v5 } // namespace op From 3ab28d3388767cfe06597457e49992a8a27ce59f Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 22 Oct 2020 10:31:15 +0300 Subject: [PATCH 128/173] Started to move op::v5::NonMaxSuppression::evaluate() into ngraph/test/runtime/interpreter. --- ngraph/test/runtime/interpreter/int_executable.hpp | 13 +++++++++++++ ngraph/test/runtime/interpreter/opset_int_tbl.hpp | 1 + 2 files changed, 14 insertions(+) diff --git a/ngraph/test/runtime/interpreter/int_executable.hpp b/ngraph/test/runtime/interpreter/int_executable.hpp index be993f6c98033e..7a1afbca25c14c 100644 --- a/ngraph/test/runtime/interpreter/int_executable.hpp +++ b/ngraph/test/runtime/interpreter/int_executable.hpp @@ -70,6 +70,7 @@ #include "ngraph/runtime/reference/max_pool.hpp" #include "ngraph/runtime/reference/min.hpp" #include "ngraph/runtime/reference/negate.hpp" +#include "ngraph/runtime/reference/non_max_suppression.hpp" #include "ngraph/runtime/reference/normalize_l2.hpp" #include "ngraph/runtime/reference/not.hpp" #include "ngraph/runtime/reference/one_hot.hpp" @@ -1444,6 +1445,18 @@ class INTERPRETER_BACKEND_API ngraph::runtime::interpreter::INTExecutable : publ norm->get_eps_mode()); break; } + case OP_TYPEID::NonMaxSuppression_v5: + { + const op::NonMaxSuppression* nms = + static_cast(&node); + + int64_t max_output_boxes_per_class = nms->max_boxes_output_from_input(); + float iou_threshold = nms->iou_threshold_from_input(); + float score_threshold = nms->score_threshold_from_input(); + float soft_nms_sigma = nms->soft_nms_sigma_from_input(); + + break; + } // Fused Ops are not supported in interpreter. They need to be decomposed before execution case OP_TYPEID::DepthToSpace: diff --git a/ngraph/test/runtime/interpreter/opset_int_tbl.hpp b/ngraph/test/runtime/interpreter/opset_int_tbl.hpp index f9e1ee46126810..9db0f38a0f4d39 100644 --- a/ngraph/test/runtime/interpreter/opset_int_tbl.hpp +++ b/ngraph/test/runtime/interpreter/opset_int_tbl.hpp @@ -59,4 +59,5 @@ NGRAPH_OP(GRUSequence, op::v5) NGRAPH_OP(RNNSequence, op::v5) NGRAPH_OP(Round, op::v5) NGRAPH_OP(LogSoftmax, op::v5) +NGRAPH_OP(NonMaxSuppression, op::v5) #undef ID_SUFFIX From a0a562eef5261570e4a708ff959429a115579de8 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 22 Oct 2020 12:34:09 +0300 Subject: [PATCH 129/173] Added NMS-5 to ngraph/test/runtime/interpreter/int_executable. --- .../runtime/interpreter/int_executable.cpp | 307 ++++++++++++++++++ .../runtime/interpreter/int_executable.hpp | 13 +- 2 files changed, 314 insertions(+), 6 deletions(-) diff --git a/ngraph/test/runtime/interpreter/int_executable.cpp b/ngraph/test/runtime/interpreter/int_executable.cpp index 9b54f73215f7c2..b44b4ad78b5117 100644 --- a/ngraph/test/runtime/interpreter/int_executable.cpp +++ b/ngraph/test/runtime/interpreter/int_executable.cpp @@ -21,12 +21,16 @@ #include "ngraph/op/util/op_types.hpp" #include "ngraph/ops.hpp" #include "ngraph/pass/manager.hpp" +#include "ngraph/type/bfloat16.hpp" +#include "ngraph/type/float16.hpp" #include "ngraph/util.hpp" #include "pass/fused_op_decomposition.hpp" #include "pass/like_replacement.hpp" #include "pass/liveness.hpp" #include "pass/opset0_downgrade.hpp" #include "pass/opset1_downgrade.hpp" +#include +#include using namespace std; using namespace ngraph; @@ -55,6 +59,309 @@ runtime::interpreter::OP_TYPEID runtime::interpreter::INTExecutable::get_typeid( return rc; } +using V5BoxEncoding = op::v5::NonMaxSuppression::BoxEncodingType; + +namespace +{ + constexpr size_t boxes_port = 0; + constexpr size_t scores_port = 1; + constexpr size_t max_output_boxes_port = 2; + constexpr size_t iou_threshold_port = 3; + constexpr size_t score_threshold_port = 4; + constexpr size_t soft_nms_sigma_port = 5; + + PartialShape infer_selected_indices_shape( + const std::vector>& inputs, + int64_t max_output_boxes_per_class) + { + const auto boxes_ps = inputs[boxes_port]->get_partial_shape(); + const auto scores_ps = inputs[scores_port]->get_partial_shape(); + + // NonMaxSuppression produces triplets + // that have the following format: [batch_index, class_index, box_index] + PartialShape result = {Dimension::dynamic(), 3}; + + if (boxes_ps.rank().is_static() && scores_ps.rank().is_static()) + { + const auto num_boxes_boxes = boxes_ps[1]; + if (num_boxes_boxes.is_static() && scores_ps[0].is_static() && scores_ps[1].is_static()) + { + const auto num_boxes = num_boxes_boxes.get_length(); + const auto num_classes = scores_ps[1].get_length(); + + result[0] = std::min(num_boxes, max_output_boxes_per_class) * num_classes * + scores_ps[0].get_length(); + } + } + + return result; + } + + void normalize_corner(float* boxes, const Shape& boxes_shape) + { + size_t total_num_of_boxes = shape_size(boxes_shape) / 4; + for (size_t i = 0; i < total_num_of_boxes; ++i) + { + float* current_box = boxes + 4 * i; + + float y1 = current_box[0]; + float x1 = current_box[1]; + float y2 = current_box[2]; + float x2 = current_box[3]; + + float ymin = std::min(y1, y2); + float ymax = std::max(y1, y2); + float xmin = std::min(x1, x2); + float xmax = std::max(x1, x2); + + current_box[0] = ymin; + current_box[1] = xmin; + current_box[2] = ymax; + current_box[3] = xmax; + } + } + + void normalize_center(float* boxes, const Shape& boxes_shape) + { + size_t total_num_of_boxes = shape_size(boxes_shape) / 4; + for (size_t i = 0; i < total_num_of_boxes; ++i) + { + float* current_box = boxes + 4 * i; + + float x_center = current_box[0]; + float y_center = current_box[1]; + float width = current_box[2]; + float height = current_box[3]; + + float y1 = y_center - height / 2.0; + float x1 = x_center - width / 2.0; + float y2 = y_center + height / 2.0; + float x2 = x_center + width / 2.0; + + current_box[0] = y1; + current_box[1] = x1; + current_box[2] = y2; + current_box[3] = x2; + } + } + + void normalize_box_encoding(float* boxes, + const Shape& boxes_shape, + const V5BoxEncoding box_encoding) + { + if (box_encoding == V5BoxEncoding::CORNER) + { + normalize_corner(boxes, boxes_shape); + } + else + { + normalize_center(boxes, boxes_shape); + } + } + + std::vector get_floats(const std::shared_ptr& input, const Shape& shape) + { + size_t input_size = shape_size(shape); + std::vector result(input_size); + + switch (input->get_element_type()) + { + case element::Type_t::bf16: + { + bfloat16* p = input->get_data_ptr(); + for (size_t i = 0; i < input_size; ++i) + { + result[i] = float(p[i]); + } + } + break; + case element::Type_t::f16: + { + float16* p = input->get_data_ptr(); + for (size_t i = 0; i < input_size; ++i) + { + result[i] = float(p[i]); + } + } + break; + case element::Type_t::f32: + { + float* p = input->get_data_ptr(); + memcpy(result.data(), p, input_size * sizeof(float)); + } + break; + default: throw std::runtime_error("Unsupported data type in op NonMaxSuppression-5"); break; + } + + return result; + } + + std::vector prepare_boxes_data(const std::shared_ptr& boxes, + const Shape& boxes_shape, + const V5BoxEncoding box_encoding) + { + auto result = get_floats(boxes, boxes_shape); + normalize_box_encoding(result.data(), boxes_shape, box_encoding); + return result; + } + + std::vector prepare_scores_data(const std::shared_ptr& scores, + const Shape& scores_shape) + { + auto result = get_floats(scores, scores_shape); + return result; + } + + void nms_postprocessing(const std::vector>& outputs, + const ngraph::element::Type output_type, + const std::vector& selected_indices, + const std::vector& selected_scores, + int64_t valid_outputs, + const ngraph::element::Type selected_scores_type) + { + size_t num_of_outputs = outputs.size(); + size_t selected_size = valid_outputs * 3; + + if (output_type == ngraph::element::i64) + { + int64_t* indices_ptr = outputs[0]->get_data_ptr(); + memcpy(indices_ptr, selected_indices.data(), selected_size * sizeof(int64_t)); + } + else + { + int32_t* indices_ptr = outputs[0]->get_data_ptr(); + for (size_t i = 0; i < selected_size; ++i) + { + indices_ptr[i] = static_cast(selected_indices[i]); + } + } + + if (num_of_outputs < 2) + { + return; + } + + size_t selected_scores_size = selected_scores.size(); + + switch (selected_scores_type) + { + case element::Type_t::bf16: + { + bfloat16* scores_ptr = outputs[1]->get_data_ptr(); + for (size_t i = 0; i < selected_scores_size; ++i) + { + scores_ptr[i] = bfloat16(selected_scores[i]); + } + } + break; + case element::Type_t::f16: + { + float16* scores_ptr = outputs[1]->get_data_ptr(); + for (size_t i = 0; i < selected_scores_size; ++i) + { + scores_ptr[i] = float16(selected_scores[i]); + } + } + break; + case element::Type_t::f32: + { + float* scores_ptr = outputs[1]->get_data_ptr(); + memcpy(scores_ptr, selected_scores.data(), selected_size * sizeof(float)); + } + break; + default:; + } + + if (num_of_outputs < 3) + { + return; + } + + if (output_type == ngraph::element::i64) + { + int64_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); + *valid_outputs_ptr = valid_outputs; + } + else + { + int32_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); + *valid_outputs_ptr = static_cast(valid_outputs); + } + } +} + +void runtime::interpreter::run_nms5( + const op::v5::NonMaxSuppression* nms5, + const std::vector>& outputs, + const std::vector>& inputs) +{ + int64_t max_output_boxes_per_class = nms->max_boxes_output_from_input(); + float iou_threshold = nms->iou_threshold_from_input(); + float score_threshold = nms->score_threshold_from_input(); + float soft_nms_sigma = nms->soft_nms_sigma_from_input(); + + auto selected_indices_shape = infer_selected_indices_shape(inputs, + max_output_boxes_per_class); + Shape out_shape = selected_indices_shape.to_shape(); + + Shape boxes_shape = inputs[boxes_port]->get_shape(); + Shape scores_shape = inputs[scores_port]->get_shape(); + + auto boxes_data = prepare_boxes_data(inputs[boxes_port], boxes_shape, nms5->get_box_encoding()); + auto scores_data = prepare_scores_data(inputs[scores_port], scores_shape); + + size_t out_shape_size = shape_size(out_shape); + + std::vector selected_indices(out_shape_size); + std::vector selected_scores(out_shape_size); + int64_t valid_outputs = 0; + + runtime::reference::non_max_suppression(boxes_data.data(), + boxes_shape, + scores_data.data(), + scores_shape, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + selected_indices.data(), + out_shape, + selected_scores.data(), + out_shape, + &valid_outputs, + nms5->get_sort_result_descending()); + + auto output_type = nms5->get_output_type(); + + outputs[0]->set_element_type(output_type); + outputs[0]->set_shape(Shape{static_cast(valid_outputs), 3}); + + size_t num_of_outputs = outputs.size(); + + size_t num_of_outputs = outputs.size(); + + auto selected_scores_type = (inputs.size() < 4) ? element::f32 : inputs[3]->get_element_type(); + + if (num_of_outputs >= 2) + { + outputs[1]->set_element_type(selected_scores_type); + outputs[1]->set_shape(Shape{static_cast(valid_outputs), 3}); + } + + if (num_of_outputs >= 3) + { + outputs[2]->set_element_type(output_type); + outputs[2]->set_shape(Shape{1}); + } + + nms_postprocessing(outputs, + output_type, + selected_indices, + selected_scores, + valid_outputs, + selected_scores_type); +} + runtime::interpreter::INTExecutable::INTExecutable(const shared_ptr& function, bool enable_performance_collection) : m_is_compiled{true} diff --git a/ngraph/test/runtime/interpreter/int_executable.hpp b/ngraph/test/runtime/interpreter/int_executable.hpp index 7a1afbca25c14c..f5571b2d894313 100644 --- a/ngraph/test/runtime/interpreter/int_executable.hpp +++ b/ngraph/test/runtime/interpreter/int_executable.hpp @@ -129,6 +129,11 @@ namespace ngraph #undef NGRAPH_OP UnknownOp }; + + void run_nms5(const op::v5::NonMaxSuppression* nms5, + const std::vector>& out, + const std::vector>& args); + } // namespace interpreter } // namespace runtime } // namespace ngraph @@ -1447,14 +1452,10 @@ class INTERPRETER_BACKEND_API ngraph::runtime::interpreter::INTExecutable : publ } case OP_TYPEID::NonMaxSuppression_v5: { - const op::NonMaxSuppression* nms = + const op::v5::NonMaxSuppression* nms = static_cast(&node); - int64_t max_output_boxes_per_class = nms->max_boxes_output_from_input(); - float iou_threshold = nms->iou_threshold_from_input(); - float score_threshold = nms->score_threshold_from_input(); - float soft_nms_sigma = nms->soft_nms_sigma_from_input(); - + run_nms5(nms, out, args); break; } From 6761c749ca3e707ce8b73567c2e22388f0d0e009 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 22 Oct 2020 12:47:43 +0300 Subject: [PATCH 130/173] Small fixes. --- ngraph/test/runtime/interpreter/int_executable.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ngraph/test/runtime/interpreter/int_executable.cpp b/ngraph/test/runtime/interpreter/int_executable.cpp index b44b4ad78b5117..034a06cf9ae56b 100644 --- a/ngraph/test/runtime/interpreter/int_executable.cpp +++ b/ngraph/test/runtime/interpreter/int_executable.cpp @@ -295,10 +295,10 @@ void runtime::interpreter::run_nms5( const std::vector>& outputs, const std::vector>& inputs) { - int64_t max_output_boxes_per_class = nms->max_boxes_output_from_input(); - float iou_threshold = nms->iou_threshold_from_input(); - float score_threshold = nms->score_threshold_from_input(); - float soft_nms_sigma = nms->soft_nms_sigma_from_input(); + int64_t max_output_boxes_per_class = nms5->max_boxes_output_from_input(); + float iou_threshold = nms5->iou_threshold_from_input(); + float score_threshold = nms5->score_threshold_from_input(); + float soft_nms_sigma = nms5->soft_nms_sigma_from_input(); auto selected_indices_shape = infer_selected_indices_shape(inputs, max_output_boxes_per_class); @@ -338,8 +338,6 @@ void runtime::interpreter::run_nms5( size_t num_of_outputs = outputs.size(); - size_t num_of_outputs = outputs.size(); - auto selected_scores_type = (inputs.size() < 4) ? element::f32 : inputs[3]->get_element_type(); if (num_of_outputs >= 2) From 02fb546746da7013486da5e37d51d39c6bfb8c2d Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 22 Oct 2020 13:21:54 +0300 Subject: [PATCH 131/173] Code style fixes. --- .../include/ngraph/op/non_max_suppression.hpp | 1 + .../runtime/interpreter/int_executable.cpp | 20 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/ngraph/core/include/ngraph/op/non_max_suppression.hpp b/ngraph/core/include/ngraph/op/non_max_suppression.hpp index ffab830ad19bb5..0c710efc889b79 100644 --- a/ngraph/core/include/ngraph/op/non_max_suppression.hpp +++ b/ngraph/core/include/ngraph/op/non_max_suppression.hpp @@ -380,6 +380,7 @@ namespace ngraph float iou_threshold_from_input() const; float score_threshold_from_input() const; float soft_nms_sigma_from_input() const; + protected: BoxEncodingType m_box_encoding = BoxEncodingType::CORNER; bool m_sort_result_descending = true; diff --git a/ngraph/test/runtime/interpreter/int_executable.cpp b/ngraph/test/runtime/interpreter/int_executable.cpp index 034a06cf9ae56b..035a9317a0aad6 100644 --- a/ngraph/test/runtime/interpreter/int_executable.cpp +++ b/ngraph/test/runtime/interpreter/int_executable.cpp @@ -15,6 +15,8 @@ //***************************************************************************** #include "int_executable.hpp" +#include +#include #include "backend_manager.hpp" #include "ngraph/chrome_trace.hpp" #include "ngraph/except.hpp" @@ -29,8 +31,6 @@ #include "pass/liveness.hpp" #include "pass/opset0_downgrade.hpp" #include "pass/opset1_downgrade.hpp" -#include -#include using namespace std; using namespace ngraph; @@ -70,9 +70,9 @@ namespace constexpr size_t score_threshold_port = 4; constexpr size_t soft_nms_sigma_port = 5; - PartialShape infer_selected_indices_shape( - const std::vector>& inputs, - int64_t max_output_boxes_per_class) + PartialShape + infer_selected_indices_shape(const std::vector>& inputs, + int64_t max_output_boxes_per_class) { const auto boxes_ps = inputs[boxes_port]->get_partial_shape(); const auto scores_ps = inputs[scores_port]->get_partial_shape(); @@ -290,18 +290,16 @@ namespace } } -void runtime::interpreter::run_nms5( - const op::v5::NonMaxSuppression* nms5, - const std::vector>& outputs, - const std::vector>& inputs) +void runtime::interpreter::run_nms5(const op::v5::NonMaxSuppression* nms5, + const std::vector>& outputs, + const std::vector>& inputs) { int64_t max_output_boxes_per_class = nms5->max_boxes_output_from_input(); float iou_threshold = nms5->iou_threshold_from_input(); float score_threshold = nms5->score_threshold_from_input(); float soft_nms_sigma = nms5->soft_nms_sigma_from_input(); - auto selected_indices_shape = infer_selected_indices_shape(inputs, - max_output_boxes_per_class); + auto selected_indices_shape = infer_selected_indices_shape(inputs, max_output_boxes_per_class); Shape out_shape = selected_indices_shape.to_shape(); Shape boxes_shape = inputs[boxes_port]->get_shape(); From 95a235ffe00b32db0758c698aaa003c2304947ef Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 22 Oct 2020 16:14:50 +0300 Subject: [PATCH 132/173] Written draft test nonmaxsuppression_center_point_box_format_backend in ngraph/test/backend. --- ngraph/test/CMakeLists.txt | 1 + .../test/backend/non_max_suppression.in.cpp | 106 ++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 ngraph/test/backend/non_max_suppression.in.cpp diff --git a/ngraph/test/CMakeLists.txt b/ngraph/test/CMakeLists.txt index a795f922ed1f43..56a7005034461d 100644 --- a/ngraph/test/CMakeLists.txt +++ b/ngraph/test/CMakeLists.txt @@ -313,6 +313,7 @@ set(MULTI_TEST_SRC backend/node_name.in.cpp backend/normalize_l2.in.cpp backend/not.in.cpp + backend/non_max_suppression.in.cpp backend/non_zero.in.cpp backend/numeric.in.cpp backend/one_hot.in.cpp diff --git a/ngraph/test/backend/non_max_suppression.in.cpp b/ngraph/test/backend/non_max_suppression.in.cpp new file mode 100644 index 00000000000000..35aaf36ab1fda4 --- /dev/null +++ b/ngraph/test/backend/non_max_suppression.in.cpp @@ -0,0 +1,106 @@ +//***************************************************************************** +// Copyright 2020 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//***************************************************************************** + +// clang-format off +#ifdef ${BACKEND_NAME}_FLOAT_TOLERANCE_BITS +#define DEFAULT_FLOAT_TOLERANCE_BITS ${BACKEND_NAME}_FLOAT_TOLERANCE_BITS +#endif + +#ifdef ${BACKEND_NAME}_DOUBLE_TOLERANCE_BITS +#define DEFAULT_DOUBLE_TOLERANCE_BITS ${BACKEND_NAME}_DOUBLE_TOLERANCE_BITS +#endif +// clang-format on + +#include "gtest/gtest.h" +#include "runtime/backend.hpp" +#include "ngraph/runtime/tensor.hpp" +#include "ngraph/ngraph.hpp" +#include "util/all_close.hpp" +#include "util/all_close_f.hpp" +#include "util/known_element_types.hpp" +#include "util/ndarray.hpp" +#include "util/test_control.hpp" +#include "util/test_tools.hpp" + +NGRAPH_SUPPRESS_DEPRECATED_START + +using namespace std; +using namespace ngraph; + +static string s_manifest = "${MANIFEST}"; + +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_center_point_box_format_backend) +{ + std::vector boxes_data = {0.5, 0.5, 1.0, 1.0, 0.5, 0.6, 1.0, 1.0, + 0.5, 0.4, 1.0, 1.0, 0.5, 10.5, 1.0, 1.0, + 0.5, 10.6, 1.0, 1.0, 0.5, 100.5, 1.0, 1.0}; + + std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; + + const int64_t max_output_boxes_per_class_data = 3; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.0f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CENTER; + const auto boxes_shape = Shape{1, 6, 4}; + const auto scores_shape = Shape{1, 1, 6}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + auto backend = runtime::Backend::create("${BACKEND_NAME}"); + + auto selected_indeces = backend->create_tensor(); + auto selected_scores = backend->create_tensor(); + auto valid_outputs = backend->create_tensor(); + + auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); + auto backend_scores = backend->create_tensor(element::f32, scores_shape); + copy_data(backend_boxes, boxes_data); + copy_data(backend_scores, scores_data); + + auto handle = backend->compile(f); + + handle->call({selected_indeces, selected_scores, valid_outputs}, + {backend_boxes, backend_scores}); + + auto selected_indeces_value = read_vector(selected_indeces); + auto selected_scores_value = read_vector(selected_scores); + int64_t valid_outputs_value = read_vector(valid_outputs); + + std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 0, 5}; + std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 0.0, 0.3}; + std::vector expected_valid_outputs = {3}; + + EXPECT_EQ(expected_selected_indices, selected_indeces_value); + EXPECT_EQ(expected_selected_scores, selected_scores_value); + EXPECT_EQ(expected_valid_outputs, valid_outputs_value); +} From 13ee76ea915a07854a41f93b52463ce6724c9894 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 22 Oct 2020 16:40:24 +0300 Subject: [PATCH 133/173] Small fix. --- ngraph/test/backend/non_max_suppression.in.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngraph/test/backend/non_max_suppression.in.cpp b/ngraph/test/backend/non_max_suppression.in.cpp index 35aaf36ab1fda4..8aa173791f8731 100644 --- a/ngraph/test/backend/non_max_suppression.in.cpp +++ b/ngraph/test/backend/non_max_suppression.in.cpp @@ -94,7 +94,7 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_center_point_box_format_backend) auto selected_indeces_value = read_vector(selected_indeces); auto selected_scores_value = read_vector(selected_scores); - int64_t valid_outputs_value = read_vector(valid_outputs); + auto valid_outputs_value = read_vector(valid_outputs); std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 0, 5}; std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 0.0, 0.3}; From c4f4edf97b46a8b7b22090c32067c2a1aa71f1c7 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 22 Oct 2020 17:20:25 +0300 Subject: [PATCH 134/173] Written draft tests of NonMaxSuppression in ngraph/test/runtime. --- .../test/backend/non_max_suppression.in.cpp | 508 ++++++++++++++++++ 1 file changed, 508 insertions(+) diff --git a/ngraph/test/backend/non_max_suppression.in.cpp b/ngraph/test/backend/non_max_suppression.in.cpp index 8aa173791f8731..367e71612950f7 100644 --- a/ngraph/test/backend/non_max_suppression.in.cpp +++ b/ngraph/test/backend/non_max_suppression.in.cpp @@ -104,3 +104,511 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_center_point_box_format_backend) EXPECT_EQ(expected_selected_scores, selected_scores_value); EXPECT_EQ(expected_valid_outputs, valid_outputs_value); } + +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_flipped_coordinates_backend) +{ + std::vector boxes_data = {1.0, 1.0, 0.0, 0.0, 0.0, 0.1, 1.0, 1.1, + 0.0, 0.9, 1.0, -0.1, 0.0, 10.0, 1.0, 11.0, + 1.0, 10.1, 0.0, 11.1, 1.0, 101.0, 0.0, 100.0}; + + std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; + + const int64_t max_output_boxes_per_class_data = 3; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.0f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; + const auto boxes_shape = Shape{1, 6, 4}; + const auto scores_shape = Shape{1, 1, 6}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + auto backend = runtime::Backend::create("${BACKEND_NAME}"); + + auto selected_indeces = backend->create_tensor(); + auto selected_scores = backend->create_tensor(); + auto valid_outputs = backend->create_tensor(); + + auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); + auto backend_scores = backend->create_tensor(element::f32, scores_shape); + copy_data(backend_boxes, boxes_data); + copy_data(backend_scores, scores_data); + + auto handle = backend->compile(f); + + handle->call({selected_indeces, selected_scores, valid_outputs}, + {backend_boxes, backend_scores}); + + auto selected_indeces_value = read_vector(selected_indeces); + auto selected_scores_value = read_vector(selected_scores); + auto valid_outputs_value = read_vector(valid_outputs); + + std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 0, 5}; + std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 0.0, 0.3}; + std::vector expected_valid_outputs = {3}; + + EXPECT_EQ(expected_selected_indices, selected_indeces_value); + EXPECT_EQ(expected_selected_scores, selected_scores_value); + EXPECT_EQ(expected_valid_outputs, valid_outputs_value); +} + +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_identical_boxes_backend) +{ + std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, + 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, + 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, + 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0}; + + std::vector scores_data = {0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9}; + + const int64_t max_output_boxes_per_class_data = 3; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.0f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; + const auto boxes_shape = Shape{1, 10, 4}; + const auto scores_shape = Shape{1, 1, 10}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + auto backend = runtime::Backend::create("${BACKEND_NAME}"); + + auto selected_indeces = backend->create_tensor(); + auto selected_scores = backend->create_tensor(); + auto valid_outputs = backend->create_tensor(); + + auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); + auto backend_scores = backend->create_tensor(element::f32, scores_shape); + copy_data(backend_boxes, boxes_data); + copy_data(backend_scores, scores_data); + + auto handle = backend->compile(f); + + handle->call({selected_indeces, selected_scores, valid_outputs}, + {backend_boxes, backend_scores}); + + auto selected_indeces_value = read_vector(selected_indeces); + auto selected_scores_value = read_vector(selected_scores); + auto valid_outputs_value = read_vector(valid_outputs); + + std::vector expected_selected_indices = {0, 0, 0}; + std::vector expected_selected_scores = {0.0, 0.0, 0.9}; + std::vector expected_valid_outputs = {1}; + + EXPECT_EQ(expected_selected_indices, selected_indeces_value); + EXPECT_EQ(expected_selected_scores, selected_scores_value); + EXPECT_EQ(expected_valid_outputs, valid_outputs_value); +} + +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_limit_output_size_backend) +{ + std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, + 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, + 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; + + std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; + + const int64_t max_output_boxes_per_class_data = 2; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.0f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; + const auto boxes_shape = Shape{1, 6, 4}; + const auto scores_shape = Shape{1, 1, 6}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + auto backend = runtime::Backend::create("${BACKEND_NAME}"); + + auto selected_indeces = backend->create_tensor(); + auto selected_scores = backend->create_tensor(); + auto valid_outputs = backend->create_tensor(); + + auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); + auto backend_scores = backend->create_tensor(element::f32, scores_shape); + copy_data(backend_boxes, boxes_data); + copy_data(backend_scores, scores_data); + + auto handle = backend->compile(f); + + handle->call({selected_indeces, selected_scores, valid_outputs}, + {backend_boxes, backend_scores}); + + auto selected_indeces_value = read_vector(selected_indeces); + auto selected_scores_value = read_vector(selected_scores); + auto valid_outputs_value = read_vector(valid_outputs); + + std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0}; + std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9}; + std::vector expected_valid_outputs = {2}; + + EXPECT_EQ(expected_selected_indices, selected_indeces_value); + EXPECT_EQ(expected_selected_scores, selected_scores_value); + EXPECT_EQ(expected_valid_outputs, valid_outputs_value); +} + +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_single_box_backend) +{ + std::vector boxes_data = {0.0, 0.0, 1.0, 1.0}; + + std::vector scores_data = {0.9}; + + const int64_t max_output_boxes_per_class_data = 3; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.0f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; + const auto boxes_shape = Shape{1, 1, 4}; + const auto scores_shape = Shape{1, 1, 1}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + auto backend = runtime::Backend::create("${BACKEND_NAME}"); + + auto selected_indeces = backend->create_tensor(); + auto selected_scores = backend->create_tensor(); + auto valid_outputs = backend->create_tensor(); + + auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); + auto backend_scores = backend->create_tensor(element::f32, scores_shape); + copy_data(backend_boxes, boxes_data); + copy_data(backend_scores, scores_data); + + auto handle = backend->compile(f); + + handle->call({selected_indeces, selected_scores, valid_outputs}, + {backend_boxes, backend_scores}); + + auto selected_indeces_value = read_vector(selected_indeces); + auto selected_scores_value = read_vector(selected_scores); + auto valid_outputs_value = read_vector(valid_outputs); + + std::vector expected_selected_indices = {0, 0, 0}; + std::vector expected_selected_scores = {0.0, 0.0, 0.9}; + std::vector expected_valid_outputs = {1}; + + EXPECT_EQ(expected_selected_indices, selected_indeces_value); + EXPECT_EQ(expected_selected_scores, selected_scores_value); + EXPECT_EQ(expected_valid_outputs, valid_outputs_value); +} + +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_suppress_by_IOU_backend) +{ + std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, + 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, + 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; + + std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; + + const int64_t max_output_boxes_per_class_data = 3; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.0f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; + const auto boxes_shape = Shape{1, 6, 4}; + const auto scores_shape = Shape{1, 1, 6}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + auto backend = runtime::Backend::create("${BACKEND_NAME}"); + + auto selected_indeces = backend->create_tensor(); + auto selected_scores = backend->create_tensor(); + auto valid_outputs = backend->create_tensor(); + + auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); + auto backend_scores = backend->create_tensor(element::f32, scores_shape); + copy_data(backend_boxes, boxes_data); + copy_data(backend_scores, scores_data); + + auto handle = backend->compile(f); + + handle->call({selected_indeces, selected_scores, valid_outputs}, + {backend_boxes, backend_scores}); + + auto selected_indeces_value = read_vector(selected_indeces); + auto selected_scores_value = read_vector(selected_scores); + auto valid_outputs_value = read_vector(valid_outputs); + + std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 0, 5}; + std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 0.0, 0.3}; + std::vector expected_valid_outputs = {3}; + + EXPECT_EQ(expected_selected_indices, selected_indeces_value); + EXPECT_EQ(expected_selected_scores, selected_scores_value); + EXPECT_EQ(expected_valid_outputs, valid_outputs_value); +} + +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_suppress_by_IOU_and_scores_backend) +{ + std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, + 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, + 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; + + std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; + + const int64_t max_output_boxes_per_class_data = 3; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.4f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; + const auto boxes_shape = Shape{1, 6, 4}; + const auto scores_shape = Shape{1, 1, 6}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + auto backend = runtime::Backend::create("${BACKEND_NAME}"); + + auto selected_indeces = backend->create_tensor(); + auto selected_scores = backend->create_tensor(); + auto valid_outputs = backend->create_tensor(); + + auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); + auto backend_scores = backend->create_tensor(element::f32, scores_shape); + copy_data(backend_boxes, boxes_data); + copy_data(backend_scores, scores_data); + + auto handle = backend->compile(f); + + handle->call({selected_indeces, selected_scores, valid_outputs}, + {backend_boxes, backend_scores}); + + auto selected_indeces_value = read_vector(selected_indeces); + auto selected_scores_value = read_vector(selected_scores); + auto valid_outputs_value = read_vector(valid_outputs); + + std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0}; + std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9}; + std::vector expected_valid_outputs = {2}; + + EXPECT_EQ(expected_selected_indices, selected_indeces_value); + EXPECT_EQ(expected_selected_scores, selected_scores_value); + EXPECT_EQ(expected_valid_outputs, valid_outputs_value); +} + +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_two_batches_backend) +{ + std::vector boxes_data = { + 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, + 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, + 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; + + std::vector scores_data = { + 0.9, 0.75, 0.6, 0.95, 0.5, 0.3, 0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; + + const int64_t max_output_boxes_per_class_data = 2; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.0f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; + const auto boxes_shape = Shape{2, 6, 4}; + const auto scores_shape = Shape{2, 1, 6}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + auto backend = runtime::Backend::create("${BACKEND_NAME}"); + + auto selected_indeces = backend->create_tensor(); + auto selected_scores = backend->create_tensor(); + auto valid_outputs = backend->create_tensor(); + + auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); + auto backend_scores = backend->create_tensor(element::f32, scores_shape); + copy_data(backend_boxes, boxes_data); + copy_data(backend_scores, scores_data); + + auto handle = backend->compile(f); + + handle->call({selected_indeces, selected_scores, valid_outputs}, + {backend_boxes, backend_scores}); + + auto selected_indeces_value = read_vector(selected_indeces); + auto selected_scores_value = read_vector(selected_scores); + auto valid_outputs_value = read_vector(valid_outputs); + + std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 1, 0, 3, 1, 0, 0}; + std::vector expected_selected_scores = { + 0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 1.0, 0.0, 0.95, 1.0, 0.0, 0.9}; + std::vector expected_valid_outputs = {4}; + + EXPECT_EQ(expected_selected_indices, selected_indeces_value); + EXPECT_EQ(expected_selected_scores, selected_scores_value); + EXPECT_EQ(expected_valid_outputs, valid_outputs_value); +} + +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_two_classes_backend) +{ + std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, + 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, + 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; + + std::vector scores_data = { + 0.9, 0.75, 0.6, 0.95, 0.5, 0.3, 0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; + + const int64_t max_output_boxes_per_class_data = 2; + const float iou_threshold_data = 0.5f; + const float score_threshold_data = 0.0f; + const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; + const auto boxes_shape = Shape{1, 6, 4}; + const auto scores_shape = Shape{1, 2, 6}; + + const auto boxes = make_shared(element::f32, boxes_shape); + const auto scores = make_shared(element::f32, scores_shape); + auto max_output_boxes_per_class = + op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); + auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); + auto score_threshold = + op::Constant::create(element::f32, Shape{}, {score_threshold_data}); + auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); + auto nms = make_shared(boxes, + scores, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + box_encoding, + false); + + auto f = make_shared(nms, ParameterVector{boxes, scores}); + + auto backend = runtime::Backend::create("${BACKEND_NAME}"); + + auto selected_indeces = backend->create_tensor(); + auto selected_scores = backend->create_tensor(); + auto valid_outputs = backend->create_tensor(); + + auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); + auto backend_scores = backend->create_tensor(element::f32, scores_shape); + copy_data(backend_boxes, boxes_data); + copy_data(backend_scores, scores_data); + + auto handle = backend->compile(f); + + handle->call({selected_indeces, selected_scores, valid_outputs}, + {backend_boxes, backend_scores}); + + auto selected_indeces_value = read_vector(selected_indeces); + auto selected_scores_value = read_vector(selected_scores); + auto valid_outputs_value = read_vector(valid_outputs); + + std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 1, 3, 0, 1, 0}; + std::vector expected_selected_scores = { + 0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 1.0, 0.95, 0.0, 1.0, 0.9}; + std::vector expected_valid_outputs = {4}; + + EXPECT_EQ(expected_selected_indices, selected_indeces_value); + EXPECT_EQ(expected_selected_scores, selected_scores_value); + EXPECT_EQ(expected_valid_outputs, valid_outputs_value); +} From 33e15ce561d2413aa5c564f3be54f95e79348d74 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 23 Oct 2020 09:42:30 +0300 Subject: [PATCH 135/173] Some changes. --- ngraph/test/backend/non_max_suppression.in.cpp | 6 +++--- ngraph/test/runtime/ie/unit_test.manifest | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/ngraph/test/backend/non_max_suppression.in.cpp b/ngraph/test/backend/non_max_suppression.in.cpp index 367e71612950f7..aec40302ee3c49 100644 --- a/ngraph/test/backend/non_max_suppression.in.cpp +++ b/ngraph/test/backend/non_max_suppression.in.cpp @@ -585,9 +585,9 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_two_classes_backend) auto backend = runtime::Backend::create("${BACKEND_NAME}"); - auto selected_indeces = backend->create_tensor(); - auto selected_scores = backend->create_tensor(); - auto valid_outputs = backend->create_tensor(); + auto selected_indeces = backend->create_tensor(element::i64, Shape{3, 3}); + auto selected_scores = backend->create_tensor(element::f32, Shape{3, 3}); + auto valid_outputs = backend->create_tensor(element::i64, Shape{1}); auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); auto backend_scores = backend->create_tensor(element::f32, scores_shape); diff --git a/ngraph/test/runtime/ie/unit_test.manifest b/ngraph/test/runtime/ie/unit_test.manifest index bbad53c317835d..f29c41fbe4de17 100644 --- a/ngraph/test/runtime/ie/unit_test.manifest +++ b/ngraph/test/runtime/ie/unit_test.manifest @@ -1139,6 +1139,17 @@ IE_CPU.log_softmax_2d_axis_neg2 IE_CPU.log_softmax_3d_axis_0 IE_CPU.log_softmax_3d_axis_neg3 +# Unsupported NMS-5 +# IE_CPU.nonmaxsuppression_center_point_box_format_backend +IE_CPU.nonmaxsuppression_flipped_coordinates_backend +IE_CPU.nonmaxsuppression_identical_boxes_backend +IE_CPU.nonmaxsuppression_limit_output_size_backend +IE_CPU.nonmaxsuppression_single_box_backend +IE_CPU.nonmaxsuppression_suppress_by_IOU_backend +IE_CPU.nonmaxsuppression_suppress_by_IOU_and_scores_backend +IE_CPU.nonmaxsuppression_two_batches_backend +IE_CPU.nonmaxsuppression_two_classes_backend + #------------------------------------------------------------------------------- # # Inference Engine GPU plugin excludes From b98c59a77991aa818160289e02cd4b1acad1c714 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 23 Oct 2020 10:27:40 +0300 Subject: [PATCH 136/173] Small changes. --- .../test/backend/non_max_suppression.in.cpp | 52 +++++++++---------- ngraph/test/runtime/ie/unit_test.manifest | 16 +++--- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/ngraph/test/backend/non_max_suppression.in.cpp b/ngraph/test/backend/non_max_suppression.in.cpp index aec40302ee3c49..2c9514d9538db2 100644 --- a/ngraph/test/backend/non_max_suppression.in.cpp +++ b/ngraph/test/backend/non_max_suppression.in.cpp @@ -78,9 +78,9 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_center_point_box_format_backend) auto backend = runtime::Backend::create("${BACKEND_NAME}"); - auto selected_indeces = backend->create_tensor(); - auto selected_scores = backend->create_tensor(); - auto valid_outputs = backend->create_tensor(); + auto selected_indeces = backend->create_tensor(element::i64, Shape{3, 3}); + auto selected_scores = backend->create_tensor(element::f32, Shape{3, 3}); + auto valid_outputs = backend->create_tensor(element::i64, Shape{1}); auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); auto backend_scores = backend->create_tensor(element::f32, scores_shape); @@ -141,9 +141,9 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_flipped_coordinates_backend) auto backend = runtime::Backend::create("${BACKEND_NAME}"); - auto selected_indeces = backend->create_tensor(); - auto selected_scores = backend->create_tensor(); - auto valid_outputs = backend->create_tensor(); + auto selected_indeces = backend->create_tensor(element::i64, Shape{3, 3}); + auto selected_scores = backend->create_tensor(element::f32, Shape{3, 3}); + auto valid_outputs = backend->create_tensor(element::i64, Shape{1}); auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); auto backend_scores = backend->create_tensor(element::f32, scores_shape); @@ -205,9 +205,9 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_identical_boxes_backend) auto backend = runtime::Backend::create("${BACKEND_NAME}"); - auto selected_indeces = backend->create_tensor(); - auto selected_scores = backend->create_tensor(); - auto valid_outputs = backend->create_tensor(); + auto selected_indeces = backend->create_tensor(element::i64, Shape{1, 3}); + auto selected_scores = backend->create_tensor(element::f32, Shape{1, 3}); + auto valid_outputs = backend->create_tensor(element::i64, Shape{1}); auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); auto backend_scores = backend->create_tensor(element::f32, scores_shape); @@ -268,9 +268,9 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_limit_output_size_backend) auto backend = runtime::Backend::create("${BACKEND_NAME}"); - auto selected_indeces = backend->create_tensor(); - auto selected_scores = backend->create_tensor(); - auto valid_outputs = backend->create_tensor(); + auto selected_indeces = backend->create_tensor(element::i64, Shape{2, 3}); + auto selected_scores = backend->create_tensor(element::f32, Shape{2, 3}); + auto valid_outputs = backend->create_tensor(element::i64, Shape{1}); auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); auto backend_scores = backend->create_tensor(element::f32, scores_shape); @@ -329,9 +329,9 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_single_box_backend) auto backend = runtime::Backend::create("${BACKEND_NAME}"); - auto selected_indeces = backend->create_tensor(); - auto selected_scores = backend->create_tensor(); - auto valid_outputs = backend->create_tensor(); + auto selected_indeces = backend->create_tensor(element::i64, Shape{1, 3}); + auto selected_scores = backend->create_tensor(element::f32, Shape{1, 3}); + auto valid_outputs = backend->create_tensor(element::i64, Shape{1}); auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); auto backend_scores = backend->create_tensor(element::f32, scores_shape); @@ -392,9 +392,9 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_suppress_by_IOU_backend) auto backend = runtime::Backend::create("${BACKEND_NAME}"); - auto selected_indeces = backend->create_tensor(); - auto selected_scores = backend->create_tensor(); - auto valid_outputs = backend->create_tensor(); + auto selected_indeces = backend->create_tensor(element::i64, Shape{3, 3}); + auto selected_scores = backend->create_tensor(element::f32, Shape{3, 3}); + auto valid_outputs = backend->create_tensor(element::i64, Shape{1}); auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); auto backend_scores = backend->create_tensor(element::f32, scores_shape); @@ -455,9 +455,9 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_suppress_by_IOU_and_scores_backen auto backend = runtime::Backend::create("${BACKEND_NAME}"); - auto selected_indeces = backend->create_tensor(); - auto selected_scores = backend->create_tensor(); - auto valid_outputs = backend->create_tensor(); + auto selected_indeces = backend->create_tensor(element::i64, Shape{2, 3}); + auto selected_scores = backend->create_tensor(element::f32, Shape{2, 3}); + auto valid_outputs = backend->create_tensor(element::i64, Shape{1}); auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); auto backend_scores = backend->create_tensor(element::f32, scores_shape); @@ -520,9 +520,9 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_two_batches_backend) auto backend = runtime::Backend::create("${BACKEND_NAME}"); - auto selected_indeces = backend->create_tensor(); - auto selected_scores = backend->create_tensor(); - auto valid_outputs = backend->create_tensor(); + auto selected_indeces = backend->create_tensor(element::i64, Shape{4, 3}); + auto selected_scores = backend->create_tensor(element::f32, Shape{4, 3}); + auto valid_outputs = backend->create_tensor(element::i64, Shape{1}); auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); auto backend_scores = backend->create_tensor(element::f32, scores_shape); @@ -585,8 +585,8 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_two_classes_backend) auto backend = runtime::Backend::create("${BACKEND_NAME}"); - auto selected_indeces = backend->create_tensor(element::i64, Shape{3, 3}); - auto selected_scores = backend->create_tensor(element::f32, Shape{3, 3}); + auto selected_indeces = backend->create_tensor(element::i64, Shape{4, 3}); + auto selected_scores = backend->create_tensor(element::f32, Shape{4, 3}); auto valid_outputs = backend->create_tensor(element::i64, Shape{1}); auto backend_boxes = backend->create_tensor(element::f32, boxes_shape); diff --git a/ngraph/test/runtime/ie/unit_test.manifest b/ngraph/test/runtime/ie/unit_test.manifest index f29c41fbe4de17..83aa4a0781a38b 100644 --- a/ngraph/test/runtime/ie/unit_test.manifest +++ b/ngraph/test/runtime/ie/unit_test.manifest @@ -1141,14 +1141,14 @@ IE_CPU.log_softmax_3d_axis_neg3 # Unsupported NMS-5 # IE_CPU.nonmaxsuppression_center_point_box_format_backend -IE_CPU.nonmaxsuppression_flipped_coordinates_backend -IE_CPU.nonmaxsuppression_identical_boxes_backend -IE_CPU.nonmaxsuppression_limit_output_size_backend -IE_CPU.nonmaxsuppression_single_box_backend -IE_CPU.nonmaxsuppression_suppress_by_IOU_backend -IE_CPU.nonmaxsuppression_suppress_by_IOU_and_scores_backend -IE_CPU.nonmaxsuppression_two_batches_backend -IE_CPU.nonmaxsuppression_two_classes_backend +# IE_CPU.nonmaxsuppression_flipped_coordinates_backend +# IE_CPU.nonmaxsuppression_identical_boxes_backend +# IE_CPU.nonmaxsuppression_limit_output_size_backend +# IE_CPU.nonmaxsuppression_single_box_backend +# IE_CPU.nonmaxsuppression_suppress_by_IOU_backend +# IE_CPU.nonmaxsuppression_suppress_by_IOU_and_scores_backend +# IE_CPU.nonmaxsuppression_two_batches_backend +# IE_CPU.nonmaxsuppression_two_classes_backend #------------------------------------------------------------------------------- # From 7cc7aac9c9acac600dee0844fbc90282d3d325e9 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 23 Oct 2020 10:51:57 +0300 Subject: [PATCH 137/173] Disabled IE_CPU tests for NMS-5. --- ngraph/test/runtime/ie/unit_test.manifest | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ngraph/test/runtime/ie/unit_test.manifest b/ngraph/test/runtime/ie/unit_test.manifest index 83aa4a0781a38b..18de0871915153 100644 --- a/ngraph/test/runtime/ie/unit_test.manifest +++ b/ngraph/test/runtime/ie/unit_test.manifest @@ -1140,15 +1140,15 @@ IE_CPU.log_softmax_3d_axis_0 IE_CPU.log_softmax_3d_axis_neg3 # Unsupported NMS-5 -# IE_CPU.nonmaxsuppression_center_point_box_format_backend -# IE_CPU.nonmaxsuppression_flipped_coordinates_backend -# IE_CPU.nonmaxsuppression_identical_boxes_backend -# IE_CPU.nonmaxsuppression_limit_output_size_backend -# IE_CPU.nonmaxsuppression_single_box_backend -# IE_CPU.nonmaxsuppression_suppress_by_IOU_backend -# IE_CPU.nonmaxsuppression_suppress_by_IOU_and_scores_backend -# IE_CPU.nonmaxsuppression_two_batches_backend -# IE_CPU.nonmaxsuppression_two_classes_backend +IE_CPU.nonmaxsuppression_center_point_box_format_backend +IE_CPU.nonmaxsuppression_flipped_coordinates_backend +IE_CPU.nonmaxsuppression_identical_boxes_backend +IE_CPU.nonmaxsuppression_limit_output_size_backend +IE_CPU.nonmaxsuppression_single_box_backend +IE_CPU.nonmaxsuppression_suppress_by_IOU_backend +IE_CPU.nonmaxsuppression_suppress_by_IOU_and_scores_backend +IE_CPU.nonmaxsuppression_two_batches_backend +IE_CPU.nonmaxsuppression_two_classes_backend #------------------------------------------------------------------------------- # From 45d1ff730e37824c72d7b802c1d88d804f9aeaf3 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 23 Oct 2020 10:56:55 +0300 Subject: [PATCH 138/173] Deleted op_eval tests for NMS-5. --- ngraph/test/CMakeLists.txt | 1 - ngraph/test/op_eval/non_max_suppression_5.cpp | 549 ------------------ 2 files changed, 550 deletions(-) delete mode 100644 ngraph/test/op_eval/non_max_suppression_5.cpp diff --git a/ngraph/test/CMakeLists.txt b/ngraph/test/CMakeLists.txt index 56a7005034461d..f58348147fb155 100644 --- a/ngraph/test/CMakeLists.txt +++ b/ngraph/test/CMakeLists.txt @@ -75,7 +75,6 @@ set(SRC op_eval/interpolate.cpp op_eval/matmul.cpp op_eval/mish.cpp - op_eval/non_max_suppression_5.cpp op_eval/non_zero.cpp op_eval/reduce_l1.cpp op_eval/reduce_l2.cpp diff --git a/ngraph/test/op_eval/non_max_suppression_5.cpp b/ngraph/test/op_eval/non_max_suppression_5.cpp deleted file mode 100644 index c47cc69b527321..00000000000000 --- a/ngraph/test/op_eval/non_max_suppression_5.cpp +++ /dev/null @@ -1,549 +0,0 @@ -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -//***************************************************************************** - -#include -#include - -#include "gtest/gtest.h" - -#include "ngraph/op/constant.hpp" -#include "ngraph/op/non_max_suppression.hpp" -#include "ngraph/runtime/host_tensor.hpp" -#include "ngraph/validation_util.hpp" -#include "runtime/backend.hpp" -#include "util/test_tools.hpp" -#include "util/type_prop.hpp" - -using namespace std; -using namespace ngraph; - -TEST(op_eval, nonmaxsuppression_center_point_box_format) -{ - std::vector boxes_data = {0.5, 0.5, 1.0, 1.0, 0.5, 0.6, 1.0, 1.0, - 0.5, 0.4, 1.0, 1.0, 0.5, 10.5, 1.0, 1.0, - 0.5, 10.6, 1.0, 1.0, 0.5, 100.5, 1.0, 1.0}; - - std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; - - const int64_t max_output_boxes_per_class_data = 3; - const float iou_threshold_data = 0.5f; - const float score_threshold_data = 0.0f; - const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CENTER; - const auto boxes_shape = Shape{1, 6, 4}; - const auto scores_shape = Shape{1, 1, 6}; - - const auto boxes = make_shared(element::f32, boxes_shape); - const auto scores = make_shared(element::f32, scores_shape); - auto max_output_boxes_per_class = - op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); - auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = - op::Constant::create(element::f32, Shape{}, {score_threshold_data}); - auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); - auto nms = make_shared(boxes, - scores, - max_output_boxes_per_class, - iou_threshold, - score_threshold, - soft_nms_sigma, - box_encoding, - false); - - auto f = make_shared(nms, ParameterVector{boxes, scores}); - - HostTensorVector results(3); - for (auto& result : results) - { - result = make_shared(); - } - ASSERT_TRUE(f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); - - std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 0, 5}; - std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 0.0, 0.3}; - std::vector expected_valid_outputs = {3}; - - EXPECT_EQ(results[0]->get_element_type(), element::i64); - EXPECT_EQ(results[1]->get_element_type(), element::f32); - EXPECT_EQ(results[2]->get_element_type(), element::i64); - EXPECT_EQ(results[0]->get_shape(), (Shape{3, 3})); - EXPECT_EQ(results[1]->get_shape(), (Shape{3, 3})); - EXPECT_EQ(results[2]->get_shape(), (Shape{1})); - EXPECT_EQ(read_vector(results[0]), expected_selected_indices); - EXPECT_EQ(read_vector(results[1]), expected_selected_scores); - EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); -} - -TEST(op_eval, nonmaxsuppression_flipped_coordinates) -{ - std::vector boxes_data = {1.0, 1.0, 0.0, 0.0, 0.0, 0.1, 1.0, 1.1, - 0.0, 0.9, 1.0, -0.1, 0.0, 10.0, 1.0, 11.0, - 1.0, 10.1, 0.0, 11.1, 1.0, 101.0, 0.0, 100.0}; - - std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; - - const int64_t max_output_boxes_per_class_data = 3; - const float iou_threshold_data = 0.5f; - const float score_threshold_data = 0.0f; - const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; - const auto boxes_shape = Shape{1, 6, 4}; - const auto scores_shape = Shape{1, 1, 6}; - - const auto boxes = make_shared(element::f32, boxes_shape); - const auto scores = make_shared(element::f32, scores_shape); - auto max_output_boxes_per_class = - op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); - auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = - op::Constant::create(element::f32, Shape{}, {score_threshold_data}); - auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); - auto nms = make_shared(boxes, - scores, - max_output_boxes_per_class, - iou_threshold, - score_threshold, - soft_nms_sigma, - box_encoding, - false); - - auto f = make_shared(nms, ParameterVector{boxes, scores}); - - HostTensorVector results(3); - for (auto& result : results) - { - result = make_shared(); - } - ASSERT_TRUE(f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); - - std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 0, 5}; - std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 0.0, 0.3}; - std::vector expected_valid_outputs = {3}; - - EXPECT_EQ(results[0]->get_element_type(), element::i64); - EXPECT_EQ(results[1]->get_element_type(), element::f32); - EXPECT_EQ(results[2]->get_element_type(), element::i64); - EXPECT_EQ(results[0]->get_shape(), (Shape{3, 3})); - EXPECT_EQ(results[1]->get_shape(), (Shape{3, 3})); - EXPECT_EQ(results[2]->get_shape(), (Shape{1})); - EXPECT_EQ(read_vector(results[0]), expected_selected_indices); - EXPECT_EQ(read_vector(results[1]), expected_selected_scores); - EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); -} - -TEST(op_eval, nonmaxsuppression_identical_boxes) -{ - std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, - 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, - 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, - 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0}; - - std::vector scores_data = {0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9}; - - const int64_t max_output_boxes_per_class_data = 3; - const float iou_threshold_data = 0.5f; - const float score_threshold_data = 0.0f; - const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; - const auto boxes_shape = Shape{1, 10, 4}; - const auto scores_shape = Shape{1, 1, 10}; - - const auto boxes = make_shared(element::f32, boxes_shape); - const auto scores = make_shared(element::f32, scores_shape); - auto max_output_boxes_per_class = - op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); - auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = - op::Constant::create(element::f32, Shape{}, {score_threshold_data}); - auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); - auto nms = make_shared(boxes, - scores, - max_output_boxes_per_class, - iou_threshold, - score_threshold, - soft_nms_sigma, - box_encoding, - false); - - auto f = make_shared(nms, ParameterVector{boxes, scores}); - - HostTensorVector results(3); - for (auto& result : results) - { - result = make_shared(); - } - ASSERT_TRUE(f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); - - std::vector expected_selected_indices = {0, 0, 0}; - std::vector expected_selected_scores = {0.0, 0.0, 0.9}; - std::vector expected_valid_outputs = {1}; - - EXPECT_EQ(results[0]->get_element_type(), element::i64); - EXPECT_EQ(results[1]->get_element_type(), element::f32); - EXPECT_EQ(results[2]->get_element_type(), element::i64); - EXPECT_EQ(results[0]->get_shape(), (Shape{1, 3})); - EXPECT_EQ(results[1]->get_shape(), (Shape{1, 3})); - EXPECT_EQ(results[2]->get_shape(), (Shape{1})); - EXPECT_EQ(read_vector(results[0]), expected_selected_indices); - EXPECT_EQ(read_vector(results[1]), expected_selected_scores); - EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); -} - -TEST(op_eval, nonmaxsuppression_limit_output_size) -{ - std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, - 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, - 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; - - std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; - - const int64_t max_output_boxes_per_class_data = 2; - const float iou_threshold_data = 0.5f; - const float score_threshold_data = 0.0f; - const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; - const auto boxes_shape = Shape{1, 6, 4}; - const auto scores_shape = Shape{1, 1, 6}; - - const auto boxes = make_shared(element::f32, boxes_shape); - const auto scores = make_shared(element::f32, scores_shape); - auto max_output_boxes_per_class = - op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); - auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = - op::Constant::create(element::f32, Shape{}, {score_threshold_data}); - auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); - auto nms = make_shared(boxes, - scores, - max_output_boxes_per_class, - iou_threshold, - score_threshold, - soft_nms_sigma, - box_encoding, - false); - - auto f = make_shared(nms, ParameterVector{boxes, scores}); - - HostTensorVector results(3); - for (auto& result : results) - { - result = make_shared(); - } - ASSERT_TRUE(f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); - - std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0}; - std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9}; - std::vector expected_valid_outputs = {2}; - - EXPECT_EQ(results[0]->get_element_type(), element::i64); - EXPECT_EQ(results[1]->get_element_type(), element::f32); - EXPECT_EQ(results[2]->get_element_type(), element::i64); - EXPECT_EQ(results[0]->get_shape(), (Shape{2, 3})); - EXPECT_EQ(results[1]->get_shape(), (Shape{2, 3})); - EXPECT_EQ(results[2]->get_shape(), (Shape{1})); - EXPECT_EQ(read_vector(results[0]), expected_selected_indices); - EXPECT_EQ(read_vector(results[1]), expected_selected_scores); - EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); -} - -TEST(op_eval, nonmaxsuppression_single_box) -{ - std::vector boxes_data = {0.0, 0.0, 1.0, 1.0}; - - std::vector scores_data = {0.9}; - - const int64_t max_output_boxes_per_class_data = 3; - const float iou_threshold_data = 0.5f; - const float score_threshold_data = 0.0f; - const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; - const auto boxes_shape = Shape{1, 1, 4}; - const auto scores_shape = Shape{1, 1, 1}; - - const auto boxes = make_shared(element::f32, boxes_shape); - const auto scores = make_shared(element::f32, scores_shape); - auto max_output_boxes_per_class = - op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); - auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = - op::Constant::create(element::f32, Shape{}, {score_threshold_data}); - auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); - auto nms = make_shared(boxes, - scores, - max_output_boxes_per_class, - iou_threshold, - score_threshold, - soft_nms_sigma, - box_encoding, - false); - - auto f = make_shared(nms, ParameterVector{boxes, scores}); - - HostTensorVector results(3); - for (auto& result : results) - { - result = make_shared(); - } - ASSERT_TRUE(f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); - - std::vector expected_selected_indices = {0, 0, 0}; - std::vector expected_selected_scores = {0.0, 0.0, 0.9}; - std::vector expected_valid_outputs = {1}; - - EXPECT_EQ(results[0]->get_element_type(), element::i64); - EXPECT_EQ(results[1]->get_element_type(), element::f32); - EXPECT_EQ(results[2]->get_element_type(), element::i64); - EXPECT_EQ(results[0]->get_shape(), (Shape{1, 3})); - EXPECT_EQ(results[1]->get_shape(), (Shape{1, 3})); - EXPECT_EQ(results[2]->get_shape(), (Shape{1})); - EXPECT_EQ(read_vector(results[0]), expected_selected_indices); - EXPECT_EQ(read_vector(results[1]), expected_selected_scores); - EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); -} - -TEST(op_eval, nonmaxsuppression_suppress_by_IOU) -{ - std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, - 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, - 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; - - std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; - - const int64_t max_output_boxes_per_class_data = 3; - const float iou_threshold_data = 0.5f; - const float score_threshold_data = 0.0f; - const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; - const auto boxes_shape = Shape{1, 6, 4}; - const auto scores_shape = Shape{1, 1, 6}; - - const auto boxes = make_shared(element::f32, boxes_shape); - const auto scores = make_shared(element::f32, scores_shape); - auto max_output_boxes_per_class = - op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); - auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = - op::Constant::create(element::f32, Shape{}, {score_threshold_data}); - auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); - auto nms = make_shared(boxes, - scores, - max_output_boxes_per_class, - iou_threshold, - score_threshold, - soft_nms_sigma, - box_encoding, - false); - - auto f = make_shared(nms, ParameterVector{boxes, scores}); - - HostTensorVector results(3); - for (auto& result : results) - { - result = make_shared(); - } - ASSERT_TRUE(f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); - - std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 0, 5}; - std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 0.0, 0.3}; - std::vector expected_valid_outputs = {3}; - - EXPECT_EQ(results[0]->get_element_type(), element::i64); - EXPECT_EQ(results[1]->get_element_type(), element::f32); - EXPECT_EQ(results[2]->get_element_type(), element::i64); - EXPECT_EQ(results[0]->get_shape(), (Shape{3, 3})); - EXPECT_EQ(results[1]->get_shape(), (Shape{3, 3})); - EXPECT_EQ(results[2]->get_shape(), (Shape{1})); - EXPECT_EQ(read_vector(results[0]), expected_selected_indices); - EXPECT_EQ(read_vector(results[1]), expected_selected_scores); - EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); -} - -TEST(op_eval, nonmaxsuppression_suppress_by_IOU_and_scores) -{ - std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, - 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, - 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; - - std::vector scores_data = {0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; - - const int64_t max_output_boxes_per_class_data = 3; - const float iou_threshold_data = 0.5f; - const float score_threshold_data = 0.4f; - const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; - const auto boxes_shape = Shape{1, 6, 4}; - const auto scores_shape = Shape{1, 1, 6}; - - const auto boxes = make_shared(element::f32, boxes_shape); - const auto scores = make_shared(element::f32, scores_shape); - auto max_output_boxes_per_class = - op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); - auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = - op::Constant::create(element::f32, Shape{}, {score_threshold_data}); - auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); - auto nms = make_shared(boxes, - scores, - max_output_boxes_per_class, - iou_threshold, - score_threshold, - soft_nms_sigma, - box_encoding, - false); - - auto f = make_shared(nms, ParameterVector{boxes, scores}); - - HostTensorVector results(3); - for (auto& result : results) - { - result = make_shared(); - } - ASSERT_TRUE(f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); - - std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0}; - std::vector expected_selected_scores = {0.0, 0.0, 0.95, 0.0, 0.0, 0.9}; - std::vector expected_valid_outputs = {2}; - - EXPECT_EQ(results[0]->get_element_type(), element::i64); - EXPECT_EQ(results[1]->get_element_type(), element::f32); - EXPECT_EQ(results[2]->get_element_type(), element::i64); - EXPECT_EQ(results[0]->get_shape(), (Shape{2, 3})); - EXPECT_EQ(results[1]->get_shape(), (Shape{2, 3})); - EXPECT_EQ(results[2]->get_shape(), (Shape{1})); - EXPECT_EQ(read_vector(results[0]), expected_selected_indices); - EXPECT_EQ(read_vector(results[1]), expected_selected_scores); - EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); -} - -TEST(op_eval, nonmaxsuppression_two_batches) -{ - std::vector boxes_data = { - 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, - 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, - 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; - - std::vector scores_data = { - 0.9, 0.75, 0.6, 0.95, 0.5, 0.3, 0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; - - const int64_t max_output_boxes_per_class_data = 2; - const float iou_threshold_data = 0.5f; - const float score_threshold_data = 0.0f; - const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; - const auto boxes_shape = Shape{2, 6, 4}; - const auto scores_shape = Shape{2, 1, 6}; - - const auto boxes = make_shared(element::f32, boxes_shape); - const auto scores = make_shared(element::f32, scores_shape); - auto max_output_boxes_per_class = - op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); - auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = - op::Constant::create(element::f32, Shape{}, {score_threshold_data}); - auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); - auto nms = make_shared(boxes, - scores, - max_output_boxes_per_class, - iou_threshold, - score_threshold, - soft_nms_sigma, - box_encoding, - false); - - auto f = make_shared(nms, ParameterVector{boxes, scores}); - - HostTensorVector results(3); - for (auto& result : results) - { - result = make_shared(); - } - ASSERT_TRUE(f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); - - std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 1, 0, 3, 1, 0, 0}; - std::vector expected_selected_scores = { - 0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 1.0, 0.0, 0.95, 1.0, 0.0, 0.9}; - std::vector expected_valid_outputs = {4}; - - EXPECT_EQ(results[0]->get_element_type(), element::i64); - EXPECT_EQ(results[1]->get_element_type(), element::f32); - EXPECT_EQ(results[2]->get_element_type(), element::i64); - EXPECT_EQ(results[0]->get_shape(), (Shape{4, 3})); - EXPECT_EQ(results[1]->get_shape(), (Shape{4, 3})); - EXPECT_EQ(results[2]->get_shape(), (Shape{1})); - EXPECT_EQ(read_vector(results[0]), expected_selected_indices); - EXPECT_EQ(read_vector(results[1]), expected_selected_scores); - EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); -} - -TEST(op_eval, nonmaxsuppression_two_classes) -{ - std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, - 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, - 0.0, 10.1, 1.0, 11.1, 0.0, 100.0, 1.0, 101.0}; - - std::vector scores_data = { - 0.9, 0.75, 0.6, 0.95, 0.5, 0.3, 0.9, 0.75, 0.6, 0.95, 0.5, 0.3}; - - const int64_t max_output_boxes_per_class_data = 2; - const float iou_threshold_data = 0.5f; - const float score_threshold_data = 0.0f; - const auto box_encoding = op::v5::NonMaxSuppression::BoxEncodingType::CORNER; - const auto boxes_shape = Shape{1, 6, 4}; - const auto scores_shape = Shape{1, 2, 6}; - - const auto boxes = make_shared(element::f32, boxes_shape); - const auto scores = make_shared(element::f32, scores_shape); - auto max_output_boxes_per_class = - op::Constant::create(element::i64, Shape{}, {max_output_boxes_per_class_data}); - auto iou_threshold = op::Constant::create(element::f32, Shape{}, {iou_threshold_data}); - auto score_threshold = - op::Constant::create(element::f32, Shape{}, {score_threshold_data}); - auto soft_nms_sigma = op::Constant::create(element::f32, Shape{}, {0.0f}); - auto nms = make_shared(boxes, - scores, - max_output_boxes_per_class, - iou_threshold, - score_threshold, - soft_nms_sigma, - box_encoding, - false); - - auto f = make_shared(nms, ParameterVector{boxes, scores}); - - HostTensorVector results(3); - for (auto& result : results) - { - result = make_shared(); - } - ASSERT_TRUE(f->evaluate(results, - {make_host_tensor(boxes_shape, boxes_data), - make_host_tensor(scores_shape, scores_data)})); - - std::vector expected_selected_indices = {0, 0, 3, 0, 0, 0, 0, 1, 3, 0, 1, 0}; - std::vector expected_selected_scores = { - 0.0, 0.0, 0.95, 0.0, 0.0, 0.9, 0.0, 1.0, 0.95, 0.0, 1.0, 0.9}; - std::vector expected_valid_outputs = {4}; - - EXPECT_EQ(results[0]->get_element_type(), element::i64); - EXPECT_EQ(results[1]->get_element_type(), element::f32); - EXPECT_EQ(results[2]->get_element_type(), element::i64); - EXPECT_EQ(results[0]->get_shape(), (Shape{4, 3})); - EXPECT_EQ(results[1]->get_shape(), (Shape{4, 3})); - EXPECT_EQ(results[2]->get_shape(), (Shape{1})); - EXPECT_EQ(read_vector(results[0]), expected_selected_indices); - EXPECT_EQ(read_vector(results[1]), expected_selected_scores); - EXPECT_EQ(read_vector(results[2]), expected_valid_outputs); -} From 304b54c3b51396e30aecf0a9b2ae2e794873ad50 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 23 Oct 2020 11:09:23 +0300 Subject: [PATCH 139/173] Deleted evaluate() method of NMS-5. --- .../include/ngraph/op/non_max_suppression.hpp | 2 - ngraph/core/src/op/non_max_suppression.cpp | 249 ------------------ 2 files changed, 251 deletions(-) diff --git a/ngraph/core/include/ngraph/op/non_max_suppression.hpp b/ngraph/core/include/ngraph/op/non_max_suppression.hpp index 0c710efc889b79..b6a93610f62a3d 100644 --- a/ngraph/core/include/ngraph/op/non_max_suppression.hpp +++ b/ngraph/core/include/ngraph/op/non_max_suppression.hpp @@ -355,8 +355,6 @@ namespace ngraph std::shared_ptr clone_with_new_inputs(const OutputVector& new_args) const override; - bool evaluate(const HostTensorVector& outputs, - const HostTensorVector& inputs) const override; BoxEncodingType get_box_encoding() const { return m_box_encoding; } void set_box_encoding(const BoxEncodingType box_encoding) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 2d581b2f2c5f46..8adc25f0e16770 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -693,194 +693,6 @@ namespace constexpr size_t score_threshold_port = 4; constexpr size_t soft_nms_sigma_port = 5; - PartialShape infer_selected_indices_shape(const HostTensorVector& inputs, - int64_t max_output_boxes_per_class) - { - const auto boxes_ps = inputs[boxes_port]->get_partial_shape(); - const auto scores_ps = inputs[scores_port]->get_partial_shape(); - - // NonMaxSuppression produces triplets - // that have the following format: [batch_index, class_index, box_index] - PartialShape result = {Dimension::dynamic(), 3}; - - if (boxes_ps.rank().is_static() && scores_ps.rank().is_static()) - { - const auto num_boxes_boxes = boxes_ps[1]; - if (num_boxes_boxes.is_static() && scores_ps[0].is_static() && scores_ps[1].is_static()) - { - const auto num_boxes = num_boxes_boxes.get_length(); - const auto num_classes = scores_ps[1].get_length(); - - result[0] = std::min(num_boxes, max_output_boxes_per_class) * num_classes * - scores_ps[0].get_length(); - } - } - - return result; - } - - void normalize_corner(float* boxes, const Shape& boxes_shape) - { - size_t total_num_of_boxes = shape_size(boxes_shape) / 4; - for (size_t i = 0; i < total_num_of_boxes; ++i) - { - float* current_box = boxes + 4 * i; - - float y1 = current_box[0]; - float x1 = current_box[1]; - float y2 = current_box[2]; - float x2 = current_box[3]; - - float ymin = std::min(y1, y2); - float ymax = std::max(y1, y2); - float xmin = std::min(x1, x2); - float xmax = std::max(x1, x2); - - current_box[0] = ymin; - current_box[1] = xmin; - current_box[2] = ymax; - current_box[3] = xmax; - } - } - - void normalize_center(float* boxes, const Shape& boxes_shape) - { - size_t total_num_of_boxes = shape_size(boxes_shape) / 4; - for (size_t i = 0; i < total_num_of_boxes; ++i) - { - float* current_box = boxes + 4 * i; - - float x_center = current_box[0]; - float y_center = current_box[1]; - float width = current_box[2]; - float height = current_box[3]; - - float y1 = y_center - height / 2.0; - float x1 = x_center - width / 2.0; - float y2 = y_center + height / 2.0; - float x2 = x_center + width / 2.0; - - current_box[0] = y1; - current_box[1] = x1; - current_box[2] = y2; - current_box[3] = x2; - } - } - - void normalize_box_encoding(float* boxes, - const Shape& boxes_shape, - const V5BoxEncoding box_encoding) - { - if (box_encoding == V5BoxEncoding::CORNER) - { - normalize_corner(boxes, boxes_shape); - } - else - { - normalize_center(boxes, boxes_shape); - } - } - - std::vector get_floats(const HostTensorPtr& input, const Shape& shape) - { - size_t input_size = shape_size(shape); - std::vector result(input_size); - - switch (input->get_element_type()) - { - case element::Type_t::bf16: - { - bfloat16* p = input->get_data_ptr(); - for (size_t i = 0; i < input_size; ++i) - { - result[i] = float(p[i]); - } - } - break; - case element::Type_t::f16: - { - float16* p = input->get_data_ptr(); - for (size_t i = 0; i < input_size; ++i) - { - result[i] = float(p[i]); - } - } - break; - case element::Type_t::f32: - { - float* p = input->get_data_ptr(); - memcpy(result.data(), p, input_size * sizeof(float)); - } - break; - default: throw std::runtime_error("Unsupported data type in op NonMaxSuppression-5"); break; - } - - return result; - } - - std::vector prepare_boxes_data(const HostTensorPtr& boxes, - const Shape& boxes_shape, - const V5BoxEncoding box_encoding) - { - auto result = get_floats(boxes, boxes_shape); - normalize_box_encoding(result.data(), boxes_shape, box_encoding); - return result; - } - - std::vector prepare_scores_data(const HostTensorPtr& scores, const Shape& scores_shape) - { - auto result = get_floats(scores, scores_shape); - return result; - } - - void evaluate_postprocessing(const HostTensorVector& outputs, - const ngraph::element::Type output_type, - const std::vector& selected_indices, - const std::vector& selected_scores, - int64_t valid_outputs) - { - size_t num_of_outputs = outputs.size(); - size_t selected_size = valid_outputs * 3; - - if (output_type == ngraph::element::i64) - { - int64_t* indices_ptr = outputs[0]->get_data_ptr(); - memcpy(indices_ptr, selected_indices.data(), selected_size * sizeof(int64_t)); - } - else - { - int32_t* indices_ptr = outputs[0]->get_data_ptr(); - for (size_t i = 0; i < selected_size; ++i) - { - indices_ptr[i] = static_cast(selected_indices[i]); - } - } - - if (num_of_outputs < 2) - { - return; - } - - float* scores_ptr = outputs[1]->get_data_ptr(); - memcpy(scores_ptr, selected_scores.data(), selected_size * sizeof(float)); - - if (num_of_outputs < 3) - { - return; - } - - if (output_type == ngraph::element::i64) - { - int64_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); - *valid_outputs_ptr = valid_outputs; - } - else - { - int32_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); - *valid_outputs_ptr = static_cast(valid_outputs); - } - } - inline bool is_float_type_admissible(const element::Type& t) { return t == element::f32 || t == element::f16 || t == element::bf16; @@ -1144,64 +956,3 @@ namespace ngraph return s << as_string(type); } } // namespace ngraph - -bool op::v5::NonMaxSuppression::evaluate(const HostTensorVector& outputs, - const HostTensorVector& inputs) const -{ - int64_t max_output_boxes_per_class = max_boxes_output_from_input(); - float iou_threshold = iou_threshold_from_input(); - float score_threshold = score_threshold_from_input(); - float soft_nms_sigma = soft_nms_sigma_from_input(); - - auto selected_indices_shape = infer_selected_indices_shape(inputs, max_output_boxes_per_class); - Shape out_shape = selected_indices_shape.to_shape(); - - Shape boxes_shape = inputs[boxes_port]->get_shape(); - Shape scores_shape = inputs[scores_port]->get_shape(); - - auto boxes_data = prepare_boxes_data(inputs[boxes_port], boxes_shape, m_box_encoding); - auto scores_data = prepare_scores_data(inputs[scores_port], scores_shape); - - size_t out_shape_size = shape_size(out_shape); - - std::vector selected_indices(out_shape_size); - std::vector selected_scores(out_shape_size); - int64_t valid_outputs = 0; - - runtime::reference::non_max_suppression(boxes_data.data(), - boxes_shape, - scores_data.data(), - scores_shape, - max_output_boxes_per_class, - iou_threshold, - score_threshold, - soft_nms_sigma, - selected_indices.data(), - out_shape, - selected_scores.data(), - out_shape, - &valid_outputs, - m_sort_result_descending); - - outputs[0]->set_element_type(m_output_type); - outputs[0]->set_shape(Shape{static_cast(valid_outputs), 3}); - - size_t num_of_outputs = outputs.size(); - - if (num_of_outputs >= 2) - { - outputs[1]->set_element_type(element::f32); - outputs[1]->set_shape(Shape{static_cast(valid_outputs), 3}); - } - - if (num_of_outputs >= 3) - { - outputs[2]->set_element_type(m_output_type); - outputs[2]->set_shape(Shape{1}); - } - - evaluate_postprocessing( - outputs, m_output_type, selected_indices, selected_scores, valid_outputs); - - return true; -} From 977e7a9b946c4a51c9a436f9f8034dc2e10f3764 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 23 Oct 2020 15:30:40 +0300 Subject: [PATCH 140/173] Now all nGraph functions in tests of the transformation NMS-5 -> NMSIE3 have one output. --- .../transformations/convert_nms5_test.cpp | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp index 29cb46fe0fd28a..038598101dd1bd 100644 --- a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp +++ b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp @@ -32,7 +32,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticSixInputs) { auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); - f = std::make_shared(nms, ParameterVector{boxes, scores}); + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); pass::Manager manager; manager.register_pass(); @@ -69,7 +69,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticSixInputs) { new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); - f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); ASSERT_TRUE(f_ref->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); ASSERT_TRUE(f_ref->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); EXPECT_EQ(f_ref->get_output_shape(2), (Shape{1})); @@ -90,7 +90,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticFiveInputs) { auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); - f = std::make_shared(nms, ParameterVector{boxes, scores}); + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); pass::Manager manager; manager.register_pass(); @@ -127,7 +127,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticFiveInputs) { new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); - f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); ASSERT_TRUE(f_ref->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); ASSERT_TRUE(f_ref->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); EXPECT_EQ(f_ref->get_output_shape(2), (Shape{1})); @@ -147,7 +147,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticFourInputs) { auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); - f = std::make_shared(nms, ParameterVector{boxes, scores}); + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); pass::Manager manager; manager.register_pass(); @@ -184,7 +184,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticFourInputs) { new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); - f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); ASSERT_TRUE(f_ref->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); ASSERT_TRUE(f_ref->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); EXPECT_EQ(f_ref->get_output_shape(2), (Shape{1})); @@ -203,7 +203,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticThreeInputs) { auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); - f = std::make_shared(nms, ParameterVector{boxes, scores}); + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); pass::Manager manager; manager.register_pass(); @@ -240,7 +240,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticThreeInputs) { new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); - f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); ASSERT_TRUE(f_ref->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); ASSERT_TRUE(f_ref->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); EXPECT_EQ(f_ref->get_output_shape(2), (Shape{1})); @@ -258,7 +258,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticTwoInputs) { auto nms = std::make_shared(boxes, scores, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); - f = std::make_shared(nms, ParameterVector{boxes, scores}); + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); pass::Manager manager; manager.register_pass(); @@ -295,7 +295,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticTwoInputs) { new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); - f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); ASSERT_TRUE(f_ref->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); ASSERT_TRUE(f_ref->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); EXPECT_EQ(f_ref->get_output_shape(2), (Shape{1})); @@ -317,7 +317,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1SixInputs) { auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); - f = std::make_shared(nms, ParameterVector{boxes, scores}); + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); const auto &orig_selected_indices_shape = f->get_output_partial_shape(0); pass::Manager manager; @@ -353,7 +353,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1SixInputs) { new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); - f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); } auto res = compare_functions(f, f_ref); @@ -371,7 +371,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1FiveInputs) { auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); - f = std::make_shared(nms, ParameterVector{boxes, scores}); + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); const auto &orig_selected_indices_shape = f->get_output_partial_shape(0); pass::Manager manager; @@ -407,7 +407,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1FiveInputs) { new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); - f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); } auto res = compare_functions(f, f_ref); @@ -424,7 +424,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1FourInputs) { auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); - f = std::make_shared(nms, ParameterVector{boxes, scores}); + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); const auto &orig_selected_indices_shape = f->get_output_partial_shape(0); pass::Manager manager; @@ -460,7 +460,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1FourInputs) { new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); - f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); } auto res = compare_functions(f, f_ref); @@ -476,7 +476,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1ThreeInputs) { auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); - f = std::make_shared(nms, ParameterVector{boxes, scores}); + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); const auto &orig_selected_indices_shape = f->get_output_partial_shape(0); pass::Manager manager; @@ -512,7 +512,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1ThreeInputs) { new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); - f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); } auto res = compare_functions(f, f_ref); @@ -527,7 +527,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1TwoInputs) { auto nms = std::make_shared(boxes, scores, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); - f = std::make_shared(nms, ParameterVector{boxes, scores}); + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); const auto &orig_selected_indices_shape = f->get_output_partial_shape(0); pass::Manager manager; @@ -563,7 +563,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1TwoInputs) { new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); - f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); } auto res = compare_functions(f, f_ref); @@ -582,7 +582,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2SixInputs) { auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); - f = std::make_shared(nms, ParameterVector{boxes, scores}); + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); pass::Manager manager; manager.register_pass(); @@ -617,7 +617,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2SixInputs) { new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); - f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); } auto res = compare_functions(f, f_ref); @@ -635,7 +635,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2FiveInputs) { auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); - f = std::make_shared(nms, ParameterVector{boxes, scores}); + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); pass::Manager manager; manager.register_pass(); @@ -670,7 +670,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2FiveInputs) { new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); - f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); } auto res = compare_functions(f, f_ref); @@ -687,7 +687,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2FourInputs) { auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); - f = std::make_shared(nms, ParameterVector{boxes, scores}); + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); pass::Manager manager; manager.register_pass(); @@ -722,7 +722,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2FourInputs) { new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); - f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); } auto res = compare_functions(f, f_ref); @@ -738,7 +738,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2ThreeInputs) { auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); - f = std::make_shared(nms, ParameterVector{boxes, scores}); + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); pass::Manager manager; manager.register_pass(); @@ -773,7 +773,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2ThreeInputs) { new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); - f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); } auto res = compare_functions(f, f_ref); @@ -788,7 +788,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2TwoInputs) { auto nms = std::make_shared(boxes, scores, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); - f = std::make_shared(nms, ParameterVector{boxes, scores}); + f = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); pass::Manager manager; manager.register_pass(); @@ -823,7 +823,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2TwoInputs) { new_soft_nms_sigma, 0, true); nms->set_friendly_name("nms"); - f_ref = std::make_shared(nms, ParameterVector{boxes, scores}); + f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); } auto res = compare_functions(f, f_ref); From cdc5d68bf042e758395b7e75ea27c6a4f678f762 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 23 Oct 2020 16:07:26 +0300 Subject: [PATCH 141/173] Now preprocessing and postprocessing of the calculation of NMS-5 in reference implementation. --- .../runtime/reference/non_max_suppression.hpp | 19 +- .../runtime/reference/non_max_suppression.cpp | 555 ++++++++++++++---- .../test/backend/non_max_suppression.in.cpp | 18 +- ngraph/test/runtime/ie/unit_test.manifest | 18 +- .../runtime/interpreter/int_executable.cpp | 299 ---------- .../runtime/interpreter/int_executable.hpp | 2 +- 6 files changed, 453 insertions(+), 458 deletions(-) diff --git a/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp b/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp index aa5725650f5da2..a0e370c8b7d950 100644 --- a/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp +++ b/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp @@ -23,8 +23,10 @@ #include #include #include +#include #include "ngraph/coordinate_transform.hpp" #include "ngraph/op/util/op_types.hpp" +#include "ngraph/ops.hpp" #include "ngraph/shape_util.hpp" namespace ngraph @@ -33,20 +35,9 @@ namespace ngraph { namespace reference { - void non_max_suppression(const float* boxes_data, - const Shape& boxes_data_shape, - const float* scores_data, - const Shape& scores_data_shape, - int64_t max_output_boxes_per_class, - float iou_threshold, - float score_threshold, - float soft_nms_sigma, - int64_t* selected_indices, - const Shape& selected_indices_shape, - float* selected_scores, - const Shape& selected_scores_shape, - int64_t* valid_outputs, - const bool sort_result_descending); + void non_max_suppression(const op::v5::NonMaxSuppression* nms5, + const std::vector>& out, + const std::vector>& args); } } } diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index 2f7e81b4ba543e..9b2b50db684ce8 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -128,169 +128,472 @@ struct BoxInfo float score = 0.0f; }; -namespace ngraph +using V5BoxEncoding = op::v5::NonMaxSuppression::BoxEncodingType; + +namespace { - namespace runtime + constexpr size_t boxes_port = 0; + constexpr size_t scores_port = 1; + constexpr size_t max_output_boxes_port = 2; + constexpr size_t iou_threshold_port = 3; + constexpr size_t score_threshold_port = 4; + constexpr size_t soft_nms_sigma_port = 5; + + PartialShape + infer_selected_indices_shape(const std::vector>& inputs, + int64_t max_output_boxes_per_class) { - namespace reference + const auto boxes_ps = inputs[boxes_port]->get_partial_shape(); + const auto scores_ps = inputs[scores_port]->get_partial_shape(); + + // NonMaxSuppression produces triplets + // that have the following format: [batch_index, class_index, box_index] + PartialShape result = {Dimension::dynamic(), 3}; + + if (boxes_ps.rank().is_static() && scores_ps.rank().is_static()) { - void non_max_suppression(const float* boxes_data, - const Shape& boxes_data_shape, - const float* scores_data, - const Shape& scores_data_shape, - int64_t max_output_boxes_per_class, - float iou_threshold, - float score_threshold, - float soft_nms_sigma, - int64_t* selected_indices, - const Shape& selected_indices_shape, - float* selected_scores, - const Shape& selected_scores_shape, - int64_t* valid_outputs, - const bool sort_result_descending) + const auto num_boxes_boxes = boxes_ps[1]; + if (num_boxes_boxes.is_static() && scores_ps[0].is_static() && scores_ps[1].is_static()) { - float scale = 0.0f; - if (soft_nms_sigma > 0.0f) - { - scale = -0.5f / soft_nms_sigma; - } + const auto num_boxes = num_boxes_boxes.get_length(); + const auto num_classes = scores_ps[1].get_length(); - auto func = [iou_threshold, scale](float iou) { - const float weight = std::exp(scale * iou * iou); - return iou <= iou_threshold ? weight : 0.0f; - }; + result[0] = std::min(num_boxes, max_output_boxes_per_class) * num_classes * + scores_ps[0].get_length(); + } + } - // boxes shape: {num_batches, num_boxes, 4} - // scores shape: {num_batches, num_classes, num_boxes} - int64_t num_batches = static_cast(scores_data_shape[0]); - int64_t num_classes = static_cast(scores_data_shape[1]); - int64_t num_boxes = static_cast(boxes_data_shape[1]); + return result; + } - SelectedIndex* selected_indices_ptr = - reinterpret_cast(selected_indices); - SelectedScore* selected_scores_ptr = - reinterpret_cast(selected_scores); + void normalize_corner(float* boxes, const Shape& boxes_shape) + { + size_t total_num_of_boxes = shape_size(boxes_shape) / 4; + for (size_t i = 0; i < total_num_of_boxes; ++i) + { + float* current_box = boxes + 4 * i; + + float y1 = current_box[0]; + float x1 = current_box[1]; + float y2 = current_box[2]; + float x2 = current_box[3]; + + float ymin = std::min(y1, y2); + float ymax = std::max(y1, y2); + float xmin = std::min(x1, x2); + float xmax = std::max(x1, x2); + + current_box[0] = ymin; + current_box[1] = xmin; + current_box[2] = ymax; + current_box[3] = xmax; + } + } - size_t boxes_per_class = static_cast(max_output_boxes_per_class); + void normalize_center(float* boxes, const Shape& boxes_shape) + { + size_t total_num_of_boxes = shape_size(boxes_shape) / 4; + for (size_t i = 0; i < total_num_of_boxes; ++i) + { + float* current_box = boxes + 4 * i; + + float x_center = current_box[0]; + float y_center = current_box[1]; + float width = current_box[2]; + float height = current_box[3]; + + float y1 = y_center - height / 2.0; + float x1 = x_center - width / 2.0; + float y2 = y_center + height / 2.0; + float x2 = x_center + width / 2.0; + + current_box[0] = y1; + current_box[1] = x1; + current_box[2] = y2; + current_box[3] = x2; + } + } - int64_t num_of_valid_boxes = 0; + void normalize_box_encoding(float* boxes, + const Shape& boxes_shape, + const V5BoxEncoding box_encoding) + { + if (box_encoding == V5BoxEncoding::CORNER) + { + normalize_corner(boxes, boxes_shape); + } + else + { + normalize_center(boxes, boxes_shape); + } + } - std::vector filteredBoxes; + std::vector get_floats(const std::shared_ptr& input, const Shape& shape) + { + size_t input_size = shape_size(shape); + std::vector result(input_size); - for (int64_t batch = 0; batch < num_batches; batch++) - { - const float* boxesPtr = boxes_data + batch * num_boxes * 4; - Rectangle* r = reinterpret_cast(const_cast(boxesPtr)); + switch (input->get_element_type()) + { + case element::Type_t::bf16: + { + bfloat16* p = input->get_data_ptr(); + for (size_t i = 0; i < input_size; ++i) + { + result[i] = float(p[i]); + } + } + break; + case element::Type_t::f16: + { + float16* p = input->get_data_ptr(); + for (size_t i = 0; i < input_size; ++i) + { + result[i] = float(p[i]); + } + } + break; + case element::Type_t::f32: + { + float* p = input->get_data_ptr(); + memcpy(result.data(), p, input_size * sizeof(float)); + } + break; + default: throw std::runtime_error("Unsupported data type in op NonMaxSuppression-5"); break; + } - for (int64_t class_idx = 0; class_idx < num_classes; class_idx++) + return result; + } + + std::vector prepare_boxes_data(const std::shared_ptr& boxes, + const Shape& boxes_shape, + const V5BoxEncoding box_encoding) + { + auto result = get_floats(boxes, boxes_shape); + normalize_box_encoding(result.data(), boxes_shape, box_encoding); + return result; + } + + std::vector prepare_scores_data(const std::shared_ptr& scores, + const Shape& scores_shape) + { + auto result = get_floats(scores, scores_shape); + return result; + } + + void nms_postprocessing(const std::vector>& outputs, + const ngraph::element::Type output_type, + const std::vector& selected_indices, + const std::vector& selected_scores, + int64_t valid_outputs, + const ngraph::element::Type selected_scores_type) + { + size_t num_of_outputs = outputs.size(); + size_t selected_size = valid_outputs * 3; + + if (output_type == ngraph::element::i64) + { + int64_t* indices_ptr = outputs[0]->get_data_ptr(); + memcpy(indices_ptr, selected_indices.data(), selected_size * sizeof(int64_t)); + } + else + { + int32_t* indices_ptr = outputs[0]->get_data_ptr(); + for (size_t i = 0; i < selected_size; ++i) + { + indices_ptr[i] = static_cast(selected_indices[i]); + } + } + + if (num_of_outputs < 2) + { + return; + } + + size_t selected_scores_size = selected_scores.size(); + + switch (selected_scores_type) + { + case element::Type_t::bf16: + { + bfloat16* scores_ptr = outputs[1]->get_data_ptr(); + for (size_t i = 0; i < selected_scores_size; ++i) + { + scores_ptr[i] = bfloat16(selected_scores[i]); + } + } + break; + case element::Type_t::f16: + { + float16* scores_ptr = outputs[1]->get_data_ptr(); + for (size_t i = 0; i < selected_scores_size; ++i) + { + scores_ptr[i] = float16(selected_scores[i]); + } + } + break; + case element::Type_t::f32: + { + float* scores_ptr = outputs[1]->get_data_ptr(); + memcpy(scores_ptr, selected_scores.data(), selected_size * sizeof(float)); + } + break; + default:; + } + + if (num_of_outputs < 3) + { + return; + } + + if (output_type == ngraph::element::i64) + { + int64_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); + *valid_outputs_ptr = valid_outputs; + } + else + { + int32_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); + *valid_outputs_ptr = static_cast(valid_outputs); + } + } + + void eval(const float* boxes_data, + const Shape& boxes_data_shape, + const float* scores_data, + const Shape& scores_data_shape, + int64_t max_output_boxes_per_class, + float iou_threshold, + float score_threshold, + float soft_nms_sigma, + int64_t* selected_indices, + const Shape& selected_indices_shape, + float* selected_scores, + const Shape& selected_scores_shape, + int64_t* valid_outputs, + const bool sort_result_descending) + { + float scale = 0.0f; + if (soft_nms_sigma > 0.0f) + { + scale = -0.5f / soft_nms_sigma; + } + + auto func = [iou_threshold, scale](float iou) { + const float weight = std::exp(scale * iou * iou); + return iou <= iou_threshold ? weight : 0.0f; + }; + + // boxes shape: {num_batches, num_boxes, 4} + // scores shape: {num_batches, num_classes, num_boxes} + int64_t num_batches = static_cast(scores_data_shape[0]); + int64_t num_classes = static_cast(scores_data_shape[1]); + int64_t num_boxes = static_cast(boxes_data_shape[1]); + + SelectedIndex* selected_indices_ptr = + reinterpret_cast(selected_indices); + SelectedScore* selected_scores_ptr = + reinterpret_cast(selected_scores); + + size_t boxes_per_class = static_cast(max_output_boxes_per_class); + + int64_t num_of_valid_boxes = 0; + + std::vector filteredBoxes; + + for (int64_t batch = 0; batch < num_batches; batch++) + { + const float* boxesPtr = boxes_data + batch * num_boxes * 4; + Rectangle* r = reinterpret_cast(const_cast(boxesPtr)); + + for (int64_t class_idx = 0; class_idx < num_classes; class_idx++) + { + const float* scoresPtr = + scores_data + batch * (num_classes * num_boxes) + class_idx * num_boxes; + + std::vector candidate_boxes; + candidate_boxes.reserve(num_boxes); + + for (size_t box_idx = 0; box_idx < num_boxes; box_idx++) + { + if (scoresPtr[box_idx] > score_threshold) { - const float* scoresPtr = - scores_data + batch * (num_classes * num_boxes) + class_idx * num_boxes; + candidate_boxes.emplace_back( + r[box_idx], box_idx, scoresPtr[box_idx], 0, batch, class_idx); + } + } - std::vector candidate_boxes; - candidate_boxes.reserve(num_boxes); + std::priority_queue sorted_boxes(std::less(), + std::move(candidate_boxes)); - for (size_t box_idx = 0; box_idx < num_boxes; box_idx++) - { - if (scoresPtr[box_idx] > score_threshold) - { - candidate_boxes.emplace_back( - r[box_idx], box_idx, scoresPtr[box_idx], 0, batch, class_idx); - } - } + std::vector selected; + // Get the next box with top score, filter by iou_threshold - std::priority_queue sorted_boxes(std::less(), - std::move(candidate_boxes)); + BoxInfo next_candidate; + float original_score; - std::vector selected; - // Get the next box with top score, filter by iou_threshold + while (!sorted_boxes.empty() && selected.size() < boxes_per_class) + { + next_candidate = sorted_boxes.top(); + original_score = next_candidate.score; + sorted_boxes.pop(); + + bool should_hard_suppress = false; + for (int64_t j = static_cast(selected.size()) - 1; + j >= next_candidate.suppress_begin_index; + --j) + { + float iou = + intersectionOverUnion(next_candidate.box, selected[j].box); + next_candidate.score *= func(iou); - BoxInfo next_candidate; - float original_score; + if (iou >= iou_threshold) + { + should_hard_suppress = true; + break; + } - while (!sorted_boxes.empty() && selected.size() < boxes_per_class) + if (next_candidate.score <= score_threshold) { - next_candidate = sorted_boxes.top(); - original_score = next_candidate.score; - sorted_boxes.pop(); - - bool should_hard_suppress = false; - for (int64_t j = static_cast(selected.size()) - 1; - j >= next_candidate.suppress_begin_index; - --j) - { - float iou = - intersectionOverUnion(next_candidate.box, selected[j].box); - next_candidate.score *= func(iou); - - if (iou >= iou_threshold) - { - should_hard_suppress = true; - break; - } - - if (next_candidate.score <= score_threshold) - { - break; - } - } - - next_candidate.suppress_begin_index = selected.size(); - - if (!should_hard_suppress) - { - if (next_candidate.score == original_score) - { - selected.push_back(next_candidate); - continue; - } - if (next_candidate.score > score_threshold) - { - sorted_boxes.push(next_candidate); - } - } + break; } + } - for (const auto& box_info : selected) + next_candidate.suppress_begin_index = selected.size(); + + if (!should_hard_suppress) + { + if (next_candidate.score == original_score) { - filteredBoxes.push_back(box_info); + selected.push_back(next_candidate); + continue; + } + if (next_candidate.score > score_threshold) + { + sorted_boxes.push(next_candidate); } } } - if (sort_result_descending) + for (const auto& box_info : selected) { - std::sort(filteredBoxes.begin(), - filteredBoxes.end(), - [](const BoxInfo& l, const BoxInfo& r) { return l.score > r.score; }); + filteredBoxes.push_back(box_info); } + } + } - size_t max_num_of_selected_indices = selected_indices_shape[0]; - size_t output_size = std::min(filteredBoxes.size(), max_num_of_selected_indices); + if (sort_result_descending) + { + std::sort(filteredBoxes.begin(), + filteredBoxes.end(), + [](const BoxInfo& l, const BoxInfo& r) { return l.score > r.score; }); + } - *valid_outputs = output_size; + size_t max_num_of_selected_indices = selected_indices_shape[0]; + size_t output_size = std::min(filteredBoxes.size(), max_num_of_selected_indices); - size_t idx; - for (idx = 0; idx < output_size; idx++) + *valid_outputs = output_size; + + size_t idx; + for (idx = 0; idx < output_size; idx++) + { + const auto& box_info = filteredBoxes[idx]; + SelectedIndex selected_index{ + box_info.batch_index, box_info.class_index, box_info.index}; + SelectedScore selected_score{static_cast(box_info.batch_index), + static_cast(box_info.class_index), + box_info.score}; + + selected_indices_ptr[idx] = selected_index; + selected_scores_ptr[idx] = selected_score; + } + + SelectedIndex selected_index_filler{0, 0, 0}; + SelectedScore selected_score_filler{0.0f, 0.0f, 0.0f}; + for (; idx < max_num_of_selected_indices; idx++) + { + selected_indices_ptr[idx] = selected_index_filler; + selected_scores_ptr[idx] = selected_score_filler; + } + } +} + +namespace ngraph +{ + namespace runtime + { + namespace reference + { + + void non_max_suppression(const op::v5::NonMaxSuppression* nms5, + const std::vector>& outputs, + const std::vector>& inputs) + { + int64_t max_output_boxes_per_class = nms5->max_boxes_output_from_input(); + float iou_threshold = nms5->iou_threshold_from_input(); + float score_threshold = nms5->score_threshold_from_input(); + float soft_nms_sigma = nms5->soft_nms_sigma_from_input(); + + auto selected_indices_shape = + infer_selected_indices_shape(inputs, max_output_boxes_per_class); + Shape out_shape = selected_indices_shape.to_shape(); + + Shape boxes_shape = inputs[boxes_port]->get_shape(); + Shape scores_shape = inputs[scores_port]->get_shape(); + + auto boxes_data = + prepare_boxes_data(inputs[boxes_port], boxes_shape, nms5->get_box_encoding()); + auto scores_data = prepare_scores_data(inputs[scores_port], scores_shape); + + size_t out_shape_size = shape_size(out_shape); + + std::vector selected_indices(out_shape_size); + std::vector selected_scores(out_shape_size); + int64_t valid_outputs = 0; + + eval(boxes_data.data(), + boxes_shape, + scores_data.data(), + scores_shape, + max_output_boxes_per_class, + iou_threshold, + score_threshold, + soft_nms_sigma, + selected_indices.data(), + out_shape, + selected_scores.data(), + out_shape, + &valid_outputs, + nms5->get_sort_result_descending()); + + auto output_type = nms5->get_output_type(); + + outputs[0]->set_element_type(output_type); + outputs[0]->set_shape(Shape{static_cast(valid_outputs), 3}); + + size_t num_of_outputs = outputs.size(); + + auto selected_scores_type = + (inputs.size() < 4) ? element::f32 : inputs[3]->get_element_type(); + + if (num_of_outputs >= 2) { - const auto& box_info = filteredBoxes[idx]; - SelectedIndex selected_index{ - box_info.batch_index, box_info.class_index, box_info.index}; - SelectedScore selected_score{static_cast(box_info.batch_index), - static_cast(box_info.class_index), - box_info.score}; - - selected_indices_ptr[idx] = selected_index; - selected_scores_ptr[idx] = selected_score; + outputs[1]->set_element_type(selected_scores_type); + outputs[1]->set_shape(Shape{static_cast(valid_outputs), 3}); } - SelectedIndex selected_index_filler{0, 0, 0}; - SelectedScore selected_score_filler{0.0f, 0.0f, 0.0f}; - for (; idx < max_num_of_selected_indices; idx++) + if (num_of_outputs >= 3) { - selected_indices_ptr[idx] = selected_index_filler; - selected_scores_ptr[idx] = selected_score_filler; + outputs[2]->set_element_type(output_type); + outputs[2]->set_shape(Shape{1}); } + + nms_postprocessing(outputs, + output_type, + selected_indices, + selected_scores, + valid_outputs, + selected_scores_type); } } } diff --git a/ngraph/test/backend/non_max_suppression.in.cpp b/ngraph/test/backend/non_max_suppression.in.cpp index 2c9514d9538db2..e258d272e41dcd 100644 --- a/ngraph/test/backend/non_max_suppression.in.cpp +++ b/ngraph/test/backend/non_max_suppression.in.cpp @@ -42,7 +42,7 @@ using namespace ngraph; static string s_manifest = "${MANIFEST}"; -NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_center_point_box_format_backend) +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_center_point_box_format) { std::vector boxes_data = {0.5, 0.5, 1.0, 1.0, 0.5, 0.6, 1.0, 1.0, 0.5, 0.4, 1.0, 1.0, 0.5, 10.5, 1.0, 1.0, @@ -105,7 +105,7 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_center_point_box_format_backend) EXPECT_EQ(expected_valid_outputs, valid_outputs_value); } -NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_flipped_coordinates_backend) +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_flipped_coordinates) { std::vector boxes_data = {1.0, 1.0, 0.0, 0.0, 0.0, 0.1, 1.0, 1.1, 0.0, 0.9, 1.0, -0.1, 0.0, 10.0, 1.0, 11.0, @@ -168,7 +168,7 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_flipped_coordinates_backend) EXPECT_EQ(expected_valid_outputs, valid_outputs_value); } -NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_identical_boxes_backend) +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_identical_boxes) { std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, @@ -232,7 +232,7 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_identical_boxes_backend) EXPECT_EQ(expected_valid_outputs, valid_outputs_value); } -NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_limit_output_size_backend) +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_limit_output_size) { std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, @@ -295,7 +295,7 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_limit_output_size_backend) EXPECT_EQ(expected_valid_outputs, valid_outputs_value); } -NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_single_box_backend) +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_single_box) { std::vector boxes_data = {0.0, 0.0, 1.0, 1.0}; @@ -356,7 +356,7 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_single_box_backend) EXPECT_EQ(expected_valid_outputs, valid_outputs_value); } -NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_suppress_by_IOU_backend) +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_suppress_by_IOU) { std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, @@ -419,7 +419,7 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_suppress_by_IOU_backend) EXPECT_EQ(expected_valid_outputs, valid_outputs_value); } -NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_suppress_by_IOU_and_scores_backend) +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_suppress_by_IOU_and_scores) { std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, @@ -482,7 +482,7 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_suppress_by_IOU_and_scores_backen EXPECT_EQ(expected_valid_outputs, valid_outputs_value); } -NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_two_batches_backend) +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_two_batches) { std::vector boxes_data = { 0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, @@ -548,7 +548,7 @@ NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_two_batches_backend) EXPECT_EQ(expected_valid_outputs, valid_outputs_value); } -NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_two_classes_backend) +NGRAPH_TEST(${BACKEND_NAME}, nonmaxsuppression_two_classes) { std::vector boxes_data = {0.0, 0.0, 1.0, 1.0, 0.0, 0.1, 1.0, 1.1, 0.0, -0.1, 1.0, 0.9, 0.0, 10.0, 1.0, 11.0, diff --git a/ngraph/test/runtime/ie/unit_test.manifest b/ngraph/test/runtime/ie/unit_test.manifest index 18de0871915153..97bb32c0f03730 100644 --- a/ngraph/test/runtime/ie/unit_test.manifest +++ b/ngraph/test/runtime/ie/unit_test.manifest @@ -1140,15 +1140,15 @@ IE_CPU.log_softmax_3d_axis_0 IE_CPU.log_softmax_3d_axis_neg3 # Unsupported NMS-5 -IE_CPU.nonmaxsuppression_center_point_box_format_backend -IE_CPU.nonmaxsuppression_flipped_coordinates_backend -IE_CPU.nonmaxsuppression_identical_boxes_backend -IE_CPU.nonmaxsuppression_limit_output_size_backend -IE_CPU.nonmaxsuppression_single_box_backend -IE_CPU.nonmaxsuppression_suppress_by_IOU_backend -IE_CPU.nonmaxsuppression_suppress_by_IOU_and_scores_backend -IE_CPU.nonmaxsuppression_two_batches_backend -IE_CPU.nonmaxsuppression_two_classes_backend +IE_CPU.nonmaxsuppression_center_point_box_format +IE_CPU.nonmaxsuppression_flipped_coordinates +IE_CPU.nonmaxsuppression_identical_boxes +IE_CPU.nonmaxsuppression_limit_output_size +IE_CPU.nonmaxsuppression_single_box +IE_CPU.nonmaxsuppression_suppress_by_IOU +IE_CPU.nonmaxsuppression_suppress_by_IOU_and_scores +IE_CPU.nonmaxsuppression_two_batches +IE_CPU.nonmaxsuppression_two_classes #------------------------------------------------------------------------------- # diff --git a/ngraph/test/runtime/interpreter/int_executable.cpp b/ngraph/test/runtime/interpreter/int_executable.cpp index d72ad211f535ff..a0bfff3463a001 100644 --- a/ngraph/test/runtime/interpreter/int_executable.cpp +++ b/ngraph/test/runtime/interpreter/int_executable.cpp @@ -59,305 +59,6 @@ runtime::interpreter::OP_TYPEID runtime::interpreter::INTExecutable::get_typeid( return rc; } -using V5BoxEncoding = op::v5::NonMaxSuppression::BoxEncodingType; - -namespace -{ - constexpr size_t boxes_port = 0; - constexpr size_t scores_port = 1; - constexpr size_t max_output_boxes_port = 2; - constexpr size_t iou_threshold_port = 3; - constexpr size_t score_threshold_port = 4; - constexpr size_t soft_nms_sigma_port = 5; - - PartialShape - infer_selected_indices_shape(const std::vector>& inputs, - int64_t max_output_boxes_per_class) - { - const auto boxes_ps = inputs[boxes_port]->get_partial_shape(); - const auto scores_ps = inputs[scores_port]->get_partial_shape(); - - // NonMaxSuppression produces triplets - // that have the following format: [batch_index, class_index, box_index] - PartialShape result = {Dimension::dynamic(), 3}; - - if (boxes_ps.rank().is_static() && scores_ps.rank().is_static()) - { - const auto num_boxes_boxes = boxes_ps[1]; - if (num_boxes_boxes.is_static() && scores_ps[0].is_static() && scores_ps[1].is_static()) - { - const auto num_boxes = num_boxes_boxes.get_length(); - const auto num_classes = scores_ps[1].get_length(); - - result[0] = std::min(num_boxes, max_output_boxes_per_class) * num_classes * - scores_ps[0].get_length(); - } - } - - return result; - } - - void normalize_corner(float* boxes, const Shape& boxes_shape) - { - size_t total_num_of_boxes = shape_size(boxes_shape) / 4; - for (size_t i = 0; i < total_num_of_boxes; ++i) - { - float* current_box = boxes + 4 * i; - - float y1 = current_box[0]; - float x1 = current_box[1]; - float y2 = current_box[2]; - float x2 = current_box[3]; - - float ymin = std::min(y1, y2); - float ymax = std::max(y1, y2); - float xmin = std::min(x1, x2); - float xmax = std::max(x1, x2); - - current_box[0] = ymin; - current_box[1] = xmin; - current_box[2] = ymax; - current_box[3] = xmax; - } - } - - void normalize_center(float* boxes, const Shape& boxes_shape) - { - size_t total_num_of_boxes = shape_size(boxes_shape) / 4; - for (size_t i = 0; i < total_num_of_boxes; ++i) - { - float* current_box = boxes + 4 * i; - - float x_center = current_box[0]; - float y_center = current_box[1]; - float width = current_box[2]; - float height = current_box[3]; - - float y1 = y_center - height / 2.0; - float x1 = x_center - width / 2.0; - float y2 = y_center + height / 2.0; - float x2 = x_center + width / 2.0; - - current_box[0] = y1; - current_box[1] = x1; - current_box[2] = y2; - current_box[3] = x2; - } - } - - void normalize_box_encoding(float* boxes, - const Shape& boxes_shape, - const V5BoxEncoding box_encoding) - { - if (box_encoding == V5BoxEncoding::CORNER) - { - normalize_corner(boxes, boxes_shape); - } - else - { - normalize_center(boxes, boxes_shape); - } - } - - std::vector get_floats(const std::shared_ptr& input, const Shape& shape) - { - size_t input_size = shape_size(shape); - std::vector result(input_size); - - switch (input->get_element_type()) - { - case element::Type_t::bf16: - { - bfloat16* p = input->get_data_ptr(); - for (size_t i = 0; i < input_size; ++i) - { - result[i] = float(p[i]); - } - } - break; - case element::Type_t::f16: - { - float16* p = input->get_data_ptr(); - for (size_t i = 0; i < input_size; ++i) - { - result[i] = float(p[i]); - } - } - break; - case element::Type_t::f32: - { - float* p = input->get_data_ptr(); - memcpy(result.data(), p, input_size * sizeof(float)); - } - break; - default: throw std::runtime_error("Unsupported data type in op NonMaxSuppression-5"); break; - } - - return result; - } - - std::vector prepare_boxes_data(const std::shared_ptr& boxes, - const Shape& boxes_shape, - const V5BoxEncoding box_encoding) - { - auto result = get_floats(boxes, boxes_shape); - normalize_box_encoding(result.data(), boxes_shape, box_encoding); - return result; - } - - std::vector prepare_scores_data(const std::shared_ptr& scores, - const Shape& scores_shape) - { - auto result = get_floats(scores, scores_shape); - return result; - } - - void nms_postprocessing(const std::vector>& outputs, - const ngraph::element::Type output_type, - const std::vector& selected_indices, - const std::vector& selected_scores, - int64_t valid_outputs, - const ngraph::element::Type selected_scores_type) - { - size_t num_of_outputs = outputs.size(); - size_t selected_size = valid_outputs * 3; - - if (output_type == ngraph::element::i64) - { - int64_t* indices_ptr = outputs[0]->get_data_ptr(); - memcpy(indices_ptr, selected_indices.data(), selected_size * sizeof(int64_t)); - } - else - { - int32_t* indices_ptr = outputs[0]->get_data_ptr(); - for (size_t i = 0; i < selected_size; ++i) - { - indices_ptr[i] = static_cast(selected_indices[i]); - } - } - - if (num_of_outputs < 2) - { - return; - } - - size_t selected_scores_size = selected_scores.size(); - - switch (selected_scores_type) - { - case element::Type_t::bf16: - { - bfloat16* scores_ptr = outputs[1]->get_data_ptr(); - for (size_t i = 0; i < selected_scores_size; ++i) - { - scores_ptr[i] = bfloat16(selected_scores[i]); - } - } - break; - case element::Type_t::f16: - { - float16* scores_ptr = outputs[1]->get_data_ptr(); - for (size_t i = 0; i < selected_scores_size; ++i) - { - scores_ptr[i] = float16(selected_scores[i]); - } - } - break; - case element::Type_t::f32: - { - float* scores_ptr = outputs[1]->get_data_ptr(); - memcpy(scores_ptr, selected_scores.data(), selected_size * sizeof(float)); - } - break; - default:; - } - - if (num_of_outputs < 3) - { - return; - } - - if (output_type == ngraph::element::i64) - { - int64_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); - *valid_outputs_ptr = valid_outputs; - } - else - { - int32_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); - *valid_outputs_ptr = static_cast(valid_outputs); - } - } -} - -void runtime::interpreter::run_nms5(const op::v5::NonMaxSuppression* nms5, - const std::vector>& outputs, - const std::vector>& inputs) -{ - int64_t max_output_boxes_per_class = nms5->max_boxes_output_from_input(); - float iou_threshold = nms5->iou_threshold_from_input(); - float score_threshold = nms5->score_threshold_from_input(); - float soft_nms_sigma = nms5->soft_nms_sigma_from_input(); - - auto selected_indices_shape = infer_selected_indices_shape(inputs, max_output_boxes_per_class); - Shape out_shape = selected_indices_shape.to_shape(); - - Shape boxes_shape = inputs[boxes_port]->get_shape(); - Shape scores_shape = inputs[scores_port]->get_shape(); - - auto boxes_data = prepare_boxes_data(inputs[boxes_port], boxes_shape, nms5->get_box_encoding()); - auto scores_data = prepare_scores_data(inputs[scores_port], scores_shape); - - size_t out_shape_size = shape_size(out_shape); - - std::vector selected_indices(out_shape_size); - std::vector selected_scores(out_shape_size); - int64_t valid_outputs = 0; - - runtime::reference::non_max_suppression(boxes_data.data(), - boxes_shape, - scores_data.data(), - scores_shape, - max_output_boxes_per_class, - iou_threshold, - score_threshold, - soft_nms_sigma, - selected_indices.data(), - out_shape, - selected_scores.data(), - out_shape, - &valid_outputs, - nms5->get_sort_result_descending()); - - auto output_type = nms5->get_output_type(); - - outputs[0]->set_element_type(output_type); - outputs[0]->set_shape(Shape{static_cast(valid_outputs), 3}); - - size_t num_of_outputs = outputs.size(); - - auto selected_scores_type = (inputs.size() < 4) ? element::f32 : inputs[3]->get_element_type(); - - if (num_of_outputs >= 2) - { - outputs[1]->set_element_type(selected_scores_type); - outputs[1]->set_shape(Shape{static_cast(valid_outputs), 3}); - } - - if (num_of_outputs >= 3) - { - outputs[2]->set_element_type(output_type); - outputs[2]->set_shape(Shape{1}); - } - - nms_postprocessing(outputs, - output_type, - selected_indices, - selected_scores, - valid_outputs, - selected_scores_type); -} - runtime::interpreter::INTExecutable::INTExecutable(const shared_ptr& function, bool enable_performance_collection) : m_is_compiled{true} diff --git a/ngraph/test/runtime/interpreter/int_executable.hpp b/ngraph/test/runtime/interpreter/int_executable.hpp index 1107cfc4a1ac6b..f86bc9574c818a 100644 --- a/ngraph/test/runtime/interpreter/int_executable.hpp +++ b/ngraph/test/runtime/interpreter/int_executable.hpp @@ -1434,7 +1434,7 @@ class INTERPRETER_BACKEND_API ngraph::runtime::interpreter::INTExecutable : publ const op::v5::NonMaxSuppression* nms = static_cast(&node); - run_nms5(nms, out, args); + reference::non_max_suppression(nms, out, args); break; } From ab5a374d564aaf8d0afea46cd21bdc2c4bd56141 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 23 Oct 2020 16:20:47 +0300 Subject: [PATCH 142/173] Code style fixes. --- .../runtime/reference/non_max_suppression.cpp | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index 9b2b50db684ce8..a6a113c7ca04f4 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -390,10 +390,8 @@ namespace int64_t num_classes = static_cast(scores_data_shape[1]); int64_t num_boxes = static_cast(boxes_data_shape[1]); - SelectedIndex* selected_indices_ptr = - reinterpret_cast(selected_indices); - SelectedScore* selected_scores_ptr = - reinterpret_cast(selected_scores); + SelectedIndex* selected_indices_ptr = reinterpret_cast(selected_indices); + SelectedScore* selected_scores_ptr = reinterpret_cast(selected_scores); size_t boxes_per_class = static_cast(max_output_boxes_per_class); @@ -443,8 +441,7 @@ namespace j >= next_candidate.suppress_begin_index; --j) { - float iou = - intersectionOverUnion(next_candidate.box, selected[j].box); + float iou = intersectionOverUnion(next_candidate.box, selected[j].box); next_candidate.score *= func(iou); if (iou >= iou_threshold) @@ -524,7 +521,6 @@ namespace ngraph { namespace reference { - void non_max_suppression(const op::v5::NonMaxSuppression* nms5, const std::vector>& outputs, const std::vector>& inputs) @@ -589,11 +585,11 @@ namespace ngraph } nms_postprocessing(outputs, - output_type, - selected_indices, - selected_scores, - valid_outputs, - selected_scores_type); + output_type, + selected_indices, + selected_scores, + valid_outputs, + selected_scores_type); } } } From 0b690a46091be69dc8b4d33f91c61fcdeb7baa62 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 23 Oct 2020 16:57:53 +0300 Subject: [PATCH 143/173] Some fixes in tests for the transformation NMS-5 -> NMSIE3. --- .../transformations/convert_nms5_test.cpp | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp index 038598101dd1bd..9f1e538637324c 100644 --- a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp +++ b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp @@ -40,8 +40,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticSixInputs) { manager.run_passes(f); ASSERT_NO_THROW(check_rt_info(f)); ASSERT_TRUE(f->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); - ASSERT_TRUE(f->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); - EXPECT_EQ(f->get_output_shape(2), (Shape{1})); } { @@ -71,8 +69,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticSixInputs) { f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); ASSERT_TRUE(f_ref->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); - ASSERT_TRUE(f_ref->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); - EXPECT_EQ(f_ref->get_output_shape(2), (Shape{1})); } auto res = compare_functions(f, f_ref); @@ -98,8 +94,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticFiveInputs) { manager.run_passes(f); ASSERT_NO_THROW(check_rt_info(f)); ASSERT_TRUE(f->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); - ASSERT_TRUE(f->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); - EXPECT_EQ(f->get_output_shape(2), (Shape{1})); } { @@ -129,8 +123,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticFiveInputs) { f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); ASSERT_TRUE(f_ref->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); - ASSERT_TRUE(f_ref->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); - EXPECT_EQ(f_ref->get_output_shape(2), (Shape{1})); } auto res = compare_functions(f, f_ref); @@ -155,8 +147,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticFourInputs) { manager.run_passes(f); ASSERT_NO_THROW(check_rt_info(f)); ASSERT_TRUE(f->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); - ASSERT_TRUE(f->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); - EXPECT_EQ(f->get_output_shape(2), (Shape{1})); } { @@ -186,8 +176,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticFourInputs) { f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); ASSERT_TRUE(f_ref->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); - ASSERT_TRUE(f_ref->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); - EXPECT_EQ(f_ref->get_output_shape(2), (Shape{1})); } auto res = compare_functions(f, f_ref); @@ -211,8 +199,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticThreeInputs) { manager.run_passes(f); ASSERT_NO_THROW(check_rt_info(f)); ASSERT_TRUE(f->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); - ASSERT_TRUE(f->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); - EXPECT_EQ(f->get_output_shape(2), (Shape{1})); } { @@ -242,8 +228,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticThreeInputs) { f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); ASSERT_TRUE(f_ref->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); - ASSERT_TRUE(f_ref->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); - EXPECT_EQ(f_ref->get_output_shape(2), (Shape{1})); } auto res = compare_functions(f, f_ref); @@ -266,8 +250,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticTwoInputs) { manager.run_passes(f); ASSERT_NO_THROW(check_rt_info(f)); ASSERT_TRUE(f->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); - ASSERT_TRUE(f->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); - EXPECT_EQ(f->get_output_shape(2), (Shape{1})); } { @@ -297,8 +279,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticTwoInputs) { f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); ASSERT_TRUE(f_ref->get_output_partial_shape(0).same_scheme(PartialShape{Dimension::dynamic(), 3})); - ASSERT_TRUE(f_ref->get_output_partial_shape(1).same_scheme(PartialShape{Dimension::dynamic(), 3})); - EXPECT_EQ(f_ref->get_output_shape(2), (Shape{1})); } auto res = compare_functions(f, f_ref); From 211963362f867e1063606acc1be00f7fa48a8856 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 23 Oct 2020 17:58:35 +0300 Subject: [PATCH 144/173] Replaced precision i64 -> i32 for some constants in tests for the transformation NMS-5 -> NMSIE3. --- .../inference_engine/transformations/convert_nms5_test.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp index 9f1e538637324c..c3aba0880fe847 100644 --- a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp +++ b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp @@ -255,7 +255,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticTwoInputs) { { auto boxes = std::make_shared(element::f32, Shape{1, 1000, 4}); auto scores = std::make_shared(element::f32, Shape{1, 1, 1000}); - auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {0}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i32, Shape{}, {0}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); @@ -521,7 +521,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1TwoInputs) { { auto boxes = std::make_shared(element::f32, PartialShape::dynamic()); auto scores = std::make_shared(element::f32, PartialShape::dynamic()); - auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {0}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i32, Shape{}, {0}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); @@ -781,7 +781,7 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2TwoInputs) { { auto boxes = std::make_shared(element::f32, PartialShape{DYN, 1000, 4}); auto scores = std::make_shared(element::f32, PartialShape{DYN, 1, 1000}); - auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {0}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i32, Shape{}, {0}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); From 5cbdf1f430df26b6686b61dbfe884feb2ef2c0bb Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Sat, 24 Oct 2020 17:32:31 +0300 Subject: [PATCH 145/173] Written creating CNNLayer for NMS-5. --- .../src/convert_function_to_cnn_network.cpp | 10 ++ .../src/ie_cnn_layer_builder_ngraph.cpp | 42 ++++++++ .../src/legacy_api/src/ngraph_ops/nms_ie.cpp | 2 +- .../non_max_suppression_tests.cpp | 99 +++++++++++-------- 4 files changed, 110 insertions(+), 43 deletions(-) diff --git a/inference-engine/src/legacy_api/src/convert_function_to_cnn_network.cpp b/inference-engine/src/legacy_api/src/convert_function_to_cnn_network.cpp index 0cf25b94f7df74..8797f651479182 100644 --- a/inference-engine/src/legacy_api/src/convert_function_to_cnn_network.cpp +++ b/inference-engine/src/legacy_api/src/convert_function_to_cnn_network.cpp @@ -535,6 +535,15 @@ InferenceEngine::details::CNNLayerCreator::CNNLayerCreator(const std::shared_ptr }); + addSpecificCreator({"NonMaxSuppressionIE3"}, [](const std::shared_ptr<::ngraph::Node>& node, + const std::map& params) -> CNNLayerPtr { + LayerParams attrs = {node->get_friendly_name(), "NonMaxSuppressionIE3", + details::convertPrecision(node->get_output_element_type(0))}; + auto res = std::make_shared(attrs); + res->params = params; + return res; + }); + addSpecificCreator({"NonMaxSuppressionIE"}, [](const std::shared_ptr<::ngraph::Node>& node, const std::map& params) -> CNNLayerPtr { LayerParams attrs = {node->get_friendly_name(), "NonMaxSuppression", details::convertPrecision(node->get_output_element_type(0))}; @@ -814,6 +823,7 @@ void convertFunctionToICNNNetwork(const std::shared_ptr>(), std::make_shared>(), std::make_shared>(), + std::make_shared>(), std::make_shared>(), }; CNNLayerPtr result; diff --git a/inference-engine/src/legacy_api/src/ie_cnn_layer_builder_ngraph.cpp b/inference-engine/src/legacy_api/src/ie_cnn_layer_builder_ngraph.cpp index 07f74fe52ba829..838468b1f76428 100644 --- a/inference-engine/src/legacy_api/src/ie_cnn_layer_builder_ngraph.cpp +++ b/inference-engine/src/legacy_api/src/ie_cnn_layer_builder_ngraph.cpp @@ -1726,6 +1726,48 @@ CNNLayer::Ptr NodeConverter::createLayer(const std: THROW_IE_EXCEPTION << "Interpolate operation should be converted to Interp"; } +template <> +CNNLayer::Ptr NodeConverter::createLayer(const std::shared_ptr& layer) const { + LayerParams params = {layer->get_friendly_name(), "NonMaxSuppressionIE3", + details::convertPrecision(layer->get_output_element_type(0))}; + auto castedLayer = ngraph::as_type_ptr(layer); + if (castedLayer == nullptr) THROW_IE_EXCEPTION << "Cannot get " << params.type << " layer " << params.name; + + auto box_encoding = castedLayer->get_box_encoding(); + + auto res = std::make_shared(params); + + switch (box_encoding) { + case ngraph::op::v5::NonMaxSuppression::BoxEncodingType::CORNER: + res->params["box_encoding"] = "corner"; + break; + case ngraph::op::v5::NonMaxSuppression::BoxEncodingType::CENTER: + res->params["box_encoding"] = "center"; + break; + default: + THROW_IE_EXCEPTION << "Unsupported box encoding for NonMaxSuppression op"; + break; + } + + auto output_type = details::convertPrecision(castedLayer->get_output_type()); + std::string output_type_str; + switch (output_type) { + case Precision::I32: + output_type_str = "I32"; + break; + case Precision::I64: + output_type_str = "I64"; + break; + default: + THROW_IE_EXCEPTION << "Unsupported output type"; + } + res->params["output_type"] = output_type_str; + + bool sort_result_descending = castedLayer->get_sort_result_descending(); + res->params["sort_result_descending"] = sort_result_descending ? "1" : "0"; + return res; +} + template <> CNNLayer::Ptr NodeConverter::createLayer(const std::shared_ptr& layer) const { LayerParams params = {layer->get_friendly_name(), "Interpolate", diff --git a/inference-engine/src/legacy_api/src/ngraph_ops/nms_ie.cpp b/inference-engine/src/legacy_api/src/ngraph_ops/nms_ie.cpp index 347e939505290b..0bee3d569db920 100644 --- a/inference-engine/src/legacy_api/src/ngraph_ops/nms_ie.cpp +++ b/inference-engine/src/legacy_api/src/ngraph_ops/nms_ie.cpp @@ -102,7 +102,7 @@ void op::NonMaxSuppressionIE2::validate_and_infer_types() { set_output_type(0, nms->output(0).get_element_type(), nms->output(0).get_partial_shape()); } -NGRAPH_RTTI_DEFINITION(op::NonMaxSuppressionIE3, "NonMaxSuppressionIE", 3); +NGRAPH_RTTI_DEFINITION(op::NonMaxSuppressionIE3, "NonMaxSuppressionIE3", 3); op::NonMaxSuppressionIE3::NonMaxSuppressionIE3(const Output& boxes, const Output& scores, diff --git a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp index bc2f3aa651eee3..ab6e69281097dc 100644 --- a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp +++ b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp @@ -4,12 +4,12 @@ #include #include "ngraph_reader_tests.hpp" -/* -TEST_F(NGraphReaderTests, ReadNonMaxSuppression) { + +TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { std::string model = R"V0G0N( - + @@ -19,7 +19,7 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression) { - + @@ -29,55 +29,62 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression) { - + - + - + - + - + - + + + + + + + - + - + 1 15130 4 - + 1 80 15130 - - - + + + + - + 15130 3 - + - + 15130 3 - + 15130 3 @@ -89,7 +96,7 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression) { - + 15130 @@ -99,14 +106,15 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression) { - - - - - - - - + + + + + + + + + )V0G0N"; @@ -151,7 +159,13 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression) { - + + + + + + + @@ -167,15 +181,16 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression) { + - + 15130 3 - + 15130 @@ -195,25 +210,25 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression) { - - - - - - - + + + + + + + + )V0G0N"; - compareIRs(model, modelV5, 16, [](Blob::Ptr& weights) { + compareIRs(model, modelV5, 20, [](Blob::Ptr& weights) { auto * i64w = weights->buffer().as(); i64w[0] = 200; auto * fp32w = weights->buffer().as(); fp32w[2] = 0.5; fp32w[3] = 0.05; + fp32w[4] = 0.0; }); } - - */ From c19e54f0e08da62b69349bf1deb95996394fd4a3 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Sun, 25 Oct 2020 15:41:42 +0300 Subject: [PATCH 146/173] Added creating CNNLayer for NonMaxSuppressionIE3. --- .../src/ie_cnn_layer_builder_ngraph.cpp | 32 ++++++++- .../non_max_suppression_tests.cpp | 69 ++++++++++++++----- .../functional_test_utils/network_utils.cpp | 17 +++++ 3 files changed, 97 insertions(+), 21 deletions(-) diff --git a/inference-engine/src/legacy_api/src/ie_cnn_layer_builder_ngraph.cpp b/inference-engine/src/legacy_api/src/ie_cnn_layer_builder_ngraph.cpp index 838468b1f76428..c066bfa00e5caa 100644 --- a/inference-engine/src/legacy_api/src/ie_cnn_layer_builder_ngraph.cpp +++ b/inference-engine/src/legacy_api/src/ie_cnn_layer_builder_ngraph.cpp @@ -1726,6 +1726,34 @@ CNNLayer::Ptr NodeConverter::createLayer(const std: THROW_IE_EXCEPTION << "Interpolate operation should be converted to Interp"; } +template <> +CNNLayer::Ptr NodeConverter::createLayer(const std::shared_ptr& layer) const { + LayerParams params = {layer->get_friendly_name(), "NonMaxSuppressionIE3", + details::convertPrecision(layer->get_output_element_type(0))}; + auto castedLayer = ngraph::as_type_ptr(layer); + if (castedLayer == nullptr) THROW_IE_EXCEPTION << "Cannot get " << params.type << " layer " << params.name; + + auto res = std::make_shared(params); + res->params["center_point_box"] = asString(castedLayer->m_center_point_box); + res->params["sort_result_descending"] = castedLayer->m_sort_result_descending ? "1" : "0"; + + auto output_type = details::convertPrecision(castedLayer->m_output_type); + std::string output_type_str; + switch (output_type) { + case Precision::I32: + output_type_str = "I32"; + break; + case Precision::I64: + output_type_str = "I64"; + break; + default: + THROW_IE_EXCEPTION << "Unsupported output type"; + } + res->params["output_type"] = output_type_str; + + return res; +} + template <> CNNLayer::Ptr NodeConverter::createLayer(const std::shared_ptr& layer) const { LayerParams params = {layer->get_friendly_name(), "NonMaxSuppressionIE3", @@ -1739,10 +1767,10 @@ CNNLayer::Ptr NodeConverter::createLayer(cons switch (box_encoding) { case ngraph::op::v5::NonMaxSuppression::BoxEncodingType::CORNER: - res->params["box_encoding"] = "corner"; + res->params["center_point_box"] = "0"; break; case ngraph::op::v5::NonMaxSuppression::BoxEncodingType::CENTER: - res->params["box_encoding"] = "center"; + res->params["center_point_box"] = "1"; break; default: THROW_IE_EXCEPTION << "Unsupported box encoding for NonMaxSuppression op"; diff --git a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp index ab6e69281097dc..60f5bafdfd9dde 100644 --- a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp +++ b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp @@ -121,7 +121,7 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { std::string modelV5 = R"V0G0N( - + @@ -131,7 +131,7 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { - + @@ -142,30 +142,46 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { - - + + 1 + + + + - - + + 1 + + + + - - + + 1 + + + + - - + + 1 + + + + - + @@ -178,32 +194,47 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { 80 15130 - - - - + + 1 + + + 1 + + + 1 + + + 1 + - 15130 + 16000 + 3 + + + 16000 3 + + 1 + - 15130 + 16000 3 - 15130 + 16000 3 - 15130 + 16000 3 diff --git a/inference-engine/tests/ie_test_utils/functional_test_utils/network_utils.cpp b/inference-engine/tests/ie_test_utils/functional_test_utils/network_utils.cpp index 8f4a507eec94ed..83709dac917a5a 100644 --- a/inference-engine/tests/ie_test_utils/functional_test_utils/network_utils.cpp +++ b/inference-engine/tests/ie_test_utils/functional_test_utils/network_utils.cpp @@ -119,6 +119,23 @@ namespace FuncTestUtils { if (refLayer->outData[i]->getTensorDesc() != layer->outData[i]->getTensorDesc()) { err_log.push_back("Layer " + layer->name + " and ref layer " + refLayer->name + " have different tensor desc for out Data"); + std::cout << "For output " << i << "\n"; + auto refDescriptor = refLayer->outData[i]->getTensorDesc(); + auto layerDescriptor = layer->outData[i]->getTensorDesc(); + std::cout << "ref precision: " << refDescriptor.getPrecision() << "\n"; + std::cout << "layer precision: " << layerDescriptor.getPrecision() << "\n"; + auto refDims = refDescriptor.getDims(); + auto layerDims = layerDescriptor.getDims(); + std::cout <<"ref dims: "; + for (size_t d : refDims) { + std::cout << d << " "; + } + std::cout << "\n"; + std::cout <<"layer dims: "; + for (size_t d : layerDims) { + std::cout << d << " "; + } + std::cout << "\n"; } success = success && refLayer->outData[i]->getTensorDesc() == layer->outData[i]->getTensorDesc(); } From d9232f08e78c7931d115fcb23b3ebd4fd77da3ab Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Mon, 26 Oct 2020 15:21:24 +0300 Subject: [PATCH 147/173] some changes. --- .../non_max_suppression_tests.cpp | 41 +++++++++++-------- .../runtime/interpreter/int_executable.cpp | 1 - .../runtime/interpreter/int_executable.hpp | 5 --- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp index 60f5bafdfd9dde..236858d1073a37 100644 --- a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp +++ b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp @@ -76,6 +76,13 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { 15130 3 + + 15130 + 3 + + + 1 + @@ -119,12 +126,11 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { )V0G0N"; std::string modelV5 = R"V0G0N( - + - - + 1 15130 4 @@ -132,9 +138,8 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { - - + 1 80 15130 @@ -143,42 +148,42 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { - + 1 - + - + 1 - + - + 1 - + - + 1 - + @@ -208,20 +213,20 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { - + 16000 3 - + 16000 3 - + 1 - + 16000 @@ -233,7 +238,7 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { - + 16000 3 diff --git a/ngraph/test/runtime/interpreter/int_executable.cpp b/ngraph/test/runtime/interpreter/int_executable.cpp index a0bfff3463a001..186c5de48fd7a7 100644 --- a/ngraph/test/runtime/interpreter/int_executable.cpp +++ b/ngraph/test/runtime/interpreter/int_executable.cpp @@ -15,7 +15,6 @@ //***************************************************************************** #include "int_executable.hpp" -#include #include #include "backend_manager.hpp" #include "ngraph/chrome_trace.hpp" diff --git a/ngraph/test/runtime/interpreter/int_executable.hpp b/ngraph/test/runtime/interpreter/int_executable.hpp index f86bc9574c818a..f11b798e3f9d7a 100644 --- a/ngraph/test/runtime/interpreter/int_executable.hpp +++ b/ngraph/test/runtime/interpreter/int_executable.hpp @@ -128,11 +128,6 @@ namespace ngraph #undef NGRAPH_OP UnknownOp }; - - void run_nms5(const op::v5::NonMaxSuppression* nms5, - const std::vector>& out, - const std::vector>& args); - } // namespace interpreter } // namespace runtime } // namespace ngraph From a01d6088914a26b456e1953d3aeaec15816005bf Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 27 Oct 2020 13:30:29 +0300 Subject: [PATCH 148/173] Now conversions NMS-1, NMS-3, NMS-4 -> NMS-5 and NMS-5 -> NMSIE3 generate NMS nodes with 5 inputs. --- .../src/legacy_api/include/legacy/ie_layers.h | 4 ++ .../include/legacy/ngraph_ops/nms_ie.hpp | 9 ++++ .../src/convert_function_to_cnn_network.cpp | 4 +- .../legacy_api/src/ie_layer_validators.cpp | 1 + .../src/legacy_api/src/ngraph_ops/nms_ie.cpp | 19 +++++++ .../convert_nms_5_to_legacy.cpp | 47 +++++++++++------ .../ir_reader_v7/ie_layer_validators.cpp | 4 +- .../convert_previous_nms_to_nms_5.cpp | 3 -- .../non_max_suppression_tests.cpp | 50 +++++++++++++++++-- ngraph/python/src/ngraph/opset5/ops.py | 11 ++-- 10 files changed, 124 insertions(+), 28 deletions(-) diff --git a/inference-engine/src/legacy_api/include/legacy/ie_layers.h b/inference-engine/src/legacy_api/include/legacy/ie_layers.h index d02532dbcc7bd4..b298ac969fa6b3 100644 --- a/inference-engine/src/legacy_api/include/legacy/ie_layers.h +++ b/inference-engine/src/legacy_api/include/legacy/ie_layers.h @@ -2064,6 +2064,10 @@ class INFERENCE_ENGINE_INTERNAL_CNNLAYER_CLASS(NonMaxSuppressionLayer): public C * classes */ bool sort_result_descending = true; + /** + * @brief Output type for first and third inputs + */ + std::string output_type = "I64"; /** * @brief Creates a new NonMaxSuppressionLayer instance. */ diff --git a/inference-engine/src/legacy_api/include/legacy/ngraph_ops/nms_ie.hpp b/inference-engine/src/legacy_api/include/legacy/ngraph_ops/nms_ie.hpp index 49e689e9bd4727..f8b04c5fcca48d 100644 --- a/inference-engine/src/legacy_api/include/legacy/ngraph_ops/nms_ie.hpp +++ b/inference-engine/src/legacy_api/include/legacy/ngraph_ops/nms_ie.hpp @@ -62,6 +62,15 @@ class INFERENCE_ENGINE_API_CLASS(NonMaxSuppressionIE3) : public Op { public: NGRAPH_RTTI_DECLARATION; + NonMaxSuppressionIE3(const Output& boxes, + const Output& scores, + const Output& max_output_boxes_per_class, + const Output& iou_threshold, + const Output& score_threshold, + int center_point_box, + bool sort_result_descending, + const ngraph::element::Type& output_type = ngraph::element::i64); + NonMaxSuppressionIE3(const Output& boxes, const Output& scores, const Output& max_output_boxes_per_class, diff --git a/inference-engine/src/legacy_api/src/convert_function_to_cnn_network.cpp b/inference-engine/src/legacy_api/src/convert_function_to_cnn_network.cpp index 3dc5c291f477f8..b6659bd2d39559 100644 --- a/inference-engine/src/legacy_api/src/convert_function_to_cnn_network.cpp +++ b/inference-engine/src/legacy_api/src/convert_function_to_cnn_network.cpp @@ -537,9 +537,9 @@ InferenceEngine::details::CNNLayerCreator::CNNLayerCreator(const std::shared_ptr addSpecificCreator({"NonMaxSuppressionIE3"}, [](const std::shared_ptr<::ngraph::Node>& node, const std::map& params) -> CNNLayerPtr { - LayerParams attrs = {node->get_friendly_name(), "NonMaxSuppressionIE3", + LayerParams attrs = {node->get_friendly_name(), "NonMaxSuppression", details::convertPrecision(node->get_output_element_type(0))}; - auto res = std::make_shared(attrs); + auto res = std::make_shared(attrs); res->params = params; return res; }); diff --git a/inference-engine/src/legacy_api/src/ie_layer_validators.cpp b/inference-engine/src/legacy_api/src/ie_layer_validators.cpp index 11e96335271103..83949bf309ebc9 100644 --- a/inference-engine/src/legacy_api/src/ie_layer_validators.cpp +++ b/inference-engine/src/legacy_api/src/ie_layer_validators.cpp @@ -1177,6 +1177,7 @@ void NMSValidator::parseParams(CNNLayer* layer) { casted->center_point_box = layer->GetParamAsBool("center_point_box", false); casted->sort_result_descending = layer->GetParamAsBool("sort_result_descending", true); + casted->output_type = layer->GetParamAsString("output_type", "I64"); } #define REG_LAYER_VALIDATOR_FOR_TYPE(__validator, __type) _validators[#__type] = std::make_shared<__validator>(#__type) diff --git a/inference-engine/src/legacy_api/src/ngraph_ops/nms_ie.cpp b/inference-engine/src/legacy_api/src/ngraph_ops/nms_ie.cpp index 0bee3d569db920..dc9ffc52eaa79a 100644 --- a/inference-engine/src/legacy_api/src/ngraph_ops/nms_ie.cpp +++ b/inference-engine/src/legacy_api/src/ngraph_ops/nms_ie.cpp @@ -104,6 +104,19 @@ void op::NonMaxSuppressionIE2::validate_and_infer_types() { NGRAPH_RTTI_DEFINITION(op::NonMaxSuppressionIE3, "NonMaxSuppressionIE3", 3); +op::NonMaxSuppressionIE3::NonMaxSuppressionIE3(const Output& boxes, + const Output& scores, + const Output& max_output_boxes_per_class, + const Output& iou_threshold, + const Output& score_threshold, + int center_point_box, + bool sort_result_descending, + const ngraph::element::Type& output_type) + : Op({boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold}), + m_center_point_box(center_point_box), m_sort_result_descending(sort_result_descending), m_output_type(output_type) { + constructor_validate_and_infer_types(); +} + op::NonMaxSuppressionIE3::NonMaxSuppressionIE3(const Output& boxes, const Output& scores, const Output& max_output_boxes_per_class, @@ -139,6 +152,12 @@ static constexpr size_t max_output_boxes_per_class_port = 2; int64_t op::NonMaxSuppressionIE3::max_boxes_output_from_input() const { int64_t max_output_boxes{0}; + size_t num_of_inputs = inputs().size(); + if (num_of_inputs < 3) + { + return 0; + } + const auto max_output_boxes_input = as_type_ptr(input_value(max_output_boxes_per_class_port).get_node_shared_ptr()); max_output_boxes = max_output_boxes_input->cast_vector().at(0); diff --git a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp index dae4adaf0510ba..f892528ce092e8 100644 --- a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp +++ b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp @@ -32,7 +32,7 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { const auto& arg2 = num_of_inputs > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i32, Shape{}, {0}); const auto& arg3 = num_of_inputs > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); const auto& arg4 = num_of_inputs > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); - const auto& arg5 = num_of_inputs > 5 ? new_args.at(5) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); +// const auto& arg5 = num_of_inputs > 5 ? new_args.at(5) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); // vector of new nGraph operations NodeVector new_ops; @@ -58,8 +58,8 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { new_score_threshold = std::make_shared(arg4, new_shape_for_score_threshold, true); new_ops.push_back(new_score_threshold.get_node_shared_ptr()); - new_soft_nms_sigma = std::make_shared(arg5, new_shape_for_soft_nms_sigma, true); - new_ops.push_back(new_soft_nms_sigma.get_node_shared_ptr()); +// new_soft_nms_sigma = std::make_shared(arg5, new_shape_for_soft_nms_sigma, true); +// new_ops.push_back(new_soft_nms_sigma.get_node_shared_ptr()); int center_point_box = 0; switch (nms_5->get_box_encoding()) { @@ -73,17 +73,36 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { throw ngraph_error("NonMaxSuppression layer " + nms_5->get_friendly_name() + " has unsupported box encoding"); } - const auto nms_legacy = std::make_shared( - new_args.at(0), - new_args.at(1), - new_max_per_class, - new_iou_threshold, - new_score_threshold, - new_soft_nms_sigma, - center_point_box, - nms_5->get_sort_result_descending(), - nms_5->get_output_type()); - new_ops.push_back(nms_legacy); + + std::shared_ptr nms_legacy{nullptr}; + + if (num_of_inputs > 5) { + new_soft_nms_sigma = std::make_shared(new_args.at(5), new_shape_for_soft_nms_sigma, true); + new_ops.push_back(new_soft_nms_sigma.get_node_shared_ptr()); + nms_legacy = std::make_shared( + new_args.at(0), + new_args.at(1), + new_max_per_class, + new_iou_threshold, + new_score_threshold, + new_soft_nms_sigma, + center_point_box, + nms_5->get_sort_result_descending(), + nms_5->get_output_type()); + new_ops.push_back(nms_legacy); + } else { + nms_legacy = std::make_shared( + new_args.at(0), + new_args.at(1), + new_max_per_class, + new_iou_threshold, + new_score_threshold, + center_point_box, + nms_5->get_sort_result_descending(), + nms_5->get_output_type()); + new_ops.push_back(nms_legacy); + } + nms_legacy->set_friendly_name(nms_5->get_friendly_name()); ngraph::copy_runtime_info(nms_5, new_ops); diff --git a/inference-engine/src/readers/ir_reader_v7/ie_layer_validators.cpp b/inference-engine/src/readers/ir_reader_v7/ie_layer_validators.cpp index 7cd900a669e9ad..392b24960afedb 100644 --- a/inference-engine/src/readers/ir_reader_v7/ie_layer_validators.cpp +++ b/inference-engine/src/readers/ir_reader_v7/ie_layer_validators.cpp @@ -2098,9 +2098,9 @@ void NMSValidator::checkParams(const CNNLayer* layer) { void NMSValidator::checkShapes(const CNNLayer* layer, const vector& inShapes) const { size_t numInputs = inShapes.size(); - if (numInputs < 2 || numInputs > 5) + if (numInputs < 2 || numInputs > 6) THROW_IE_EXCEPTION << layer->name - << " NonMaxSuppression can take 2 - 5 inputs, but actually it has: " << numInputs; + << " NonMaxSuppression can take 2 - 6 inputs, but actually it has: " << numInputs; if (inShapes[0].size() != 3 || inShapes[0][2] != 4) THROW_IE_EXCEPTION << layer->name << " 'boxes' should be with shape [num_batches, spatial_dimension, 4]"; diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp index e16b62daf7d22a..7dc66e44d4493a 100644 --- a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp @@ -142,12 +142,10 @@ namespace { const auto& arg2 = num_of_args > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i64, Shape{}, {0}); const auto& arg3 = num_of_args > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); const auto& arg4 = num_of_args > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); - const auto& arg5 = ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); // list of new nGraph operations std::list> new_ops_list; - new_ops_list.push_front(arg5); if (num_of_args <= 4) { new_ops_list.push_front(arg4.get_node_shared_ptr()); } @@ -164,7 +162,6 @@ namespace { arg2, arg3, arg4, - arg5, attrs.box_encoding, attrs.sort_result_descending, attrs.output_type); diff --git a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp index 236858d1073a37..4cfcb6a978f055 100644 --- a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp +++ b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp @@ -111,6 +111,28 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { + + + + 1 + + + 1 + + + + + 1 + + + + + + + 1 + + + @@ -121,7 +143,10 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { + + + )V0G0N"; @@ -186,8 +211,8 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { - - + + 1 @@ -226,7 +251,8 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { - + + 16000 @@ -244,6 +270,22 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { + + + + + 1 + + + 1 + + + + + 1 + + + @@ -254,6 +296,8 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { + + )V0G0N"; diff --git a/ngraph/python/src/ngraph/opset5/ops.py b/ngraph/python/src/ngraph/opset5/ops.py index 2d1da821864a69..17ea4257e26378 100644 --- a/ngraph/python/src/ngraph/opset5/ops.py +++ b/ngraph/python/src/ngraph/opset5/ops.py @@ -153,11 +153,14 @@ def non_max_suppression( if score_threshold is None: score_threshold = make_constant_node(0, np.float32) if soft_nms_sigma is None: - soft_nms_sigma = make_constant_node(0, np.float32) + inputs = as_nodes( + boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold + ) + else: + inputs = as_nodes( + boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, soft_nms_sigma + ) - inputs = as_nodes( - boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, soft_nms_sigma - ) attributes = { "box_encoding": box_encoding, "sort_result_descending": sort_result_descending, From cbae8cfb45d0af81a97f0c3ff7d28a1a03e95f67 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 27 Oct 2020 13:36:11 +0300 Subject: [PATCH 149/173] Fixed ctor in MKLDNN NonMaxSuppressionImpl: validation of number of output edges. --- .../src/mkldnn_plugin/nodes/non_max_suppression.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp b/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp index 407eb51228ffbd..9b943f78021d9e 100644 --- a/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp +++ b/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp @@ -23,7 +23,7 @@ class NonMaxSuppressionImpl: public ExtLayerBase { if (layer->insData.size() < 2 || layer->insData.size() > 5) THROW_IE_EXCEPTION << layer->name << " Incorrect number of input edges!"; - if (layer->outData.size() != 1) + if (layer->outData.size() > 3) THROW_IE_EXCEPTION << layer->name << " Incorrect number of output edges!"; if (layer->insData[NMS_BOXES].lock()->getTensorDesc().getPrecision() != Precision::FP32) From 70ffa574f9030aa16807df63c010c75af793389e Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 27 Oct 2020 13:56:24 +0300 Subject: [PATCH 150/173] Added conversion of output_type for NMS-5. --- .../src/transformations/convert_precision.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/inference-engine/src/transformations/src/transformations/convert_precision.cpp b/inference-engine/src/transformations/src/transformations/convert_precision.cpp index bb7aeaca89fc15..10112e7f1cbf52 100644 --- a/inference-engine/src/transformations/src/transformations/convert_precision.cpp +++ b/inference-engine/src/transformations/src/transformations/convert_precision.cpp @@ -21,6 +21,7 @@ bool fuse_type_to_parameter(std::shared_ptr & node, ngraph::elemen bool fuse_type_to_convert(std::shared_ptr & node, ngraph::element::Type to, size_t idx); bool fuse_type_to_nms3(std::shared_ptr & node, ngraph::element::Type to, size_t idx); bool fuse_type_to_nms4(std::shared_ptr & node, ngraph::element::Type to, size_t idx); +bool fuse_type_to_nms5(std::shared_ptr & node, ngraph::element::Type to, size_t idx); bool fuse_type_to_topk(std::shared_ptr & node, ngraph::element::Type to, size_t idx); bool fuse_type_to_nonzero(std::shared_ptr & node, ngraph::element::Type to, size_t idx); bool fuse_type_to_bucketize(std::shared_ptr & node, ngraph::element::Type to, size_t idx); @@ -81,6 +82,7 @@ bool ngraph::pass::ConvertPrecision::run_on_function(std::shared_ptr & node, ngraph::element::Ty return false; } +bool fuse_type_to_nms5(std::shared_ptr & node, ngraph::element::Type to, size_t idx) { + if (auto nms = as_type_ptr(node)) { + nms->set_output_type(to); + return true; + } + return false; +} + bool fuse_type_to_topk(std::shared_ptr & node, ngraph::element::Type to, size_t idx) { if (auto topk = as_type_ptr(node)) { if (idx == 1 && (to == element::i32 || to == element::i64)) { From 730ec8a35eaf051bf94fa62eeee95be7dd33cdfa Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 27 Oct 2020 15:18:29 +0300 Subject: [PATCH 151/173] Fixes in the transformation NMS5 -> NMSIE3. --- .../transformations/convert_nms5_test.cpp | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp index c3aba0880fe847..27f787482b4996 100644 --- a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp +++ b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp @@ -102,7 +102,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticFiveInputs) { auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto one_dim_shape = Shape{1}; auto new_max_per_class = std::make_shared(max_output_boxes_per_class, @@ -114,11 +113,8 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticFiveInputs) { auto new_score_threshold = std::make_shared(score_threshold, opset5::Constant::create(ngraph::element::i64, one_dim_shape, one_dim_shape), true); - auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, - opset5::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, - new_soft_nms_sigma, 0, true); + 0, true); nms->set_friendly_name("nms"); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); @@ -155,7 +151,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticFourInputs) { auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto one_dim_shape = Shape{1}; auto new_max_per_class = std::make_shared(max_output_boxes_per_class, @@ -167,11 +162,8 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticFourInputs) { auto new_score_threshold = std::make_shared(score_threshold, opset5::Constant::create(ngraph::element::i64, one_dim_shape, one_dim_shape), true); - auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, - opset5::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, - new_soft_nms_sigma, 0, true); + 0, true); nms->set_friendly_name("nms"); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); @@ -207,7 +199,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticThreeInputs) { auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto one_dim_shape = Shape{1}; auto new_max_per_class = std::make_shared(max_output_boxes_per_class, @@ -219,11 +210,8 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticThreeInputs) { auto new_score_threshold = std::make_shared(score_threshold, opset5::Constant::create(ngraph::element::i64, one_dim_shape, one_dim_shape), true); - auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, - opset5::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, - new_soft_nms_sigma, 0, true); + 0, true); nms->set_friendly_name("nms"); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); From 45da4c76f894e9d3f3a9cfa0021c75e65a60949c Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 28 Oct 2020 11:07:52 +0300 Subject: [PATCH 152/173] Fixes in the conversion of NMS-5 to NMSIE3. --- .../convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp index f892528ce092e8..ad37ba8f33f0cb 100644 --- a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp +++ b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp @@ -32,7 +32,6 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { const auto& arg2 = num_of_inputs > 2 ? new_args.at(2) : ngraph::opset5::Constant::create(element::i32, Shape{}, {0}); const auto& arg3 = num_of_inputs > 3 ? new_args.at(3) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); const auto& arg4 = num_of_inputs > 4 ? new_args.at(4) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); -// const auto& arg5 = num_of_inputs > 5 ? new_args.at(5) : ngraph::opset5::Constant::create(element::f32, Shape{}, {.0f}); // vector of new nGraph operations NodeVector new_ops; @@ -58,9 +57,6 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { new_score_threshold = std::make_shared(arg4, new_shape_for_score_threshold, true); new_ops.push_back(new_score_threshold.get_node_shared_ptr()); -// new_soft_nms_sigma = std::make_shared(arg5, new_shape_for_soft_nms_sigma, true); -// new_ops.push_back(new_soft_nms_sigma.get_node_shared_ptr()); - int center_point_box = 0; switch (nms_5->get_box_encoding()) { case ::ngraph::opset5::NonMaxSuppression::BoxEncodingType::CENTER: @@ -76,7 +72,7 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { std::shared_ptr nms_legacy{nullptr}; - if (num_of_inputs > 5) { + if (num_of_inputs > 5 && nms_5->soft_nms_sigma_from_input() != 0.0f) { new_soft_nms_sigma = std::make_shared(new_args.at(5), new_shape_for_soft_nms_sigma, true); new_ops.push_back(new_soft_nms_sigma.get_node_shared_ptr()); nms_legacy = std::make_shared( @@ -103,7 +99,6 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { new_ops.push_back(nms_legacy); } - nms_legacy->set_friendly_name(nms_5->get_friendly_name()); ngraph::copy_runtime_info(nms_5, new_ops); ngraph::replace_node(nms_5, nms_legacy); From b7f3de0799675187d86197b3f03c8159ede88e7f Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 28 Oct 2020 11:08:55 +0300 Subject: [PATCH 153/173] Fixes in MKLDNN NMS ctor. --- .../nodes/non_max_suppression.cpp | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp b/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp index 9b943f78021d9e..e899adcda4f0ff 100644 --- a/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp +++ b/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp @@ -20,11 +20,10 @@ class NonMaxSuppressionImpl: public ExtLayerBase { public: explicit NonMaxSuppressionImpl(const CNNLayer* layer) { try { - if (layer->insData.size() < 2 || layer->insData.size() > 5) - THROW_IE_EXCEPTION << layer->name << " Incorrect number of input edges!"; - - if (layer->outData.size() > 3) - THROW_IE_EXCEPTION << layer->name << " Incorrect number of output edges!"; + if (layer->insData.size() < 2 || layer->insData.size() > 6) + THROW_IE_EXCEPTION << "NMS" << "has incorrect number of input edges: " << layer->insData.size(); + if (layer->outData.size() < 1 || layer->outData.size() > 3) + THROW_IE_EXCEPTION << "NMS" << "has incorrect number of output edges: " << layer->outData.size(); if (layer->insData[NMS_BOXES].lock()->getTensorDesc().getPrecision() != Precision::FP32) THROW_IE_EXCEPTION << layer->name << " Incorrect 'boxes' input precision. Only FP32 is supported!"; @@ -73,21 +72,28 @@ class NonMaxSuppressionImpl: public ExtLayerBase { if (selected_indices_dims.size() != 2 || selected_indices_dims[1] != 3) THROW_IE_EXCEPTION << layer->name << " 'selected_indices' should be with shape [num_selected_indices, 3]"; - center_point_box = layer->GetParamAsBool("center_point_box", false); - sort_result_descending = layer->GetParamAsBool("sort_result_descending", true); - - if (layer->insData.size() == 2) { - addConfig(layer, { DataConfigurator(ConfLayout::PLN), DataConfigurator(ConfLayout::PLN) }, { DataConfigurator(ConfLayout::PLN) }); - } else if (layer->insData.size() == 3) { - addConfig(layer, { DataConfigurator(ConfLayout::PLN), DataConfigurator(ConfLayout::PLN), DataConfigurator(ConfLayout::PLN) }, - { DataConfigurator(ConfLayout::PLN) }); - } else if (layer->insData.size() == 4) { - addConfig(layer, { DataConfigurator(ConfLayout::PLN), DataConfigurator(ConfLayout::PLN), DataConfigurator(ConfLayout::PLN), - DataConfigurator(ConfLayout::PLN) }, { DataConfigurator(ConfLayout::PLN) }); - } else { - addConfig(layer, { DataConfigurator(ConfLayout::PLN), DataConfigurator(ConfLayout::PLN), DataConfigurator(ConfLayout::PLN), - DataConfigurator(ConfLayout::PLN), DataConfigurator(ConfLayout::PLN) }, { DataConfigurator(ConfLayout::PLN) }); + LayerConfig config; + for (size_t i = 0; i < layer->insData.size(); i++) { + DataConfig inConfig; + Precision inPrecision = Precision::FP32; + if (i == NMS_MAXOUTPUTBOXESPERCLASS) + inPrecision = Precision::I32; + const SizeVector& inDims = layer->insData[i].lock()->getTensorDesc().getDims(); + inConfig.desc = TensorDesc(inPrecision, inDims, InferenceEngine::TensorDesc::getLayoutByDims(inDims)); + config.inConfs.push_back(inConfig); + } + for (size_t i = 0; i < layer->outData.size(); i++) { + DataConfig outConfig; + Precision outPrecision = Precision::I32; + if (i == 1) + outPrecision = Precision::FP32; + const SizeVector& outDims = layer->outData[i]->getTensorDesc().getDims(); + outConfig.desc = TensorDesc(outPrecision, outDims, InferenceEngine::TensorDesc::getLayoutByDims(outDims)); + config.outConfs.push_back(outConfig); } + + config.dynBatchSupport = false; + confs.push_back(config); } catch (InferenceEngine::details::InferenceEngineException &ex) { errorMsg = ex.what(); } From 230ed5f64059d23a93ce077764afaed6ba04611c Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 28 Oct 2020 11:09:32 +0300 Subject: [PATCH 154/173] Small fix. --- .../transformations/src/transformations/convert_precision.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/inference-engine/src/transformations/src/transformations/convert_precision.cpp b/inference-engine/src/transformations/src/transformations/convert_precision.cpp index 10112e7f1cbf52..c08fe61dbd841f 100644 --- a/inference-engine/src/transformations/src/transformations/convert_precision.cpp +++ b/inference-engine/src/transformations/src/transformations/convert_precision.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include From 29eca217a46df83904b51e7b486df3e299534b2c Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 28 Oct 2020 11:11:23 +0300 Subject: [PATCH 155/173] Fixed tests for the transformation NMS5 -> NMSIE3. --- .../transformations/convert_nms5_test.cpp | 54 ++++--------------- 1 file changed, 9 insertions(+), 45 deletions(-) diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp index 27f787482b4996..6ba473952a4bf4 100644 --- a/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp +++ b/inference-engine/tests/functional/inference_engine/transformations/convert_nms5_test.cpp @@ -246,7 +246,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticTwoInputs) { auto max_output_boxes_per_class = opset5::Constant::create(element::i32, Shape{}, {0}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto one_dim_shape = Shape{1}; auto new_max_per_class = std::make_shared(max_output_boxes_per_class, @@ -258,11 +257,8 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEStaticTwoInputs) { auto new_score_threshold = std::make_shared(score_threshold, opset5::Constant::create(ngraph::element::i64, one_dim_shape, one_dim_shape), true); - auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, - opset5::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, - new_soft_nms_sigma, 0, true); + 0, true); nms->set_friendly_name("nms"); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); @@ -356,7 +352,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1FiveInputs) { auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto one_dim_shape = Shape{1}; auto new_max_per_class = std::make_shared(max_output_boxes_per_class, @@ -368,11 +363,8 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1FiveInputs) { auto new_score_threshold = std::make_shared(score_threshold, opset5::Constant::create(ngraph::element::i64, one_dim_shape, one_dim_shape), true); - auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, - opset5::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, - new_soft_nms_sigma, 0, true); + 0, true); nms->set_friendly_name("nms"); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); @@ -409,7 +401,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1FourInputs) { auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto one_dim_shape = Shape{1}; auto new_max_per_class = std::make_shared(max_output_boxes_per_class, @@ -421,11 +412,8 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1FourInputs) { auto new_score_threshold = std::make_shared(score_threshold, opset5::Constant::create(ngraph::element::i64, one_dim_shape, one_dim_shape), true); - auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, - opset5::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, - new_soft_nms_sigma, 0, true); + 0, true); nms->set_friendly_name("nms"); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); @@ -461,7 +449,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1ThreeInputs) { auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto one_dim_shape = Shape{1}; auto new_max_per_class = std::make_shared(max_output_boxes_per_class, @@ -473,11 +460,8 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1ThreeInputs) { auto new_score_threshold = std::make_shared(score_threshold, opset5::Constant::create(ngraph::element::i64, one_dim_shape, one_dim_shape), true); - auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, - opset5::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, - new_soft_nms_sigma, 0, true); + 0, true); nms->set_friendly_name("nms"); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); @@ -512,7 +496,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1TwoInputs) { auto max_output_boxes_per_class = opset5::Constant::create(element::i32, Shape{}, {0}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto one_dim_shape = Shape{1}; auto new_max_per_class = std::make_shared(max_output_boxes_per_class, @@ -524,11 +507,8 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic1TwoInputs) { auto new_score_threshold = std::make_shared(score_threshold, opset5::Constant::create(ngraph::element::i64, one_dim_shape, one_dim_shape), true); - auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, - opset5::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, - new_soft_nms_sigma, 0, true); + 0, true); nms->set_friendly_name("nms"); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); @@ -619,7 +599,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2FiveInputs) { auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto one_dim_shape = Shape{1}; auto new_max_per_class = std::make_shared(max_output_boxes_per_class, @@ -631,11 +610,8 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2FiveInputs) { auto new_score_threshold = std::make_shared(score_threshold, opset5::Constant::create(ngraph::element::i64, one_dim_shape, one_dim_shape), true); - auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, - opset5::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, - new_soft_nms_sigma, 0, true); + 0, true); nms->set_friendly_name("nms"); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); @@ -671,7 +647,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2FourInputs) { auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto one_dim_shape = Shape{1}; auto new_max_per_class = std::make_shared(max_output_boxes_per_class, @@ -683,11 +658,8 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2FourInputs) { auto new_score_threshold = std::make_shared(score_threshold, opset5::Constant::create(ngraph::element::i64, one_dim_shape, one_dim_shape), true); - auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, - opset5::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, - new_soft_nms_sigma, 0, true); + 0, true); nms->set_friendly_name("nms"); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); @@ -722,7 +694,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2ThreeInputs) { auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto one_dim_shape = Shape{1}; auto new_max_per_class = std::make_shared(max_output_boxes_per_class, @@ -734,11 +705,8 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2ThreeInputs) { auto new_score_threshold = std::make_shared(score_threshold, opset5::Constant::create(ngraph::element::i64, one_dim_shape, one_dim_shape), true); - auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, - opset5::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, - new_soft_nms_sigma, 0, true); + 0, true); nms->set_friendly_name("nms"); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); @@ -772,7 +740,6 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2TwoInputs) { auto max_output_boxes_per_class = opset5::Constant::create(element::i32, Shape{}, {0}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); - auto soft_nms_sigma = ngraph::opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto one_dim_shape = Shape{1}; auto new_max_per_class = std::make_shared(max_output_boxes_per_class, @@ -784,11 +751,8 @@ TEST(TransformationTests, ConvertNMS5ToNMSIEDynamic2TwoInputs) { auto new_score_threshold = std::make_shared(score_threshold, opset5::Constant::create(ngraph::element::i64, one_dim_shape, one_dim_shape), true); - auto new_soft_nms_sigma = std::make_shared(soft_nms_sigma, - opset5::Constant::create(ngraph::element::i64, one_dim_shape, - one_dim_shape), true); auto nms = std::make_shared(boxes, scores, new_max_per_class, new_iou_threshold, new_score_threshold, - new_soft_nms_sigma, 0, true); + 0, true); nms->set_friendly_name("nms"); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); From 98b402f05f17ae3d1c35db8f399f5a95527d41b8 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Wed, 28 Oct 2020 11:12:40 +0300 Subject: [PATCH 156/173] Fixed tests for conversions NMS-1, NMS-3, NMS-4 -> NMS-5. --- .../convert_previous_nms_to_nms_5.cpp | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/inference-engine/tests/functional/inference_engine/transformations/convert_previous_nms_to_nms_5.cpp b/inference-engine/tests/functional/inference_engine/transformations/convert_previous_nms_to_nms_5.cpp index 0f96d32b690c49..a1120f41c419b4 100644 --- a/inference-engine/tests/functional/inference_engine/transformations/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/tests/functional/inference_engine/transformations/convert_previous_nms_to_nms_5.cpp @@ -49,9 +49,8 @@ TEST(TransformationTests, ConvertNMS4FiveInputsToNMS5) { auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); - auto soft_nms_sigma = opset5::Constant::create(element::f32, Shape{}, {.0f}); auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, - soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); } @@ -82,9 +81,8 @@ TEST(TransformationTests, ConvertNMS4TwoInputsToNMS5) { auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {0}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); - auto soft_nms_sigma = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, - soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); } @@ -119,9 +117,8 @@ TEST(TransformationTests, ConvertNMS3FiveInputsToNMS5) { auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); - auto soft_nms_sigma = opset5::Constant::create(element::f32, Shape{}, {.0f}); auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, - soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); } @@ -152,9 +149,8 @@ TEST(TransformationTests, ConvertNMS3TwoInputsToNMS5) { auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {0}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); - auto soft_nms_sigma = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, - soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); } @@ -189,9 +185,8 @@ TEST(TransformationTests, ConvertNMS1FiveInputsToNMS5) { auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {10}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.75}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.7}); - auto soft_nms_sigma = opset5::Constant::create(element::f32, Shape{}, {.0f}); auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, - soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); } @@ -222,9 +217,8 @@ TEST(TransformationTests, ConvertNMS1TwoInputsToNMS5) { auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {0}); auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.0f}); - auto soft_nms_sigma = opset5::Constant::create(element::f32, Shape{}, {0.0f}); auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, - soft_nms_sigma, opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true); f_ref = std::make_shared(NodeVector{nms}, ParameterVector{boxes, scores}); } From fbba3a3006b12f56748d793a788574dcee1aa77a Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 29 Oct 2020 10:31:15 +0300 Subject: [PATCH 157/173] Small fixes in MKLDNN NMS ctor. --- .../nodes/non_max_suppression.cpp | 2 + .../non_max_suppression_tests.cpp | 322 ++++++++++++++++-- .../functional_test_utils/network_utils.cpp | 13 +- 3 files changed, 307 insertions(+), 30 deletions(-) diff --git a/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp b/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp index e899adcda4f0ff..e7f293075a2a7b 100644 --- a/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp +++ b/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp @@ -72,6 +72,8 @@ class NonMaxSuppressionImpl: public ExtLayerBase { if (selected_indices_dims.size() != 2 || selected_indices_dims[1] != 3) THROW_IE_EXCEPTION << layer->name << " 'selected_indices' should be with shape [num_selected_indices, 3]"; + center_point_box = layer->GetParamAsBool("center_point_box", false); + sort_result_descending = layer->GetParamAsBool("sort_result_descending", true); LayerConfig config; for (size_t i = 0; i < layer->insData.size(); i++) { DataConfig inConfig; diff --git a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp index 4cfcb6a978f055..c5a434badfe1e9 100644 --- a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp +++ b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp @@ -133,6 +133,32 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { + + + + 15130 + 3 + + + 15130 + 3 + + + + + 15130 + 3 + + + + + + + 15130 + 3 + + + @@ -143,10 +169,13 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { + + + )V0G0N"; @@ -201,17 +230,7 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { - - - - 1 - - - - - - - + @@ -233,25 +252,22 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { 1 - - 1 - - + 16000 3 - + 16000 3 - + 1 - + @@ -270,7 +286,7 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { - + @@ -286,18 +302,38 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { + + + + + 16000 + 3 + + + 16000 + 3 + + + + + 16000 + 3 + + + - - - - - - - - - - + + + + + + + + + + + )V0G0N"; @@ -312,3 +348,231 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { fp32w[4] = 0.0; }); } + +//TEST_F(NGraphReaderTests, ReadNonMaxSuppression4) { +// std::string model = R"V0G0N( +// +// +// +// +// +// +// 1 +// 15130 +// 4 +// +// +// +// +// +// +// +// 1 +// 80 +// 15130 +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// 1 +// 15130 +// 4 +// +// +// 1 +// 80 +// 15130 +// +// +// +// +// +// +// +// 16000 +// 3 +// +// +// +// +// +// +// 16000 +// 3 +// +// +// 16000 +// 3 +// +// +// +// +// 16000 +// 3 +// +// +// +// +// +// +// 16000 +// 3 +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//)V0G0N"; +// std::string modelV5 = R"V0G0N( +// +// +// +// +// +// 1 +// 15130 +// 4 +// +// +// +// +// +// +// 1 +// 80 +// 15130 +// +// +// +// +// +// +// 1 +// +// +// +// +// +// +// +// +// +// 1 +// +// +// +// +// +// +// +// +// +// 1 +// +// +// +// +// +// +// +// +// +// +// 1 +// 15130 +// 4 +// +// +// 1 +// 80 +// 15130 +// +// +// 1 +// +// +// 1 +// +// +// 1 +// +// +// +// +// 16000 +// 3 +// +// +// +// +// +// +// +// 16000 +// 3 +// +// +// 16000 +// 3 +// +// +// +// +// 16000 +// 3 +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//)V0G0N"; +// +// compareIRs(model, modelV5, 16, [](Blob::Ptr& weights) { +// auto * i64w = weights->buffer().as(); +// i64w[0] = 200; +// +// auto * fp32w = weights->buffer().as(); +// fp32w[2] = 0.5; +// fp32w[3] = 0.05; +// }); +//} diff --git a/inference-engine/tests/ie_test_utils/functional_test_utils/network_utils.cpp b/inference-engine/tests/ie_test_utils/functional_test_utils/network_utils.cpp index 83709dac917a5a..2fc65edcdb3519 100644 --- a/inference-engine/tests/ie_test_utils/functional_test_utils/network_utils.cpp +++ b/inference-engine/tests/ie_test_utils/functional_test_utils/network_utils.cpp @@ -202,8 +202,19 @@ namespace FuncTestUtils { break; success = success && old_info.find(it.first) != old_info.end(); } - if (!success) + if (!success) { + std::cout << "new output: "; + for (const auto &p : new_info) { + std::cout << p.first << " "; + } + std::cout << "\n"; + std::cout << "old output: "; + for (const auto &p : old_info) { + std::cout << p.first << " "; + } + std::cout << "\n"; THROW_IE_EXCEPTION << err_msg; + } } void From 259ba7fdc163d9cae738e3920ca47567fca71a91 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 29 Oct 2020 17:38:40 +0300 Subject: [PATCH 158/173] Rewritten create_layer() functions for NMS-5 and NMSIE3 as addSpecificCreator() functions. --- .../src/convert_function_to_cnn_network.cpp | 69 +++++++++++++++++- .../src/ie_cnn_layer_builder_ngraph.cpp | 70 ------------------- .../non_max_suppression_tests.cpp | 2 +- 3 files changed, 69 insertions(+), 72 deletions(-) diff --git a/inference-engine/src/legacy_api/src/convert_function_to_cnn_network.cpp b/inference-engine/src/legacy_api/src/convert_function_to_cnn_network.cpp index b6659bd2d39559..87e09cd531fd04 100644 --- a/inference-engine/src/legacy_api/src/convert_function_to_cnn_network.cpp +++ b/inference-engine/src/legacy_api/src/convert_function_to_cnn_network.cpp @@ -539,8 +539,76 @@ InferenceEngine::details::CNNLayerCreator::CNNLayerCreator(const std::shared_ptr const std::map& params) -> CNNLayerPtr { LayerParams attrs = {node->get_friendly_name(), "NonMaxSuppression", details::convertPrecision(node->get_output_element_type(0))}; + + auto castedLayer = ::ngraph::as_type_ptr<::ngraph::op::NonMaxSuppressionIE3>(node); + IE_ASSERT(castedLayer) << " Operation " << node->description() << " with name " + << node->get_friendly_name() << " cannot be casted to ngraph::op::NonMaxSuppressionIE3"; + + auto res = std::make_shared(attrs); + res->params = params; + + res->params["center_point_box"] = castedLayer->m_center_point_box ? "true" : "false"; + res->params["sort_result_descending"] = castedLayer->m_sort_result_descending ? "true" : "false"; + + auto output_type = details::convertPrecision(castedLayer->m_output_type); + std::string output_type_str; + switch (output_type) { + case Precision::I32: + output_type_str = "I32"; + break; + case Precision::I64: + output_type_str = "I64"; + break; + default: + THROW_IE_EXCEPTION << "Unsupported output type"; + } + res->params["output_type"] = output_type_str; + + return res; + }); + + addSpecificCreator({"NonMaxSuppression"}, [](const std::shared_ptr<::ngraph::Node>& node, + const std::map& params) -> CNNLayerPtr { + LayerParams attrs = {node->get_friendly_name(), "NonMaxSuppression", + details::convertPrecision(node->get_output_element_type(0))}; + + auto castedLayer = ::ngraph::as_type_ptr<::ngraph::op::v5::NonMaxSuppression>(node); + IE_ASSERT(castedLayer) << " Operation " << node->description() << " with name " + << node->get_friendly_name() << " cannot be casted to ngraph::op::v5::NonMaxSuppression"; + auto res = std::make_shared(attrs); res->params = params; + + auto box_encoding = castedLayer->get_box_encoding(); + switch (box_encoding) { + case ngraph::op::v5::NonMaxSuppression::BoxEncodingType::CORNER: + res->params["center_point_box"] = "false"; + break; + case ngraph::op::v5::NonMaxSuppression::BoxEncodingType::CENTER: + res->params["center_point_box"] = "true"; + break; + default: + THROW_IE_EXCEPTION << "Unsupported box encoding for NonMaxSuppression op"; + break; + } + + auto output_type = details::convertPrecision(castedLayer->get_output_type()); + std::string output_type_str; + switch (output_type) { + case Precision::I32: + output_type_str = "I32"; + break; + case Precision::I64: + output_type_str = "I64"; + break; + default: + THROW_IE_EXCEPTION << "Unsupported output type"; + } + res->params["output_type"] = output_type_str; + + bool sort_result_descending = castedLayer->get_sort_result_descending(); + res->params["sort_result_descending"] = sort_result_descending ? "true" : "false"; + return res; }); @@ -823,7 +891,6 @@ void convertFunctionToICNNNetwork(const std::shared_ptr>(), std::make_shared>(), std::make_shared>(), - std::make_shared>(), std::make_shared>(), }; CNNLayerPtr result; diff --git a/inference-engine/src/legacy_api/src/ie_cnn_layer_builder_ngraph.cpp b/inference-engine/src/legacy_api/src/ie_cnn_layer_builder_ngraph.cpp index 7ce5f648926f72..e846c646a773b3 100644 --- a/inference-engine/src/legacy_api/src/ie_cnn_layer_builder_ngraph.cpp +++ b/inference-engine/src/legacy_api/src/ie_cnn_layer_builder_ngraph.cpp @@ -1726,76 +1726,6 @@ CNNLayer::Ptr NodeConverter::createLayer(const std: THROW_IE_EXCEPTION << "Interpolate operation should be converted to Interp"; } -template <> -CNNLayer::Ptr NodeConverter::createLayer(const std::shared_ptr& layer) const { - LayerParams params = {layer->get_friendly_name(), "NonMaxSuppressionIE3", - details::convertPrecision(layer->get_output_element_type(0))}; - auto castedLayer = ngraph::as_type_ptr(layer); - if (castedLayer == nullptr) THROW_IE_EXCEPTION << "Cannot get " << params.type << " layer " << params.name; - - auto res = std::make_shared(params); - res->params["center_point_box"] = asString(castedLayer->m_center_point_box); - res->params["sort_result_descending"] = castedLayer->m_sort_result_descending ? "1" : "0"; - - auto output_type = details::convertPrecision(castedLayer->m_output_type); - std::string output_type_str; - switch (output_type) { - case Precision::I32: - output_type_str = "I32"; - break; - case Precision::I64: - output_type_str = "I64"; - break; - default: - THROW_IE_EXCEPTION << "Unsupported output type"; - } - res->params["output_type"] = output_type_str; - - return res; -} - -template <> -CNNLayer::Ptr NodeConverter::createLayer(const std::shared_ptr& layer) const { - LayerParams params = {layer->get_friendly_name(), "NonMaxSuppressionIE3", - details::convertPrecision(layer->get_output_element_type(0))}; - auto castedLayer = ngraph::as_type_ptr(layer); - if (castedLayer == nullptr) THROW_IE_EXCEPTION << "Cannot get " << params.type << " layer " << params.name; - - auto box_encoding = castedLayer->get_box_encoding(); - - auto res = std::make_shared(params); - - switch (box_encoding) { - case ngraph::op::v5::NonMaxSuppression::BoxEncodingType::CORNER: - res->params["center_point_box"] = "0"; - break; - case ngraph::op::v5::NonMaxSuppression::BoxEncodingType::CENTER: - res->params["center_point_box"] = "1"; - break; - default: - THROW_IE_EXCEPTION << "Unsupported box encoding for NonMaxSuppression op"; - break; - } - - auto output_type = details::convertPrecision(castedLayer->get_output_type()); - std::string output_type_str; - switch (output_type) { - case Precision::I32: - output_type_str = "I32"; - break; - case Precision::I64: - output_type_str = "I64"; - break; - default: - THROW_IE_EXCEPTION << "Unsupported output type"; - } - res->params["output_type"] = output_type_str; - - bool sort_result_descending = castedLayer->get_sort_result_descending(); - res->params["sort_result_descending"] = sort_result_descending ? "1" : "0"; - return res; -} - template <> CNNLayer::Ptr NodeConverter::createLayer(const std::shared_ptr& layer) const { LayerParams params = {layer->get_friendly_name(), "Interpolate", diff --git a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp index c5a434badfe1e9..deec162a6cfdc4 100644 --- a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp +++ b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp @@ -231,7 +231,7 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { - + 1 From 13df494ad45065677716a15e40b26c4c197a5991 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 30 Oct 2020 16:38:47 +0300 Subject: [PATCH 159/173] Disabled tests for IE IR reader for NMS-4. --- .../non_max_suppression_tests.cpp | 525 ++++++++++-------- 1 file changed, 298 insertions(+), 227 deletions(-) diff --git a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp index deec162a6cfdc4..eaa96c33397b2d 100644 --- a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp +++ b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp @@ -4,6 +4,15 @@ #include #include "ngraph_reader_tests.hpp" +#include +#include + +#include "generic_ie.hpp" +#include +#include +#include + +#include "legacy/convert_function_to_cnn_network.hpp" TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { std::string model = R"V0G0N( @@ -349,230 +358,292 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { }); } -//TEST_F(NGraphReaderTests, ReadNonMaxSuppression4) { -// std::string model = R"V0G0N( -// -// -// -// -// -// -// 1 -// 15130 -// 4 -// -// -// -// -// -// -// -// 1 -// 80 -// 15130 -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// 1 -// 15130 -// 4 -// -// -// 1 -// 80 -// 15130 -// -// -// -// -// -// -// -// 16000 -// 3 -// -// -// -// -// -// -// 16000 -// 3 -// -// -// 16000 -// 3 -// -// -// -// -// 16000 -// 3 -// -// -// -// -// -// -// 16000 -// 3 -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -//)V0G0N"; -// std::string modelV5 = R"V0G0N( -// -// -// -// -// -// 1 -// 15130 -// 4 -// -// -// -// -// -// -// 1 -// 80 -// 15130 -// -// -// -// -// -// -// 1 -// -// -// -// -// -// -// -// -// -// 1 -// -// -// -// -// -// -// -// -// -// 1 -// -// -// -// -// -// -// -// -// -// -// 1 -// 15130 -// 4 -// -// -// 1 -// 80 -// 15130 -// -// -// 1 -// -// -// 1 -// -// -// 1 -// -// -// -// -// 16000 -// 3 -// -// -// -// -// -// -// -// 16000 -// 3 -// -// -// 16000 -// 3 -// -// -// -// -// 16000 -// 3 -// -// -// -// -// -// -// -// -// -// -// -// -// -// -//)V0G0N"; -// -// compareIRs(model, modelV5, 16, [](Blob::Ptr& weights) { -// auto * i64w = weights->buffer().as(); -// i64w[0] = 200; -// -// auto * fp32w = weights->buffer().as(); -// fp32w[2] = 0.5; -// fp32w[3] = 0.05; -// }); -//} +static InferenceEngine::details::CNNNetworkImplPtr read_network_v10(const std::string& model, Blob::Ptr weights) { + Core ie; + auto result = std::make_shared(); + + auto ngraphImpl = ie.ReadNetwork(model, weights); + auto graph = ngraph::clone_function(*ngraphImpl.getFunction()); + + ::ngraph::op::GenericIE::DisableReshape noReshape(graph); + + ::ngraph::pass::Manager manager; + manager.register_pass<::ngraph::pass::InitNodeInfo>(); + // WA: ConvertPriorBox must be executed before the 1st ConstantFolding pass + manager.register_pass<::ngraph::pass::ConvertPriorBox>(); + manager.register_pass<::ngraph::pass::CommonOptimizations>(); + manager.run_passes(graph); + + IE_SUPPRESS_DEPRECATED_START + InferenceEngine::details::convertFunctionToICNNNetwork(graph, ngraphImpl, result.get(), false); + IE_SUPPRESS_DEPRECATED_END + + return result; +} + +static void compareIRsV10(const std::string& model, const std::string& ref, size_t weightsSize = 0, const std::function& fillBlob = {}) { + Core ie; + Blob::Ptr weights; + + if (weightsSize) { + weights = make_shared_blob(TensorDesc(Precision::U8, {weightsSize}, Layout::C)); + weights->allocate(); + CommonTestUtils::fill_data(weights->buffer().as(), weights->size() / sizeof(float)); + if (fillBlob) + fillBlob(weights); + } + + auto network = read_network_v10(model, weights); + auto cnnNetwork = ie.ReadNetwork(ref, weights); + + IE_SUPPRESS_DEPRECATED_START + FuncTestUtils::compareCNNNetworks(InferenceEngine::CNNNetwork(network), cnnNetwork, false); + + for (auto it = details::CNNNetworkIterator(network.get()); it != details::CNNNetworkIterator(); it++) { + InferenceEngine::CNNLayerPtr layer = *it; + ASSERT_NE(nullptr, layer->getNode()); + } + + ASSERT_EQ(nullptr, cnnNetwork.getFunction()); + for (auto it = details::CNNNetworkIterator(cnnNetwork); it != details::CNNNetworkIterator(); it++) { + InferenceEngine::CNNLayerPtr layer = *it; + ASSERT_EQ(nullptr, layer->getNode()); + } + IE_SUPPRESS_DEPRECATED_END +} + +// When IRv10 model contains NMS-1, NMS-3, or NMS-4, we read the network, and then CNNNetworkImpl constructor +// performs all nGraph transformations, and converts a resulting graph to legacy IR. In particular, NMS-1, NMS-3, NMS-4 +// are converted into NMS-5, and then NMS-5 will be converted into the inner operation NMSIE3. According to +// specifications, each of operations NMS-1, NMS-3, NMS-4 has one output port, but NMS-5 has three output ports, +// and one or two output ports can be disconnected. Hence, we will get legacy IR with CNNLayer "NonMaxSuppression" +// with three output ports and with one element in OutputDataMap. Hence, we can create neither reference legacy IR with +// one output, nor legacy IR with three outputs, because the function that compares instances of CNNLayer, uses +// a comparison of the number of all output ports, not only connected ports. Moreover, we cannot creates IRs V10 +// with NMS-5, because output shapes of NMS-5 are dynamic. +TEST_F(NGraphReaderTests, DISABLED_ReadNonMaxSuppression4) { + std::string model = R"V0G0N( + + + + + + + 1 + 15130 + 4 + + + + + + + + 1 + 80 + 15130 + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 15130 + 4 + + + 1 + 80 + 15130 + + + + + + + + 16000 + 3 + + + + + + + 16000 + 3 + + + 16000 + 3 + + + + + 16000 + 3 + + + + + + + 16000 + 3 + + + + + + + + + + + + + + + +)V0G0N"; + std::string refModel = R"V0G0N( + + + + + + + 1 + 15130 + 4 + + + + + + + + 1 + 80 + 15130 + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 15130 + 4 + + + 1 + 80 + 15130 + + + + + + + + 16000 + 3 + + + 16000 + 3 + + + 1 + + + + + + + 16000 + 3 + + + 16000 + 3 + + + + + 16000 + 3 + + + + + + + 16000 + 3 + + + + + + + + + + + + + + + +)V0G0N"; + + compareIRsV10(model, refModel, 16, [](Blob::Ptr& weights) { + auto * i64w = weights->buffer().as(); + i64w[0] = 200; + + auto * fp32w = weights->buffer().as(); + fp32w[2] = 0.5; + fp32w[3] = 0.05; + }); +} From d82f3e1c9e2d6ea36b7ec0d025f465f8a5b8b869 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 3 Nov 2020 15:12:27 +0300 Subject: [PATCH 160/173] Deleted debug code. --- .../functional_test_utils/network_utils.cpp | 30 +------------------ 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/inference-engine/tests/ie_test_utils/functional_test_utils/network_utils.cpp b/inference-engine/tests/ie_test_utils/functional_test_utils/network_utils.cpp index 2fc65edcdb3519..8f4a507eec94ed 100644 --- a/inference-engine/tests/ie_test_utils/functional_test_utils/network_utils.cpp +++ b/inference-engine/tests/ie_test_utils/functional_test_utils/network_utils.cpp @@ -119,23 +119,6 @@ namespace FuncTestUtils { if (refLayer->outData[i]->getTensorDesc() != layer->outData[i]->getTensorDesc()) { err_log.push_back("Layer " + layer->name + " and ref layer " + refLayer->name + " have different tensor desc for out Data"); - std::cout << "For output " << i << "\n"; - auto refDescriptor = refLayer->outData[i]->getTensorDesc(); - auto layerDescriptor = layer->outData[i]->getTensorDesc(); - std::cout << "ref precision: " << refDescriptor.getPrecision() << "\n"; - std::cout << "layer precision: " << layerDescriptor.getPrecision() << "\n"; - auto refDims = refDescriptor.getDims(); - auto layerDims = layerDescriptor.getDims(); - std::cout <<"ref dims: "; - for (size_t d : refDims) { - std::cout << d << " "; - } - std::cout << "\n"; - std::cout <<"layer dims: "; - for (size_t d : layerDims) { - std::cout << d << " "; - } - std::cout << "\n"; } success = success && refLayer->outData[i]->getTensorDesc() == layer->outData[i]->getTensorDesc(); } @@ -202,19 +185,8 @@ namespace FuncTestUtils { break; success = success && old_info.find(it.first) != old_info.end(); } - if (!success) { - std::cout << "new output: "; - for (const auto &p : new_info) { - std::cout << p.first << " "; - } - std::cout << "\n"; - std::cout << "old output: "; - for (const auto &p : old_info) { - std::cout << p.first << " "; - } - std::cout << "\n"; + if (!success) THROW_IE_EXCEPTION << err_msg; - } } void From 007b895c5cec8f75f4568c79591c9f1a62a0cd96 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 3 Nov 2020 15:31:37 +0300 Subject: [PATCH 161/173] Added comment about disabling tests IE_CPU.nonmaxsuppression. --- ngraph/test/runtime/ie/unit_test.manifest | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ngraph/test/runtime/ie/unit_test.manifest b/ngraph/test/runtime/ie/unit_test.manifest index 9055a893f2543a..3d66b925a56e9a 100644 --- a/ngraph/test/runtime/ie/unit_test.manifest +++ b/ngraph/test/runtime/ie/unit_test.manifest @@ -1136,7 +1136,16 @@ IE_CPU.log_softmax_2d_axis_neg2 IE_CPU.log_softmax_3d_axis_0 IE_CPU.log_softmax_3d_axis_neg3 -# Unsupported NMS-5 +# Next nine tests fails in CPU for the following reason. The nGraph function +# for NMS-5 are passed to the method compile() of the backend, but this +# method doesn't apply any nGraph transformations to the passed function, +# and the plugin backend gets CNNNetwork with NMS-5, NMS-5 has dynamic shapes +# for two of three outputs, and results of these two outputs are interpreted +# as scalars. If we apply all needed nGraph transformations to the nGraph +# function with NMS-5 to get the nGraph function with NMSIE3 (internal +# operation, similar with NMS-5, but with all static output shapes), before +# the method compile() call, then tests for INTERPRETER backend for NMS-5 will +# fail, because NMSIE3 has not the reference implementation, but NMS-5 has one. IE_CPU.nonmaxsuppression_center_point_box_format IE_CPU.nonmaxsuppression_flipped_coordinates IE_CPU.nonmaxsuppression_identical_boxes From 2d7a6342e6c1eaafd3fd7e75b847bc5123bbf453 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Tue, 3 Nov 2020 18:01:03 +0300 Subject: [PATCH 162/173] Written IE IR Reader test for NMS-4. --- .../non_max_suppression_tests.cpp | 199 ++++-------------- 1 file changed, 39 insertions(+), 160 deletions(-) diff --git a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp index eaa96c33397b2d..55e6b00091862e 100644 --- a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp +++ b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp @@ -4,16 +4,25 @@ #include #include "ngraph_reader_tests.hpp" + +#include #include +#include +#include +#include +#include +#include +#include +#include #include +#include "common_test_utils/ngraph_test_utils.hpp" #include "generic_ie.hpp" -#include -#include -#include #include "legacy/convert_function_to_cnn_network.hpp" +using namespace ngraph; + TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { std::string model = R"V0G0N( @@ -358,31 +367,16 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { }); } -static InferenceEngine::details::CNNNetworkImplPtr read_network_v10(const std::string& model, Blob::Ptr weights) { +static std::shared_ptr function_from_model(const std::string& model, Blob::Ptr weights) { Core ie; - auto result = std::make_shared(); auto ngraphImpl = ie.ReadNetwork(model, weights); auto graph = ngraph::clone_function(*ngraphImpl.getFunction()); - ::ngraph::op::GenericIE::DisableReshape noReshape(graph); - - ::ngraph::pass::Manager manager; - manager.register_pass<::ngraph::pass::InitNodeInfo>(); - // WA: ConvertPriorBox must be executed before the 1st ConstantFolding pass - manager.register_pass<::ngraph::pass::ConvertPriorBox>(); - manager.register_pass<::ngraph::pass::CommonOptimizations>(); - manager.run_passes(graph); - - IE_SUPPRESS_DEPRECATED_START - InferenceEngine::details::convertFunctionToICNNNetwork(graph, ngraphImpl, result.get(), false); - IE_SUPPRESS_DEPRECATED_END - - return result; + return graph; } -static void compareIRsV10(const std::string& model, const std::string& ref, size_t weightsSize = 0, const std::function& fillBlob = {}) { - Core ie; +static Blob::Ptr construct_weights(size_t weightsSize = 0, const std::function& fillBlob = {}) { Blob::Ptr weights; if (weightsSize) { @@ -393,35 +387,10 @@ static void compareIRsV10(const std::string& model, const std::string& ref, size fillBlob(weights); } - auto network = read_network_v10(model, weights); - auto cnnNetwork = ie.ReadNetwork(ref, weights); - - IE_SUPPRESS_DEPRECATED_START - FuncTestUtils::compareCNNNetworks(InferenceEngine::CNNNetwork(network), cnnNetwork, false); - - for (auto it = details::CNNNetworkIterator(network.get()); it != details::CNNNetworkIterator(); it++) { - InferenceEngine::CNNLayerPtr layer = *it; - ASSERT_NE(nullptr, layer->getNode()); - } - - ASSERT_EQ(nullptr, cnnNetwork.getFunction()); - for (auto it = details::CNNNetworkIterator(cnnNetwork); it != details::CNNNetworkIterator(); it++) { - InferenceEngine::CNNLayerPtr layer = *it; - ASSERT_EQ(nullptr, layer->getNode()); - } - IE_SUPPRESS_DEPRECATED_END + return weights; } -// When IRv10 model contains NMS-1, NMS-3, or NMS-4, we read the network, and then CNNNetworkImpl constructor -// performs all nGraph transformations, and converts a resulting graph to legacy IR. In particular, NMS-1, NMS-3, NMS-4 -// are converted into NMS-5, and then NMS-5 will be converted into the inner operation NMSIE3. According to -// specifications, each of operations NMS-1, NMS-3, NMS-4 has one output port, but NMS-5 has three output ports, -// and one or two output ports can be disconnected. Hence, we will get legacy IR with CNNLayer "NonMaxSuppression" -// with three output ports and with one element in OutputDataMap. Hence, we can create neither reference legacy IR with -// one output, nor legacy IR with three outputs, because the function that compares instances of CNNLayer, uses -// a comparison of the number of all output ports, not only connected ports. Moreover, we cannot creates IRs V10 -// with NMS-5, because output shapes of NMS-5 are dynamic. -TEST_F(NGraphReaderTests, DISABLED_ReadNonMaxSuppression4) { +TEST_F(NGraphReaderTests, ReadNonMaxSuppression4) { std::string model = R"V0G0N( @@ -506,122 +475,11 @@ TEST_F(NGraphReaderTests, DISABLED_ReadNonMaxSuppression4) { - - - 16000 - 3 - - - - - - - - - - - - - - - -)V0G0N"; - std::string refModel = R"V0G0N( - - - - - - - 1 - 15130 - 4 - - - - - - - - 1 - 80 - 15130 - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - 15130 - 4 - - - 1 - 80 - 15130 - - - - - - - - 16000 - 3 - - - 16000 - 3 - - - 1 - - - - 16000 3 - - 16000 - 3 - - - - - 16000 - 3 - - - - - - - 16000 - 3 - @@ -638,7 +496,7 @@ TEST_F(NGraphReaderTests, DISABLED_ReadNonMaxSuppression4) { )V0G0N"; - compareIRsV10(model, refModel, 16, [](Blob::Ptr& weights) { + auto weights = construct_weights(16, [](Blob::Ptr& weights) { auto * i64w = weights->buffer().as(); i64w[0] = 200; @@ -646,4 +504,25 @@ TEST_F(NGraphReaderTests, DISABLED_ReadNonMaxSuppression4) { fp32w[2] = 0.5; fp32w[3] = 0.05; }); + + auto graph = function_from_model(model, weights); + + ::ngraph::pass::Manager manager; + manager.register_pass<::ngraph::pass::InitNodeInfo>(); + manager.register_pass<::ngraph::pass::CommonOptimizations>(); + manager.run_passes(graph); + + auto boxes = std::make_shared(element::f32, Shape{1, 15130, 4}); + auto scores = std::make_shared(element::f32, Shape{1, 80, 15130}); + auto max_output_boxes_per_class = opset5::Constant::create(element::i64, Shape{}, {200}); + auto iou_threshold = opset5::Constant::create(element::f32, Shape{}, {0.5}); + auto score_threshold = opset5::Constant::create(element::f32, Shape{}, {0.05}); + auto nms = std::make_shared(boxes, scores, max_output_boxes_per_class, iou_threshold, score_threshold, + opset5::NonMaxSuppression::BoxEncodingType::CORNER, true, ngraph::element::i32); + + auto mul = std::make_shared(nms, nms); + auto graph_ref = std::make_shared(NodeVector{mul}, ParameterVector{boxes, scores}); + + auto res = compare_functions(graph, graph_ref); + ASSERT_TRUE(res.first) << res.second; } From 0e1e135bb5bf26d9d3a92fbf406f941ae054f5d6 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 5 Nov 2020 09:52:33 +0300 Subject: [PATCH 163/173] Deleted function float_from_constant_node. --- ngraph/core/src/op/non_max_suppression.cpp | 27 +++------------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 8adc25f0e16770..8afabca37cce04 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -709,24 +709,6 @@ namespace return is_scalar(shape) || (is_vector(shape) && (shape[0] == 1)); } - - float float_from_constant_node(const std::shared_ptr& constant, - const element::Type& et) - { - float result = 0.0f; - switch (et) - { - case element::Type_t::bf16: - result = static_cast(constant->cast_vector().at(0)); - break; - case element::Type_t::f16: - result = static_cast(constant->cast_vector().at(0)); - break; - case element::Type_t::f32: result = constant->cast_vector().at(0); break; - default: throw std::runtime_error("Unsupported data type in op NonMaxSuppression-5"); break; - } - return result; - } } void op::v5::NonMaxSuppression::validate() @@ -868,8 +850,7 @@ float op::v5::NonMaxSuppression::iou_threshold_from_input() const const auto iou_threshold_input = as_type_ptr(input_value(iou_threshold_port).get_node_shared_ptr()); - iou_threshold = - float_from_constant_node(iou_threshold_input, get_input_element_type(iou_threshold_port)); + iou_threshold = iou_threshold_input->cast_vector().at(0); return iou_threshold; } @@ -886,8 +867,7 @@ float op::v5::NonMaxSuppression::score_threshold_from_input() const const auto score_threshold_input = as_type_ptr(input_value(score_threshold_port).get_node_shared_ptr()); - score_threshold = float_from_constant_node(score_threshold_input, - get_input_element_type(score_threshold_port)); + score_threshold = score_threshold_input->cast_vector().at(0); return score_threshold; } @@ -904,8 +884,7 @@ float op::v5::NonMaxSuppression::soft_nms_sigma_from_input() const const auto soft_nms_sigma_input = as_type_ptr(input_value(soft_nms_sigma_port).get_node_shared_ptr()); - soft_nms_sigma = - float_from_constant_node(soft_nms_sigma_input, get_input_element_type(soft_nms_sigma_port)); + soft_nms_sigma = soft_nms_sigma_input->cast_vector().at(0); return soft_nms_sigma; } From c10979b6179e8f12825a5e32fb33b2f39e50485e Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 5 Nov 2020 13:40:41 +0300 Subject: [PATCH 164/173] Small fixes. --- .../src/legacy_api/src/ngraph_ops/nms_ie.cpp | 3 +-- ngraph/core/src/op/non_max_suppression.cpp | 14 ++++---------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/inference-engine/src/legacy_api/src/ngraph_ops/nms_ie.cpp b/inference-engine/src/legacy_api/src/ngraph_ops/nms_ie.cpp index dc9ffc52eaa79a..be26e1c291e151 100644 --- a/inference-engine/src/legacy_api/src/ngraph_ops/nms_ie.cpp +++ b/inference-engine/src/legacy_api/src/ngraph_ops/nms_ie.cpp @@ -153,8 +153,7 @@ int64_t op::NonMaxSuppressionIE3::max_boxes_output_from_input() const { int64_t max_output_boxes{0}; size_t num_of_inputs = inputs().size(); - if (num_of_inputs < 3) - { + if (num_of_inputs < 3) { return 0; } diff --git a/ngraph/core/src/op/non_max_suppression.cpp b/ngraph/core/src/op/non_max_suppression.cpp index 8afabca37cce04..f5b94ac263c70f 100644 --- a/ngraph/core/src/op/non_max_suppression.cpp +++ b/ngraph/core/src/op/non_max_suppression.cpp @@ -682,8 +682,6 @@ std::shared_ptr } } -using V5BoxEncoding = op::v5::NonMaxSuppression::BoxEncodingType; - namespace { constexpr size_t boxes_port = 0; @@ -825,8 +823,7 @@ int64_t op::v5::NonMaxSuppression::max_boxes_output_from_input() const { int64_t max_output_boxes{0}; - size_t num_of_inputs = inputs().size(); - if (num_of_inputs < 3) + if (inputs().size() < 3) { return 0; } @@ -842,8 +839,7 @@ float op::v5::NonMaxSuppression::iou_threshold_from_input() const { float iou_threshold = 0.0f; - size_t num_of_inputs = inputs().size(); - if (num_of_inputs < 4) + if (inputs().size() < 4) { return iou_threshold; } @@ -859,8 +855,7 @@ float op::v5::NonMaxSuppression::score_threshold_from_input() const { float score_threshold = 0.0f; - size_t num_of_inputs = inputs().size(); - if (num_of_inputs < 5) + if (inputs().size() < 5) { return score_threshold; } @@ -876,8 +871,7 @@ float op::v5::NonMaxSuppression::soft_nms_sigma_from_input() const { float soft_nms_sigma = 0.0f; - size_t num_of_inputs = inputs().size(); - if (num_of_inputs < 6) + if (inputs().size() < 6) { return soft_nms_sigma; } From 1e10629ecd09f5fe01afe3c39fa29764a6b442e6 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 5 Nov 2020 15:42:43 +0300 Subject: [PATCH 165/173] Deleted functions function_from_model and construct_weights. --- .../non_max_suppression_tests.cpp | 48 +++++++------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp index 55e6b00091862e..f37713d5c8feed 100644 --- a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp +++ b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp @@ -367,29 +367,6 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression5) { }); } -static std::shared_ptr function_from_model(const std::string& model, Blob::Ptr weights) { - Core ie; - - auto ngraphImpl = ie.ReadNetwork(model, weights); - auto graph = ngraph::clone_function(*ngraphImpl.getFunction()); - - return graph; -} - -static Blob::Ptr construct_weights(size_t weightsSize = 0, const std::function& fillBlob = {}) { - Blob::Ptr weights; - - if (weightsSize) { - weights = make_shared_blob(TensorDesc(Precision::U8, {weightsSize}, Layout::C)); - weights->allocate(); - CommonTestUtils::fill_data(weights->buffer().as(), weights->size() / sizeof(float)); - if (fillBlob) - fillBlob(weights); - } - - return weights; -} - TEST_F(NGraphReaderTests, ReadNonMaxSuppression4) { std::string model = R"V0G0N( @@ -496,14 +473,25 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression4) { )V0G0N"; - auto weights = construct_weights(16, [](Blob::Ptr& weights) { - auto * i64w = weights->buffer().as(); - i64w[0] = 200; + constexpr size_t weightsSize = 16; - auto * fp32w = weights->buffer().as(); - fp32w[2] = 0.5; - fp32w[3] = 0.05; - }); + Blob::Ptr weights; + + weights = make_shared_blob(TensorDesc(Precision::U8, {weightsSize}, Layout::C)); + weights->allocate(); + CommonTestUtils::fill_data(weights->buffer().as(), weights->size() / sizeof(float)); + + auto * i64w = weights->buffer().as(); + i64w[0] = 200; + + auto * fp32w = weights->buffer().as(); + fp32w[2] = 0.5; + fp32w[3] = 0.05; + + Core ie; + + auto ngraphImpl = ie.ReadNetwork(model, weights); + auto graph = ngraph::clone_function(*ngraphImpl.getFunction()); auto graph = function_from_model(model, weights); From a11cd97ce769cc97c2689be5c0750130390fdc4d Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 5 Nov 2020 15:44:02 +0300 Subject: [PATCH 166/173] Small fix. --- .../ngraph_reader/non_max_suppression_tests.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp index f37713d5c8feed..27ee39178690ed 100644 --- a/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp +++ b/inference-engine/tests/functional/inference_engine/ngraph_reader/non_max_suppression_tests.cpp @@ -493,8 +493,6 @@ TEST_F(NGraphReaderTests, ReadNonMaxSuppression4) { auto ngraphImpl = ie.ReadNetwork(model, weights); auto graph = ngraph::clone_function(*ngraphImpl.getFunction()); - auto graph = function_from_model(model, weights); - ::ngraph::pass::Manager manager; manager.register_pass<::ngraph::pass::InitNodeInfo>(); manager.register_pass<::ngraph::pass::CommonOptimizations>(); From c86a7cdb3da7c42fe9a179bb3460f74b0dfa21e1 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 5 Nov 2020 17:08:38 +0300 Subject: [PATCH 167/173] Replaced push_back with emplace_back in the conversion of NMS-5 to legacy. --- .../convert_nms_5_to_legacy.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp index ad37ba8f33f0cb..8ef630e16879d0 100644 --- a/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp +++ b/inference-engine/src/legacy_api/src/transformations/convert_opset1_to_legacy/convert_nms_5_to_legacy.cpp @@ -49,13 +49,13 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { Output new_shape_for_soft_nms_sigma = opset1::Constant::create(ngraph::element::i64, Shape{1}, {1}); new_max_per_class = std::make_shared(arg2, new_shape_for_max_per_class, true); - new_ops.push_back(new_max_per_class.get_node_shared_ptr()); + new_ops.emplace_back(new_max_per_class.get_node_shared_ptr()); new_iou_threshold = std::make_shared(arg3, new_shape_for_iou_threshold, true); - new_ops.push_back(new_iou_threshold.get_node_shared_ptr()); + new_ops.emplace_back(new_iou_threshold.get_node_shared_ptr()); new_score_threshold = std::make_shared(arg4, new_shape_for_score_threshold, true); - new_ops.push_back(new_score_threshold.get_node_shared_ptr()); + new_ops.emplace_back(new_score_threshold.get_node_shared_ptr()); int center_point_box = 0; switch (nms_5->get_box_encoding()) { @@ -74,7 +74,7 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { if (num_of_inputs > 5 && nms_5->soft_nms_sigma_from_input() != 0.0f) { new_soft_nms_sigma = std::make_shared(new_args.at(5), new_shape_for_soft_nms_sigma, true); - new_ops.push_back(new_soft_nms_sigma.get_node_shared_ptr()); + new_ops.emplace_back(new_soft_nms_sigma.get_node_shared_ptr()); nms_legacy = std::make_shared( new_args.at(0), new_args.at(1), @@ -85,7 +85,7 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { center_point_box, nms_5->get_sort_result_descending(), nms_5->get_output_type()); - new_ops.push_back(nms_legacy); + new_ops.emplace_back(nms_legacy); } else { nms_legacy = std::make_shared( new_args.at(0), @@ -96,7 +96,7 @@ ngraph::pass::ConvertNMS5ToLegacyMatcher::ConvertNMS5ToLegacyMatcher() { center_point_box, nms_5->get_sort_result_descending(), nms_5->get_output_type()); - new_ops.push_back(nms_legacy); + new_ops.emplace_back(nms_legacy); } nms_legacy->set_friendly_name(nms_5->get_friendly_name()); From 8a1f6a85a804d7ac7242c3441ffe9c0e3138ab0c Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 5 Nov 2020 17:09:31 +0300 Subject: [PATCH 168/173] Small changes. --- .../src/mkldnn_plugin/nodes/non_max_suppression.cpp | 4 ++-- .../op_conversions/convert_previous_nms_to_nms_5.cpp | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp b/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp index e7f293075a2a7b..920196ad067908 100644 --- a/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp +++ b/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp @@ -21,9 +21,9 @@ class NonMaxSuppressionImpl: public ExtLayerBase { explicit NonMaxSuppressionImpl(const CNNLayer* layer) { try { if (layer->insData.size() < 2 || layer->insData.size() > 6) - THROW_IE_EXCEPTION << "NMS" << "has incorrect number of input edges: " << layer->insData.size(); + THROW_IE_EXCEPTION << layer->name << "has incorrect number of input edges: " << layer->insData.size(); if (layer->outData.size() < 1 || layer->outData.size() > 3) - THROW_IE_EXCEPTION << "NMS" << "has incorrect number of output edges: " << layer->outData.size(); + THROW_IE_EXCEPTION << layer->name << "has incorrect number of output edges: " << layer->outData.size(); if (layer->insData[NMS_BOXES].lock()->getTensorDesc().getPrecision() != Precision::FP32) THROW_IE_EXCEPTION << layer->name << " Incorrect 'boxes' input precision. Only FP32 is supported!"; diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp index 7dc66e44d4493a..890ce49c55a20b 100644 --- a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp @@ -15,11 +15,9 @@ #include #include -using NMS5BoxEncoding = ngraph::opset5::NonMaxSuppression::BoxEncodingType; - struct NMSAttributes { ngraph::element::Type output_type; - NMS5BoxEncoding box_encoding; + ngraph::opset5::NonMaxSuppression::BoxEncodingType box_encoding; bool sort_result_descending; bool is_supported_nms; }; From 4e4fdee1fddadce48cb3ceb866b2ba57d954affd Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Thu, 5 Nov 2020 17:18:18 +0300 Subject: [PATCH 169/173] Some fixes. --- .../src/mkldnn_plugin/nodes/non_max_suppression.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp b/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp index 920196ad067908..c3d1bd949fcb13 100644 --- a/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp +++ b/inference-engine/src/mkldnn_plugin/nodes/non_max_suppression.cpp @@ -80,7 +80,9 @@ class NonMaxSuppressionImpl: public ExtLayerBase { Precision inPrecision = Precision::FP32; if (i == NMS_MAXOUTPUTBOXESPERCLASS) inPrecision = Precision::I32; - const SizeVector& inDims = layer->insData[i].lock()->getTensorDesc().getDims(); + auto inData = layer->insData[i].lock(); + if (!inData) THROW_IE_EXCEPTION << "Layer " << layer->name << " has empty input with index: " << i; + const SizeVector& inDims = inData ->getTensorDesc().getDims(); inConfig.desc = TensorDesc(inPrecision, inDims, InferenceEngine::TensorDesc::getLayoutByDims(inDims)); config.inConfs.push_back(inConfig); } From dddf4dc3a3527a059f14eff0a776bb80bd047eff Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 6 Nov 2020 11:57:24 +0300 Subject: [PATCH 170/173] Refactored reference implementation of NMS-5. --- .../runtime/reference/non_max_suppression.hpp | 46 +- .../runtime/reference/non_max_suppression.cpp | 476 +++++++++--------- .../runtime/interpreter/int_executable.hpp | 31 +- 3 files changed, 299 insertions(+), 254 deletions(-) diff --git a/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp b/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp index a0e370c8b7d950..db343abd46dce3 100644 --- a/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp +++ b/ngraph/core/reference/include/ngraph/runtime/reference/non_max_suppression.hpp @@ -24,7 +24,8 @@ #include #include #include -#include "ngraph/coordinate_transform.hpp" +#include +#include "ngraph/node.hpp" #include "ngraph/op/util/op_types.hpp" #include "ngraph/ops.hpp" #include "ngraph/shape_util.hpp" @@ -35,9 +36,46 @@ namespace ngraph { namespace reference { - void non_max_suppression(const op::v5::NonMaxSuppression* nms5, - const std::vector>& out, - const std::vector>& args); + struct InfoForNMS5 + { + int64_t max_output_boxes_per_class; + float iou_threshold; + float score_threshold; + float soft_nms_sigma; + Shape out_shape; + Shape boxes_shape; + Shape scores_shape; + std::vector boxes_data; + std::vector scores_data; + size_t out_shape_size; + bool sort_result_descending; + ngraph::element::Type output_type; + }; + + InfoForNMS5 get_info_for_nms5_evaluation(const op::v5::NonMaxSuppression* nms5, + const HostTensorVector& inputs); + + void non_max_suppression(const float* boxes_data, + const Shape& boxes_data_shape, + const float* scores_data, + const Shape& scores_data_shape, + int64_t max_output_boxes_per_class, + float iou_threshold, + float score_threshold, + float soft_nms_sigma, + int64_t* selected_indices, + const Shape& selected_indices_shape, + float* selected_scores, + const Shape& selected_scores_shape, + int64_t* valid_outputs, + const bool sort_result_descending); + + void nms5_postprocessing(const HostTensorVector& outputs, + const ngraph::element::Type output_type, + const std::vector& selected_indices, + const std::vector& selected_scores, + int64_t valid_outputs, + const ngraph::element::Type selected_scores_type); } } } diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index a6a113c7ca04f4..233fda5c81d868 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -280,298 +280,215 @@ namespace auto result = get_floats(scores, scores_shape); return result; } +} - void nms_postprocessing(const std::vector>& outputs, - const ngraph::element::Type output_type, - const std::vector& selected_indices, - const std::vector& selected_scores, - int64_t valid_outputs, - const ngraph::element::Type selected_scores_type) +namespace ngraph +{ + namespace runtime { - size_t num_of_outputs = outputs.size(); - size_t selected_size = valid_outputs * 3; - - if (output_type == ngraph::element::i64) - { - int64_t* indices_ptr = outputs[0]->get_data_ptr(); - memcpy(indices_ptr, selected_indices.data(), selected_size * sizeof(int64_t)); - } - else + namespace reference { - int32_t* indices_ptr = outputs[0]->get_data_ptr(); - for (size_t i = 0; i < selected_size; ++i) + InfoForNMS5 get_info_for_nms5_evaluation(const op::v5::NonMaxSuppression* nms5, + const HostTensorVector& inputs) { - indices_ptr[i] = static_cast(selected_indices[i]); - } - } + InfoForNMS5 result; - if (num_of_outputs < 2) - { - return; - } + result.max_output_boxes_per_class = nms5->max_boxes_output_from_input(); + result.iou_threshold = nms5->iou_threshold_from_input(); + result.score_threshold = nms5->score_threshold_from_input(); + result.soft_nms_sigma = nms5->soft_nms_sigma_from_input(); - size_t selected_scores_size = selected_scores.size(); + auto selected_indices_shape = + infer_selected_indices_shape(inputs, result.max_output_boxes_per_class); + result.out_shape = selected_indices_shape.to_shape(); - switch (selected_scores_type) - { - case element::Type_t::bf16: - { - bfloat16* scores_ptr = outputs[1]->get_data_ptr(); - for (size_t i = 0; i < selected_scores_size; ++i) - { - scores_ptr[i] = bfloat16(selected_scores[i]); - } - } - break; - case element::Type_t::f16: - { - float16* scores_ptr = outputs[1]->get_data_ptr(); - for (size_t i = 0; i < selected_scores_size; ++i) - { - scores_ptr[i] = float16(selected_scores[i]); - } - } - break; - case element::Type_t::f32: - { - float* scores_ptr = outputs[1]->get_data_ptr(); - memcpy(scores_ptr, selected_scores.data(), selected_size * sizeof(float)); - } - break; - default:; - } + result.boxes_shape = inputs[boxes_port]->get_shape(); + result.scores_shape = inputs[scores_port]->get_shape(); - if (num_of_outputs < 3) - { - return; - } + result.boxes_data = prepare_boxes_data(inputs[boxes_port], + result.boxes_shape, + nms5->get_box_encoding()); + result.scores_data = prepare_scores_data(inputs[scores_port], result.scores_shape); - if (output_type == ngraph::element::i64) - { - int64_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); - *valid_outputs_ptr = valid_outputs; - } - else - { - int32_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); - *valid_outputs_ptr = static_cast(valid_outputs); - } - } + result.out_shape_size = shape_size(result.out_shape); - void eval(const float* boxes_data, - const Shape& boxes_data_shape, - const float* scores_data, - const Shape& scores_data_shape, - int64_t max_output_boxes_per_class, - float iou_threshold, - float score_threshold, - float soft_nms_sigma, - int64_t* selected_indices, - const Shape& selected_indices_shape, - float* selected_scores, - const Shape& selected_scores_shape, - int64_t* valid_outputs, - const bool sort_result_descending) - { - float scale = 0.0f; - if (soft_nms_sigma > 0.0f) - { - scale = -0.5f / soft_nms_sigma; - } + result.sort_result_descending = nms5->get_sort_result_descending(); - auto func = [iou_threshold, scale](float iou) { - const float weight = std::exp(scale * iou * iou); - return iou <= iou_threshold ? weight : 0.0f; - }; + result.output_type = nms5->get_output_type(); - // boxes shape: {num_batches, num_boxes, 4} - // scores shape: {num_batches, num_classes, num_boxes} - int64_t num_batches = static_cast(scores_data_shape[0]); - int64_t num_classes = static_cast(scores_data_shape[1]); - int64_t num_boxes = static_cast(boxes_data_shape[1]); + return result; + } - SelectedIndex* selected_indices_ptr = reinterpret_cast(selected_indices); - SelectedScore* selected_scores_ptr = reinterpret_cast(selected_scores); + void non_max_suppression(const float* boxes_data, + const Shape& boxes_data_shape, + const float* scores_data, + const Shape& scores_data_shape, + int64_t max_output_boxes_per_class, + float iou_threshold, + float score_threshold, + float soft_nms_sigma, + int64_t* selected_indices, + const Shape& selected_indices_shape, + float* selected_scores, + const Shape& selected_scores_shape, + int64_t* valid_outputs, + const bool sort_result_descending) + { + float scale = 0.0f; + if (soft_nms_sigma > 0.0f) + { + scale = -0.5f / soft_nms_sigma; + } - size_t boxes_per_class = static_cast(max_output_boxes_per_class); + auto func = [iou_threshold, scale](float iou) { + const float weight = std::exp(scale * iou * iou); + return iou <= iou_threshold ? weight : 0.0f; + }; - int64_t num_of_valid_boxes = 0; + // boxes shape: {num_batches, num_boxes, 4} + // scores shape: {num_batches, num_classes, num_boxes} + int64_t num_batches = static_cast(scores_data_shape[0]); + int64_t num_classes = static_cast(scores_data_shape[1]); + int64_t num_boxes = static_cast(boxes_data_shape[1]); - std::vector filteredBoxes; + SelectedIndex* selected_indices_ptr = + reinterpret_cast(selected_indices); + SelectedScore* selected_scores_ptr = + reinterpret_cast(selected_scores); - for (int64_t batch = 0; batch < num_batches; batch++) - { - const float* boxesPtr = boxes_data + batch * num_boxes * 4; - Rectangle* r = reinterpret_cast(const_cast(boxesPtr)); + size_t boxes_per_class = static_cast(max_output_boxes_per_class); - for (int64_t class_idx = 0; class_idx < num_classes; class_idx++) - { - const float* scoresPtr = - scores_data + batch * (num_classes * num_boxes) + class_idx * num_boxes; + int64_t num_of_valid_boxes = 0; - std::vector candidate_boxes; - candidate_boxes.reserve(num_boxes); + std::vector filteredBoxes; - for (size_t box_idx = 0; box_idx < num_boxes; box_idx++) + for (int64_t batch = 0; batch < num_batches; batch++) { - if (scoresPtr[box_idx] > score_threshold) - { - candidate_boxes.emplace_back( - r[box_idx], box_idx, scoresPtr[box_idx], 0, batch, class_idx); - } - } - - std::priority_queue sorted_boxes(std::less(), - std::move(candidate_boxes)); - - std::vector selected; - // Get the next box with top score, filter by iou_threshold + const float* boxesPtr = boxes_data + batch * num_boxes * 4; + Rectangle* r = reinterpret_cast(const_cast(boxesPtr)); - BoxInfo next_candidate; - float original_score; - - while (!sorted_boxes.empty() && selected.size() < boxes_per_class) - { - next_candidate = sorted_boxes.top(); - original_score = next_candidate.score; - sorted_boxes.pop(); - - bool should_hard_suppress = false; - for (int64_t j = static_cast(selected.size()) - 1; - j >= next_candidate.suppress_begin_index; - --j) + for (int64_t class_idx = 0; class_idx < num_classes; class_idx++) { - float iou = intersectionOverUnion(next_candidate.box, selected[j].box); - next_candidate.score *= func(iou); + const float* scoresPtr = + scores_data + batch * (num_classes * num_boxes) + class_idx * num_boxes; - if (iou >= iou_threshold) - { - should_hard_suppress = true; - break; - } + std::vector candidate_boxes; + candidate_boxes.reserve(num_boxes); - if (next_candidate.score <= score_threshold) + for (size_t box_idx = 0; box_idx < num_boxes; box_idx++) { - break; + if (scoresPtr[box_idx] > score_threshold) + { + candidate_boxes.emplace_back( + r[box_idx], box_idx, scoresPtr[box_idx], 0, batch, class_idx); + } } - } - next_candidate.suppress_begin_index = selected.size(); + std::priority_queue sorted_boxes(std::less(), + std::move(candidate_boxes)); - if (!should_hard_suppress) - { - if (next_candidate.score == original_score) + std::vector selected; + // Get the next box with top score, filter by iou_threshold + + BoxInfo next_candidate; + float original_score; + + while (!sorted_boxes.empty() && selected.size() < boxes_per_class) { - selected.push_back(next_candidate); - continue; + next_candidate = sorted_boxes.top(); + original_score = next_candidate.score; + sorted_boxes.pop(); + + bool should_hard_suppress = false; + for (int64_t j = static_cast(selected.size()) - 1; + j >= next_candidate.suppress_begin_index; + --j) + { + float iou = intersectionOverUnion(next_candidate.box, selected[j].box); + next_candidate.score *= func(iou); + + if (iou >= iou_threshold) + { + should_hard_suppress = true; + break; + } + + if (next_candidate.score <= score_threshold) + { + break; + } + } + + next_candidate.suppress_begin_index = selected.size(); + + if (!should_hard_suppress) + { + if (next_candidate.score == original_score) + { + selected.push_back(next_candidate); + continue; + } + if (next_candidate.score > score_threshold) + { + sorted_boxes.push(next_candidate); + } + } } - if (next_candidate.score > score_threshold) + + for (const auto& box_info : selected) { - sorted_boxes.push(next_candidate); + filteredBoxes.push_back(box_info); } } } - for (const auto& box_info : selected) + if (sort_result_descending) { - filteredBoxes.push_back(box_info); + std::sort(filteredBoxes.begin(), + filteredBoxes.end(), + [](const BoxInfo& l, const BoxInfo& r) { return l.score > r.score; }); } - } - } - if (sort_result_descending) - { - std::sort(filteredBoxes.begin(), - filteredBoxes.end(), - [](const BoxInfo& l, const BoxInfo& r) { return l.score > r.score; }); - } + size_t max_num_of_selected_indices = selected_indices_shape[0]; + size_t output_size = std::min(filteredBoxes.size(), max_num_of_selected_indices); - size_t max_num_of_selected_indices = selected_indices_shape[0]; - size_t output_size = std::min(filteredBoxes.size(), max_num_of_selected_indices); + *valid_outputs = output_size; - *valid_outputs = output_size; - - size_t idx; - for (idx = 0; idx < output_size; idx++) - { - const auto& box_info = filteredBoxes[idx]; - SelectedIndex selected_index{ - box_info.batch_index, box_info.class_index, box_info.index}; - SelectedScore selected_score{static_cast(box_info.batch_index), - static_cast(box_info.class_index), - box_info.score}; - - selected_indices_ptr[idx] = selected_index; - selected_scores_ptr[idx] = selected_score; - } + size_t idx; + for (idx = 0; idx < output_size; idx++) + { + const auto& box_info = filteredBoxes[idx]; + SelectedIndex selected_index{ + box_info.batch_index, box_info.class_index, box_info.index}; + SelectedScore selected_score{static_cast(box_info.batch_index), + static_cast(box_info.class_index), + box_info.score}; + + selected_indices_ptr[idx] = selected_index; + selected_scores_ptr[idx] = selected_score; + } - SelectedIndex selected_index_filler{0, 0, 0}; - SelectedScore selected_score_filler{0.0f, 0.0f, 0.0f}; - for (; idx < max_num_of_selected_indices; idx++) - { - selected_indices_ptr[idx] = selected_index_filler; - selected_scores_ptr[idx] = selected_score_filler; - } - } -} + SelectedIndex selected_index_filler{0, 0, 0}; + SelectedScore selected_score_filler{0.0f, 0.0f, 0.0f}; + for (; idx < max_num_of_selected_indices; idx++) + { + selected_indices_ptr[idx] = selected_index_filler; + selected_scores_ptr[idx] = selected_score_filler; + } + } -namespace ngraph -{ - namespace runtime - { - namespace reference - { - void non_max_suppression(const op::v5::NonMaxSuppression* nms5, - const std::vector>& outputs, - const std::vector>& inputs) + void nms5_postprocessing(const HostTensorVector& outputs, + const ngraph::element::Type output_type, + const std::vector& selected_indices, + const std::vector& selected_scores, + int64_t valid_outputs, + const ngraph::element::Type selected_scores_type) { - int64_t max_output_boxes_per_class = nms5->max_boxes_output_from_input(); - float iou_threshold = nms5->iou_threshold_from_input(); - float score_threshold = nms5->score_threshold_from_input(); - float soft_nms_sigma = nms5->soft_nms_sigma_from_input(); - - auto selected_indices_shape = - infer_selected_indices_shape(inputs, max_output_boxes_per_class); - Shape out_shape = selected_indices_shape.to_shape(); - - Shape boxes_shape = inputs[boxes_port]->get_shape(); - Shape scores_shape = inputs[scores_port]->get_shape(); - - auto boxes_data = - prepare_boxes_data(inputs[boxes_port], boxes_shape, nms5->get_box_encoding()); - auto scores_data = prepare_scores_data(inputs[scores_port], scores_shape); - - size_t out_shape_size = shape_size(out_shape); - - std::vector selected_indices(out_shape_size); - std::vector selected_scores(out_shape_size); - int64_t valid_outputs = 0; - - eval(boxes_data.data(), - boxes_shape, - scores_data.data(), - scores_shape, - max_output_boxes_per_class, - iou_threshold, - score_threshold, - soft_nms_sigma, - selected_indices.data(), - out_shape, - selected_scores.data(), - out_shape, - &valid_outputs, - nms5->get_sort_result_descending()); - - auto output_type = nms5->get_output_type(); - outputs[0]->set_element_type(output_type); outputs[0]->set_shape(Shape{static_cast(valid_outputs), 3}); size_t num_of_outputs = outputs.size(); - auto selected_scores_type = - (inputs.size() < 4) ? element::f32 : inputs[3]->get_element_type(); - if (num_of_outputs >= 2) { outputs[1]->set_element_type(selected_scores_type); @@ -584,12 +501,73 @@ namespace ngraph outputs[2]->set_shape(Shape{1}); } - nms_postprocessing(outputs, - output_type, - selected_indices, - selected_scores, - valid_outputs, - selected_scores_type); + size_t selected_size = valid_outputs * 3; + + if (output_type == ngraph::element::i64) + { + int64_t* indices_ptr = outputs[0]->get_data_ptr(); + memcpy(indices_ptr, selected_indices.data(), selected_size * sizeof(int64_t)); + } + else + { + int32_t* indices_ptr = outputs[0]->get_data_ptr(); + for (size_t i = 0; i < selected_size; ++i) + { + indices_ptr[i] = static_cast(selected_indices[i]); + } + } + + if (num_of_outputs < 2) + { + return; + } + + size_t selected_scores_size = selected_scores.size(); + + switch (selected_scores_type) + { + case element::Type_t::bf16: + { + bfloat16* scores_ptr = outputs[1]->get_data_ptr(); + for (size_t i = 0; i < selected_scores_size; ++i) + { + scores_ptr[i] = bfloat16(selected_scores[i]); + } + } + break; + case element::Type_t::f16: + { + float16* scores_ptr = outputs[1]->get_data_ptr(); + for (size_t i = 0; i < selected_scores_size; ++i) + { + scores_ptr[i] = float16(selected_scores[i]); + } + } + break; + case element::Type_t::f32: + { + float* scores_ptr = outputs[1]->get_data_ptr(); + memcpy(scores_ptr, selected_scores.data(), selected_size * sizeof(float)); + } + break; + default:; + } + + if (num_of_outputs < 3) + { + return; + } + + if (output_type == ngraph::element::i64) + { + int64_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); + *valid_outputs_ptr = valid_outputs; + } + else + { + int32_t* valid_outputs_ptr = outputs[2]->get_data_ptr(); + *valid_outputs_ptr = static_cast(valid_outputs); + } } } } diff --git a/ngraph/test/runtime/interpreter/int_executable.hpp b/ngraph/test/runtime/interpreter/int_executable.hpp index 075bb78f5606ec..1e6ce57a313d5f 100644 --- a/ngraph/test/runtime/interpreter/int_executable.hpp +++ b/ngraph/test/runtime/interpreter/int_executable.hpp @@ -1342,7 +1342,36 @@ class INTERPRETER_BACKEND_API ngraph::runtime::interpreter::INTExecutable : publ const op::v5::NonMaxSuppression* nms = static_cast(&node); - reference::non_max_suppression(nms, out, args); + auto info = reference::get_info_for_nms5_evaluation(nms, args); + + std::vector selected_indices(info.out_shape_size); + std::vector selected_scores(info.out_shape_size); + int64_t valid_outputs = 0; + + reference::non_max_suppression(info.boxes_data.data(), + info.boxes_shape, + info.scores_data.data(), + info.scores_shape, + info.max_output_boxes_per_class, + info.iou_threshold, + info.score_threshold, + info.soft_nms_sigma, + selected_indices.data(), + info.out_shape, + selected_scores.data(), + info.out_shape, + &valid_outputs, + info.sort_result_descending); + + auto selected_scores_type = + (args.size() < 4) ? element::f32 : args[3]->get_element_type(); + + reference::nms5_postprocessing(out, + info.output_type, + selected_indices, + selected_scores, + valid_outputs, + selected_scores_type); break; } From 11863f1c8b043e6ee24594d56863758936b69cc6 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 6 Nov 2020 12:31:51 +0300 Subject: [PATCH 171/173] Moved structure NMSAttributes to unnamed namespace. --- .../op_conversions/convert_previous_nms_to_nms_5.cpp | 6 +++--- .../reference/src/runtime/reference/non_max_suppression.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp index 890ce49c55a20b..3328f02058d2a7 100644 --- a/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp +++ b/inference-engine/src/transformations/src/transformations/op_conversions/convert_previous_nms_to_nms_5.cpp @@ -15,6 +15,9 @@ #include #include +using namespace ngraph; + +namespace { struct NMSAttributes { ngraph::element::Type output_type; ngraph::opset5::NonMaxSuppression::BoxEncodingType box_encoding; @@ -22,9 +25,6 @@ struct NMSAttributes { bool is_supported_nms; }; -using namespace ngraph; - -namespace { NMSAttributes get_nms4_attrs(const std::shared_ptr& nms4) { NMSAttributes attrs; diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index 233fda5c81d868..39af8cef8ec048 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -461,8 +461,8 @@ namespace ngraph SelectedIndex selected_index{ box_info.batch_index, box_info.class_index, box_info.index}; SelectedScore selected_score{static_cast(box_info.batch_index), - static_cast(box_info.class_index), - box_info.score}; + static_cast(box_info.class_index), + box_info.score}; selected_indices_ptr[idx] = selected_index; selected_scores_ptr[idx] = selected_score; From db454e9b6ccf08badfcba556bf08fdfb5fe2029a Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 6 Nov 2020 12:55:50 +0300 Subject: [PATCH 172/173] Code style fixes. --- .../runtime/reference/non_max_suppression.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index 39af8cef8ec048..81dbf707253319 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -305,9 +305,8 @@ namespace ngraph result.boxes_shape = inputs[boxes_port]->get_shape(); result.scores_shape = inputs[scores_port]->get_shape(); - result.boxes_data = prepare_boxes_data(inputs[boxes_port], - result.boxes_shape, - nms5->get_box_encoding()); + result.boxes_data = prepare_boxes_data( + inputs[boxes_port], result.boxes_shape, nms5->get_box_encoding()); result.scores_data = prepare_scores_data(inputs[scores_port], result.scores_shape); result.out_shape_size = shape_size(result.out_shape); @@ -385,7 +384,7 @@ namespace ngraph } std::priority_queue sorted_boxes(std::less(), - std::move(candidate_boxes)); + std::move(candidate_boxes)); std::vector selected; // Get the next box with top score, filter by iou_threshold @@ -401,10 +400,11 @@ namespace ngraph bool should_hard_suppress = false; for (int64_t j = static_cast(selected.size()) - 1; - j >= next_candidate.suppress_begin_index; - --j) + j >= next_candidate.suppress_begin_index; + --j) { - float iou = intersectionOverUnion(next_candidate.box, selected[j].box); + float iou = + intersectionOverUnion(next_candidate.box, selected[j].box); next_candidate.score *= func(iou); if (iou >= iou_threshold) @@ -445,8 +445,8 @@ namespace ngraph if (sort_result_descending) { std::sort(filteredBoxes.begin(), - filteredBoxes.end(), - [](const BoxInfo& l, const BoxInfo& r) { return l.score > r.score; }); + filteredBoxes.end(), + [](const BoxInfo& l, const BoxInfo& r) { return l.score > r.score; }); } size_t max_num_of_selected_indices = selected_indices_shape[0]; From 279c2d9061a4fc1e7ea6274dc8f2438c3db44eb1 Mon Sep 17 00:00:00 2001 From: VGavrilov Date: Fri, 6 Nov 2020 13:03:34 +0300 Subject: [PATCH 173/173] Small fix. --- .../reference/src/runtime/reference/non_max_suppression.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp index 81dbf707253319..44fafd726afa43 100644 --- a/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp +++ b/ngraph/core/reference/src/runtime/reference/non_max_suppression.cpp @@ -305,7 +305,7 @@ namespace ngraph result.boxes_shape = inputs[boxes_port]->get_shape(); result.scores_shape = inputs[scores_port]->get_shape(); - result.boxes_data = prepare_boxes_data( + result.boxes_data = prepare_boxes_data( inputs[boxes_port], result.boxes_shape, nms5->get_box_encoding()); result.scores_data = prepare_scores_data(inputs[scores_port], result.scores_shape);