From 9c7455194b46b08942e1b255a13ec88ce193c943 Mon Sep 17 00:00:00 2001 From: Hwanjun Song Date: Tue, 5 Apr 2022 22:36:03 +0900 Subject: [PATCH 01/26] Add files via upload Upload ViDT+ for joint-learning --- arguments.py | 33 +- datasets/transforms.py | 1 + figures/architecture.png | Bin 0 -> 102142 bytes figures/overview.png | Bin 0 -> 190418 bytes main.py | 20 +- methods/__init__.py | 4 - methods/vidt/criterion.py | 80 ++++- methods/vidt/dct.py | 194 ++++++++++ methods/vidt/detector.py | 132 +++++-- methods/vidt/postprocessor.py | 112 +++--- util/detectron2/layers/mask_ops.py | 275 ++++++++++++++ util/detectron2/layers/roi_align.py | 74 ++++ util/detectron2/structures/boxes.py | 423 ++++++++++++++++++++++ util/detectron2/structures/masks.py | 531 ++++++++++++++++++++++++++++ util/detectron2/utils/memory.py | 84 +++++ util/misc.py | 5 +- 16 files changed, 1867 insertions(+), 101 deletions(-) create mode 100644 figures/architecture.png create mode 100644 figures/overview.png create mode 100644 methods/vidt/dct.py create mode 100644 util/detectron2/layers/mask_ops.py create mode 100644 util/detectron2/layers/roi_align.py create mode 100644 util/detectron2/structures/boxes.py create mode 100644 util/detectron2/structures/masks.py create mode 100644 util/detectron2/utils/memory.py diff --git a/arguments.py b/arguments.py index dd21b16..8a3248f 100644 --- a/arguments.py +++ b/arguments.py @@ -136,13 +136,38 @@ def get_args_parser(): parser.add_argument('--distil_model_path', default=None, type=str, help="Distillation model path to load") ####### - # cross-scale fusion - parser.add_argument('--cross_scale_fusion', default=False, type=lambda x: (str(x).lower() == 'true'), help='use of scale fusion') + ####### For ViDT+ + + ## EPFF + parser.add_argument('--epff', default=False, type=lambda x: (str(x).lower() == 'true'), + help='use of EPFF module for pyramid feature fusion') + + ## UQR + parser.add_argument('--masks', default=False, action='store_true', help="Train segmentation head if the flag is provided") + parser.add_argument('--with_vector', default=False, type=bool) + parser.add_argument('--n_keep', default=256, type=int, + help="Number of coeffs to be remained") + parser.add_argument('--gt_mask_len', default=128, type=int, + help="Size of target mask") + parser.add_argument('--vector_loss_coef', default=0.7, type=float) + parser.add_argument('--vector_hidden_dim', default=256, type=int, + help="Size of the vector embeddings (dimension of the transformer)") + parser.add_argument('--no_vector_loss_norm', default=False, action='store_true') + parser.add_argument('--activation', default='relu', type=str, help="Activation function to use") + parser.add_argument('--checkpoint', default=False, action='store_true') + parser.add_argument('--vector_start_stage', default=0, type=int) + parser.add_argument('--num_machines', default=1, type=int) + parser.add_argument('--loss_type', default='l1', type=str) + parser.add_argument('--dcn', default=False, action='store_true') + + ## New losses # iou-aware - parser.add_argument('--iou_aware', default=False, type=lambda x: (str(x).lower() == 'true'), help='use of iou-aware loss') + parser.add_argument('--iou_aware', default=False, type=lambda x: (str(x).lower() == 'true'), + help='use of iou-aware loss') parser.add_argument('--iouaware_loss_coef', default=2, type=float) # token label - parser.add_argument('--token_label', default=False, type=lambda x: (str(x).lower() == 'true'), help='use of token label loss') + parser.add_argument('--token_label', default=False, type=lambda x: (str(x).lower() == 'true'), + help='use of token label loss') parser.add_argument('--token_loss_coef', default=2, type=float) ####### diff --git a/datasets/transforms.py b/datasets/transforms.py index 1477a84..3783959 100644 --- a/datasets/transforms.py +++ b/datasets/transforms.py @@ -270,6 +270,7 @@ def __call__(self, image, target=None): target = target.copy() h, w = image.shape[-2:] if "boxes" in target: + target['xyxy_boxes'] = target["boxes"] boxes = target["boxes"] boxes = box_xyxy_to_cxcywh(boxes) boxes = boxes / torch.tensor([w, h, w, h], dtype=torch.float32) diff --git a/figures/architecture.png b/figures/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..270c1dc0e9cea96d8fb3e86058eadd80aeae3685 GIT binary patch literal 102142 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 JE0ZyQ_g^(@;~W40 literal 0 HcmV?d00001 diff --git a/figures/overview.png b/figures/overview.png new file mode 100644 index 0000000000000000000000000000000000000000..6804376c0eb29bf7ec1a4ddccddc4bb62bd6a20c GIT binary patch literal 190418 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%0D!1j(sG}Ns_Be~gmmb% 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
=K3~})^W!R%K^?O@t!S+hO z{-UbB?0vymWCg)U{)L|5hn9^qX(aBsKXgGNA~VmK#xI`_s{shGzaxV{m`Fbq>${Jp zYc{6ogdq}+j9u#N?TVVKFB`o;Rq(4Elc3;=(88q4U}jN6FIQOLf!E_VsR#AYcDe)6 zQ7`v%xnwC3TfN2yK1O;c;ob<~1iW63nz%hhS`9%YHiO-4qW0ASdFJv?F-2V8-FSN8 zR-o&>YE#677RMr4lFP2L4ig(0{){ICUH1V%JW$GJuc1!jyRDaECp*nhfpChZZ)!EF5reHUOu^>&gNrDDbKKP=& zesn#NEF0SFDl^?@r{w7Hx!G_5lM+|$1|W)(`qa}XT^Ev&wD$gjpr{mk7r>494w><= ze#&8`n60zF#ljn`Eej*JWMMV~>1$!_?O*Ww?|9|uL0eXo!D87eMHwcV`fD+%vj zY0SI=-&Fa?kgR2bSw5~?6lH>93`0ilYvd@bs@Nu6i-`i&@SVIKYs0fkxExSi<+-rk zU%X<1i+|`b8UT2WXwTvmK*7A-%qrut>O+QD-Tqf@(JiirGc7Lr*$9ig!zSesf2TV0mK$O6{nIv_T#7-l8zfLvF!)vzn^rSM&gWcreox zg=BNDo%eL+07M+gH9cI_~qGV|{vNT?SY)fa?z z>9~34LLMttT01_HHc+vo@w0|DU=VJ+m8#~v?U2nU=?@BFS*O#} zN8P0^x79nhQT8E0UYuA>n0{<=^OKBM0S*&bUemcg#u8O0G10!Jq0*v5)#Smq)s2ek zs_Yd+D(s5Dg9cVt5+mvOZ(ma<3~J3E{~RV;+@CBw*G~%D ztRAI(C&l1}BZyFm25zc+-*$$R|25fVB_?jlI3IQZP=vKID0)|Qv1$?lqw0n=7!xU- zAE&_%1rv*e-XSd!!}rxqN>ute{jVE56V{Rt2~&OS)NlZDD=O4byDuTz1J6e#_-Sde z<4sWp#6_OqiLX%K!)ub<`x3nz^NVj0q+xC48>#E{<9n_S>oj%VoWVwGK})39x#+&= za!)M&3ubrQ75=Q0i|s1&T2Vcg+CD!GyZ@%V*{G2YXn0sR{oe_|I}T_IXV7%y|1tHB zQJJ;x|8Q=yZQ}})jcMYQZ8zDj$+m4b*-f5|smYq`CfoY&`}bYzSi|U7rm}eY@D74wrumhto`7 z76#J!f$L2H6cm)-^HI6gTv@lv|4ueonbNobGX?fcIecVE7LP&Oa4zCZIrnEw05HYW zWzv2N)Iva63}7$eF;r{t)P(TrJ4?C~_MCE6;Ff@lw(RNeO{CJ>2$V2beH&Pv1k*bm z#N;`j5@d~nF?4@W*0{80xi4r(k|UJ7p<2=rSMC4yrrX`>ssAeoJCLYEqn?(k<>g6E zUKS#FyxIBkbJK7AwVce~;RUqgZdc%Mb2E;HLWFv{iiJa_cje_+?Gu!zN_h3S(LT^w zo5$Y4rRyWdQJlr=Uk=+y;2%txNSSyxjF8-Dim{XNQj^p(40vQ|W2_CKLLaFj7; zF?_X8o>&r;H)9qFCG6xvF^H6c;6nkKOS}5eu)}{g{ALDDil@ zn23vutEs7(nVGq|a_Ys#$A=@`W7D@msM>G#2>HD}0&3Z=eGh6t&gwJ(-=9hZ=F7^K zsUCU&r){uyCyxFuW!!4oVL&Fc&al`&od7er0WuXU@(+iA)n|Tq4;AUa!@`JAalb&* zZgAx-KJoHDyu`;Lg7XtiTb+jKn`?&rlf$mMyel!E`>WU+y!dmWdOH$Y3T$bsNq`)8 zM-VK_G5}(ehKEaUBvD9s3lyge^ab8lD)(7yvZiv`Dd<+nB1UoFv!M&-0cGbODuy-~ zZF`lg-S3b<^5ZYZ>&hj&UlY8XYi8MyQYzd{$yH-uBdfn1ZYLG0WJbhr8c$y$U1vLe+U=|`4 zlRiZ>?)EbzE=j_wXz+}Nca;$wpORJJ7n^Y^c*(;pn~N{R4SrW+uP?@rMDIg9P*H3l z&i-drydbu$8s3gs*2q|jF&-~$RK4`;heg+a?>0#%vP+o%?e)OhLf3p8MX;0&i&bDx zn}0*dcd;)A1i2@eM0_Mt#NJ{0LZ7(?0D<%X zQUO|SF1wZHOnFBFgLol~V_;%zrwbr`FtYeu4s@-ZfH}|}fQM4dl}IB)E}HBKTqhEZ zWKHb<9gQ@8gSZqxws9Ai`tMtT5n}HI0J(OZm?xRWNwH3qYo=`UD62@W!ig@*eZlb2<{h)XJadoRz8@UX54 zdfd*$x)3Dxu(J~a0o}yDrXGNAUOY6$ss#JGKc8l;^WbIzgbiproS8l+hWZoH2LgZZ zwj*yn%=v675@M(`b^C4Lf7T3Y9oJhg9v-k#g6Uf>W@PW}(bmsRqu_x=6`7+y=5X0| z)!11Tvi#5J{s&`KPFlZ}VYLym=nZ|?{htVKog7}h3}+*lk1o%aC&6&59cj|3cLg?) zK)fv-@OZoi9#G8CW97)QhC<6pTWIW-m~DLmKb2jokB$;n+R4Sv8MsZ~#iVA;Lv*5= zc|iYW0)W)QPmP%`P4R223;(Rk9R0CCecCjx1^ZAaBgSod?hAQEMbKYD&Ak}i*<`R=&;I~~3jK;dMBX2FvmJW=w%V=g_V5C~_m@@CNZ~^9#v?YTd*;&?PYpKN zKP@OfE25$8Hg>#6z)ZLSxzLSpP&f;TDsf=1AAt3*s9v$UF1)6)9!NN25+p`PcP`44 zf3mnOBq2h!t@dR!1m#26ZDVX{kkjgpA3_;l#{93BYQ{S!zltT3{ zfd6AFn_UhkFMfTzZ*+f;EAc?+A?#x(TZCfaL(t&-7Hq8kCAAXqq!jGkvcjWfYrYGYdf)WXqLOX zM$ueP44yrw_7+CaTs!=Ex_LVIG}5O62p@>plinmKQ61BfD#z^n|NZk0gs~Cw>`+vJj6BZ0Bn3maVBNXGFgBnm?TVMAARLk-@M{}=FJx=p?D^Ud4+Bq&NVakU3XS)-q)uXgIC&u%vPk~SSXQaUcU>eAJj7~|ix#)Jy+fJ}Sx z(ea*oufBly-I0$b&-?jrP4BrkQ-)MWkx$MIdy88guPx*??)~CESdNSPzy_LPO**Ai>AwF zK?{L2{$im@b1F~BxO-Q+G9W^HXS(p~whKUkX#*&2_Qy|HGBKoV;g{1UZo3s~4adrq zk4m5{YXb2X2OqO8CaUN3;?(cwmUMIwEFqzBzx41f{bZg*C=wbWGI%(s@UQPMyAX@% z$9Z5@GYJLytJv}4pRQ@?_ir8(8D7pSvdeUeva)aIot`{D_YQ#$MOjxDDu=&2qgRq+ zW)MBZt@ir+PCVb#{k3I;4Y*&VYrCOG1!$BBp;VU7my|)1dln`n`qG^IO={Zb=YMHt zPrj&4NqcK=MvG2hEKRVsv;gn5|658pB+vD$4=d!Tdk$tfW|!@bs-_A}l*RG@3UNN% z(%->Z2sja5l@jLgrX3)nU0_h6VIS!l$}Ywc70lI(J?>?@&1iZa0=|4I;S`PbYqRg0 z|CMggd68WfGj#|oV<8Pyp!pD)OuSv5r?bfbb4rYnf zcx$_@=~CYH{V1)&*yW_J9Wa0h$jBkgvKk7t{9#~5+!Z}kNOs*i&-vc3%IGM!p`IHi7`YnLFH?ha8bDcXs}s7jeAK7d$LtnN&?2pzbvM(3AtP4fpu2v)6$v z*iE67$Bf$%`u;k7`HRpub#5rsU}58@N1o8l*9sET-E);Q87=VZ)&-azhpE~(Z?}`V z;_>)cBA&~FYrh88xZ8&Z4{CL} z|JmXEU#~_0KZuH|s;Q~z?)xfOK_Ma5`@LZhdX9{bN(rQD2R*X0`1JSJz6}0Oa;&4o0X*N}I(D{iVIDpC^V&-y8kF)de|;gY zcrmkHxJq%O=*}nFUSrHS7epfgl!+}WAi-=n6lUYs4qoE7`O82FREY3@El*Qn%;A%r zy$Lc92@uifN~+ggEMY@~GLNq0lsaC#9C!{mNdXB)POekK+K({gloYs_#>qeBknkg4 zE%tZq_?$E#Cq#p)T9p{SU0+7F`ffvR_Z5!Y`wjv2+{v<{!0npLv zPu9SIuCaf|B}P;b2CKl39s_ns2UrW>hynB-fOPXOeg`q?;cTg_d=@8xfIE z40S!;i+x8nS138)e?FDYJfOheABD;w`;O=IiHfJGOlvBT+}`rWjzDM*MLmp31zB~trmxDB3b`#eReHR9Hb|< z1Q_yP9EpTw7%T{&y9P8^wbL;(sR~c56aGtm0gm7QP;}Pkf9gv0aYUbudJ`zVGU7y? z?4}KwXg6B|p-Nq<4#dHx-^`7k@3Bo1F@UD)?kha3tsusChLbX$i}}UZJ}hITuTFMu z@69Cj#@5UT5^sFsV+*si9w06M5qAjkH6dY(J?rmxb;T-kL|kB>*i_Mx&UQ9D7MLcB zU|RVE1bfL?sJ^~YVa-}{26(g;R3#2z^r>s4ySB7?z%OAKS!S)zx2FA#bN@o=uA5|u z*Qe}m2N-uan1_FI#p9U#y?f!?;N#++ItWktuTnFEMM1rt0G3F`$-1# zrB1$rl%n3s?tme-Zm*)}8(dsJ{d|TPdpl^dfQ+X8k+(vo$i&uTBEu!8-i3J%;HX(! zTYtR2b-ErknvTSQxJiV(U9b1XPzX7JqHe`HU_(svim;AP?ak3^PXuwKa7me?S0>ai zrkiHc4On6qYLu||iI>|ha4OoTSjt6~HmajtKB%+{w&$^WOsY~HjG;hk#R&2NbKBy)ue2G(}DG>9VXyz>@Y zE>lmudRv}8>mHqI@gyYYXUJK$%i72Z%bi*MzBym#DS<^$9bgB8k&7732-)fp>g2Lp z`|-UTb-mN|bawlpmc&1NTu8vf_TglT3Jy(lD7-85KWCy)c}h|`dEX($fcx$1#HU~S zCz>Wn{Rzvpzy+PdwhS8DP?DAZd+*y=ERs2CPuv4&8-35V>-IIW3h9ukZ-1r*irn}g z4RUTy=xbR*dQ7f>X0bsIrIF;eHs2BT1ABaU9% ze3{QtsZct2RXq_n?3=0J3Xc;fiP+*@JgNC_`|d{5=to~QN33sQIJ<%ERS#Tw{2a&S zsQ=r`A5qiF2%K2EGRm63Wf3MmrHr3f>WG^=>W@BPGUi`I9b&_$_j~;i{OMDkc zHouv}Wr#qHP=it}+rV|!TzYH$FM5DoOPL%7(U^3J7OXaX9Zm8em_NSo%hb}of<>bh zHJhO3S+2qtj1vZvJ)}y0&+~<Sv{UJnz)h%Rj^d zGxc}7M(J5kY6ZQxYkKP^ow&heWCODdqF-9;8>ByJ2`k7?P!B&pdU*|(l_Fk4y~THu z33&101w(?vQj=A0Z~IH_KNbx&pBaub0QY!khWF<-zaa zsCJv)yK!XEQP3uoT@Y0nw%;s@e5Z&vny+S`IwdlYHNJ$QM|0m??GJub5rYsA5uX8D zIxdseWm+7v174u$zr`9|0J~VK)^-83iMiZom8L=8=(}3|-=7Wv!fvz8skl2^#!c-{ zBxa3uNN98q0ZaU2|%!`FtBagEM)KY)SlnIdy02W>npxeh_w#SMON4FGKd`BT zs^^>lHxOX95GTW+>-4`PUAxkLazO?rkXCM(}az|-Mq#} z^4YL6XH0fZ{NMDr79aPz|4S%C%n^-TFD$(GHN;>dtw2^q;t5XCv4!e+xME_^Jkwh+Efb`-j(Hli0L}>zMA*_ zOC*Fi^_(~*>;jzW!XGg{^F$&ij`v$`V^yq1EU?O~FiHx4?Q#7`?XUY2|f*Z|nylJ(7VbO?_xojLy&NgtB87jm85Ska7|; zF>ylDf&_(MC$7TL&G(x)Xp7U%D4FYy#>a`Iyj zg5JZv>nsmKKPU6<_<#DooCh0UX^Tp>4oQ0bO=|5_sSXjR>aOLZp8xomRJi*Zj z462^z<0>Er;lA^tCph0t6@`fVKYaOht<%Hd?j?0z8ah#$+TfR;FK}%JJWAOd7Lx!I z9~>04@jeNm4p@dkJSFJ+MEU575;Lqr9IS(BPQ*7R#3x{=6ENkA3321|??pm3K?$ZC zXTcL4%5kc|{jL@`yUur6r|PQ6(<+%$*()YbgO{>#gzDYxTV_QWZERQ{LWQYvep*?` zWL4YI6L_!%)NfSL6$oh}n>CGVIMKhzKLjAZ5s^F8_oNk?Bcmzkefx7VO!u8~9kXfx z+$JPtcD|k?l1S`pjS5Ye zmbeO(B|mn9=HB0Jro%f=nk##IK6`sf zI`@AXCtKcg-n&NRzA+66IGtJc4!5*@Mb`9dSMuHydk?pK2OZp zmp)Bc>(rOjfBO&|WX3dvy8a_e*vSLg9RhQpDCNZVg^gOChNN`B8K@-=!z|2pHe3tz zSGW1Dc>XKer3n!;ws-s|#H!}!y`6jKOZbzteve6}cM%ljT+4hmeSK)0UEAo%iN(hK z`tMbkx#zw4Z*MNCFfusu%TP**zV_nV2(A8mceWNpwVcLiXg3yK&Mz#Mxv1M$+xY?g z@g;jd{5({%6zfLJSji^4)mA`WY}o1kCsB>2uT|62^ARO`E8AZmx2qor$~ibZ)U4^# z*Wg+f4Uxx+6u(vwzZPrh16xv(Xqrp+kE4btnaa`}nR&5)IoMOfG$|jb{`rj;O7W)jAi8z&#L$w~dJ^&~mrNsa*LO*?HASXP3IjHZj&EMCy}H_c|3 zNvoh3itD!yf3#wkJiyq_AVEAk1mr<8e(pn&(K;pNJX1c0RZV^Osu~6x;XqJhM@NXW zO2>Y`-`dzueh^Zif%D>so|S`wN&sG5S|x(e6f?aH-M;6`A|Q*Cyi}a^C@SqV3C-#a=378F1ztC@XF(~yEYv^Uu$F+62!B^5CxbRKGa^nb#bN;i#uO$t-HRs&Ylv900a z&_ur1ABS;c`vWN%K7G+CD~@ucBB(SGe_Rymw51NOO>dsA=phAjso2uVa_OY|BjUVv zJFYxT61kBPw3}0nmU@Vf^<~7uvL^RJNrN;GdSQI65qol~ zHnWC_QeeM2xd*vY0Wm`zqM~}WV5&xl;}3 zOX!$&nzgz-uU4ge+HNHmfEUl085|s(LA&$*@j}(c#>Uank@Hh$IQ~2-NQQP4HB`m4 zen+i{3E`uolv{viu^wx4TziYbKt!2PhSY#s7*lebddcS(CLEDn@BcEdAsLaCaOSgY zp0?_Dln%7Ohu4u0mE+(T6Q$3j1s0kisg@|mFPU`rp?_!)qW~;X832;e6Zg&LpQt)t z9?suQcO?z|09nJwi(#_lCQy-dGy&;B!1j>~?f9FJf|ar{C8ERCRqwrvWzFDj)VXwJ(oh&UCr~$FYAd5Fi{nJY^`e=qDoS#UXh;x*W zkC&5{tTjrH;}q#^&6XdC(=+^dY*GVgm)yLWu#f7B9{-yyA3>t5J&7W5i{<2c&{Kv?dlFRnu1f8cgbX7L`_f3 zDYu~*NgQE&p#ltGf2ff=(ZaeVq(N%Y9pxmF8aa^NrO zMO>N*scW5yu1v$+a3e)}KdJhgqzlz7m4;rIn`fWSuMminbw&+a1WfMKT~XzsUA#6F zMRpW_GwqZXW?=B?Rn$gJSDOD{u7__u<^{~+WcKnjvH_INBNU7HK4pF)Ul;x06qe0E zdu!9Of@ZY)+9W8F&FfV>=3fZf`yLi$A+2^~9yKJB^^ZbJ!hV2jFJP>eUnS~Mh~%;+ z!7-+h;K;1bv2MUD#!az>_4*p{`lqRR6en!p0j!b#W!n9)9e*-!d zEbz29lgcI;SK!+WzClN=ov2TlsD;!pNek}x7hQya0cncDo=((2VgXIroEeNH4nGj9 z2N}tAU`xKIEBM{qE!o@K=lVH(EcNHg^ZSn#f?NGiG&9dg_}j^r<@(a)HADgFCaYknYGaWqtq zZ=Aa9WYD!HqAZpD);dp}Rct4+Pcaz(hgSi8bPjf={IDahXT@BhK^qXy+|zZo`Rf-T zcKm6-?o6#N^Z$)JkA_LQ49k)-y*D>zHTIU{*3DEdL*zOuRA*7JXf&-+p@1k>-}+ah zQ$g~Epj#=UMwt_CQ;6hP=xo(b)e-yHNOF2X2U@ZyPvvxN*D&h`w>pIX4t{S%ASv*!s6fqLu9loB z887e7&`F$|6_vHS86x@<6bc5ti(zZbNii31@54NfyhUSU)ev|BS5z4FIvm<6 zjA~z9n`yDrcs}x+U^^4X+VPi!x*1KqInWbm8*}_Tv#m}ZK9FC z?&B>U`T~pR=jZkfuo4mtoeiBs;>amV9i2ohC_g(p1tvWXHaer|KSlk%zVd&c`gSmp z$-&dcVd3{6bWupju{AAw%S4wvMpDiYJ zAX{pe8PWC#QVj?Ss>f7$tVs?2uw=J<$wjZpL&~UVNGGQCuzPn2&i^>(w_K=gs?eq{ z&almtIn>t#cGGk~h&c)DK%4C@@CF>!rFWHa!VSS0d3mV6U@!B^zdxJ<-V2>3^VryMqOo=;ysEDB^1;vg;jHmlo+c1EX_aNPR7ApPm67~fPL{sZt z|JoVTSOL<1vjQV_Y5UkNI`ssVaz}ZgmL-2bmKuI3wyKWm!CoH@L8N@F5yn48xE9!@5(YwEjvtFCgE1M_p=nyk9*`V8q$>HZDgANHsO?tJb-C(w|AhrF5Q)*|SXYFNjJEM24`hHzxnBqmr+zVr)!;|5@bWO*JRb__?GT z4Us|a{TaGX(RrT<&h9C$zMLMiECVjc>R}8hN8#kHj@fzO&lz$yx{;Gl1B&dokxG4U7&1xtj!{gTUW?{Kig) zns|ioqD@qz>EETKJnUDE58jbE;f!h8KY^^)oXWljn|>T2^ScmmQneIom5~!?pJWIBE`_K@1bMx`dUd*=0VvtSjLYWy$<^)% zaK<;)s;SmXIX_-*_XWe}3wW^5(ZL%Gr+`a!YSN^s0oDk(ZMV4|snGnj07kY?8Z)v| zt&u;pnq4;+{3R{UwBp3{YU?X&wKa+$LZiQciOlpA7sR)4Nl;sb2<)Zs z)E&aEj~g+u;E|?2&VRDj>Ts5mvtf@OEspsPT{6i-TBk*1d_S3Ja8%UPGC2D> zp&CPMtB`WBjzl*}y0Y)~LHLuJBvY(S<@DNZWb1OQviaXyMY6fO<)+nqJ)5ai92uMI z?IlIAq$rw%p!4#-8LHuadEf7MA1`$p*^7J_h+295O&X-raV$9|`~P1Q!2FZ}w6tN|vY**dhkhbuAvlN>1=X|~AV+M&^Hj&jb&=h& z?d)yDf4A~BpUoyGhw=Bx*j|)9=F>>AaRk;|jz*Tl>PBBj!Y_hgH21~LAwDV2!HV{lcuu>W#vUbmCO?Xd7QSC3D}359>mM_3qOV| z&^0KBKE}cH4h@*g`3EL}0cJ(R-VWKbO}m#uJa4IB;m^}rkW!NFmJ7zie$=(%Q`~#P z8*JzK!@1@Z)#3_pCXD1^>pEO*#&dnE7opZPFkIa7y_67KBcqc85^Ma8Km3c~YAhUn z$(tkud~(>?4uo*n>5_qY1U~bZ++a8qLUuE0W#z<{9|ndRyu|1KW{S7B5AwY?l)rol zY-)0$S!&f81@C;`Syu7mjUr6>8qIVdiNhY$6d6bmQef+iEtw}aLycA9RBvweM6&5d ztbrL`Bd8{h8IK`pW~#R?Tx#R3aGA@e|DJxW1n>WbQiwS1ca+9Uti>T^tig#MHwKMsO>o$ z8PCOFl9&C!AM#+$`{d~j|5pvTaPAq&>*?9c^}g2-0g@>*)0^1XSovK^e>nmHsMcZ7 zpC8oCdsR(Ir;j@oSQh=oy7QM0VX;J9Pk?Wp@O2&n!$%y~?;QZw0Sr&~MV&}Gl$hm< zf^RuPG{}O+x@Bc->_4{%z^t|4`UVN7rOwNjs5DiVOrD%HB@Kl4O=OY_lZuN&EOyS! ziG4Pvzhp;S|Ko=UrwHc*p@#pti%y#RA$%zYita4|_Qyc0(XE{0Ig2M*+z$qg9YVRI z7H(R`YzuvVsiUF(8XiQAyvYssVhE`Zh zL}vuN*Djh`Ui#S8^~+{+14Sy1KbQB|)AH9qpzrj_{l%vD$@ZV-Q>8rmB{L?t-_)XHB8g=X%X5)72&HfNxJ8$&7ou#$Jep4d5)T{bP_gtp(zE?lkY-FrRbV% z)?CVLR1ktb_VJ4j^ZM*`-EzM3B89_CLiT!|WiPkLlzZ8;(HEFP(!e`nlCGe=75|;~ zok|MCl|t^`Ki<}tE~89L6J?6}#rsogw~R>xY~)g`E@sVV&N#y18eGXW1^%3Cpgv~pzxJcuB!F6KU&7%VWN zkj3wM8)A%Q9-G~6xib;ETh40O@pjBGm6L#}jVOa_=r`gKlbDeh^|{Q}SUa5(kfILj z3w2oRiPR7^p9-IAc~Z+{2c@gQ4yv649|5+PXxb!`>qA$MlY3nlH@Ez7ovZ72zrQa> zp@(R3>%5)D<2z1N&v&{VA6ncDNdJ&Td^*yK129V&ujnvL_b&Ud*f_f+wIj!1B7TZ` zE0QZTq^W&@E2aoV?-=pFuefx}EYV7BeVy`fj^)fn4?XM|G8Cq*4T(kd^gbNis1dWp zRbopdjsj@zq<|DelDFfr-$O0~-K3BVHBr@rqr<(D&PA`ZF@(%_aVniZ_ zmm*d~`~JJ?2_k50Ps;qp7n;)Dc2mfu@NI2B8<%O5cglTWjppQ#Wxez7)54o0*t3?J zJzEo)DLYaPEFFZ$(JhI+>$jkR);hz*lYAC`H=V|9YYodl&Yfhboq1u6AgD?o5+U-_ zG13t}-z=WT2AS-hs$Am9oE7)ujNO@Bn|r&AU$%WA`}f(tSd?ndm<4a-EMFkGz+>_S zcv8PT2|lqg7#1?t0`&<)eyEKjPzS#(UwtvbTqF9?c%)$2-&N@5{z^EFnC2CM+C?S> z?SuFi!A&}TFQzXu*{>?>nZ(UG27p>N#wX~<^2hA#hKSFc^CEXTCZf(TBNxILtxC!2 zv$22kzmD7Guf(NldW0D6{jCEIvF~}!LQE8qhACla{GA1wyAFP5{#&&o*^09W2ssmU zoph}&Ppx;eyHna-NvCw*@g{Vg|E+LP7KTWSk9#?}-kr98cj&@KJPOE7biEkOvwP_d zHs$hx+A_M0N&2lF+1dI1WU&UwcfR*`PW+K;@)_UbV#C(~t);nner(KYC<-TaB2JrV z7hFHvMj57Qwlg+B#Q$-9=8${9zk%Gbi{<`)e5Zax+j7Bcwg~BPVWQ2h99EEkNF1I> zyTyBM%r{kp(7@|L{QD(_7-;}4QfAFTc)796GX8FQK1hZvZABH)t6_FOLK3wyt3ZoLRy|b;CPL*!b8Uy7U9hH`$#N> zst}o_f)exvZZu+4RF2!}b3pt^xs;uU*3IQHgSByEObRR2KEl;sGqs?Hx0B%KJ5m9Y zPb@-}Nw(Gd$S^Q`3=UJ~FRe8n8(rqwDh2SxjAYe7Wj2J*ql= z1W=#RXJ|NNd_HIDz{|WuD6&GZR-|5`TEyRfr}G*A9_M2wZc`rrefn{ESs&3BqDQG& z$wla9UD^frRc;I8n~#aIxtyG>inXL9*hG$6kW@|1n>4O`n($U<|8S7zuDV3$`FHi+ zB*|wp>C98=3)LKf*`dOs>@MwrY(qg)|R45WxBqX)PxAH0a>gGx+CN3?>j ze38XZATMG`f`g3D#umTn%LnHvjlqdV`$st>KN0IZcQaq(_wpo*8Q9Xjj=avF=zeti z(Ns;D%{=8}M=MQ%ts_5aEe)`eh{-}5(44P7dO9&0GMd2VBk=!LxeO{IsVj%=`v7ka zVjU6ph$rBx;8$>sVmV)w&$6kR4+dIrK-CW|hD`ur2-LxNi9;wc6Z{_M{iid&{;gKi zT^B_-FSV!q&h9N|$A#VDw*;M5`#1V}c&8QWt5lzdn3-I!^eELmMMH@H9;t8hcAX)Nl@D8y&{uiSEj!d^*e;rA$gu zv;UrIAMWMP7XCc|wEYGe(#*f8fa^0B=fCGI*xc%$kO?xH1s>MVO)D~`KVD2RbLW{r zS1{VekOP8yjVAjh60-3Y)qKgx@o%G?#5fI@RPhkYUZp+haVt~!o4;IE)0kJ>&-siG zy6np9zU|>b;z%fOeVCM=m9W(4ygz9^_?a%*^Ai&0R)_l9V`SkRHd-u3^sI+Ftoie^ zvy+eSP8`g#;v5O>oU0Ro`w7grp{g zTpna9^T*elq-yhAdfYAFo>PG+()E|icD%$0NJK(%Dd7#yLYol`c>5;?3GChx0Aafy z1sTx{4Gj&1p`2{~`Y1>Hs9mBv2EceA6`<2;7Y;ErhzHKg%c~ybe!j-{c=UQXfR@gp zKdZUMQK_lJr~}XfZ)x7gU)S zVGVLkf@`z2Wnx-N_)Q{4S=sA|k+qta{5RH1iU|hkTSKfr6Gs303lEJ~_nqxgZqOT> zYs?Il;0GX2ZDiw@L}26SpyreN@*7?+CotCQ?%(1(LNQ$|L4*4d)JV&?22+-hkJ7Jv zB2LT2=E(7hRe!-%^zY z=Pl-Ck5#ZpXeu@i&eP*#7mrTOF;LMIiE}e%%_c7e+7EvjzHnXKXf;781}qT)=`R$BR$RCW{DowSoDpN6nub>H9EP>8AL)48|rpzarz zhoE?&bZ@z3Vl6pr9UlLbV{I8`jN04DvFV$GX{EXGdM>==H!dB>xJ=o79B(OmUZM+B z#k0PKIC$uIvxW#AEG2{;R@e)9^3nt4o#4jv!XFNycngJWyjG`5DeNRv-abwq;bC^2 ztZsKqvlgniPF6$ZKUZ6y@0M`ic=*027hMl+oBzPb8bjfDar-3W$j{TEgiDya-E~rS zGa&b&C!>~0XH*kEjiuqp&)T6xke*R*e_9~om*03Usi0LT$R3TCHdetzzW=qhrn&?j zf>Y#e@YJA2K+aXJ6bB*zzJs!WH55rw+3d1{fW+Tw+G^r>D@cu+s(_d~POPz)u%TC6 z^m%i|=LX|u%fBvf$8UG5f8U~Z_nY`KlS-VN^3&uOkV|YMW*m?71^npG#2&Yypz>b@ z*ayw)3^r2-1=Iat#sL(5k}jw;w4j=ZRD51utg3{>)4SuoZ$IH#{0!n>^VgkS+HX%6 zK*X(|A`rl?wv{96c(oorLKojV4E&FuIeF-GqviM_p?#XkImjQkx#ICy!O=Ijr%>KS z4kZs&CTffz(C6bZyD1B_*StA))vY4>2(@?PWc#DlHY_ z>>b~mp7FEG2rYte2yss;c+-+XX@oj4L$8BF^P^=gs`1%;KM_#iIu#vVIgk<2-Z{{u z*e$G{pD~SkFP+9}_&%p7(x+&))f=ea>Hc>1b90M%8gMpudf!BB<|$&s&WR70pCbJRnRmYOFgc$oxai{2$`)YL{L8AE8m0D`1sX9Kz4r(AK+#xW}(N|;w+pu5H-eVz)h zN}q68`$6#!&I`3H*Ya1gYjqKy@a2VF|@0Py6JacX&t+xsp zO3#y*oEBcN+vRNj3vA0LtNXK(b!nvhUT~hE*KDPFRVWhP^V3tLxWl)*N?cGwMx10B z`a!bV9;HIP$9!Tp9e2$t(9Tv!gm*6aM3)rZCRUjho1V5>l>;)N0Vp7AK$vuTM~J+D zuTZ5#@8jzW5UaezaV~{t!9OBrna?o(FLzg%S?SO=87bB}yfDD;=wir`Ro8PrWwzw}T3` z+N^IhckTKj#U`d-$`!VJKV}y+^3d=(KQTGNJR8Q9*jpt_kmE|C}PuA+_ag{hWabOeo3A6~nS#g0b8)8?C!bSU+X);YG*nZ7n* zs-o%1-z!&)ZvRDd(SF>EF-^zJ@6YDy4#Ubk=-M5-g>QG#lbpT^c*|MRgqzd-sB()+ z4Kb5do!-IY$@$KGF>8AC^+8YFnc;Mckdg}84*t#VMm4-bs`yQLcA3~ftChY?I#a1q zhEaLwAaZ~sQIF1;-b)Pf?mlzZREqb^eTX(8=Bi!Lj4;>L-ZPHAOes;RNP#Iys`!!O zXzd4-mzrMn`ZC~S4vL~*pvAVrludE+>W^2@Q<2q<6hyuZP|HzY?|2$LqXL+NiI)oE`)^QiJneKn}m~5t|lsjV4!%f zaQ^JkM2d7u`rJcLAEipZRopSN!Zi*y2%5Kt@Fz8Bsiwh^F6QcrD4JZ!@7OUj7o`ja z`FxDc!^o+%3i74Pe#;I#_*EH^DoT`LNfmFFG#i)%J8!!@Ct)s$julHxogJAN%OUem z^xH;>2?WE~;1D^2IKCwnJld8)y^@$1W{)$1=p+OpV?eH{Fow3IjwzKBB~%xJy#!l`;&e0tZhN5q3Fm2O_Y9=(5{+o8|6$dlS$kPb_2_)o;kCVb zw2)_(gbY&8hODKVE_^&>j&Jrh>d0W2*3?hqG$%7;>vk;Y`|S<~T&U+1%ox=$AuHQi zbb7yvZmzPNu((aUD09P+Pe`l?}N04W!oNj*A%?Jc!zZVEBac63L>m1E}97BKD){67&o1f-88?lkAM8P z?o*rhHR}tm%h=HMS1`3ZKbXC+-uiB+gYdGqrS8A(~?b)G#1E{(3FtIGBQEy0qV6T-d8MYvKI9on;qUje@MN_ zE~lu-w3&K?#t_8Rlq=-1H}^f5#QRUVZwQ+%^EiL~4^H(-t5&wR^5pY&r^4yK$|Na5 z6-vc(GzCyD*kvrjFv-RWI9jUabyq6rSj6Qc7)j-JMU7%tUDgQs(F|i;ReDO3B1MYs zpAYXMoRp@npAW%NnuLW2_Y|4WZcIyM2>MJ8K|%!!7WHY1)Jn7&!Ue)v5kl}N74xTe zEhzo@z)q+*oB4^^;@P1}Kfw7J9aISMX!^H~>Fg%7EdMt#P9v}TTa*zZ*pILoG<|K6 zrSf>4Do1U#+&2yxu-M`8P4cf;+2eWZ+9+ABB=dNu>Q``%!u?7#KKk9Jg0%@KW#=>) zDhj+6C-Fq<;0l$Q7VmlX>xiITB0HsRmUBkr?+*6N&9(~;<<{>-HvfN5HH%}G#XdqX zC}NS+`JJo*Is^uwsnpyjsE6Ir;1<@`qE(?}G4tFMd>>&y8hYl(tv8Zy9o9>|+rzg= zA~YJPu@TL_pZnX2kI_0PF5f7Fa)jkM{ipV3@D8IRUy9}#BH?6oe4qCMQYd5hrmXCx z{eIZAJZ|kzE80+A7P(Q}8r~Ez!pu|+g(&C20#&|b zr$>^nBIpM)nXWq{&McHDKWe|~i=7YFJ%6NM;yPb&I&P1oSI8~LdHo&rJ+&ip8IS6| zhAm!){7?vrFHepYwG0{4vuz1X^CWg&jP%-c6QzcC(V#VC-J~i_={y#i=xz;q=BV!0 zvu;3BfwI;!l+mi?G;y@2K!y!H&`Wvk77s1J=Sv&iJPU6)d$Ki-Xogy8yJ}%A3ys9+ zFo#QvWK3E0J5Q(l^&4dCJRVsEV^moKG6tk1=xt9V3~XRaAhDDu8c9P-Th}(@;hk$y z5-_xF=iumATU#q7B?TUqS5WzcQK>l;{gaxw#hPmL5xG0+)9~Ceo1kd(d^5D9_gWBj z%TBoIbF((09PWuyvu?-BnpPr3OTO0Z?BcY^w)wg2y3=CVI>FnDE+bV)^E0|BxFa7e1? zf>zWx%DUE0LuyCE*Zjy99ZQgYGO8fdEeXpS#*cTS;e1o^>&tz26r4Gs25*bSD#NLZxP ziUJjCfPq)uq z!jr9d92u&9ldwX>r)eMgm~ZAb-(u0~v^-B`8j%Jk?fZ`SY-jAJGrSl(U;-OunF(|^ zVN9N{yzY~0sOyJ>k$FRYg5`6)$%3w$>@TmjA6njINk!1GiEBLK;=`a^uM5IPO&iNY zw-HJlN)@tg5L1vsX_BKP`bE}8qu2C|45dx*zJf3mU-0-2Jm|nJ_3{6Y=pz%S%pf|d z=RfJa>iGN7Ka6AZpH@_NqV$_Tuu&KqRM_Et!o@Voo>l3JesOFvI8L9OM zo1nHfyU_cb<9GkjB|y^H$9{b@>i->sPS!PUaS*`@u=s# zIpO=L(Lu+-8z(6@OANi$X`uzbh@F2gWP)4)1+ESmOV~sqF5}N;eiS$vK~KfbGK7jt zO$9-zRVbZCh$;%>TE1ND2_oBDP8z{cFw*`fQm#~1Uu&xa2^NfyX1{F!uiUlZR~KBJ zE05lszFg9|*N&dvFaa^%AY;i9fWoeW^7RuhB+w_76HcAK6}sfQrh~Th9B6 z9Pe=&-KdctzVva{yrIuU`u=u*z zuf9HY&&~%6cxc8RH|L49R*TJI&-&BbX429&N8QD~K9B30hCGhHAdG^}aTc40t1L;X zHls^@>WH0WYSCQ24>9+JlUP4l#)kwT&+s-SQTP=okhsAJXgj^)8+{;1=r?$*}M7 z_^OEb_%W$ACnYiVyc$mrp|UpsAQ4YE6ek(D=dfp*msE&;%IB$S<%=zkjPG zeD~jTDWCWcpO`eq;LTJn^S{5YlOnZ6YVBhca+e?9d~KghZ#zw7(jB+^<`cv*2!P+O+hYZrProt1 z!NJvQcmqWYUElZli3xgMUS7cY+Fv#|r{us(1jTNxk9eyym z4VCr!JhKEL_~BK85{KtK;xcq zw14b6oFDMIW~&mqJ~Cu6d7aTDo0S0so1?@&qv)UMXAPN6?`xyG6VJT2lydmIHaAd% zy%)ts^(u-!brYQ>|2Ggc1X0TQ5672}>Iws`b+n)fa8k#c(ZjLnkMfCs^LPaG2NS#r z9etdq^~qeO<7?S2efPWQd;5Qzp;g_Z-GUSEYVO;vFm$GW59^5|4dXZR&TvQK;HPn4 z>d~Ij4A})bCo>&0)i36`6=4g<3!c6Z+Z=TF4S5~Rnhu9gm`F(z)6?FDcv5Tr9h0Wj zvS%vz^>mGHw&3u;1J9vh$LLxqih^(`C@!YyEM9h$o5f9?q_t|=^gl?Ui5x*_+Hz$5ZCpog@P+bW~+Vg%Dy zOHX$!hThUz`$mO*-PTi?VLDc$^_pc5mhL6KIZ_yQ z+62uf68PTtzY20Vs&I&*UYIld*T>57mq`>_abhuo)hd%`&P##Bq}*ABL=e3!*{8*t zX`!YD7le`M-i3Vo3d*v*pE)sEF=W9(HIFgMqy8cl81+v;#0P}yc7UPXh-4n~mRZdq zY62Nb7`x7xx*NIm@pQyG`>kzTUwcR{fzL0d`3!H1qryN=xY*%8=33>Vn5O$UV;UH~clYB8 zA8EO0Gdx)tb*;s8C)Ku!*nHb2=sef+BoB<8fKBa(;ZBOSnsOZW(-URWDnKCAUY>27 z+?z*{Vp@8^NTYAgze|`+N)2ad${)R5z7b2#T&X)@dJzz3=}#GEDlkNF8N**Ho3G2< z$X9A%gS`FhmXtQ@@^Cj)4<@ooqqTQQbcDKCd9dBAeyt0dRi(>nvznd*fO*m9tHyf? zK@5tD5aQo9SQ`{~Ns^(2BCh&Aa0^v?finmp!2#l~>&6P7njQO(RxeWmBpoL{i{ALU z+v^kvBudr%ovq>&`CYMZL`%tPcOOk?WO#;qdhyV_6&V~U7<B-610o^~CO~&BV;8%Qmw=Q@KZQ6-2FwqGxF>x*)_hw$y1Gpa2 zeYY_s^QX<{G}^;5@9xHK_QiBD?Z&-PjqRtJL)Sf|*FG?!-&D#C^ZC?S^DQe2l53jL zRBX29@#ONtzla(>yIYr)1;*CcepI=TscK(`iHC$Edb_)xrX9ai((|0h4#bwS(gZ1# zE8pH|M`t;8U=A~G{`iN;gLQAdX!%@#-vLQw@5(-Y%-l0v^aH0OzhHnMWuZvq5~Lcy zEUs0ZyHgx|sxDD~=d1>QMtFA&-EnrQ3A`2w8J&WqnkAmS@}4QBQ=bxyKpOde(ok^%8KdzmT$RNlKxNDP^lE5 z>P-Ra@+07>-Zl0v+Z^>@B>hmGnFiMShZ>VcA(soynN1-mPStUWb5Ct)5Uz91VctbW z!SR2d;f|Qsy_ew*y0p&?o4Yz?&qc0_RDgEZ)j<|4z3JRlPh2t=lajW>;w$tjcp4QX z9^OaH*JEf>^ibYn%W)tjN9>=Fk+JSLP-nyGd1`2Y_RR=GzT#|?$E>ORT}Fuo)+n|P zG7E+Bfe6s2&IjX!J>@sC=E1VPdDpDhm)X^k1mxefIhv^5cD6`!?xQh;jiLw6yQ@Hz z(?AWJ302AjXuho(4DDRC#Uy~G0KgjG?8L>Gz1U%|rtFy(N3muvkp}YGDJl}gAB-cG zGR;-UD9S5?Cd2OZJf&V>o7NXg>? zxj8YX@BIeXfcS#j`Q#3O{d9FK_J?BwP3`n_%~F-}vUd+WyrqNnR#&i?Mtyo>U^)I= z^Y$gdi?qgn&Kq&|il)&zb(Vvf9G|2TX!5AG#nRuu4CL!Jf9y>8Wd?Au)tI-M`9iQ} zLNg&aHZDg!H4o-`Lna_KP`Bql{eHj0^0r0^s%D~gXfniC!B-p#6|m2(mLzX1<lGyEc?^Zk4l!LSN6@$*4pn5B*jYuvsA!A2g%TV z^#{j>G=5llQnjalG*!)BR$+MdRmU=^(sHw5H=6%#DaSgv|JKS@rzJT(;s6Z;swT`2 zRX(hF&eetCw8kEU$3|=qY6Sd)Q-8Yy;nCOD*S)>G+CCB+8y`z0_b)F|bhm)bE*-;E zE^u;HuHDMU!=ptJ-@W*h(}Wg)fmh>fc8FE}Dx+QJwC|v)xhgU%c4xKrPv3MkK2|LU z^@o#3Kt`IA!~ETKI?;9_Y@Tc$BLp#ZTaCd`ws&%~vHkd`MNJ0wpy(qZkLzHuX9o#9 z$SQOAue<4heqiE}w1>&cHt6T4*}CJ-TLI$0qs>Cw`B0>4{1sF?+}z#H+n}E6yJ#S% zRO1fn|1XmUP%oeTF4nt~$LSvhfzo7(5sl@cFln%E$Vqm6BrJZ~2W=QBBSfM!Dt=Jt zpf_>1g)o=%P@3wyircr^TS2Pfv}E8!7r!;zT%YAvnZen}EsoZcu~K+rcGNeJ*&Nz5 zdgl&C894=qW$#b&atVhYBgZ<7ZN4$Tit%Lgu|o4#A7D15x6|?dTWi8;Uu>)ieEBqg zcU2En02@k~EhN(Xo2ZV1nusr@e0f_gF@Ib}WGB+SbTYdy++AgW<&7{A-Ve+B?t{ zHnidBQS5i!Dl}{AsREEr_Uc7rcFV+CR`kQ|GH10|Frid~b%&G9>oIz|95;6+Z~Jb| z=zgF-xf_G%l`HN7&}HDTXS8}4moM4IH3dPJ;;G(LY;6dI!ivR;7FIWC4HGnj?9U$d z7+2#&tz_}*NPO<1BAl@iVdYjAT9jY)E`yx|65A%u5i0MhR9S!-&7&;(uga5>76RcV zL{9gcd=SzH044R0?tHsJq{RVX5?TPz^frN75%?0rB?I!tl;mX7^>go%bwfLU?E9CZ zb%>%{#=9c}6+1S%HnjwCg`)39_Ga?s_t52>?Z8frMLirq0KIIb!P9-F zHm?W#YE+Z3Uv{?}sfx4uO9k;a1*hHjMwg%Etd=;-Ov?1ev zNyM`m-R(#q0c_Pji3xMKDl>4uh=9BkBZ<}9`?v1&J(LL?q;CMq)~DvS!`ko<%EfxQ z)M+QM5I%~2DF3{eDSqcCD7%G-@a|wVknn(*qyu^&l5%nkQii1*D98kKMmv2$=1qu{6;XtY9ov^f zWdi4`E=$eswgzq#&}3)|b9SzfA^<5loaw7MV{@Po8!f})gk`4*C~2+YN}=Evl}e?~ zdZ7%S@2ABePWR+HFr~Yq7Ob~ioapW*p{KvN*WL`iNmSdB^jiFO!fChMZ^R_mcUY6c zA}#y2JA3--d^u-);}T6ybLM3?OEmFbG&(hI^CeNGNQFj8BR=lxPeVoxY`_T#T(HBlB7B~5^dCx^qF$L96}-)sYnwog{kOdDr_y;N0$DO1dM)XctP zBP$-oY}AB~Gs!Gb)31-R4eeuS2YpYO}(Y&(E1>7lXx^URg$7*Zq>m83;H^__ob+| zr>5Ut#6Q_5=e2=wwdwUH3c3}p(RQ)n@TE6vUrj6TY_Huj-v+ufrfUdioS!W>}i92S09bnK&?MBvF9E<|2fB+a1v#Y^9OqO|eX2g|gv`f$lL9rxtgh5DeH1bmE`n=Rh#(_zP;PKtWy zK2O=0nCuzPrY6|K_tUU8NX+JjRrqg&o55!1bGCQ1$IIz9H9-RLp$o61pNSi2h-4;tmlgZZ3=wByLYJ{k}aK%$}_;UngmTOg|{O z^^M1iNsZP8pf~{`5aL+zq#{+3;`x(y&qp@P1*`QI=PTnR#PL#G@O3rblI2p1XTgCw zj*|w#jjXvD!^7Cq@vy$?d<)jUbJ7 zLJhJ)&;}<*{UE=#DCcux*jBx&JYU(Ku8NS{3a_WzWDsS1EFKQ;i1+1h6*{=G_N>W8 zs5}+FDm3Nl$!q%n(@Sq;+Y@Qr0xgQ<(t6l=rE=%QVR;?!^vsEkZjW3vL5`vIsi^yAc3-gbmfyM(E zp%xEi>Zst@QebVKT)~lejwzK)4UXU{&Taa#gL}wQ@{H*5sB+fdM6*6n%KCDqV52}7 zKP=XfZEG|~8FVXE>*q4#WeZ$3SYt5^nivd?3jB1mJKjGfdSo1%!!eW zDl=I9rHi0Ik~&t*1d+6+Ddzw|e>pcqY3=+p*@RZ3Q!jj!b#o_~M6mP^^3l}zMHiw2 z&xTr3qAiAZ%daR!KBPJLsL zymLrmim%=t1yTBZsqF_&f_w-?z75ygEgNe%8%~j!ob7U)9!ZuU@qm|-$v=vGY{ zPEH}LX_8Au{`s&mmQXm0VGrv0VF_aoC6O6}%IRx1I6&sIF%)wcDky4mQ&QY+y+X_N zU3hfMF9K>Vsi4>Ar1?pJm^U7#=3iTD5qra1gZ-niYunU|RT5)>zBJ|Sn)i;@+f*^_ ze!S)CYQs6>gSaq@2%M*mx!T!wJ-Vh#X8UY?&X61xUbEeWe+Qn96hBJwZtK& z=b(qS6oU*aZhU}Cl*QxVR%wnr#9x^iTDPwO9E#LqQ<5c52`3z2KejGKDwJqx7qS9O z(3e&nZZgmz*=kU~y>OwejMM{~42Pd04+WD)|@4 zfZd!{cG_&THJ|mL2KXli1OxyBaZnsPCzS4KK$$R{v_sivi(q?jS$iVGbn2Z2XAg4( z#F3-6t4m$=v+-D=S&PcguNzaHnv0=>3e8_9eib_lj3PI0ahL_}hU4$q2p8?`4L-dF z4NyLBs&P4OnYC&@L#ZhSMEs*B%eM{~?vh^Sb~Sp0nSfW?%VVS#-w4L8vzaknpXY$& zsgW;b$7xE9K0qPlG9N^=NM%uhA|;e79IZpZ;$rV%;Nc>j8lB(YsIAgsx>>om7>($6 zNEEf4vfPxUu~a$N2jvQV5{fGrmM&<0t+M$i^MU|0{gJEea-5&7QzIw>o=p?R`r=km z@6yJ_G*H3v7D)EZZ8hd%7zXN+^kfv7aldHiaO-eV=g8~+Rt5S@R@Q~&ZO^#d@CEs~ z3*9oaVkH0PA1}@qs`LknT2Jq)G;`KWTzA9iRCKJMq&g14m`TDIH%FLG`8ZLMPV4({ z>|wErMIgf751RT7z5gVQEc}$9JDnYxMTtdDrf{6rF zKX93_@)wAtbT>1HLoDk7Yw4UGWhOL_a)zlYp`8oPv7-;w(v5g}k5}q%KOn*a)sr&@ zfQ-c91lWZrT=r&2!an@2<>>K-B+dUz{`WUFl;b6f-@DXtO;h<}OyadlbwhE|y0m

d7KlMPAvBE1lfMLEDrTC6gdG-`({de6(1$TiXtX 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

M%9XX&e&+T1w(cCF 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 literal 0 HcmV?d00001 diff --git a/main.py b/main.py index 215d0c9..ba9df4d 100644 --- a/main.py +++ b/main.py @@ -264,12 +264,26 @@ def build_optimizer(model, args): ''' for testing args.method = 'vidt' - args.backbone_name = 'swin_tiny' - args.batch_size = 2 - args.num_workers = 2 + args.backbone_name = 'swin_nano' + args.batch_size = 4 + args.num_workers = 4 args.aux_loss = True args.with_box_refine = True args.output_dir = 'testing' + args.coco_path = '/mnt/ddn/datasets/COCO2017_Seg/train' + + # seg + args.epff = True + args.token_label = False #True + args.iou_aware = False #True + args.with_vector = False #True + args.masks = False #True + args.vector_hidden_dim = 256 #1024 + args.vector_loss_coef = 3.0 + args.det_token_num = 300 + + args.resume = '/mnt/backbone-nfs/hwanjun/pami2022/optimized_checkpoints/vidt_plus_swin_nano_optimized.pth' + args.eval = True ''' # set dim_feedforward differently diff --git a/methods/__init__.py b/methods/__init__.py index 9c34d6c..a23a772 100644 --- a/methods/__init__.py +++ b/methods/__init__.py @@ -5,7 +5,6 @@ ''' from methods.vidt.detector import build as vidt_build -from methods.vidt_wo_neck.detector import build as vidt_wo_neck_build def build_model(args, is_teacher=False): available_methods = ['vidt_wo_neck', 'vidt'] @@ -15,6 +14,3 @@ def build_model(args, is_teacher=False): elif args.method == 'vidt': return vidt_build(args, is_teacher=is_teacher) - - elif args.method == 'vidt_wo_neck': - return vidt_wo_neck_build(args) diff --git a/methods/vidt/criterion.py b/methods/vidt/criterion.py index 58ecb19..c942b90 100644 --- a/methods/vidt/criterion.py +++ b/methods/vidt/criterion.py @@ -19,6 +19,9 @@ is_dist_avail_and_initialized) from methods.segmentation import (dice_loss, sigmoid_focal_loss) import copy +from util.detectron2.structures.masks import BitMasks +import cv2 +import numpy as np class SetCriterion(nn.Module): @@ -28,7 +31,13 @@ class SetCriterion(nn.Module): 2) we supervise each pair of matched ground-truth / prediction (supervise class and box) """ - def __init__(self, num_classes, matcher, weight_dict, losses, focal_alpha=0.25): + def __init__(self, num_classes, matcher, weight_dict, losses, focal_alpha=0.25, + # For UQR module for instance segmentation + with_vector=False, + processor_dct=None, + vector_loss_coef=0.7, + no_vector_loss_norm=False, + vector_start_stage=0): """ Create the criterion. Parameters: num_classes: number of object categories, omitting the special no-object category @@ -44,6 +53,18 @@ def __init__(self, num_classes, matcher, weight_dict, losses, focal_alpha=0.25): self.weight_dict = weight_dict self.losses = losses self.focal_alpha = focal_alpha + # + self.with_vector = with_vector + self.processor_dct = processor_dct + self.vector_loss_coef = vector_loss_coef + self.no_vector_loss_norm = no_vector_loss_norm + self.vector_start_stage = vector_start_stage + + if self.with_vector is True: + print(f'Training with {6-self.vector_start_stage} vector stages.') + print(f"Training with vector_loss_coef {self.vector_loss_coef}.") + if not self.no_vector_loss_norm: + print('Training with vector_loss_norm.') def loss_labels(self, outputs, targets, indices, num_boxes, log=True): """Classification loss (NLL) @@ -113,30 +134,52 @@ def loss_masks(self, outputs, targets, indices, num_boxes): """Compute the losses related to the masks: the focal loss and the dice loss. targets dicts must contain the key "masks" containing a tensor of dim [nb_target_boxes, h, w] """ - - assert "pred_masks" in outputs + assert "pred_vectors" in outputs src_idx = self._get_src_permutation_idx(indices) tgt_idx = self._get_tgt_permutation_idx(indices) - src_masks = outputs["pred_masks"] + src_masks = outputs["pred_vectors"] + src_boxes = outputs['pred_boxes'] # TODO use valid to mask invalid areas due to padding in loss + target_boxes = torch.cat([t['xyxy_boxes'][i] for t, (_, i) in zip(targets, indices)], dim=0) target_masks, valid = nested_tensor_from_tensor_list([t["masks"] for t in targets]).decompose() target_masks = target_masks.to(src_masks) + src_vectors = src_masks[src_idx] + src_boxes = src_boxes[src_idx] + target_masks = target_masks[tgt_idx] + + # crop gt_masks + n_keep, gt_mask_len = self.processor_dct.n_keep, self.processor_dct.gt_mask_len + gt_masks = BitMasks(target_masks) + gt_masks = gt_masks.crop_and_resize(target_boxes, gt_mask_len).to(device=src_masks.device).float() + target_masks = gt_masks + + if target_masks.shape[0] == 0: + losses = { + "loss_vector": src_vectors.sum() * 0 + } + return losses + + # perform dct transform + target_vectors = [] + for i in range(target_masks.shape[0]): + gt_mask_i = ((target_masks[i,:,:] >= 0.5)* 1).to(dtype=torch.uint8) + gt_mask_i = gt_mask_i.cpu().numpy().astype(np.float32) + coeffs = cv2.dct(gt_mask_i) + coeffs = torch.from_numpy(coeffs).flatten() + coeffs = coeffs[torch.tensor(self.processor_dct.zigzag_table)] + gt_label = coeffs.unsqueeze(0) + target_vectors.append(gt_label) + + target_vectors = torch.cat(target_vectors, dim=0).to(device=src_vectors.device) + losses = {} + if self.no_vector_loss_norm: + losses['loss_vector'] = self.vector_loss_coef * F.l1_loss(src_vectors, target_vectors, reduction='none').sum() / num_boxes + else: + losses['loss_vector'] = self.vector_loss_coef * F.l1_loss(src_vectors, target_vectors, reduction='mean') - src_masks = src_masks[src_idx] - # upsample predictions to the target size - src_masks = interpolate(src_masks[:, None], size=target_masks.shape[-2:], - mode="bilinear", align_corners=False) - src_masks = src_masks[:, 0].flatten(1) - - target_masks = target_masks[tgt_idx].flatten(1) - - losses = { - "loss_mask": sigmoid_focal_loss(src_masks, target_masks, num_boxes), - "loss_dice": dice_loss(src_masks, target_masks, num_boxes), - } return losses def loss_iouaware(self, outputs, targets, indices, num_boxes): @@ -220,13 +263,13 @@ def forward(self, outputs, targets, distil_tokens=None): outputs: dict of tensors, see the output specification of the model for the format targets: list of dicts, such that len(targets) == batch_size. The expected keys in each dict depends on the losses applied, see each loss' doc + distil_tokens: for token distillation """ outputs_without_aux = {k: v for k, v in outputs.items() if k != 'aux_outputs' and k != 'enc_outputs'} # Retrieve the matching between the outputs of the last layer and the targets indices = self.matcher(outputs_without_aux, targets) - _indices = indices # Compute the average number of target boxes accross all nodes, for normalization purposes num_boxes = sum(len(t["labels"]) for t in targets) @@ -246,7 +289,7 @@ def forward(self, outputs, targets, distil_tokens=None): for i, aux_outputs in enumerate(outputs['aux_outputs']): indices = self.matcher(aux_outputs, targets) for loss in self.losses: - if loss == 'masks': + if loss == 'masks' and i < self.vector_start_stage: # Intermediate masks losses are too costly to compute, we ignore them. continue kwargs = {} @@ -255,7 +298,6 @@ def forward(self, outputs, targets, distil_tokens=None): kwargs['log'] = False l_dict = self.get_loss(loss, aux_outputs, targets, indices, num_boxes, **kwargs) l_dict = {k + f'_{i}': v for k, v in l_dict.items()} - losses.update(l_dict) if 'enc_outputs' in outputs: diff --git a/methods/vidt/dct.py b/methods/vidt/dct.py new file mode 100644 index 0000000..7fc48c3 --- /dev/null +++ b/methods/vidt/dct.py @@ -0,0 +1,194 @@ +# ------------------------------------------------------------------------ +# Copyright (c) 2021 megvii-model. All Rights Reserved. +# ------------------------------------------------------------------------ +import numpy as np +import functools + +print = functools.partial(print, flush=True) + +class ProcessorDCT(object): + def __init__(self, n_keep, gt_mask_len): + self.n_keep = n_keep + self.gt_mask_len = gt_mask_len + + inputs = np.zeros((self.gt_mask_len, self.gt_mask_len)) + _, zigzag_table = self.zigzag(inputs) + self.zigzag_table = zigzag_table[:self.n_keep] + + def sigmoid(self, x): + """Apply the sigmoid operation. + """ + y = 1. / (1. + 1. / np.exp(x)) + dy = y * (1 - y) + return y + + def inverse_sigmoid(self, x): + """Apply the inverse sigmoid operation. + y = -ln(1-x/x) + """ + y = -1 * np.log((1 - x) / x) + return y + + def zigzag(self, input, gt=None): + """ + Zigzag scan of a matrix + Argument is a two-dimensional matrix of any size, + not strictly a square one. + Function returns a 1-by-(m*n) array, + where m and n are sizes of an input matrix, + consisting of its items scanned by a zigzag method. + + Args: + input (np.array): shape [h,w], value belong to [-127, 128], transformed from gt. + gt (np.array): shape [h,w], value belong to {0,1}, original instance segmentation gt mask. + Returns: + output (np.array): zig-zag encoded values, shape [h*w]. + indicator (np.array): positive sample indicator, shape [h,w]. + """ + # initializing the variables + h = 0 + v = 0 + + vmin = 0 + hmin = 0 + + vmax = input.shape[0] + hmax = input.shape[1] + assert vmax == hmax + + i = 0 + output = np.zeros((vmax * hmax)) + indicator = [] + + while ((v < vmax) and (h < hmax)): + if ((h + v) % 2) == 0: # going up + if (v == vmin): + output[i] = input[v, h] # if we got to the first line + indicator.append(v * vmax + h) + if (h == hmax): + v = v + 1 + else: + h = h + 1 + i = i + 1 + elif ((h == hmax - 1) and (v < vmax)): # if we got to the last column + output[i] = input[v, h] + indicator.append(v * vmax + h) + v = v + 1 + i = i + 1 + elif ((v > vmin) and (h < hmax - 1)): # all other cases + output[i] = input[v, h] + indicator.append(v * vmax + h) + v = v - 1 + h = h + 1 + i = i + 1 + else: # going down + if ((v == vmax - 1) and (h <= hmax - 1)): # if we got to the last line + output[i] = input[v, h] + indicator.append(v * vmax + h) + h = h + 1 + i = i + 1 + elif (h == hmin): # if we got to the first column + output[i] = input[v, h] + indicator.append(v * vmax + h) + if (v == vmax - 1): + h = h + 1 + else: + v = v + 1 + i = i + 1 + elif ((v < vmax - 1) and (h > hmin)): # all other cases + output[i] = input[v, h] + indicator.append(v * vmax + h) + v = v + 1 + h = h - 1 + i = i + 1 + if ((v == vmax - 1) and (h == hmax - 1)): # bottom right element + output[i] = input[v, h] + indicator.append(v * vmax + h) + break + return output, indicator + + def inverse_zigzag(self, input, vmax, hmax): + """ + Zigzag scan of a matrix + Argument is a two-dimensional matrix of any size, + not strictly a square one. + Function returns a 1-by-(m*n) array, + where m and n are sizes of an input matrix, + consisting of its items scanned by a zigzag method. + """ + # initializing the variables + h = 0 + v = 0 + + vmin = 0 + hmin = 0 + + output = np.zeros((vmax, hmax)) + i = 0 + while ((v < vmax) and (h < hmax)): + if ((h + v) % 2) == 0: # going up + if (v == vmin): + output[v, h] = input[i] # if we got to the first line + if (h == hmax): + v = v + 1 + else: + h = h + 1 + i = i + 1 + elif ((h == hmax - 1) and (v < vmax)): # if we got to the last column + output[v, h] = input[i] + v = v + 1 + i = i + 1 + elif ((v > vmin) and (h < hmax - 1)): # all other cases + output[v, h] = input[i] + v = v - 1 + h = h + 1 + i = i + 1 + else: # going down + if ((v == vmax - 1) and (h <= hmax - 1)): # if we got to the last line + output[v, h] = input[i] + h = h + 1 + i = i + 1 + elif (h == hmin): # if we got to the first column + output[v, h] = input[i] + if (v == vmax - 1): + h = h + 1 + else: + v = v + 1 + i = i + 1 + elif ((v < vmax - 1) and (h > hmin)): # all other cases + output[v, h] = input[i] + v = v + 1 + h = h - 1 + i = i + 1 + if ((v == vmax - 1) and (h == hmax - 1)): # bottom right element + output[v, h] = input[i] + break + return output + +''' +if __name__ == "__main__": + img_path = '/data/dongbin/projects/SparseR-CNN/projects/SparseRCNN/debug/tgt_1.jpg' + + # complete API + img_dir = '/data/dongbin/projects/Deformable-DETR-main/exps/paper_figs' + name = 'gt_4.jpg' + img_path = os.path.join(img_dir, name) + gt_mask_len = 128 + n_keep = 256 + processor_dct = ProcessorDCT(n_keep=n_keep, gt_mask_len=gt_mask_len) + mask = cv2.imread(img_path, 0).astype(np.float32) + coeffs = cv2.dct(mask) + cv2.imwrite(os.path.join(img_dir, '{}_coeffs.png'.format(name.split('.')[0])), coeffs) + + idct = np.zeros((gt_mask_len ** 2)) + vectors = torch.from_numpy(coeffs).flatten() + vectors = vectors[torch.tensor(processor_dct.zigzag_table)] + idct[:n_keep] = vectors.cpu().numpy() + idct = processor_dct.inverse_zigzag(idct, gt_mask_len, gt_mask_len) + cv2.imwrite(os.path.join(img_dir, '{}_i_coeffs.png'.format(name.split('.')[0])), idct) + re_mask = cv2.idct(idct) + max_v = np.max(re_mask) + min_v = np.min(re_mask) + re_mask = np.where(re_mask > (max_v + min_v) / 2., 255, 0) + cv2.imwrite(os.path.join(img_dir, '{}_recover.png'.format(name.split('.')[0])), re_mask) +''' \ No newline at end of file diff --git a/methods/vidt/detector.py b/methods/vidt/detector.py index 43f0d86..ac570fa 100644 --- a/methods/vidt/detector.py +++ b/methods/vidt/detector.py @@ -17,12 +17,12 @@ from methods.coat_w_ram import coat_lite_tiny, coat_lite_mini, coat_lite_small from .matcher import build_matcher from .criterion import SetCriterion -from .postprocessor import PostProcess +from .postprocessor import PostProcess, PostProcessSegm from .deformable_transformer import build_deforamble_transformer from methods.vidt.fpn_fusion import FPNFusionModule import copy import math - +from .dct import ProcessorDCT def _get_clones(module, N): """ Clone a moudle N times """ @@ -35,9 +35,10 @@ class Detector(nn.Module): def __init__(self, backbone, transformer, num_classes, num_queries, aux_loss=False, with_box_refine=False, - # The three techniques were not used in ViDT paper. - # After submitting our paper, we saw the ViDT performance could be further enhanced with them. - cross_scale_fusion=None, iou_aware=False, token_label=False, + # The three additional techniques for ViDT+ + epff=None,# (1) Efficient Pyramid Feature Fusion Module + with_vector=False, processor_dct=None, vector_hidden_dim=256,# (2) UQR Module + iou_aware=False, token_label=False, # (3) Additional losses distil=False): """ Initializes the model. Parameters: @@ -48,7 +49,7 @@ def __init__(self, backbone, transformer, num_classes, num_queries, DETR can detect in a single image. For COCO, we recommend 100 queries. aux_loss: True if auxiliary decoding losses (loss at each decoder layer) are to be used. with_box_refine: iterative bounding box refinement - cross_scale_fusion: None or fusion module available + epff: None or fusion module available iou_aware: True if iou_aware is to be used. see the original paper https://arxiv.org/abs/1912.05992 token_label: True if token_label is to be used. @@ -68,16 +69,24 @@ def __init__(self, backbone, transformer, num_classes, num_queries, self.aux_loss = aux_loss self.with_box_refine = with_box_refine - # three additional techniques not used in the ViDT paper - # optional use, we will revise our paper for the below techniques + + # For UQR module for ViDT+ + self.with_vector = with_vector + self.processor_dct = processor_dct + if self.with_vector: + print(f'Training with vector_hidden_dim {vector_hidden_dim}.', flush=True) + self.vector_embed = MLP(hidden_dim, vector_hidden_dim, self.processor_dct.n_keep, 3) + + ############ Modified for ViDT+ + # For two additional losses for ViDT+ self.iou_aware = iou_aware self.token_label = token_label # distillation self.distil = distil - # [PATCH] token channel reduction for the input to transformer decoder - if cross_scale_fusion is None: + # For EPFF module for ViDT+ + if epff is None: num_backbone_outs = len(backbone.num_channels) input_proj_list = [] for _ in range(num_backbone_outs): @@ -96,7 +105,8 @@ def __init__(self, backbone, transformer, num_classes, num_queries, self.fusion = None else: # the cross scale fusion module has its own reduction layers - self.fusion = cross_scale_fusion + self.fusion = epff + ############ # channel dim reduction for [DET] tokens self.tgt_proj = nn.Sequential( @@ -125,6 +135,12 @@ def __init__(self, backbone, transformer, num_classes, num_queries, nn.init.xavier_uniform_(self.query_pos_proj[0].weight, gain=1) nn.init.constant_(self.query_pos_proj[0].bias, 0) + ############ Added for UQR + if self.with_vector: + nn.init.constant_(self.vector_embed.layers[-1].weight.data, 0) + nn.init.constant_(self.vector_embed.layers[-1].bias.data, 0) + ############ + # the prediction is made for each decoding layers + the standalone detector (Swin with RAM) num_pred = transformer.decoder.num_layers + 1 @@ -141,6 +157,12 @@ def __init__(self, backbone, transformer, num_classes, num_queries, self.bbox_embed = nn.ModuleList([self.bbox_embed for _ in range(num_pred)]) self.transformer.decoder.bbox_embed = None + ############ Added for UQR + if self.with_vector: + nn.init.constant_(self.vector_embed.layers[-1].bias.data[2:], -2.0) + self.vector_embed = nn.ModuleList([self.vector_embed for _ in range(num_pred)]) + ############ + if self.iou_aware: self.iou_embed = MLP(hidden_dim, hidden_dim, 1, 3) if with_box_refine: @@ -170,8 +192,7 @@ def forward(self, samples: NestedTensor): If iou_aware is True, "pred_ious" is also returns as one of the key in "aux_outputs" - "enc_tokens": If token_label is True, "enc_tokens" is returned to be used - Note that aux_loss and box refinement is used in ViDT in default. The detailed ablation of using - the cross_scale_fusion, iou_aware & token_lablel loss will be discussed in a later version + Note that aux_loss and box refinement is used in ViDT in default. """ if isinstance(samples, (list, torch.Tensor)): @@ -197,7 +218,7 @@ def forward(self, samples: NestedTensor): for l, src in enumerate(features): srcs.append(self.input_proj[l](src)) else: - # multi-scale fusion is used if fusion is not None + # EPFF (multi-scale fusion) is used if fusion is activated srcs = self.fusion(features) masks = [] @@ -237,12 +258,29 @@ def forward(self, samples: NestedTensor): outputs_class = torch.stack(outputs_classes) outputs_coord = torch.stack(outputs_coords) + ############ Added for UQR + outputs_vector = None + if self.with_vector: + outputs_vectors = [] + for lvl in range(hs.shape[0]): + outputs_vector = self.vector_embed[lvl](hs[lvl]) + outputs_vectors.append(outputs_vector) + outputs_vector = torch.stack(outputs_vectors) + ############ + # final prediction is made the last decoding layer out = {'pred_logits': outputs_class[-1], 'pred_boxes': outputs_coord[-1]} + ############ Added for UQR + if self.with_vector: + out.update({'pred_vectors': outputs_vector[-1]}) + + ############ + # aux loss is defined by using the rest predictions if self.aux_loss and self.transformer.decoder.num_layers > 0: - out['aux_outputs'] = self._set_aux_loss(outputs_class, outputs_coord) + out['aux_outputs'] = self._set_aux_loss(outputs_class, outputs_coord, outputs_vector) + # iou awareness loss is defined for each decoding layer similar to auxiliary decoding loss if self.iou_aware: @@ -268,13 +306,17 @@ def forward(self, samples: NestedTensor): return out @torch.jit.unused - def _set_aux_loss(self, outputs_class, outputs_coord): + def _set_aux_loss(self, outputs_class, outputs_coord, outputs_vector): # this is a workaround to make torchscript happy, as torchscript # doesn't support dictionary with non-homogeneous values, such # as a dict having both a Tensor and a list. - return [{'pred_logits': a, 'pred_boxes': b} - for a, b in zip(outputs_class[:-1], outputs_coord[:-1])] + if outputs_vector is None: + return [{'pred_logits': a, 'pred_boxes': b} + for a, b in zip(outputs_class[:-1], outputs_coord[:-1])] + else: + return [{'pred_logits': a, 'pred_boxes': b, 'pred_vectors': c} + for a, b, c in zip(outputs_class[:-1], outputs_coord[:-1], outputs_vector[:-1])] class MLP(nn.Module): @@ -293,10 +335,14 @@ def forward(self, x): def build(args, is_teacher=False): - # a teacher model for distilation + # distillation is deprecated if is_teacher: - return build_teacher(args) - # + print('Token Distillation is deprecated in this version. Please use the previous version of ViDT.') + assert is_teacher is False + # a teacher model for distilation + #if is_teacher: + # return build_teacher(args) + ############################ if args.dataset_file == 'coco': num_classes = 91 @@ -329,12 +375,16 @@ def build(args, is_teacher=False): pos_dim=args.reduced_dim, cross_indices=args.cross_indices) - cross_scale_fusion = None - if args.cross_scale_fusion: - cross_scale_fusion = FPNFusionModule(backbone.num_channels, fuse_dim=args.reduced_dim) + epff = None + if args.epff: + epff = FPNFusionModule(backbone.num_channels, fuse_dim=args.reduced_dim) deform_transformers = build_deforamble_transformer(args) + # Added for UQR module + if args.with_vector: + processor_dct = ProcessorDCT(args.n_keep, args.gt_mask_len) + model = Detector( backbone, deform_transformers, @@ -343,10 +393,15 @@ def build(args, is_teacher=False): # two essential techniques used in ViDT aux_loss=args.aux_loss, with_box_refine=args.with_box_refine, - # three additional techniques (optionally) - cross_scale_fusion=cross_scale_fusion, + # an epff module for ViDT+ + epff=epff, + # an UQR module for ViDT+ + with_vector=args.with_vector, + processor_dct=processor_dct if args.with_vector else None, + # two additional losses for VIDT+ iou_aware=args.iou_aware, token_label=args.token_label, + vector_hidden_dim=args.vector_hidden_dim, # distil distil=False if args.distil_model is None else True, ) @@ -363,6 +418,10 @@ def build(args, is_teacher=False): weight_dict['loss_token_focal'] = args.token_loss_coef weight_dict['loss_token_dice'] = args.token_loss_coef + # For UQR module + if args.masks: + weight_dict["loss_vector"] = 1 + if args.distil_model is not None: weight_dict['loss_distil'] = args.distil_loss_coef @@ -378,14 +437,28 @@ def build(args, is_teacher=False): if args.iou_aware: losses += ['iouaware'] + # For UQR + if args.masks: + losses += ["masks"] + # num_classes, matcher, weight_dict, losses, focal_alpha=0.25 - criterion = SetCriterion(num_classes, matcher, weight_dict, losses, focal_alpha=args.focal_alpha) + criterion = SetCriterion(num_classes, matcher, weight_dict, losses, + focal_alpha=args.focal_alpha, + # For UQR + with_vector=args.with_vector, + processor_dct=processor_dct if args.with_vector else None, + vector_loss_coef=args.vector_loss_coef, + no_vector_loss_norm=args.no_vector_loss_norm, + vector_start_stage=args.vector_start_stage) criterion.to(device) - postprocessors = {'bbox': PostProcess(args.dataset_file)} + # HER -=> post도 고쳐야함. + postprocessors = {'bbox': PostProcess(processor_dct=processor_dct if (args.with_vector) else None)} + if args.masks: + postprocessors['segm'] = PostProcessSegm(processor_dct=processor_dct if args.with_vector else None) return model, criterion, postprocessors - +''' deprecated def build_teacher(args): if args.dataset_file == 'coco': @@ -433,3 +506,4 @@ def build_teacher(args): ) return model +''' \ No newline at end of file diff --git a/methods/vidt/postprocessor.py b/methods/vidt/postprocessor.py index d0cd8f0..cbe2f25 100644 --- a/methods/vidt/postprocessor.py +++ b/methods/vidt/postprocessor.py @@ -11,17 +11,20 @@ from torch import nn from util import box_ops import torch.nn.functional as F - +from util.detectron2.utils.memory import retry_if_cuda_oom +from util.detectron2.layers.mask_ops import paste_masks_in_image +import numpy as np class PostProcess(nn.Module): """ This module converts the model's output into the format expected by the coco api""" - def __init__(self, dataset_file): + def __init__(self, processor_dct=None): super().__init__() - self.dataset_file = dataset_file + # For instance segmentation using UQR module + self.processor_dct = processor_dct @torch.no_grad() - def forward(self, outputs, target_sizes, target_boxes=None): + def forward(self, outputs, target_sizes): """ Perform the computation Parameters: @@ -31,48 +34,77 @@ def forward(self, outputs, target_sizes, target_boxes=None): For visualization, this should be the image size after data augment, but before padding """ - out_logits, out_bbox = outputs['pred_logits'], outputs['pred_boxes'] + if self.processor_dct is not None: + out_logits, out_bbox, out_vector = outputs['pred_logits'], outputs['pred_boxes'], outputs['pred_vectors'] + else: + out_logits, out_bbox = outputs['pred_logits'], outputs['pred_boxes'] assert len(out_logits) == len(target_sizes) assert target_sizes.shape[1] == 2 - if self.dataset_file == 'coco': - prob = out_logits.sigmoid() - topk_values, topk_indexes = torch.topk(prob.view(out_logits.shape[0], -1), 100, dim=1) - scores = topk_values - topk_boxes = topk_indexes // out_logits.shape[2] - labels = topk_indexes % out_logits.shape[2] - boxes = box_ops.box_cxcywh_to_xyxy(out_bbox) - boxes = torch.gather(boxes, 1, topk_boxes.unsqueeze(-1).repeat(1, 1, 4)) - - # and from relative [0, 1] to absolute [0, height] coordinates - img_h, img_w = target_sizes.unbind(1) - scale_fct = torch.stack([img_w, img_h, img_w, img_h], dim=1).to(torch.float32) - boxes = boxes * scale_fct[:, None, :] - + prob = out_logits.sigmoid() + topk_values, topk_indexes = torch.topk(prob.view(out_logits.shape[0], -1), 100, dim=1) + scores = topk_values + topk_boxes = topk_indexes // out_logits.shape[2] + labels = topk_indexes % out_logits.shape[2] + boxes = box_ops.box_cxcywh_to_xyxy(out_bbox) + boxes = torch.gather(boxes, 1, topk_boxes.unsqueeze(-1).repeat(1, 1, 4)) + + # Added for Instance Segmentation + if self.processor_dct is not None: + n_keep = self.processor_dct.n_keep + vectors = torch.gather(out_vector, 1, topk_boxes.unsqueeze(-1).repeat(1, 1, n_keep)) + ########### + + # and from relative [0, 1] to absolute [0, height] coordinates + img_h, img_w = target_sizes.unbind(1) + scale_fct = torch.stack([img_w, img_h, img_w, img_h], dim=1).to(torch.float32) + boxes = boxes * scale_fct[:, None, :] + + # Added for Instance Segmentation + if self.processor_dct is not None: + masks = [] + n_keep, gt_mask_len = self.processor_dct.n_keep, self.processor_dct.gt_mask_len + b, r, c = vectors.shape + for bi in range(b): + outputs_masks_per_image = [] + for ri in range(r): + # here visual for training + idct = np.zeros((gt_mask_len ** 2)) + idct[:n_keep] = vectors[bi, ri].cpu().numpy() + idct = self.processor_dct.inverse_zigzag(idct, gt_mask_len, gt_mask_len) + re_mask = cv2.idct(idct) + max_v = np.max(re_mask) + min_v = np.min(re_mask) + re_mask = np.where(re_mask > (max_v + min_v) / 2., 1, 0) + re_mask = torch.from_numpy(re_mask)[None].float() + outputs_masks_per_image.append(re_mask) + outputs_masks_per_image = torch.cat(outputs_masks_per_image, dim=0).to(out_vector.device) + # here padding local mask to global mask + outputs_masks_per_image = retry_if_cuda_oom(paste_masks_in_image)( + outputs_masks_per_image, # N, 1, M, M + boxes[bi], + (img_h[bi], img_w[bi]), + threshold=0.5, + ) + outputs_masks_per_image = outputs_masks_per_image.unsqueeze(1).cpu() + masks.append(outputs_masks_per_image) + ########### + + if self.processor_dct is None: results = [{'scores': s, 'labels': l, 'boxes': b} for s, l, b in zip(scores, labels, boxes)] + else: + results = [{'scores': s, 'labels': l, 'boxes': b, 'masks': m} for s, l, b, m in zip(scores, labels, boxes, masks)] - return results - - elif self.dataset_file == 'voc': - - prob = F.softmax(out_logits, -1) - scores, labels = prob.max(-1) + return results - # convert to [x0, y0, x1, y1] format - boxes = box_ops.box_cxcywh_to_xyxy(out_bbox) - # and from relative [0, 1] to absolute [0, height] coordinates - img_h, img_w = target_sizes.unbind(1) - scale_fct = torch.stack([img_w, img_h, img_w, img_h], dim=1).to(torch.float32) - boxes = boxes * scale_fct[:, None, :] - results = [{'scores': s, 'labels': l, 'boxes': b} for s, l, b in zip(scores, labels, boxes)] - - true_boxes = [] - for id in range(len(target_boxes)): - true_box = box_ops.box_cxcywh_to_xyxy(target_boxes[id]) - scale_fct = torch.stack([img_w[id], img_h[id], img_w[id], img_h[id]], dim=0).to(torch.float32) - true_boxes.append(true_box * scale_fct) - - return results, true_boxes +class PostProcessSegm(nn.Module): + def __init__(self, threshold=0.5, processor_dct=None): + super().__init__() + self.threshold = threshold + self.processor_dct = processor_dct + @torch.no_grad() + def forward(self, results, outputs, orig_target_sizes, max_target_sizes): + return results \ No newline at end of file diff --git a/util/detectron2/layers/mask_ops.py b/util/detectron2/layers/mask_ops.py new file mode 100644 index 0000000..ce83929 --- /dev/null +++ b/util/detectron2/layers/mask_ops.py @@ -0,0 +1,275 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +import numpy as np +from typing import Tuple +import torch +from PIL import Image +from torch.nn import functional as F + +__all__ = ["paste_masks_in_image"] + + +BYTES_PER_FLOAT = 4 +# TODO: This memory limit may be too much or too little. It would be better to +# determine it based on available resources. +GPU_MEM_LIMIT = 1024 ** 3 # 1 GB memory limit + + +def _do_paste_mask(masks, boxes, img_h: int, img_w: int, skip_empty: bool = True): + """ + Args: + masks: N, 1, H, W + boxes: N, 4 + img_h, img_w (int): + skip_empty (bool): only paste masks within the region that + tightly bound all boxes, and returns the results this region only. + An important optimization for CPU. + + Returns: + if skip_empty == False, a mask of shape (N, img_h, img_w) + if skip_empty == True, a mask of shape (N, h', w'), and the slice + object for the corresponding region. + """ + # On GPU, paste all masks together (up to chunk size) + # by using the entire image to sample the masks + # Compared to pasting them one by one, + # this has more operations but is faster on COCO-scale dataset. + device = masks.device + + if skip_empty and not torch.jit.is_scripting(): + x0_int, y0_int = torch.clamp(boxes.min(dim=0).values.floor()[:2] - 1, min=0).to( + dtype=torch.int32 + ) + x1_int = torch.clamp(boxes[:, 2].max().ceil() + 1, max=img_w).to(dtype=torch.int32) + y1_int = torch.clamp(boxes[:, 3].max().ceil() + 1, max=img_h).to(dtype=torch.int32) + else: + x0_int, y0_int = 0, 0 + x1_int, y1_int = img_w, img_h + x0, y0, x1, y1 = torch.split(boxes, 1, dim=1) # each is Nx1 + + N = masks.shape[0] + + img_y = torch.arange(y0_int, y1_int, device=device, dtype=torch.float32) + 0.5 + img_x = torch.arange(x0_int, x1_int, device=device, dtype=torch.float32) + 0.5 + img_y = (img_y - y0) / (y1 - y0) * 2 - 1 + img_x = (img_x - x0) / (x1 - x0) * 2 - 1 + # img_x, img_y have shapes (N, w), (N, h) + + gx = img_x[:, None, :].expand(N, img_y.size(1), img_x.size(1)) + gy = img_y[:, :, None].expand(N, img_y.size(1), img_x.size(1)) + grid = torch.stack([gx, gy], dim=3) + + if not torch.jit.is_scripting(): + if not masks.dtype.is_floating_point: + masks = masks.float() + img_masks = F.grid_sample(masks, grid.to(masks.dtype), align_corners=False) + + if skip_empty and not torch.jit.is_scripting(): + return img_masks[:, 0], (slice(y0_int, y1_int), slice(x0_int, x1_int)) + else: + return img_masks[:, 0], () + + +# Annotate boxes as Tensor (but not Boxes) in order to use scripting +#@torch.jit.script_if_tracing +def paste_masks_in_image( + masks: torch.Tensor, boxes: torch.Tensor, image_shape: Tuple[int, int], threshold: float = 0.5 +): + """ + Paste a set of masks that are of a fixed resolution (e.g., 28 x 28) into an image. + The location, height, and width for pasting each mask is determined by their + corresponding bounding boxes in boxes. + + Note: + This is a complicated but more accurate implementation. In actual deployment, it is + often enough to use a faster but less accurate implementation. + See :func:`paste_mask_in_image_old` in this file for an alternative implementation. + + Args: + masks (tensor): Tensor of shape (Bimg, Hmask, Wmask), where Bimg is the number of + detected object instances in the image and Hmask, Wmask are the mask width and mask + height of the predicted mask (e.g., Hmask = Wmask = 28). Values are in [0, 1]. + boxes (Boxes or Tensor): A Boxes of length Bimg or Tensor of shape (Bimg, 4). + boxes[i] and masks[i] correspond to the same object instance. + image_shape (tuple): height, width + threshold (float): A threshold in [0, 1] for converting the (soft) masks to + binary masks. + + Returns: + img_masks (Tensor): A tensor of shape (Bimg, Himage, Wimage), where Bimg is the + number of detected object instances and Himage, Wimage are the image width + and height. img_masks[i] is a binary mask for object instance i. + """ + + assert masks.shape[-1] == masks.shape[-2], "Only square mask predictions are supported" + N = len(masks) + if N == 0: + return masks.new_empty((0,) + image_shape, dtype=torch.uint8) + if not isinstance(boxes, torch.Tensor): + boxes = boxes.tensor + device = boxes.device + assert len(boxes) == N, boxes.shape + + img_h, img_w = image_shape + + # The actual implementation split the input into chunks, + # and paste them chunk by chunk. + if device.type == "cpu" or torch.jit.is_scripting(): + # CPU is most efficient when they are pasted one by one with skip_empty=True + # so that it performs minimal number of operations. + num_chunks = N + else: + # GPU benefits from parallelism for larger chunks, but may have memory issue + # int(img_h) because shape may be tensors in tracing + num_chunks = int(np.ceil(N * int(img_h) * int(img_w) * BYTES_PER_FLOAT / GPU_MEM_LIMIT)) + assert ( + num_chunks <= N + ), "Default GPU_MEM_LIMIT in mask_ops.py is too small; try increasing it" + chunks = torch.chunk(torch.arange(N, device=device), num_chunks) + + img_masks = torch.zeros( + N, img_h, img_w, device=device, dtype=torch.bool if threshold >= 0 else torch.uint8 + ) + for inds in chunks: + masks_chunk, spatial_inds = _do_paste_mask( + masks[inds, None, :, :], boxes[inds], img_h, img_w, skip_empty=device.type == "cpu" + ) + + if threshold >= 0: + masks_chunk = (masks_chunk >= threshold).to(dtype=torch.bool) + else: + # for visualization and debugging + masks_chunk = (masks_chunk * 255).to(dtype=torch.uint8) + + if torch.jit.is_scripting(): # Scripting does not use the optimized codepath + img_masks[inds] = masks_chunk + else: + img_masks[(inds,) + spatial_inds] = masks_chunk + return img_masks + + +# The below are the original paste function (from Detectron1) which has +# larger quantization error. +# It is faster on CPU, while the aligned one is faster on GPU thanks to grid_sample. + + +def paste_mask_in_image_old(mask, box, img_h, img_w, threshold): + """ + Paste a single mask in an image. + This is a per-box implementation of :func:`paste_masks_in_image`. + This function has larger quantization error due to incorrect pixel + modeling and is not used any more. + + Args: + mask (Tensor): A tensor of shape (Hmask, Wmask) storing the mask of a single + object instance. Values are in [0, 1]. + box (Tensor): A tensor of shape (4, ) storing the x0, y0, x1, y1 box corners + of the object instance. + img_h, img_w (int): Image height and width. + threshold (float): Mask binarization threshold in [0, 1]. + + Returns: + im_mask (Tensor): + The resized and binarized object mask pasted into the original + image plane (a tensor of shape (img_h, img_w)). + """ + # Conversion from continuous box coordinates to discrete pixel coordinates + # via truncation (cast to int32). This determines which pixels to paste the + # mask onto. + box = box.to(dtype=torch.int32) # Continuous to discrete coordinate conversion + # An example (1D) box with continuous coordinates (x0=0.7, x1=4.3) will map to + # a discrete coordinates (x0=0, x1=4). Note that box is mapped to 5 = x1 - x0 + 1 + # pixels (not x1 - x0 pixels). + samples_w = box[2] - box[0] + 1 # Number of pixel samples, *not* geometric width + samples_h = box[3] - box[1] + 1 # Number of pixel samples, *not* geometric height + + # Resample the mask from it's original grid to the new samples_w x samples_h grid + mask = Image.fromarray(mask.cpu().numpy()) + mask = mask.resize((samples_w, samples_h), resample=Image.BILINEAR) + mask = np.array(mask, copy=False) + + if threshold >= 0: + mask = np.array(mask > threshold, dtype=np.uint8) + mask = torch.from_numpy(mask) + else: + # for visualization and debugging, we also + # allow it to return an unmodified mask + mask = torch.from_numpy(mask * 255).to(torch.uint8) + + im_mask = torch.zeros((img_h, img_w), dtype=torch.uint8) + x_0 = max(box[0], 0) + x_1 = min(box[2] + 1, img_w) + y_0 = max(box[1], 0) + y_1 = min(box[3] + 1, img_h) + + im_mask[y_0:y_1, x_0:x_1] = mask[ + (y_0 - box[1]) : (y_1 - box[1]), (x_0 - box[0]) : (x_1 - box[0]) + ] + return im_mask + + +# Our pixel modeling requires extrapolation for any continuous +# coordinate < 0.5 or > length - 0.5. When sampling pixels on the masks, +# we would like this extrapolation to be an interpolation between boundary values and zero, +# instead of using absolute zero or boundary values. +# Therefore `paste_mask_in_image_old` is often used with zero padding around the masks like this: +# masks, scale = pad_masks(masks[:, 0, :, :], 1) +# boxes = scale_boxes(boxes.tensor, scale) + + +def pad_masks(masks, padding): + """ + Args: + masks (tensor): A tensor of shape (B, M, M) representing B masks. + padding (int): Number of cells to pad on all sides. + + Returns: + The padded masks and the scale factor of the padding size / original size. + """ + B = masks.shape[0] + M = masks.shape[-1] + pad2 = 2 * padding + scale = float(M + pad2) / M + padded_masks = masks.new_zeros((B, M + pad2, M + pad2)) + padded_masks[:, padding:-padding, padding:-padding] = masks + return padded_masks, scale + + +def scale_boxes(boxes, scale): + """ + Args: + boxes (tensor): A tensor of shape (B, 4) representing B boxes with 4 + coords representing the corners x0, y0, x1, y1, + scale (float): The box scaling factor. + + Returns: + Scaled boxes. + """ + w_half = (boxes[:, 2] - boxes[:, 0]) * 0.5 + h_half = (boxes[:, 3] - boxes[:, 1]) * 0.5 + x_c = (boxes[:, 2] + boxes[:, 0]) * 0.5 + y_c = (boxes[:, 3] + boxes[:, 1]) * 0.5 + + w_half *= scale + h_half *= scale + + scaled_boxes = torch.zeros_like(boxes) + scaled_boxes[:, 0] = x_c - w_half + scaled_boxes[:, 2] = x_c + w_half + scaled_boxes[:, 1] = y_c - h_half + scaled_boxes[:, 3] = y_c + h_half + return scaled_boxes + + +#@torch.jit.script_if_tracing +def _paste_masks_tensor_shape( + masks: torch.Tensor, + boxes: torch.Tensor, + image_shape: Tuple[torch.Tensor, torch.Tensor], + threshold: float = 0.5, +): + """ + A wrapper of paste_masks_in_image where image_shape is Tensor. + During tracing, shapes might be tensors instead of ints. The Tensor->int + conversion should be scripted rather than traced. + """ + return paste_masks_in_image(masks, boxes, (int(image_shape[0]), int(image_shape[1])), threshold) \ No newline at end of file diff --git a/util/detectron2/layers/roi_align.py b/util/detectron2/layers/roi_align.py new file mode 100644 index 0000000..a511afd --- /dev/null +++ b/util/detectron2/layers/roi_align.py @@ -0,0 +1,74 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +from torch import nn +from torchvision.ops import roi_align + + +# NOTE: torchvision's RoIAlign has a different default aligned=False +class ROIAlign(nn.Module): + def __init__(self, output_size, spatial_scale, sampling_ratio, aligned=True): + """ + Args: + output_size (tuple): h, w + spatial_scale (float): scale the input boxes by this number + sampling_ratio (int): number of inputs samples to take for each output + sample. 0 to take samples densely. + aligned (bool): if False, use the legacy implementation in + Detectron. If True, align the results more perfectly. + + Note: + The meaning of aligned=True: + + Given a continuous coordinate c, its two neighboring pixel indices (in our + pixel model) are computed by floor(c - 0.5) and ceil(c - 0.5). For example, + c=1.3 has pixel neighbors with discrete indices [0] and [1] (which are sampled + from the underlying signal at continuous coordinates 0.5 and 1.5). But the original + roi_align (aligned=False) does not subtract the 0.5 when computing neighboring + pixel indices and therefore it uses pixels with a slightly incorrect alignment + (relative to our pixel model) when performing bilinear interpolation. + + With `aligned=True`, + we first appropriately scale the ROI and then shift it by -0.5 + prior to calling roi_align. This produces the correct neighbors; see + detectron2/tests/test_roi_align.py for verification. + + The difference does not make a difference to the model's performance if + ROIAlign is used together with conv layers. + """ + super().__init__() + self.output_size = output_size + self.spatial_scale = spatial_scale + self.sampling_ratio = sampling_ratio + self.aligned = aligned + + from torchvision import __version__ + + version = tuple(int(x) for x in __version__.split(".")[:2]) + # https://github.com/pytorch/vision/pull/2438 + #assert version >= (0, 7), "Require torchvision >= 0.7" + + def forward(self, input, rois): + """ + Args: + input: NCHW images + rois: Bx5 boxes. First column is the index into N. The other 4 columns are xyxy. + """ + assert rois.dim() == 2 and rois.size(1) == 5 + if input.is_quantized: + input = input.dequantize() + return roi_align( + input, + rois.to(dtype=input.dtype), + self.output_size, + self.spatial_scale, + self.sampling_ratio, + self.aligned, + ) + + def __repr__(self): + tmpstr = self.__class__.__name__ + "(" + tmpstr += "output_size=" + str(self.output_size) + tmpstr += ", spatial_scale=" + str(self.spatial_scale) + tmpstr += ", sampling_ratio=" + str(self.sampling_ratio) + tmpstr += ", aligned=" + str(self.aligned) + tmpstr += ")" + return tmpstr \ No newline at end of file diff --git a/util/detectron2/structures/boxes.py b/util/detectron2/structures/boxes.py new file mode 100644 index 0000000..8b0c005 --- /dev/null +++ b/util/detectron2/structures/boxes.py @@ -0,0 +1,423 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +import math +import numpy as np +from enum import IntEnum, unique +from typing import List, Tuple, Union +import torch +from torch import device + +_RawBoxType = Union[List[float], Tuple[float, ...], torch.Tensor, np.ndarray] + + +@unique +class BoxMode(IntEnum): + """ + Enum of different ways to represent a box. + """ + + XYXY_ABS = 0 + """ + (x0, y0, x1, y1) in absolute floating points coordinates. + The coordinates in range [0, width or height]. + """ + XYWH_ABS = 1 + """ + (x0, y0, w, h) in absolute floating points coordinates. + """ + XYXY_REL = 2 + """ + Not yet supported! + (x0, y0, x1, y1) in range [0, 1]. They are relative to the size of the image. + """ + XYWH_REL = 3 + """ + Not yet supported! + (x0, y0, w, h) in range [0, 1]. They are relative to the size of the image. + """ + XYWHA_ABS = 4 + """ + (xc, yc, w, h, a) in absolute floating points coordinates. + (xc, yc) is the center of the rotated box, and the angle a is in degrees ccw. + """ + + @staticmethod + def convert(box: _RawBoxType, from_mode: "BoxMode", to_mode: "BoxMode") -> _RawBoxType: + """ + Args: + box: can be a k-tuple, k-list or an Nxk array/tensor, where k = 4 or 5 + from_mode, to_mode (BoxMode) + + Returns: + The converted box of the same type. + """ + if from_mode == to_mode: + return box + + original_type = type(box) + is_numpy = isinstance(box, np.ndarray) + single_box = isinstance(box, (list, tuple)) + if single_box: + assert len(box) == 4 or len(box) == 5, ( + "BoxMode.convert takes either a k-tuple/list or an Nxk array/tensor," + " where k == 4 or 5" + ) + arr = torch.tensor(box)[None, :] + else: + # avoid modifying the input box + if is_numpy: + arr = torch.from_numpy(np.asarray(box)).clone() + else: + arr = box.clone() + + assert to_mode not in [BoxMode.XYXY_REL, BoxMode.XYWH_REL] and from_mode not in [ + BoxMode.XYXY_REL, + BoxMode.XYWH_REL, + ], "Relative mode not yet supported!" + + if from_mode == BoxMode.XYWHA_ABS and to_mode == BoxMode.XYXY_ABS: + assert ( + arr.shape[-1] == 5 + ), "The last dimension of input shape must be 5 for XYWHA format" + original_dtype = arr.dtype + arr = arr.double() + + w = arr[:, 2] + h = arr[:, 3] + a = arr[:, 4] + c = torch.abs(torch.cos(a * math.pi / 180.0)) + s = torch.abs(torch.sin(a * math.pi / 180.0)) + # This basically computes the horizontal bounding rectangle of the rotated box + new_w = c * w + s * h + new_h = c * h + s * w + + # convert center to top-left corner + arr[:, 0] -= new_w / 2.0 + arr[:, 1] -= new_h / 2.0 + # bottom-right corner + arr[:, 2] = arr[:, 0] + new_w + arr[:, 3] = arr[:, 1] + new_h + + arr = arr[:, :4].to(dtype=original_dtype) + elif from_mode == BoxMode.XYWH_ABS and to_mode == BoxMode.XYWHA_ABS: + original_dtype = arr.dtype + arr = arr.double() + arr[:, 0] += arr[:, 2] / 2.0 + arr[:, 1] += arr[:, 3] / 2.0 + angles = torch.zeros((arr.shape[0], 1), dtype=arr.dtype) + arr = torch.cat((arr, angles), axis=1).to(dtype=original_dtype) + else: + if to_mode == BoxMode.XYXY_ABS and from_mode == BoxMode.XYWH_ABS: + arr[:, 2] += arr[:, 0] + arr[:, 3] += arr[:, 1] + elif from_mode == BoxMode.XYXY_ABS and to_mode == BoxMode.XYWH_ABS: + arr[:, 2] -= arr[:, 0] + arr[:, 3] -= arr[:, 1] + else: + raise NotImplementedError( + "Conversion from BoxMode {} to {} is not supported yet".format( + from_mode, to_mode + ) + ) + + if single_box: + return original_type(arr.flatten().tolist()) + if is_numpy: + return arr.numpy() + else: + return arr + + +class Boxes: + """ + This structure stores a list of boxes as a Nx4 torch.Tensor. + It supports some common methods about boxes + (`area`, `clip`, `nonempty`, etc), + and also behaves like a Tensor + (support indexing, `to(device)`, `.device`, and iteration over all boxes) + + Attributes: + tensor (torch.Tensor): float matrix of Nx4. Each row is (x1, y1, x2, y2). + """ + + def __init__(self, tensor: torch.Tensor): + """ + Args: + tensor (Tensor[float]): a Nx4 matrix. Each row is (x1, y1, x2, y2). + """ + device = tensor.device if isinstance(tensor, torch.Tensor) else torch.device("cpu") + tensor = torch.as_tensor(tensor, dtype=torch.float32, device=device) + if tensor.numel() == 0: + # Use reshape, so we don't end up creating a new tensor that does not depend on + # the inputs (and consequently confuses jit) + tensor = tensor.reshape((-1, 4)).to(dtype=torch.float32, device=device) + assert tensor.dim() == 2 and tensor.size(-1) == 4, tensor.size() + + self.tensor = tensor + + def clone(self) -> "Boxes": + """ + Clone the Boxes. + + Returns: + Boxes + """ + return Boxes(self.tensor.clone()) + + def to(self, device: torch.device): + # Boxes are assumed float32 and does not support to(dtype) + return Boxes(self.tensor.to(device=device)) + + def area(self) -> torch.Tensor: + """ + Computes the area of all the boxes. + + Returns: + torch.Tensor: a vector with areas of each box. + """ + box = self.tensor + area = (box[:, 2] - box[:, 0]) * (box[:, 3] - box[:, 1]) + return area + + def clip(self, box_size: Tuple[int, int]) -> None: + """ + Clip (in place) the boxes by limiting x coordinates to the range [0, width] + and y coordinates to the range [0, height]. + + Args: + box_size (height, width): The clipping box's size. + """ + assert torch.isfinite(self.tensor).all(), "Box tensor contains infinite or NaN!" + h, w = box_size + x1 = self.tensor[:, 0].clamp(min=0, max=w) + y1 = self.tensor[:, 1].clamp(min=0, max=h) + x2 = self.tensor[:, 2].clamp(min=0, max=w) + y2 = self.tensor[:, 3].clamp(min=0, max=h) + self.tensor = torch.stack((x1, y1, x2, y2), dim=-1) + + def nonempty(self, threshold: float = 0.0) -> torch.Tensor: + """ + Find boxes that are non-empty. + A box is considered empty, if either of its side is no larger than threshold. + + Returns: + Tensor: + a binary vector which represents whether each box is empty + (False) or non-empty (True). + """ + box = self.tensor + widths = box[:, 2] - box[:, 0] + heights = box[:, 3] - box[:, 1] + keep = (widths > threshold) & (heights > threshold) + return keep + + def __getitem__(self, item) -> "Boxes": + """ + Args: + item: int, slice, or a BoolTensor + + Returns: + Boxes: Create a new :class:`Boxes` by indexing. + + The following usage are allowed: + + 1. `new_boxes = boxes[3]`: return a `Boxes` which contains only one box. + 2. `new_boxes = boxes[2:10]`: return a slice of boxes. + 3. `new_boxes = boxes[vector]`, where vector is a torch.BoolTensor + with `length = len(boxes)`. Nonzero elements in the vector will be selected. + + Note that the returned Boxes might share storage with this Boxes, + subject to Pytorch's indexing semantics. + """ + if isinstance(item, int): + return Boxes(self.tensor[item].view(1, -1)) + b = self.tensor[item] + assert b.dim() == 2, "Indexing on Boxes with {} failed to return a matrix!".format(item) + return Boxes(b) + + def __len__(self) -> int: + return self.tensor.shape[0] + + def __repr__(self) -> str: + return "Boxes(" + str(self.tensor) + ")" + + def inside_box(self, box_size: Tuple[int, int], boundary_threshold: int = 0) -> torch.Tensor: + """ + Args: + box_size (height, width): Size of the reference box. + boundary_threshold (int): Boxes that extend beyond the reference box + boundary by more than boundary_threshold are considered "outside". + + Returns: + a binary vector, indicating whether each box is inside the reference box. + """ + height, width = box_size + inds_inside = ( + (self.tensor[..., 0] >= -boundary_threshold) + & (self.tensor[..., 1] >= -boundary_threshold) + & (self.tensor[..., 2] < width + boundary_threshold) + & (self.tensor[..., 3] < height + boundary_threshold) + ) + return inds_inside + + def get_centers(self) -> torch.Tensor: + """ + Returns: + The box centers in a Nx2 array of (x, y). + """ + return (self.tensor[:, :2] + self.tensor[:, 2:]) / 2 + + def scale(self, scale_x: float, scale_y: float) -> None: + """ + Scale the box with horizontal and vertical scaling factors + """ + self.tensor[:, 0::2] *= scale_x + self.tensor[:, 1::2] *= scale_y + + @classmethod + def cat(cls, boxes_list: List["Boxes"]) -> "Boxes": + """ + Concatenates a list of Boxes into a single Boxes + + Arguments: + boxes_list (list[Boxes]) + + Returns: + Boxes: the concatenated Boxes + """ + assert isinstance(boxes_list, (list, tuple)) + if len(boxes_list) == 0: + return cls(torch.empty(0)) + assert all([isinstance(box, Boxes) for box in boxes_list]) + + # use torch.cat (v.s. layers.cat) so the returned boxes never share storage with input + cat_boxes = cls(torch.cat([b.tensor for b in boxes_list], dim=0)) + return cat_boxes + + @property + def device(self) -> device: + return self.tensor.device + + # type "Iterator[torch.Tensor]", yield, and iter() not supported by torchscript + # https://github.com/pytorch/pytorch/issues/18627 + @torch.jit.unused + def __iter__(self): + """ + Yield a box as a Tensor of shape (4,) at a time. + """ + yield from self.tensor + + +def pairwise_intersection(boxes1: Boxes, boxes2: Boxes) -> torch.Tensor: + """ + Given two lists of boxes of size N and M, + compute the intersection area between __all__ N x M pairs of boxes. + The box order must be (xmin, ymin, xmax, ymax) + + Args: + boxes1,boxes2 (Boxes): two `Boxes`. Contains N & M boxes, respectively. + + Returns: + Tensor: intersection, sized [N,M]. + """ + boxes1, boxes2 = boxes1.tensor, boxes2.tensor + width_height = torch.min(boxes1[:, None, 2:], boxes2[:, 2:]) - torch.max( + boxes1[:, None, :2], boxes2[:, :2] + ) # [N,M,2] + + width_height.clamp_(min=0) # [N,M,2] + intersection = width_height.prod(dim=2) # [N,M] + return intersection + + +# implementation from https://github.com/kuangliu/torchcv/blob/master/torchcv/utils/box.py +# with slight modifications +def pairwise_iou(boxes1: Boxes, boxes2: Boxes) -> torch.Tensor: + """ + Given two lists of boxes of size N and M, compute the IoU + (intersection over union) between **all** N x M pairs of boxes. + The box order must be (xmin, ymin, xmax, ymax). + + Args: + boxes1,boxes2 (Boxes): two `Boxes`. Contains N & M boxes, respectively. + + Returns: + Tensor: IoU, sized [N,M]. + """ + area1 = boxes1.area() # [N] + area2 = boxes2.area() # [M] + inter = pairwise_intersection(boxes1, boxes2) + + # handle empty boxes + iou = torch.where( + inter > 0, + inter / (area1[:, None] + area2 - inter), + torch.zeros(1, dtype=inter.dtype, device=inter.device), + ) + return iou + + +def pairwise_ioa(boxes1: Boxes, boxes2: Boxes) -> torch.Tensor: + """ + Similar to :func:`pariwise_iou` but compute the IoA (intersection over boxes2 area). + + Args: + boxes1,boxes2 (Boxes): two `Boxes`. Contains N & M boxes, respectively. + + Returns: + Tensor: IoA, sized [N,M]. + """ + area2 = boxes2.area() # [M] + inter = pairwise_intersection(boxes1, boxes2) + + # handle empty boxes + ioa = torch.where( + inter > 0, inter / area2, torch.zeros(1, dtype=inter.dtype, device=inter.device) + ) + return ioa + + +def pairwise_point_box_distance(points: torch.Tensor, boxes: Boxes): + """ + Pairwise distance between N points and M boxes. The distance between a + point and a box is represented by the distance from the point to 4 edges + of the box. Distances are all positive when the point is inside the box. + + Args: + points: Nx2 coordinates. Each row is (x, y) + boxes: M boxes + + Returns: + Tensor: distances of size (N, M, 4). The 4 values are distances from + the point to the left, top, right, bottom of the box. + """ + x, y = points.unsqueeze(dim=2).unbind(dim=1) # (N, 1) + x0, y0, x1, y1 = boxes.tensor.unsqueeze(dim=0).unbind(dim=2) # (1, M) + return torch.stack([x - x0, y - y0, x1 - x, y1 - y], dim=2) + + +def matched_pairwise_iou(boxes1: Boxes, boxes2: Boxes) -> torch.Tensor: + """ + Compute pairwise intersection over union (IOU) of two sets of matched + boxes that have the same number of boxes. + Similar to :func:`pairwise_iou`, but computes only diagonal elements of the matrix. + + Args: + boxes1 (Boxes): bounding boxes, sized [N,4]. + boxes2 (Boxes): same length as boxes1 + Returns: + Tensor: iou, sized [N]. + """ + assert len(boxes1) == len( + boxes2 + ), "boxlists should have the same" "number of entries, got {}, {}".format( + len(boxes1), len(boxes2) + ) + area1 = boxes1.area() # [N] + area2 = boxes2.area() # [N] + box1, box2 = boxes1.tensor, boxes2.tensor + lt = torch.max(box1[:, :2], box2[:, :2]) # [N,2] + rb = torch.min(box1[:, 2:], box2[:, 2:]) # [N,2] + wh = (rb - lt).clamp(min=0) # [N,2] + inter = wh[:, 0] * wh[:, 1] # [N] + iou = inter / (area1 + area2 - inter) # [N] + return iou \ No newline at end of file diff --git a/util/detectron2/structures/masks.py b/util/detectron2/structures/masks.py new file mode 100644 index 0000000..6c40b20 --- /dev/null +++ b/util/detectron2/structures/masks.py @@ -0,0 +1,531 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +import copy +import itertools +import numpy as np +from typing import Any, Iterator, List, Union +import pycocotools.mask as mask_util +import torch +from torch import device +from ..layers.roi_align import ROIAlign +from ..utils.memory import retry_if_cuda_oom + +from .boxes import Boxes + + +def polygon_area(x, y): + # Using the shoelace formula + # https://stackoverflow.com/questions/24467972/calculate-area-of-polygon-given-x-y-coordinates + return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) + + +def polygons_to_bitmask(polygons: List[np.ndarray], height: int, width: int) -> np.ndarray: + """ + Args: + polygons (list[ndarray]): each array has shape (Nx2,) + height, width (int) + + Returns: + ndarray: a bool mask of shape (height, width) + """ + if len(polygons) == 0: + # COCOAPI does not support empty polygons + return np.zeros((height, width)).astype(np.bool) + rles = mask_util.frPyObjects(polygons, height, width) + rle = mask_util.merge(rles) + return mask_util.decode(rle).astype(np.bool) + + +def rasterize_polygons_within_box( + polygons: List[np.ndarray], box: np.ndarray, mask_size: int +) -> torch.Tensor: + """ + Rasterize the polygons into a mask image and + crop the mask content in the given box. + The cropped mask is resized to (mask_size, mask_size). + + This function is used when generating training targets for mask head in Mask R-CNN. + Given original ground-truth masks for an image, new ground-truth mask + training targets in the size of `mask_size x mask_size` + must be provided for each predicted box. This function will be called to + produce such targets. + + Args: + polygons (list[ndarray[float]]): a list of polygons, which represents an instance. + box: 4-element numpy array + mask_size (int): + + Returns: + Tensor: BoolTensor of shape (mask_size, mask_size) + """ + # 1. Shift the polygons w.r.t the boxes + w, h = box[2] - box[0], box[3] - box[1] + + polygons = copy.deepcopy(polygons) + for p in polygons: + p[0::2] = p[0::2] - box[0] + p[1::2] = p[1::2] - box[1] + + # 2. Rescale the polygons to the new box size + # max() to avoid division by small number + ratio_h = mask_size / max(h, 0.1) + ratio_w = mask_size / max(w, 0.1) + + if ratio_h == ratio_w: + for p in polygons: + p *= ratio_h + else: + for p in polygons: + p[0::2] *= ratio_w + p[1::2] *= ratio_h + + # 3. Rasterize the polygons with coco api + mask = polygons_to_bitmask(polygons, mask_size, mask_size) + mask = torch.from_numpy(mask) + return mask + + +class BitMasks: + """ + This class stores the segmentation masks for all objects in one image, in + the form of bitmaps. + + Attributes: + tensor: bool Tensor of N,H,W, representing N instances in the image. + """ + + def __init__(self, tensor: Union[torch.Tensor, np.ndarray]): + """ + Args: + tensor: bool Tensor of N,H,W, representing N instances in the image. + """ + device = tensor.device if isinstance(tensor, torch.Tensor) else torch.device("cpu") + tensor = torch.as_tensor(tensor, dtype=torch.bool, device=device) + assert tensor.dim() == 3, tensor.size() + self.image_size = tensor.shape[1:] + self.tensor = tensor + + @torch.jit.unused + def to(self, *args: Any, **kwargs: Any) -> "BitMasks": + return BitMasks(self.tensor.to(*args, **kwargs)) + + @property + def device(self) -> torch.device: + return self.tensor.device + + @torch.jit.unused + def __getitem__(self, item: Union[int, slice, torch.BoolTensor]) -> "BitMasks": + """ + Returns: + BitMasks: Create a new :class:`BitMasks` by indexing. + + The following usage are allowed: + + 1. `new_masks = masks[3]`: return a `BitMasks` which contains only one mask. + 2. `new_masks = masks[2:10]`: return a slice of masks. + 3. `new_masks = masks[vector]`, where vector is a torch.BoolTensor + with `length = len(masks)`. Nonzero elements in the vector will be selected. + + Note that the returned object might share storage with this object, + subject to Pytorch's indexing semantics. + """ + if isinstance(item, int): + return BitMasks(self.tensor[item].unsqueeze(0)) + m = self.tensor[item] + assert m.dim() == 3, "Indexing on BitMasks with {} returns a tensor with shape {}!".format( + item, m.shape + ) + return BitMasks(m) + + @torch.jit.unused + def __iter__(self) -> torch.Tensor: + yield from self.tensor + + @torch.jit.unused + def __repr__(self) -> str: + s = self.__class__.__name__ + "(" + s += "num_instances={})".format(len(self.tensor)) + return s + + def __len__(self) -> int: + return self.tensor.shape[0] + + def nonempty(self) -> torch.Tensor: + """ + Find masks that are non-empty. + + Returns: + Tensor: a BoolTensor which represents + whether each mask is empty (False) or non-empty (True). + """ + return self.tensor.flatten(1).any(dim=1) + + @staticmethod + def from_polygon_masks( + polygon_masks: Union["PolygonMasks", List[List[np.ndarray]]], height: int, width: int + ) -> "BitMasks": + """ + Args: + polygon_masks (list[list[ndarray]] or PolygonMasks) + height, width (int) + """ + if isinstance(polygon_masks, PolygonMasks): + polygon_masks = polygon_masks.polygons + masks = [polygons_to_bitmask(p, height, width) for p in polygon_masks] + if len(masks): + return BitMasks(torch.stack([torch.from_numpy(x) for x in masks])) + else: + return BitMasks(torch.empty(0, height, width, dtype=torch.bool)) + + @staticmethod + def from_roi_masks(roi_masks: "ROIMasks", height: int, width: int) -> "BitMasks": + """ + Args: + roi_masks: + height, width (int): + """ + return roi_masks.to_bitmasks(height, width) + + def crop_and_resize(self, boxes: torch.Tensor, mask_size: int) -> torch.Tensor: + """ + Crop each bitmask by the given box, and resize results to (mask_size, mask_size). + This can be used to prepare training targets for Mask R-CNN. + It has less reconstruction error compared to rasterization with polygons. + However we observe no difference in accuracy, + but BitMasks requires more memory to store all the masks. + + Args: + boxes (Tensor): Nx4 tensor storing the boxes for each mask + mask_size (int): the size of the rasterized mask. + + Returns: + Tensor: + A bool tensor of shape (N, mask_size, mask_size), where + N is the number of predicted boxes for this image. + """ + assert len(boxes) == len(self), "{} != {}".format(len(boxes), len(self)) + device = self.tensor.device + + batch_inds = torch.arange(len(boxes), device=device).to(dtype=boxes.dtype)[:, None] + rois = torch.cat([batch_inds, boxes], dim=1) # Nx5 + + bit_masks = self.tensor.to(dtype=torch.float32) + rois = rois.to(device=device) + output = ( + ROIAlign((mask_size, mask_size), 1.0, 0, aligned=True) + .forward(bit_masks[:, None, :, :], rois) + .squeeze(1) + ) + output = output >= 0.5 + return output + + def get_bounding_boxes(self) -> Boxes: + """ + Returns: + Boxes: tight bounding boxes around bitmasks. + If a mask is empty, it's bounding box will be all zero. + """ + boxes = torch.zeros(self.tensor.shape[0], 4, dtype=torch.float32) + x_any = torch.any(self.tensor, dim=1) + y_any = torch.any(self.tensor, dim=2) + for idx in range(self.tensor.shape[0]): + x = torch.where(x_any[idx, :])[0] + y = torch.where(y_any[idx, :])[0] + if len(x) > 0 and len(y) > 0: + boxes[idx, :] = torch.as_tensor( + [x[0], y[0], x[-1] + 1, y[-1] + 1], dtype=torch.float32 + ) + return Boxes(boxes) + + @staticmethod + def cat(bitmasks_list: List["BitMasks"]) -> "BitMasks": + """ + Concatenates a list of BitMasks into a single BitMasks + + Arguments: + bitmasks_list (list[BitMasks]) + + Returns: + BitMasks: the concatenated BitMasks + """ + assert isinstance(bitmasks_list, (list, tuple)) + assert len(bitmasks_list) > 0 + assert all(isinstance(bitmask, BitMasks) for bitmask in bitmasks_list) + + cat_bitmasks = type(bitmasks_list[0])(torch.cat([bm.tensor for bm in bitmasks_list], dim=0)) + return cat_bitmasks + + +class PolygonMasks: + """ + This class stores the segmentation masks for all objects in one image, in the form of polygons. + + Attributes: + polygons: list[list[ndarray]]. Each ndarray is a float64 vector representing a polygon. + """ + + def __init__(self, polygons: List[List[Union[torch.Tensor, np.ndarray]]]): + """ + Arguments: + polygons (list[list[np.ndarray]]): The first + level of the list correspond to individual instances, + the second level to all the polygons that compose the + instance, and the third level to the polygon coordinates. + The third level array should have the format of + [x0, y0, x1, y1, ..., xn, yn] (n >= 3). + """ + if not isinstance(polygons, list): + raise ValueError( + "Cannot create PolygonMasks: Expect a list of list of polygons per image. " + "Got '{}' instead.".format(type(polygons)) + ) + + def _make_array(t: Union[torch.Tensor, np.ndarray]) -> np.ndarray: + # Use float64 for higher precision, because why not? + # Always put polygons on CPU (self.to is a no-op) since they + # are supposed to be small tensors. + # May need to change this assumption if GPU placement becomes useful + if isinstance(t, torch.Tensor): + t = t.cpu().numpy() + return np.asarray(t).astype("float64") + + def process_polygons( + polygons_per_instance: List[Union[torch.Tensor, np.ndarray]] + ) -> List[np.ndarray]: + if not isinstance(polygons_per_instance, list): + raise ValueError( + "Cannot create polygons: Expect a list of polygons per instance. " + "Got '{}' instead.".format(type(polygons_per_instance)) + ) + # transform each polygon to a numpy array + polygons_per_instance = [_make_array(p) for p in polygons_per_instance] + for polygon in polygons_per_instance: + if len(polygon) % 2 != 0 or len(polygon) < 6: + raise ValueError(f"Cannot create a polygon from {len(polygon)} coordinates.") + return polygons_per_instance + + self.polygons: List[List[np.ndarray]] = [ + process_polygons(polygons_per_instance) for polygons_per_instance in polygons + ] + + def to(self, *args: Any, **kwargs: Any) -> "PolygonMasks": + return self + + @property + def device(self) -> torch.device: + return torch.device("cpu") + + def get_bounding_boxes(self) -> Boxes: + """ + Returns: + Boxes: tight bounding boxes around polygon masks. + """ + boxes = torch.zeros(len(self.polygons), 4, dtype=torch.float32) + for idx, polygons_per_instance in enumerate(self.polygons): + minxy = torch.as_tensor([float("inf"), float("inf")], dtype=torch.float32) + maxxy = torch.zeros(2, dtype=torch.float32) + for polygon in polygons_per_instance: + coords = torch.from_numpy(polygon).view(-1, 2).to(dtype=torch.float32) + minxy = torch.min(minxy, torch.min(coords, dim=0).values) + maxxy = torch.max(maxxy, torch.max(coords, dim=0).values) + boxes[idx, :2] = minxy + boxes[idx, 2:] = maxxy + return Boxes(boxes) + + def nonempty(self) -> torch.Tensor: + """ + Find masks that are non-empty. + + Returns: + Tensor: + a BoolTensor which represents whether each mask is empty (False) or not (True). + """ + keep = [1 if len(polygon) > 0 else 0 for polygon in self.polygons] + return torch.from_numpy(np.asarray(keep, dtype=np.bool)) + + def __getitem__(self, item: Union[int, slice, List[int], torch.BoolTensor]) -> "PolygonMasks": + """ + Support indexing over the instances and return a `PolygonMasks` object. + `item` can be: + + 1. An integer. It will return an object with only one instance. + 2. A slice. It will return an object with the selected instances. + 3. A list[int]. It will return an object with the selected instances, + correpsonding to the indices in the list. + 4. A vector mask of type BoolTensor, whose length is num_instances. + It will return an object with the instances whose mask is nonzero. + """ + if isinstance(item, int): + selected_polygons = [self.polygons[item]] + elif isinstance(item, slice): + selected_polygons = self.polygons[item] + elif isinstance(item, list): + selected_polygons = [self.polygons[i] for i in item] + elif isinstance(item, torch.Tensor): + # Polygons is a list, so we have to move the indices back to CPU. + if item.dtype == torch.bool: + assert item.dim() == 1, item.shape + item = item.nonzero().squeeze(1).cpu().numpy().tolist() + elif item.dtype in [torch.int32, torch.int64]: + item = item.cpu().numpy().tolist() + else: + raise ValueError("Unsupported tensor dtype={} for indexing!".format(item.dtype)) + selected_polygons = [self.polygons[i] for i in item] + return PolygonMasks(selected_polygons) + + def __iter__(self) -> Iterator[List[np.ndarray]]: + """ + Yields: + list[ndarray]: the polygons for one instance. + Each Tensor is a float64 vector representing a polygon. + """ + return iter(self.polygons) + + def __repr__(self) -> str: + s = self.__class__.__name__ + "(" + s += "num_instances={})".format(len(self.polygons)) + return s + + def __len__(self) -> int: + return len(self.polygons) + + def crop_and_resize(self, boxes: torch.Tensor, mask_size: int) -> torch.Tensor: + """ + Crop each mask by the given box, and resize results to (mask_size, mask_size). + This can be used to prepare training targets for Mask R-CNN. + + Args: + boxes (Tensor): Nx4 tensor storing the boxes for each mask + mask_size (int): the size of the rasterized mask. + + Returns: + Tensor: A bool tensor of shape (N, mask_size, mask_size), where + N is the number of predicted boxes for this image. + """ + assert len(boxes) == len(self), "{} != {}".format(len(boxes), len(self)) + + device = boxes.device + # Put boxes on the CPU, as the polygon representation is not efficient GPU-wise + # (several small tensors for representing a single instance mask) + boxes = boxes.to(torch.device("cpu")) + + results = [ + rasterize_polygons_within_box(poly, box.numpy(), mask_size) + for poly, box in zip(self.polygons, boxes) + ] + """ + poly: list[list[float]], the polygons for one instance + box: a tensor of shape (4,) + """ + if len(results) == 0: + return torch.empty(0, mask_size, mask_size, dtype=torch.bool, device=device) + return torch.stack(results, dim=0).to(device=device) + + def area(self): + """ + Computes area of the mask. + Only works with Polygons, using the shoelace formula: + https://stackoverflow.com/questions/24467972/calculate-area-of-polygon-given-x-y-coordinates + + Returns: + Tensor: a vector, area for each instance + """ + + area = [] + for polygons_per_instance in self.polygons: + area_per_instance = 0 + for p in polygons_per_instance: + area_per_instance += polygon_area(p[0::2], p[1::2]) + area.append(area_per_instance) + + return torch.tensor(area) + + @staticmethod + def cat(polymasks_list: List["PolygonMasks"]) -> "PolygonMasks": + """ + Concatenates a list of PolygonMasks into a single PolygonMasks + + Arguments: + polymasks_list (list[PolygonMasks]) + + Returns: + PolygonMasks: the concatenated PolygonMasks + """ + assert isinstance(polymasks_list, (list, tuple)) + assert len(polymasks_list) > 0 + assert all(isinstance(polymask, PolygonMasks) for polymask in polymasks_list) + + cat_polymasks = type(polymasks_list[0])( + list(itertools.chain.from_iterable(pm.polygons for pm in polymasks_list)) + ) + return cat_polymasks + + +class ROIMasks: + """ + Represent masks by N smaller masks defined in some ROIs. Once ROI boxes are given, + full-image bitmask can be obtained by "pasting" the mask on the region defined + by the corresponding ROI box. + """ + + def __init__(self, tensor: torch.Tensor): + """ + Args: + tensor: (N, M, M) mask tensor that defines the mask within each ROI. + """ + if tensor.dim() != 3: + raise ValueError("ROIMasks must take a masks of 3 dimension.") + self.tensor = tensor + + def to(self, device: torch.device) -> "ROIMasks": + return ROIMasks(self.tensor.to(device)) + + @property + def device(self) -> device: + return self.tensor.device + + def __len__(self): + return self.tensor.shape[0] + + def __getitem__(self, item) -> "ROIMasks": + """ + Returns: + ROIMasks: Create a new :class:`ROIMasks` by indexing. + + The following usage are allowed: + + 1. `new_masks = masks[2:10]`: return a slice of masks. + 2. `new_masks = masks[vector]`, where vector is a torch.BoolTensor + with `length = len(masks)`. Nonzero elements in the vector will be selected. + + Note that the returned object might share storage with this object, + subject to Pytorch's indexing semantics. + """ + t = self.tensor[item] + if t.dim() != 3: + raise ValueError( + f"Indexing on ROIMasks with {item} returns a tensor with shape {t.shape}!" + ) + return ROIMasks(t) + + @torch.jit.unused + def __repr__(self) -> str: + s = self.__class__.__name__ + "(" + s += "num_instances={})".format(len(self.tensor)) + return s + + @torch.jit.unused + def to_bitmasks(self, boxes: torch.Tensor, height, width, threshold=0.5): + """ + Args: see documentation of :func:`paste_masks_in_image`. + """ + from detectron2.layers.mask_ops import paste_masks_in_image, _paste_masks_tensor_shape + + if torch.jit.is_tracing(): + if isinstance(height, torch.Tensor): + paste_func = _paste_masks_tensor_shape + else: + paste_func = paste_masks_in_image + else: + paste_func = retry_if_cuda_oom(paste_masks_in_image) + bitmasks = paste_func(self.tensor, boxes.tensor, (height, width), threshold=threshold) + return BitMasks(bitmasks) \ No newline at end of file diff --git a/util/detectron2/utils/memory.py b/util/detectron2/utils/memory.py new file mode 100644 index 0000000..3ecd7d7 --- /dev/null +++ b/util/detectron2/utils/memory.py @@ -0,0 +1,84 @@ +# Copyright (c) Facebook, Inc. and its affiliates. + +import logging +from contextlib import contextmanager +from functools import wraps +import torch + +__all__ = ["retry_if_cuda_oom"] + + +@contextmanager +def _ignore_torch_cuda_oom(): + """ + A context which ignores CUDA OOM exception from pytorch. + """ + try: + yield + except RuntimeError as e: + # NOTE: the string may change? + if "CUDA out of memory. " in str(e): + pass + else: + raise + + +def retry_if_cuda_oom(func): + """ + Makes a function retry itself after encountering + pytorch's CUDA OOM error. + It will first retry after calling `torch.cuda.empty_cache()`. + + If that still fails, it will then retry by trying to convert inputs to CPUs. + In this case, it expects the function to dispatch to CPU implementation. + The return values may become CPU tensors as well and it's user's + responsibility to convert it back to CUDA tensor if needed. + + Args: + func: a stateless callable that takes tensor-like objects as arguments + + Returns: + a callable which retries `func` if OOM is encountered. + + Examples: + :: + output = retry_if_cuda_oom(some_torch_function)(input1, input2) + # output may be on CPU even if inputs are on GPU + + Note: + 1. When converting inputs to CPU, it will only look at each argument and check + if it has `.device` and `.to` for conversion. Nested structures of tensors + are not supported. + + 2. Since the function might be called more than once, it has to be + stateless. + """ + + def maybe_to_cpu(x): + try: + like_gpu_tensor = x.device.type == "cuda" and hasattr(x, "to") + except AttributeError: + like_gpu_tensor = False + if like_gpu_tensor: + return x.to(device="cpu") + else: + return x + + @wraps(func) + def wrapped(*args, **kwargs): + with _ignore_torch_cuda_oom(): + return func(*args, **kwargs) + + # Clear cache and retry + torch.cuda.empty_cache() + with _ignore_torch_cuda_oom(): + return func(*args, **kwargs) + + # Try on CPU. This slows down the code significantly, therefore print a notice. + logger = logging.getLogger(__name__) + logger.info("Attempting to copy inputs of {} to CPU due to CUDA OOM".format(str(func))) + new_args = (maybe_to_cpu(x) for x in args) + new_kwargs = {k: maybe_to_cpu(v) for k, v in kwargs.items()} + return func(*new_args, **new_kwargs) + + return wrapped \ No newline at end of file diff --git a/util/misc.py b/util/misc.py index acae144..964f722 100644 --- a/util/misc.py +++ b/util/misc.py @@ -18,7 +18,8 @@ # needed due to empty tensor bug in pytorch and torchvision 0.5 import torchvision -if float(torchvision.__version__[:3]) < 0.7: +if float(torchvision.__version__[:3]) < 0.7 and float(torchvision.__version__[:3]) != 0.1: +#if float(torchvision.__version__[:3]) < 0.7: from torchvision.ops import _new_empty_tensor from torchvision.ops.misc import _output_size @@ -470,7 +471,7 @@ def interpolate(input, size=None, scale_factor=None, mode="nearest", align_corne This will eventually be supported natively by PyTorch, and this class can go away. """ - if float(torchvision.__version__[:3]) < 0.7: + if float(torchvision.__version__[:3]) < 0.7 and float(torchvision.__version__[:3]) != 0.1: if input.numel() > 0: return torch.nn.functional.interpolate( input, size, scale_factor, mode, align_corners From b8a01f1dbc59cf3dce99424075ec9304b74d44b4 Mon Sep 17 00:00:00 2001 From: Hwanjun Song Date: Tue, 5 Apr 2022 22:38:10 +0900 Subject: [PATCH 02/26] Update README.md --- README.md | 482 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 354 insertions(+), 128 deletions(-) diff --git a/README.md b/README.md index 313551d..30dc864 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,34 @@ -# ViDT: An Efficient and Effective Fully Transformer-based Object Detector (ICLR'22) +# An Extendable, Efficient and Effective Transformer-based Object Detector (Extension of VIDT published at ICLR2022) by [Hwanjun Song](https://scholar.google.com/citations?user=Ijzuc-8AAAAJ&hl=en&oi=ao)1, [Deqing Sun](https://scholar.google.com/citations?hl=en&user=t4rgICIAAAAJ)2, [Sanghyuk Chun](https://scholar.google.com/citations?user=4_uj0xcAAAAJ&hl=en&oi=ao)1, [Varun Jampani](https://scholar.google.com/citations?hl=en&user=1Cv6Sf4AAAAJ)2, [Dongyoon Han](https://scholar.google.com/citations?user=jcP7m1QAAAAJ&hl=en)1,
[Byeongho Heo](https://scholar.google.com/citations?user=4_7rLDIAAAAJ&hl=en)1, [Wonjae Kim](https://scholar.google.com/citations?hl=en&user=UpZ41EwAAAAJ)1, and [Ming-Hsuan Yang](https://scholar.google.com/citations?hl=en&user=p9-ohHsAAAAJ)2,3 1 NAVER AI Lab, 2 Google Research, 3 University California Merced -* **`Oct 8, 2021`:** **Our work is publicly available at [ArXiv](https://arxiv.org/abs/2110.03921).** -* **`Oct 18, 2021`:** **ViDT now supports [Co-scale conv-attentional image Transformers (CoaT)](https://arxiv.org/pdf/2104.06399.pdf) as another body structure.** -* **`Oct 22, 2021`:** **ViDT introduces and incorporates a cross-scale fusion module based on feature pyramid networks.** -* **`Oct 26, 2021`:** **[IoU-awareness loss](https://arxiv.org/pdf/1912.05992.pdf), and [token labeling loss](https://arxiv.org/pdf/2104.10858.pdf) are available with ViDT.** -* **`Nov 5, 2021`:** **The official code is released!** -* **`Jan 21, 2022`:** **ViDT is accepted to ICLR 2022!** +* **`April 4, 2022`:** **The official code is released!**
We obtained a light-weight transformer-based detector, achieving 47.0AP only with 14M parameters and 41.9 FPS (NVIDIA A100). -## ViDT: Vision and Detection Transformers - -### Highlight +## ViDT+ for Joint-learning of Object Detection and Instance Segmentation +### Extension to ViDT+

- +

-ViDT is an end-to-end fully transformer-based object detector, which directly produces predictions without using convolutional layers. Our main contributions are summarized as follows: - -* ViDT introduces a modified attention mechanism, named **Reconfigured Attention Module (RAM)**, that facilitates any ViT variant to handling the appened [DET] and [PATCH] tokens for a standalone object detection. Thus, we can modify the lastest Swin Transformer backbone with RAM to be an object detector and obtain high scalability using its local attetention mechanism with linear complexity. - -* ViDT adopts a **lightweight encoder-free neck** architecture to reduce the computational overhead while still enabling the additional optimization techniques on the neck module. As a result, ViDT obtains better performance than neck-free counterparts. - -* We introdcue a new concept of **token matching for knowledge distillation**, which brings additional performance gains from a large model to a small model without compromising detection efficiency. - -**Architectural Advantages**. First, ViDT enables to combine Swin Transformer and the sequent-to-sequence paradigm for detection. Second, ViDT can use the multi-scale features and additional techniques without a significant computation overhead. Therefore, as a fully transformer-based object detector, ViDT facilitates better integration of vision and detection transformers. - -**Component Summary**. There are four components: (1) RAM to extend Swin Transformer as a standalone object detector, (2) the neck decoder to exploit multi-scale features with two additional techniques, auxiliary decoding loss and iterative box refinement, (3) knowledge distillation to benefit from a large model, and (4) decoding layer drop to further accelerate inference speed. +We extend ViDT into ViDT+, supporting a joint-learning of object detection and instance segmentation in an end-to-end manner. Three new components have been leveraged for extensions: (1) *An efficient pyramid feature fusion (EPFF) module*, (2) *An unified query representation module*, and (3) two auxiliary losses of IoU-aware and token labeling. +Compared with the vanilla ViDT, ViDT+ provides a significant performance improvement without comprising inference speed. Only 1M parameters are added into the model. ### Evaluation **Index:** [[A. ViT Backbone](#A)], [[B. Main Results](#B)], [[C. Complete Analysis](#C)] ``` |--- A. ViT Backbone used for ViDT -|--- B. Main Results in the ViDT Paper - |--- B.1. ViDT for 50 and 150 Epochs - |--- B.2. Distillation with Token Matching +|--- B. Main Results in the ViDT+ Paper + |--- B.1. VIDT+ compared with the vanilla ViDT for Object Detection + |--- B.2. VIDT+ compared with other CNN-based methods for Object Detection and Instance Segmentation |--- C. Complete Component Analysis ```
-#### A. [ViT Backbone used for ViDT](#content) +#### A. [ViT Backbone used for ViDT+](#content) | Backbone and Size | Training Data | Epochs | Resulution | Params | ImageNet Acc. | Checkpoint | | :------------: | :------------: | :------------: | :------------: | :------------: | :------------: | :------------: | @@ -53,52 +38,52 @@ ViDT is an end-to-end fully transformer-based object detector, which directly pr | `Swin-base` | ImageNet-22K | 90 | 224 | 88M | 86.3% | [Github](https://github.com/SwinTransformer/storage/releases/download/v1.0.0/swin_base_patch4_window7_224_22k.pth) | -#### B. [Main Results in the ViDT Paper](#content) - -In main experiments, auxiliary decoding loss and iterative box refinement were used as the auxiliary techniques on the neck structure.
-The efficiacy of distillation with token mathcing and decoding layer drop are verified independently in [Compelete Component Analysis](#C).
-`All the models were re-trained with the final version of source codes. Thus, the value may be very slightly different from those in the paper.` - -##### B.1. VIDT for 50 and 150 epochs - -| Backbone | Epochs | AP | AP50 | AP75 | AP_S | AP_M | AP_L | Params | FPS | Checkpoint / Log | -| :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | -| `Swin-nano` | 50 (150) | 40.4 (42.6) | 59.9 (62.2) | 43.0 (45.7) | 23.1 (24.9) | 42.8 (45.4) | 55.9 (59.1) | 16M | 20.0 | [Github](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_nano_50.pth) / [Log](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_nano_50.txt)
([Github](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_nano_150.pth) / [Log](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_nano_150.txt))| -| `Swin-tiny` | 50 (150)| 44.9 (47.2) | 64.7 (66.7) | 48.3 (51.4) | 27.5 (28.4) | 47.9 (50.2) | 61.9 (64.7) | 38M | 17.2 | [Github](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_tiny_50.pth) / [Log](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_tiny_50.txt)
([Github](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_tiny_150.pth) / [Log](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_tiny_150.txt))| -| `Swin-small` | 50 (150) | 47.4 (48.8) | 67.7 (68.8) | 51.2 (53.0) | 30.4 (30.7) | 50.7 (52.0) | 64.6 (65.9) | 60M | 12.1 | [Github](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_small_50.pth) / [Log](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_small_50.txt)
([Github](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_small_150.pth) / [Log](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_small_150.txt))| -| `Swin-base` | 50 (150) | 49.4 (50.4) | 69.6 (70.4) | 53.4 (54.8) | 31.6 (34.1) | 52.4 (54.2) | 66.8 (67.4) | 0.1B | 9.0 | [Github](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_base_50.pth) / [Log](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_base_50.txt)
([Github](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_base_150.pth) / [Log](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_base_150.txt)) | - -##### B.2. Distillation with Token Matching (Coefficient 4.0) - -All the models are trained for 50 epochs with distillation. - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TeacherViDT (Swin-base) trained for 50 epochs
StudentViDT (Swin-nano)ViDT (Swin-tiny)ViDT (Swin-Small)
Coefficient = 0.040.444.947.4
Coefficient = 4.041.8 (Github / Log)46.6 (Github / Log)49.2 (Github / Log)
+#### B. [Main Results in the ViDT+ Paper](#content) + +`All the models were re-trained with the final version of source codes. Thus, the value may be very slightly different from those in the paper. Note that a single 'NVIDIA A100 GPU' was used to compute FPS for the input of batch size 1.`
+Compared with the vailla version, ViDT+ leverages three additional components or techniques:
+(1) An efficient pyramid feature fusion (EPFF) module.
+(2) An unified query representation moudle (UQR).
+(3) Two additional losses of IoU-aware loss and token-labeling loss. + +##### B.1. VIDT+ compared with the vanilla ViDT for Object Detection + +| Method | Backbone | Epochs | AP | AP50 | AP75 | AP_S | AP_M | AP_L | Params | FPS | Checkpoint / Log | +| :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | +| ViDT+ | `Swin-nano` | 50 | 45.3 | 62.3 | 48.9 | 27.3 | 48.2 | 61.5 | 16M | 37.6 | [Github](https://github.com/naver-ai/) / [Log](https://github.com/naver-ai/)| +| ViDT+ | `Swin-tiny` | 50 | 49.7 | 67.7 | 54.2 | 31.6 | 53.4 | 65.9 | 38M | 30.4 | [Github](https://github.com/naver-ai/) / [Log](https://github.com/naver-ai/) | +| ViDT+ | `Swin-small` | 50 | 51.2 | 69.5 | 55.9 | 33.8 | 54.5 | 67.8 | 61M | 20.6 | [Github](https://github.com/naver-ai/) / [Log](https://github.com/naver-ai/)| +| ViDT+ | `Swin-base` | 50 | 53.2 | 71.6 | 58.3 | 36.0 | 57.1 | 69.2 | 100M | 19.3 | [Github](https://github.com/naver-ai/) / [Log](https://github.com/naver-ai/)| + +| Method | Backbone | Epochs | AP | AP50 | AP75 | AP_S | AP_M | AP_L | Params | FPS | Checkpoint / Log | +| :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | +| ViDT | `Swin-nano` | 50 | 40.4 | 59.9 | 43.0 | 23.1 | 42.8 | 55.9 | 15M | 40.8 | [Github](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_nano_50.pth) / [Log](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_nano_50.txt)| +| ViDT | `Swin-tiny` | 50 | 44.9 | 64.7 | 48.3 | 27.5 | 47.9 | 61.9 | 37M | 33.5 | [Github](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_tiny_50.pth) / [Log](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_tiny_50.txt)| +| ViDT | `Swin-small` | 50 | 47.4 | 67.7 | 51.2 | 30.4 | 50.7 | 64.6 | 60M | 24.7 | [Github](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_small_50.pth) / [Log](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_small_50.txt)| +| ViDT | `Swin-base` | 50 | 49.4 | 69.6 | 53.4 | 31.6 | 52.4 | 66.8 | 99M | 20.5 | [Github](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_base_50.pth) / [Log](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt/vidt_base_50.txt)| + +##### B.2. VIDT+ compared with other CNN-based methods for Object Detection and Instance Segmentation + +For fair comparison w.r.t the number of parameters, Swin-tiny and Swin-small backbones are used for ViDT+, which have similar number of parameters to ResNet-50 and ResNet-101, respectively.
+ViDT+ shows much higher detection AP than other joint-learning methods, but its segmentation AP is only higher than others for the medium- and large-size objects in general. + +| Method | Backbone | Epochs | Box AP | Mask AP | Mask AP_S | Mask AP_M | Mask AP_L | +| :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | +| Mask R-CNN | `ResNet-50 + FPN` | 36 | 41.3 |37.5 | 21.1 | 39.6 | 48.3 | +| HTC | `ResNet-50 + FPN` | 36 | 44.9 |39.7 | 22.6 | 42.2 | 50.6 | +| SOLOv2 | `ResNet-50 + FPN` | 72 |40.4 | 38.8 | 16.5 | 41.7 | 56.2 | +| QueryInst | `ResNet-50 + FPN` | 36 |45.6 | 40.6 | 23.4 | 42.5 | 52.8 | +| SOLQ | `ResNet-50` | 50 | 47.8 | 39.7 | 21.5 | 42.5 | 53.1 | +| **ViDT+** | `Swin-tiny` | 50 | 49.7 | 39.5 | 21.5 | 43.4 | 58.2 | + +| Method | Backbone | Epochs | Box AP | Mask AP | Mask AP_S | Mask AP_M | Mask AP_L | +| :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | +| Mask R-CNN | `ResNet-101 + FPN` | 50 | 41.3 | 38.8 | 21.8 | 41.4 | 50.5 | +| HTC | `ResNet-101 + FPN` | 50 | 44.3 | 40.8 | 23.0 | 43.5 | 58.2 | +| SOLOv2 | `ResNet-101 + FPN` | 50 |42.6 | 39.7 | 17.3 | 42.9 | 58.2 | +| QueryInst | `ResNet-101 + FPN` | 50 | 48.1 | 42.8 | 24.6 | 45.0 | 58.2 | +| SOLQ | `ResNet-101` | 50 |48.7 | 40.9 | 22.5 | 43.8 | 58.2 | +| **ViDT+** | `Swin-small` | 50 | 51.2 | 40.8 | 22.6 | 44.3 | 60.1 | @@ -106,21 +91,22 @@ All the models are trained for 50 epochs with distillation. We combined the four proposed components (even with distillation with token matching and decoding layer drop) to achieve high accuracy and speed for object detection. For distillation, ViDT (Swin-base) trained for 50 epochs was used for all models. +We combined all the proposed components (even with longer training epochs and decoding layer dropping) to achive high accuracy and speed for object detection. As summarized in below table, there are *eight* components for extension: (1) RAM, (2) the neck decoder, (3) the IoU-aware and token labeling losses, (4) the EPFF module, (5) the UQR module, (6) the use of more detection tokens, (6) the use of longer training epochs, and (8) decoding layer drop. + +`The numbers (3), (6), and (8) are the performance of the vanilla ViDT, its extension to ViDT+, and the fully optimized ViDT+.` + - + - - - - + @@ -135,67 +121,107 @@ We combined the four proposed components (even with distillation with token matc - - - - + - + - + - + - - - - + - - - - - - + + + + + + - + - - - - - - - - - - - + + + + + + + + - + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ComponentAdded Swin-nano Swin-tiny Swin-small
#RAMNeckDistilDropModule AP Params FPS
(1):heavy_check_mark:+ RAM 28.7 7M36.572.4 36.3 29M28.651.8 41.6 52M16.833.5
(2):heavy_check_mark::heavy_check_mark:+ Encoder-free Neck 40.416M20.044.938M17.247.415M40.844.837M33.547.5 60M12.124.7
(3):heavy_check_mark::heavy_check_mark::heavy_check_mark:41.816M20.046.638M17.249.2+ IoU-aware & Token Label41.015M40.845.937M33.548.5 60M12.124.7
(4):heavy_check_mark::heavy_check_mark::heavy_check_mark::heavy_check_mark:41.613M23.046.435M19.549.158M13.0+ EPFF Module42.516M38.047.138M30.949.361M23.0
(5)+ UQR Module43.916M38.047.938M30.950.161M23.0
(6)+ 300 [DET] Tokens45.316M37.649.738M30.451.261M22.6
(7)+ 150 Training Epochs47.616M37.651.438M30.452.361M22.6
(8)+ Decoding Layer Drop47.014M41.950.836M33.951.859M24.6
@@ -223,13 +249,17 @@ python test.py ```bash pip install -r requirements.txt ``` +## Training and Evaluation -### Training +If you want to test with a single GPU, see [colab examples](https://github.com/EherSenaw/ViDT_colab/blob/main/vidt_colab.ipynb). Thanks to [EherSenaw](https://github.com/EherSenaw) for making this example.
+The below codes are for training with multi GPUs. -We used the below commands to train ViDT models with a single node having 8 NVIDIA V100 GPUs. +### Training for ViDT+ + +We used the below commands to train ViDT+ models with a single node having 8 NVIDIA GPUs.
-Run this command to train the ViDT (Swin-nano) model in the paper : +Run this command to train the ViDT+ (Swin-nano) model in the paper :

 python -m torch.distributed.launch \
        --nproc_per_node=8 \
@@ -244,6 +274,12 @@ python -m torch.distributed.launch \
        --num_workers 2 \
        --aux_loss True \
        --with_box_refine True \
+       --det_token_num 300 \
+       --epff True \
+       --token_label True \
+       --iou_aware True \
+       --with_vector True \
+       --masks True \
        --coco_path /path/to/coco \
        --output_dir /path/for/output
 
@@ -266,6 +302,12 @@ python -m torch.distributed.launch \ --num_workers 2 \ --aux_loss True \ --with_box_refine True \ + --det_token_num 300 \ + --epff True \ + --token_label True \ + --iou_aware True \ + --with_vector True \ + --masks True \ --coco_path /path/to/coco \ --output_dir /path/for/output @@ -288,6 +330,12 @@ python -m torch.distributed.launch \ --num_workers 2 \ --aux_loss True \ --with_box_refine True \ + --det_token_num 300 \ + --epff True \ + --token_label True \ + --iou_aware True \ + --with_vector True \ + --masks True \ --coco_path /path/to/coco \ --output_dir /path/for/output @@ -310,15 +358,113 @@ python -m torch.distributed.launch \ --num_workers 2 \ --aux_loss True \ --with_box_refine True \ + --det_token_num 300 \ + --epff True \ + --token_label True \ + --iou_aware True \ + --with_vector True \ + --masks True \ --coco_path /path/to/coco \ --output_dir /path/for/output
-When a large pre-trained ViDT model is available, distillation with token matching can be applied for training a smaller ViDT model. +### Evaluation for ViDT+ + +
+Run this command to evaluate the ViDT (Swin-nano) model on COCO : +

+python -m torch.distributed.launch \
+       --nproc_per_node=8 \ 
+       --nnodes=1 \
+       --use_env main.py \
+       --method vidt \
+       --backbone_name swin_nano \
+       --batch_size 2 \
+       --num_workers 2 \
+       --aux_loss True \
+       --with_box_refine True \
+       --det_token_num 300 \
+       --epff True \
+       --coco_path /path/to/coco \
+       --resume /path/to/vidt_nano \
+       --pre_trained none \
+       --eval True
+
+
+ +
+Run this command to evaluate the ViDT (Swin-tiny) model on COCO : +

+python -m torch.distributed.launch \
+       --nproc_per_node=8 \
+       --nnodes=1 \
+       --use_env main.py \
+       --method vidt \
+       --backbone_name swin_tiny \
+       --batch_size 2 \
+       --num_workers 2 \
+       --aux_loss True \
+       --with_box_refine True \
+       --det_token_num 300 \
+       --epff True \
+       --coco_path /path/to/coco \
+       --resume /path/to/vidt_tiny\
+       --pre_trained none \
+       --eval True
+
+
+ +
+Run this command to evaluate the ViDT (Swin-small) model on COCO : +

+python -m torch.distributed.launch \
+       --nproc_per_node=8 \
+       --nnodes=1 \
+       --use_env main.py \
+       --method vidt \
+       --backbone_name swin_small \
+       --batch_size 2 \
+       --num_workers 2 \
+       --aux_loss True \
+       --with_box_refine True \
+       --det_token_num 300 \
+       --epff True \
+       --coco_path /path/to/coco \
+       --resume /path/to/vidt_small \
+       --pre_trained none \
+       --eval True
+
+
+ +
+Run this command to evaluate the ViDT (Swin-base) model on COCO : +

+python -m torch.distributed.launch \
+       --nproc_per_node=8 \
+       --nnodes=1 \
+       --use_env main.py \
+       --method vidt \
+       --backbone_name swin_base_win7_22k \
+       --batch_size 2 \
+       --num_workers 2 \
+       --aux_loss True \
+       --with_box_refine True \
+       --det_token_num 300 \
+       --epff True \
+       --coco_path /path/to/coco \
+       --resume /path/to/vidt_base \
+       --pre_trained none \
+       --eval True
+
+
+ +### Training for ViDT + +We used the below commands to train ViDT+ models with a single node having 8 NVIDIA GPUs.
-Run this command when training ViDT (Swin-nano) using a large ViDT (Swin-base) via Knowledge Distillation : +Run this command to train the ViDT+ (Swin-nano) model in the paper :

 python -m torch.distributed.launch \
        --nproc_per_node=8 \
@@ -333,14 +479,82 @@ python -m torch.distributed.launch \
        --num_workers 2 \
        --aux_loss True \
        --with_box_refine True \
-       --distil_model vidt_base \
-       --distil_path /path/to/vidt_base (or url) \
+       --det_token_num 100 \
+       --coco_path /path/to/coco \
+       --output_dir /path/for/output
+
+
+ + +
+Run this command to train the ViDT (Swin-tiny) model in the paper : +

+python -m torch.distributed.launch \
+       --nproc_per_node=8 \
+       --nnodes=1 \
+       --use_env main.py \
+       --method vidt \
+       --backbone_name swin_tiny \
+       --epochs 50 \
+       --lr 1e-4 \
+       --min-lr 1e-7 \
+       --batch_size 2 \
+       --num_workers 2 \
+       --aux_loss True \
+       --with_box_refine True \
+       --det_token_num 100 \
        --coco_path /path/to/coco \
        --output_dir /path/for/output
 
-### Evaluation + +
+Run this command to train the ViDT (Swin-small) model in the paper : +

+python -m torch.distributed.launch \
+       --nproc_per_node=8 \
+       --nnodes=1 \
+       --use_env main.py \
+       --method vidt \
+       --backbone_name swin_small \
+       --epochs 50 \
+       --lr 1e-4 \
+       --min-lr 1e-7 \
+       --batch_size 2 \
+       --num_workers 2 \
+       --aux_loss True \
+       --with_box_refine True \
+       --det_token_num 100 \
+       --coco_path /path/to/coco \
+       --output_dir /path/for/output
+
+
+ + +
+Run this command to train the ViDT (Swin-base) model in the paper : +

+python -m torch.distributed.launch \
+       --nproc_per_node=8 \
+       --nnodes=1 \
+       --use_env main.py \
+       --method vidt \
+       --backbone_name swin_base_win7_22k \
+       --epochs 50 \
+       --lr 1e-4 \
+       --min-lr 1e-7 \
+       --batch_size 2 \
+       --num_workers 2 \
+       --aux_loss True \
+       --with_box_refine True \
+       --det_token_num 100 \
+       --coco_path /path/to/coco \
+       --output_dir /path/for/output
+
+
+ +### Evaluation for ViDT
Run this command to evaluate the ViDT (Swin-nano) model on COCO : @@ -355,6 +569,7 @@ python -m torch.distributed.launch \ --num_workers 2 \ --aux_loss True \ --with_box_refine True \ + --det_token_num 100 \ --coco_path /path/to/coco \ --resume /path/to/vidt_nano \ --pre_trained none \ @@ -375,6 +590,7 @@ python -m torch.distributed.launch \ --num_workers 2 \ --aux_loss True \ --with_box_refine True \ + --det_token_num 100 \ --coco_path /path/to/coco \ --resume /path/to/vidt_tiny\ --pre_trained none \ @@ -395,6 +611,7 @@ python -m torch.distributed.launch \ --num_workers 2 \ --aux_loss True \ --with_box_refine True \ + --det_token_num 100 \ --coco_path /path/to/coco \ --resume /path/to/vidt_small \ --pre_trained none \ @@ -415,6 +632,7 @@ python -m torch.distributed.launch \ --num_workers 2 \ --aux_loss True \ --with_box_refine True \ + --det_token_num 100 \ --coco_path /path/to/coco \ --resume /path/to/vidt_base \ --pre_trained none \ @@ -435,6 +653,14 @@ Please consider citation if our paper is useful in your research. } ``` +```BibTeX +@inproceedings{song2022vidt, + title={An Extendable, Efficient and Effective Transformer-based Object Detector}, + author={Song, Hwanjun and Sun, Deqing and Chun, Sanghyuk and Jampani, Varun and Han, Dongyoon and Heo, Byeongho and Kim, Wonjae and Yang, Ming-Hsuan}, + year={2022} +} +``` + ## License ``` From 93ceb0b84276e546d5630cabf7b8d257aa7e6cfb Mon Sep 17 00:00:00 2001 From: Hwanjun Song Date: Wed, 6 Apr 2022 00:18:55 +0900 Subject: [PATCH 03/26] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 30dc864..edfed07 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # An Extendable, Efficient and Effective Transformer-based Object Detector (Extension of VIDT published at ICLR2022) +**Please see the [vidt branch](https://github.com/naver-ai/vidt/tree/main) if you are interested in the vanilla ViDT model. This is an extension of ViDT for joint-learning of object detection and instance segmentation.** + by [Hwanjun Song](https://scholar.google.com/citations?user=Ijzuc-8AAAAJ&hl=en&oi=ao)1, [Deqing Sun](https://scholar.google.com/citations?hl=en&user=t4rgICIAAAAJ)2, [Sanghyuk Chun](https://scholar.google.com/citations?user=4_uj0xcAAAAJ&hl=en&oi=ao)1, [Varun Jampani](https://scholar.google.com/citations?hl=en&user=1Cv6Sf4AAAAJ)2, [Dongyoon Han](https://scholar.google.com/citations?user=jcP7m1QAAAAJ&hl=en)1,
[Byeongho Heo](https://scholar.google.com/citations?user=4_7rLDIAAAAJ&hl=en)1, [Wonjae Kim](https://scholar.google.com/citations?hl=en&user=UpZ41EwAAAAJ)1, and [Ming-Hsuan Yang](https://scholar.google.com/citations?hl=en&user=p9-ohHsAAAAJ)2,3 1 NAVER AI Lab, 2 Google Research, 3 University California Merced From c1e0f2fcdc9072d0a8011b57e81700591b9caf03 Mon Sep 17 00:00:00 2001 From: Hwanjun Song Date: Wed, 6 Apr 2022 00:21:50 +0900 Subject: [PATCH 04/26] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index edfed07..1f46fe7 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,10 @@ Compared with the vailla version, ViDT+ leverages three additional components or | Method | Backbone | Epochs | AP | AP50 | AP75 | AP_S | AP_M | AP_L | Params | FPS | Checkpoint / Log | | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | -| ViDT+ | `Swin-nano` | 50 | 45.3 | 62.3 | 48.9 | 27.3 | 48.2 | 61.5 | 16M | 37.6 | [Github](https://github.com/naver-ai/) / [Log](https://github.com/naver-ai/)| -| ViDT+ | `Swin-tiny` | 50 | 49.7 | 67.7 | 54.2 | 31.6 | 53.4 | 65.9 | 38M | 30.4 | [Github](https://github.com/naver-ai/) / [Log](https://github.com/naver-ai/) | -| ViDT+ | `Swin-small` | 50 | 51.2 | 69.5 | 55.9 | 33.8 | 54.5 | 67.8 | 61M | 20.6 | [Github](https://github.com/naver-ai/) / [Log](https://github.com/naver-ai/)| -| ViDT+ | `Swin-base` | 50 | 53.2 | 71.6 | 58.3 | 36.0 | 57.1 | 69.2 | 100M | 19.3 | [Github](https://github.com/naver-ai/) / [Log](https://github.com/naver-ai/)| +| ViDT+ | `Swin-nano` | 50 | 45.3 | 62.3 | 48.9 | 27.3 | 48.2 | 61.5 | 16M | 37.6 | [Github](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus/vidt_plus_nano_det300.pth) / [Log](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus/vidt_plus_nano_det300.txt)| +| ViDT+ | `Swin-tiny` | 50 | 49.7 | 67.7 | 54.2 | 31.6 | 53.4 | 65.9 | 38M | 30.4 | [Github](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus/vidt_plus_tiny_det300.pth) / [Log](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus/vidt_plus_tiny_det300.txt) | +| ViDT+ | `Swin-small` | 50 | 51.2 | 69.5 | 55.9 | 33.8 | 54.5 | 67.8 | 61M | 20.6 | [Github](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus/vidt_plus_small_det300.pth) / [Log](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus/vidt_plus_small_det300.txt)| +| ViDT+ | `Swin-base` | 50 | 53.2 | 71.6 | 58.3 | 36.0 | 57.1 | 69.2 | 100M | 19.3 | [Github](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus/vidt_plus_base_det300.pth) / [Log](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus/vidt_plus_base_det300.txt)| | Method | Backbone | Epochs | AP | AP50 | AP75 | AP_S | AP_M | AP_L | Params | FPS | Checkpoint / Log | | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | From 07419f589d24dbc2f911284942cc8053ee5abbef Mon Sep 17 00:00:00 2001 From: Hwanjun Song Date: Wed, 6 Apr 2022 00:23:06 +0900 Subject: [PATCH 05/26] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f46fe7..a3e7f25 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # An Extendable, Efficient and Effective Transformer-based Object Detector (Extension of VIDT published at ICLR2022) -**Please see the [vidt branch](https://github.com/naver-ai/vidt/tree/main) if you are interested in the vanilla ViDT model. This is an extension of ViDT for joint-learning of object detection and instance segmentation.** +**Please see the [vidt branch](https://github.com/naver-ai/vidt/tree/main) if you are interested in the vanilla ViDT model.**
**This is an extension of ViDT for joint-learning of object detection and instance segmentation.** by [Hwanjun Song](https://scholar.google.com/citations?user=Ijzuc-8AAAAJ&hl=en&oi=ao)1, [Deqing Sun](https://scholar.google.com/citations?hl=en&user=t4rgICIAAAAJ)2, [Sanghyuk Chun](https://scholar.google.com/citations?user=4_uj0xcAAAAJ&hl=en&oi=ao)1, [Varun Jampani](https://scholar.google.com/citations?hl=en&user=1Cv6Sf4AAAAJ)2, [Dongyoon Han](https://scholar.google.com/citations?user=jcP7m1QAAAAJ&hl=en)1,
[Byeongho Heo](https://scholar.google.com/citations?user=4_7rLDIAAAAJ&hl=en)1, [Wonjae Kim](https://scholar.google.com/citations?hl=en&user=UpZ41EwAAAAJ)1, and [Ming-Hsuan Yang](https://scholar.google.com/citations?hl=en&user=p9-ohHsAAAAJ)2,3 From 7df485eb90d8b53e13190df37b336768ddcab1a4 Mon Sep 17 00:00:00 2001 From: Hwanjun Song Date: Wed, 6 Apr 2022 00:23:21 +0900 Subject: [PATCH 06/26] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a3e7f25..8438080 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ by [Hwanjun Song](https://scholar.google.com/citations?user=Ijzuc-8AAAAJ&hl=en&o 1 NAVER AI Lab, 2 Google Research, 3 University California Merced -* **`April 4, 2022`:** **The official code is released!**
We obtained a light-weight transformer-based detector, achieving 47.0AP only with 14M parameters and 41.9 FPS (NVIDIA A100). +* **`April 6, 2022`:** **The official code is released!**
We obtained a light-weight transformer-based detector, achieving 47.0AP only with 14M parameters and 41.9 FPS (NVIDIA A100). ## ViDT+ for Joint-learning of Object Detection and Instance Segmentation From 3c1d59a25c2fa83fe798b4652436ee267cf911b9 Mon Sep 17 00:00:00 2001 From: Hwanjun Song Date: Wed, 6 Apr 2022 00:28:00 +0900 Subject: [PATCH 07/26] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 8438080..2ad546e 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,9 @@ We combined all the proposed components (even with longer training epochs and de +The optimized ViDT+ models can be found:
+[ViDT+ (Swin-nano)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_nano_optimized.pth), [ViDT+ (Swin-tiny)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_tiny_optimized.pth), [ViDT+ (Swin-small)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_small_optimized.pth), and [ViDT+ (Swin-base)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_base_optimized.pth). + ### Requirements This codebase has been developed with the setting used in [Deformable DETR](https://github.com/fundamentalvision/Deformable-DETR):
Linux, CUDA>=9.2, GCC>=5.4, Python>=3.7, PyTorch>=1.5.1, and torchvision>=0.6.1. From 60649ddcbd9483cef953dccf12ddabab256764e7 Mon Sep 17 00:00:00 2001 From: Hwanjun Song Date: Wed, 6 Apr 2022 00:28:51 +0900 Subject: [PATCH 08/26] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ad546e..c9aa35f 100644 --- a/README.md +++ b/README.md @@ -229,7 +229,7 @@ We combined all the proposed components (even with longer training epochs and de The optimized ViDT+ models can be found:
-[ViDT+ (Swin-nano)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_nano_optimized.pth), [ViDT+ (Swin-tiny)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_tiny_optimized.pth), [ViDT+ (Swin-small)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_small_optimized.pth), and [ViDT+ (Swin-base)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_base_optimized.pth). +[ViDT+ (Swin-nano)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_nano_optimized.pth), [ViDT+ (Swin-tiny)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_tiny_optimized.pth), and [ViDT+ (Swin-small)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_small_optimized.pth). ### Requirements From bc8565d2a1e714592772f4e5df47193d103ef9f1 Mon Sep 17 00:00:00 2001 From: Hwanjun Song Date: Wed, 6 Apr 2022 00:29:42 +0900 Subject: [PATCH 09/26] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c9aa35f..7a92ea7 100644 --- a/README.md +++ b/README.md @@ -229,7 +229,7 @@ We combined all the proposed components (even with longer training epochs and de The optimized ViDT+ models can be found:
-[ViDT+ (Swin-nano)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_nano_optimized.pth), [ViDT+ (Swin-tiny)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_tiny_optimized.pth), and [ViDT+ (Swin-small)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_small_optimized.pth). +[ViDT+ (Swin-nano)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_swin_nano_optimized.pth), [ViDT+ (Swin-tiny)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_swin_tiny_optimized.pth), and [ViDT+ (Swin-small)](https://github.com/naver-ai/vidt/releases/download/v0.1-vidt-plus-optimized/vidt_plus_swin_small_optimized.pth). ### Requirements From 994b1bfae0961e95ca0840e1bea2daa9134376b0 Mon Sep 17 00:00:00 2001 From: Hwanjun Song Date: Wed, 6 Apr 2022 11:20:32 +0900 Subject: [PATCH 10/26] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7a92ea7..70583ed 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ We combined the four proposed components (even with distillation with token matc We combined all the proposed components (even with longer training epochs and decoding layer dropping) to achive high accuracy and speed for object detection. As summarized in below table, there are *eight* components for extension: (1) RAM, (2) the neck decoder, (3) the IoU-aware and token labeling losses, (4) the EPFF module, (5) the UQR module, (6) the use of more detection tokens, (6) the use of longer training epochs, and (8) decoding layer drop. -`The numbers (3), (6), and (8) are the performance of the vanilla ViDT, its extension to ViDT+, and the fully optimized ViDT+.` +`The numbers (2), (6), and (8) are the performance of the vanilla ViDT, its extension to ViDT+, and the fully optimized ViDT+.` From 4feff9a83f57c2133a4a897ce26c433c57e2e51f Mon Sep 17 00:00:00 2001 From: Hwanjun Song Date: Wed, 6 Apr 2022 11:31:54 +0900 Subject: [PATCH 11/26] Update README.md --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 70583ed..7380e30 100644 --- a/README.md +++ b/README.md @@ -292,7 +292,7 @@ python -m torch.distributed.launch \
-Run this command to train the ViDT (Swin-tiny) model in the paper : +Run this command to train the ViDT+ (Swin-tiny) model in the paper :

 python -m torch.distributed.launch \
        --nproc_per_node=8 \
@@ -320,7 +320,7 @@ python -m torch.distributed.launch \
 
 
 
-Run this command to train the ViDT (Swin-small) model in the paper : +Run this command to train the ViDT+ (Swin-small) model in the paper :

 python -m torch.distributed.launch \
        --nproc_per_node=8 \
@@ -348,7 +348,7 @@ python -m torch.distributed.launch \
 
 
 
-Run this command to train the ViDT (Swin-base) model in the paper : +Run this command to train the ViDT+ (Swin-base) model in the paper :

 python -m torch.distributed.launch \
        --nproc_per_node=8 \
@@ -377,7 +377,7 @@ python -m torch.distributed.launch \
 ### Evaluation for ViDT+
 
 
-Run this command to evaluate the ViDT (Swin-nano) model on COCO : +Run this command to evaluate the ViDT+ (Swin-nano) model on COCO :

 python -m torch.distributed.launch \
        --nproc_per_node=8 \ 
@@ -399,7 +399,7 @@ python -m torch.distributed.launch \
 
-Run this command to evaluate the ViDT (Swin-tiny) model on COCO : +Run this command to evaluate the ViDT+ (Swin-tiny) model on COCO :

 python -m torch.distributed.launch \
        --nproc_per_node=8 \
@@ -421,7 +421,7 @@ python -m torch.distributed.launch \
 
-Run this command to evaluate the ViDT (Swin-small) model on COCO : +Run this command to evaluate the ViDT+ (Swin-small) model on COCO :

 python -m torch.distributed.launch \
        --nproc_per_node=8 \
@@ -443,7 +443,7 @@ python -m torch.distributed.launch \
 
-Run this command to evaluate the ViDT (Swin-base) model on COCO : +Run this command to evaluate the ViDT+ (Swin-base) model on COCO :

 python -m torch.distributed.launch \
        --nproc_per_node=8 \
@@ -466,10 +466,10 @@ python -m torch.distributed.launch \
 
 ### Training for ViDT
 
-We used the below commands to train ViDT+ models with a single node having 8 NVIDIA GPUs.
+We used the below commands to train ViDT models with a single node having 8 NVIDIA GPUs.
 
 
-Run this command to train the ViDT+ (Swin-nano) model in the paper : +Run this command to train the ViDT (Swin-nano) model in the paper :

 python -m torch.distributed.launch \
        --nproc_per_node=8 \

From 96415a6c516a81c40d0d072a63fd9b0601c32a24 Mon Sep 17 00:00:00 2001
From: Hwanjun Song 
Date: Wed, 6 Apr 2022 11:32:20 +0900
Subject: [PATCH 12/26] Update README.md

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 7380e30..3e42790 100644
--- a/README.md
+++ b/README.md
@@ -659,7 +659,7 @@ Please consider citation if our paper is useful in your research.
 ```
 
 ```BibTeX
-@inproceedings{song2022vidt,
+@inproceedings{song2022vidtplus,
   title={An Extendable, Efficient and Effective Transformer-based Object Detector},
   author={Song, Hwanjun and Sun, Deqing and Chun, Sanghyuk and Jampani, Varun and Han, Dongyoon and Heo, Byeongho and Kim, Wonjae and Yang, Ming-Hsuan},
   year={2022}

From 6214fc8f46ede2d75e9c24e4ddba9495a1b3d274 Mon Sep 17 00:00:00 2001
From: Hwanjun Song 
Date: Wed, 6 Apr 2022 12:35:43 +0900
Subject: [PATCH 13/26] Update README.md

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 3e42790..b9e9c25 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ by [Hwanjun Song](https://scholar.google.com/citations?user=Ijzuc-8AAAAJ&hl=en&o
 
 1 NAVER AI Lab, 2 Google Research, 3 University California Merced
 
-* **`April 6, 2022`:** **The official code is released!** 
We obtained a light-weight transformer-based detector, achieving 47.0AP only with 14M parameters and 41.9 FPS (NVIDIA A100). +* **`April 6, 2022`:** **The official code is released!**
We obtained a light-weight transformer-based detector, achieving 47.0AP only with 14M parameters and 41.9 FPS (NVIDIA A100).
See [[C. Complete Analysis](#C)]. ## ViDT+ for Joint-learning of Object Detection and Instance Segmentation From 72b54b504b39457e6af84fc47f8e368102c8cd58 Mon Sep 17 00:00:00 2001 From: Hwanjun Song Date: Wed, 6 Apr 2022 12:36:05 +0900 Subject: [PATCH 14/26] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b9e9c25..c6c7dff 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ by [Hwanjun Song](https://scholar.google.com/citations?user=Ijzuc-8AAAAJ&hl=en&o 1 NAVER AI Lab, 2 Google Research, 3 University California Merced -* **`April 6, 2022`:** **The official code is released!**
We obtained a light-weight transformer-based detector, achieving 47.0AP only with 14M parameters and 41.9 FPS (NVIDIA A100).
See [[C. Complete Analysis](#C)]. +* **`April 6, 2022`:** **The official code is released!**
We obtained a light-weight transformer-based detector, achieving 47.0AP only with 14M parameters and 41.9 FPS (NVIDIA A100).
See [Complete Analysis](#C). ## ViDT+ for Joint-learning of Object Detection and Instance Segmentation From b963cb2212b93d50c73f0553a9ed2d006a10dcbb Mon Sep 17 00:00:00 2001 From: Hwanjun Song Date: Wed, 6 Apr 2022 22:04:51 +0900 Subject: [PATCH 15/26] Update arguments.py --- arguments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arguments.py b/arguments.py index 8a3248f..a0841e3 100644 --- a/arguments.py +++ b/arguments.py @@ -149,7 +149,7 @@ def get_args_parser(): help="Number of coeffs to be remained") parser.add_argument('--gt_mask_len', default=128, type=int, help="Size of target mask") - parser.add_argument('--vector_loss_coef', default=0.7, type=float) + parser.add_argument('--vector_loss_coef', default=3.0, type=float) parser.add_argument('--vector_hidden_dim', default=256, type=int, help="Size of the vector embeddings (dimension of the transformer)") parser.add_argument('--no_vector_loss_norm', default=False, action='store_true') From 3722d1bc254da8da0f976b9777f3a512f365077b Mon Sep 17 00:00:00 2001 From: Hwanjun Song Date: Tue, 19 Apr 2022 16:27:59 +0900 Subject: [PATCH 16/26] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c6c7dff..7a914fb 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ by [Hwanjun Song](https://scholar.google.com/citations?user=Ijzuc-8AAAAJ&hl=en&o 1 NAVER AI Lab, 2 Google Research, 3 University California Merced * **`April 6, 2022`:** **The official code is released!**
We obtained a light-weight transformer-based detector, achieving 47.0AP only with 14M parameters and 41.9 FPS (NVIDIA A100).
See [Complete Analysis](#C). +* **`April 19, 2022`:** **The preprint is uploaded, See [[here](https://arxiv.org/abs/2204.07962)]!** ## ViDT+ for Joint-learning of Object Detection and Instance Segmentation From d956c35a453a9904ed7a07ed92332aec45ebed9d Mon Sep 17 00:00:00 2001 From: Hwanjun Song Date: Wed, 20 Apr 2022 21:07:32 +0900 Subject: [PATCH 17/26] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7a914fb..353511b 100644 --- a/README.md +++ b/README.md @@ -660,9 +660,10 @@ Please consider citation if our paper is useful in your research. ``` ```BibTeX -@inproceedings{song2022vidtplus, +@article{song2022vidtplus, title={An Extendable, Efficient and Effective Transformer-based Object Detector}, author={Song, Hwanjun and Sun, Deqing and Chun, Sanghyuk and Jampani, Varun and Han, Dongyoon and Heo, Byeongho and Kim, Wonjae and Yang, Ming-Hsuan}, + journal={arXiv preprint arXiv:2204.07962}, year={2022} } ``` From b9911e3917b1c958f0fddb44b8a97fb71ee1ede3 Mon Sep 17 00:00:00 2001 From: youdaoyzbx Date: Wed, 10 Aug 2022 17:13:34 +0800 Subject: [PATCH 18/26] protect main branch --- .dockerignore | 5 + .gitignore | 5 + datasets/voc.py | 5 +- datasets/ymir.py | 21 +++ main.py | 31 +++-- methods/__init__.py | 1 + methods/swin_w_ram.py | 204 ++++++++++++++++++---------- methods/vidt/detector.py | 3 + methods/vidt/postprocessor.py | 4 +- methods/vidt_wo_neck/detector.py | 3 + mypy.ini | 4 + ymir/docker/cuda102.dockerfile | 1 + ymir/img-man/infer-template.yaml | 9 ++ ymir/img-man/mining-template.yaml | 10 ++ ymir/img-man/training-template.yaml | 16 +++ ymir/readme.md | 5 + ymir/start.py | 67 +++++++++ ymir/ymir_training.py | 58 ++++++++ 18 files changed, 360 insertions(+), 92 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 datasets/ymir.py create mode 100644 mypy.ini create mode 100644 ymir/img-man/infer-template.yaml create mode 100644 ymir/img-man/mining-template.yaml create mode 100644 ymir/img-man/training-template.yaml create mode 100644 ymir/start.py create mode 100644 ymir/ymir_training.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..280c8de --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +*.pth +__pycache__ +*.egg-info +ops/build +ops/dist diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..280c8de --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pth +__pycache__ +*.egg-info +ops/build +ops/dist diff --git a/datasets/voc.py b/datasets/voc.py index 5b5ff0b..5614265 100644 --- a/datasets/voc.py +++ b/datasets/voc.py @@ -25,11 +25,12 @@ # c1, c2 = (int(box[0]), int(box[1])), (int(box[2]), int(box[3])) # cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA) # cv2.imwrite(file_path, img) - + class CocoDetection(torchvision.datasets.CocoDetection): - def __init__(self, img_folder, ann_file, transforms, return_masks): + def __init__(self, image_set, img_folder, ann_file, transforms, return_masks): super(CocoDetection, self).__init__(img_folder, ann_file) + self.image_set = image_set self._transforms = transforms self.prepare = ConvertCocoPolysToMask(return_masks) diff --git a/datasets/ymir.py b/datasets/ymir.py new file mode 100644 index 0000000..427a9f2 --- /dev/null +++ b/datasets/ymir.py @@ -0,0 +1,21 @@ +from pathlib import Path + +from ymir_exc.util import convert_ymir_to_coco + +from .voc import CocoDetection, make_coco_transforms + + +def build(image_set, args): + root = Path(args.coco_path) + assert root.exists(), f'provided ymir path {root} does not exist' + + data_info = convert_ymir_to_coco(cat_id_from_zero=True) + + datasets = [] + for split in image_set: + img_folder = data_info[split]['img_dir'] + ann_file = data_info[split]['ann_file'] + dataset = CocoDetection(split, img_folder, ann_file, + transforms=make_coco_transforms(split), return_masks=False) + datasets.append(dataset) + return datasets diff --git a/main.py b/main.py index 2af0eed..30f8c54 100644 --- a/main.py +++ b/main.py @@ -5,25 +5,26 @@ # Additionally modified by NAVER Corp. for ViDT # ------------------------------------------------------------------------ -import os +import argparse import datetime import json +import os import random +import resource import time from pathlib import Path import numpy as np import torch from torch.utils.data import DataLoader, DistributedSampler -import resource + import datasets import util.misc as utils +from arguments import get_args_parser from datasets import build_dataset, get_coco_api_from_dataset from engine import evaluate, train_one_epoch, train_one_epoch_with_teacher from methods import build_model from util.scheduler import create_scheduler -from arguments import get_args_parser -import argparse def build_distil_model(args): @@ -40,9 +41,8 @@ def main(args): # Gradient accumulation setup if args.n_iter_to_acc > 1: if args.batch_size % args.n_iter_to_acc != 0: - print("Not supported divisor for acc grade.") - import sys - sys.exit(1) + raise Exception(f"Not supported divisor for acc grade with batch size {args.batch_size} and n_iter_to_acc {args.n_iter_to_acc}") + print("Gradient Accumulation is applied.") print("The batch: ", args.batch_size, "->", int(args.batch_size / args.n_iter_to_acc), 'but updated every ', args.n_iter_to_acc, 'steps.') @@ -51,7 +51,7 @@ def main(args): # distributed data parallel setup utils.init_distributed_mode(args) - print("git:\n {}\n".format(utils.get_sha())) + # print("git:\n {}\n".format(utils.get_sha())) print(args) device = torch.device(args.device) @@ -74,11 +74,13 @@ def main(args): if 'http' in args.distil_model_path or 'https' in args.distil_model_path: # load from a url - torch.hub.download_url_to_file( - url=args.distil_model_path, - dst="checkpoint.pth" - ) - checkpoint = torch.load("checkpoint.pth", map_location="cpu") + url=args.distil_model_path + filename = os.path.basename(url) + model_dir = '/out' + filepath = os.path.join(model_dir, filename) + if not os.path.exists(filepath): + torch.hub.download_url_to_file(url=url, dst=filepath) + checkpoint = torch.load(filepath, map_location='cpu') teacher_model.load_state_dict(checkpoint["model"]) else: # load from a local path @@ -141,8 +143,7 @@ def build_optimizer(model, args): lr_scheduler, _ = create_scheduler(args, optimizer) # build data loader - dataset_train = build_dataset(image_set='train', args=args) - dataset_val = build_dataset(image_set='val', args=args) + dataset_train, dataset_val = build_dataset(image_set=['train', 'val'], args=args) print("# train:", len(dataset_train), ", # val", len(dataset_val)) # data samplers diff --git a/methods/__init__.py b/methods/__init__.py index 9c34d6c..8689031 100644 --- a/methods/__init__.py +++ b/methods/__init__.py @@ -7,6 +7,7 @@ from methods.vidt.detector import build as vidt_build from methods.vidt_wo_neck.detector import build as vidt_wo_neck_build + def build_model(args, is_teacher=False): available_methods = ['vidt_wo_neck', 'vidt'] diff --git a/methods/swin_w_ram.py b/methods/swin_w_ram.py index e5c80dc..7fe107d 100644 --- a/methods/swin_w_ram.py +++ b/methods/swin_w_ram.py @@ -8,11 +8,13 @@ # -------------------------------------------------------- import math +import os + +import numpy as np import torch import torch.nn as nn import torch.nn.functional as F import torch.utils.checkpoint as checkpoint -import numpy as np from timm.models.layers import DropPath, to_2tuple, trunc_normal_ @@ -36,6 +38,7 @@ def forward(self, x): x = self.drop(x) return x + def masked_sin_pos_encoding(x, mask, num_pos_feats, temperature=10000, scale=2 * math.pi): """ Masked Sinusoidal Positional Encoding @@ -66,8 +69,10 @@ def masked_sin_pos_encoding(x, mask, num_pos_feats, temperature=10000, scale=2 * pos_x = x_embed[:, :, :, None] / dim_t pos_y = y_embed[:, :, :, None] / dim_t - pos_x = torch.stack((pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4).flatten(3) - pos_y = torch.stack((pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4).flatten(3) + pos_x = torch.stack((pos_x[:, :, :, 0::2].sin(), + pos_x[:, :, :, 1::2].cos()), dim=4).flatten(3) + pos_y = torch.stack((pos_y[:, :, :, 0::2].sin(), + pos_y[:, :, :, 1::2].cos()), dim=4).flatten(3) pos = torch.cat((pos_y, pos_x), dim=3) return pos @@ -83,8 +88,10 @@ def window_partition(x, window_size): windows: (num_windows*B, window_size, window_size, C) """ B, H, W, C = x.shape - x = x.view(B, H // window_size, window_size, W // window_size, window_size, C) - windows = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, C) + x = x.view(B, H // window_size, window_size, + W // window_size, window_size, C) + windows = x.permute(0, 1, 3, 2, 4, 5).contiguous( + ).view(-1, window_size, window_size, C) return windows @@ -100,7 +107,8 @@ def window_reverse(windows, window_size, H, W): x: (B, H, W, C) """ B = int(windows.shape[0] / (H * W / window_size / window_size)) - x = windows.view(B, H // window_size, W // window_size, window_size, window_size, -1) + x = windows.view(B, H // window_size, W // window_size, + window_size, window_size, -1) x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, H, W, -1) return x @@ -143,13 +151,17 @@ def __init__(self, dim, window_size, num_heads, qkv_bias=True, qk_scale=None, at coords_w = torch.arange(self.window_size[1]) coords = torch.stack(torch.meshgrid([coords_h, coords_w])) # 2, Wh, Ww coords_flatten = torch.flatten(coords, 1) # 2, Wh*Ww - relative_coords = coords_flatten[:, :, None] - coords_flatten[:, None, :] # 2, Wh*Ww, Wh*Ww - relative_coords = relative_coords.permute(1, 2, 0).contiguous() # Wh*Ww, Wh*Ww, 2 - relative_coords[:, :, 0] += self.window_size[0] - 1 # shift to start from 0 + relative_coords = coords_flatten[:, :, None] - \ + coords_flatten[:, None, :] # 2, Wh*Ww, Wh*Ww + relative_coords = relative_coords.permute( + 1, 2, 0).contiguous() # Wh*Ww, Wh*Ww, 2 + relative_coords[:, :, 0] += self.window_size[0] - \ + 1 # shift to start from 0 relative_coords[:, :, 1] += self.window_size[1] - 1 relative_coords[:, :, 0] *= 2 * self.window_size[1] - 1 relative_position_index = relative_coords.sum(-1) # Wh*Ww, Wh*Ww - self.register_buffer("relative_position_index", relative_position_index) + self.register_buffer("relative_position_index", + relative_position_index) self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) self.attn_drop = nn.Dropout(attn_drop) @@ -159,7 +171,6 @@ def __init__(self, dim, window_size, num_heads, qkv_bias=True, qk_scale=None, at trunc_normal_(self.relative_position_bias_table, std=.02) self.softmax = nn.Softmax(dim=-1) - def forward(self, x, det, mask=None, cross_attn=False, cross_attn_mask=None): """ Forward function. RAM module receives [Patch] and [DET] tokens and returns their calibrated ones @@ -201,13 +212,16 @@ def forward(self, x, det, mask=None, cross_attn=False, cross_attn_mask=None): x = torch.cat([shifted_x, cross_x, det], dim=1) full_qkv = self.qkv(x) patch_qkv, cross_patch_qkv, det_qkv = \ - full_qkv[:, :N, :], full_qkv[:, N:N + ori_N, :], full_qkv[:, N + ori_N:, :] + full_qkv[:, :N, :], full_qkv[:, N:N + + ori_N, :], full_qkv[:, N + ori_N:, :] patch_qkv = patch_qkv.view(B, H, W, -1) # window partitioning for [PATCH] tokens - patch_qkv = window_partition(patch_qkv, window_size) # nW*B, window_size, window_size, C + # nW*B, window_size, window_size, C + patch_qkv = window_partition(patch_qkv, window_size) B_ = patch_qkv.shape[0] - patch_qkv = patch_qkv.reshape(B_, window_size * window_size, 3, self.num_heads, C // self.num_heads) + patch_qkv = patch_qkv.reshape( + B_, window_size * window_size, 3, self.num_heads, C // self.num_heads) _patch_qkv = patch_qkv.permute(2, 0, 3, 1, 4) patch_q, patch_k, patch_v = _patch_qkv[0], _patch_qkv[1], _patch_qkv[2] @@ -217,19 +231,22 @@ def forward(self, x, det, mask=None, cross_attn=False, cross_attn_mask=None): # add relative pos bias for [patch x patch] self-attention relative_position_bias = self.relative_position_bias_table[self.relative_position_index.view(-1)].view( self.window_size[0] * self.window_size[1], self.window_size[0] * self.window_size[1], -1) # Wh*Ww,Wh*Ww,nH - relative_position_bias = relative_position_bias.permute(2, 0, 1).contiguous() # nH, Wh*Ww, Wh*Ww + relative_position_bias = relative_position_bias.permute( + 2, 0, 1).contiguous() # nH, Wh*Ww, Wh*Ww patch_attn = patch_attn + relative_position_bias.unsqueeze(0) # if shifted window is used, it needs to apply the mask if mask is not None: nW = mask.shape[0] patch_attn = patch_attn.view(B_ // nW, nW, self.num_heads, local_map_size, local_map_size) + \ - mask.unsqueeze(1).unsqueeze(0) - patch_attn = patch_attn.view(-1, self.num_heads, local_map_size, local_map_size) + mask.unsqueeze(1).unsqueeze(0) + patch_attn = patch_attn.view(-1, self.num_heads, + local_map_size, local_map_size) patch_attn = self.softmax(patch_attn) patch_attn = self.attn_drop(patch_attn) - patch_x = (patch_attn @ patch_v).transpose(1, 2).reshape(B_, window_size, window_size, C) + patch_x = (patch_attn @ patch_v).transpose(1, + 2).reshape(B_, window_size, window_size, C) # extract qkv for [DET] tokens det_qkv = det_qkv.view(B, -1, 3, self.num_heads, C // self.num_heads) @@ -240,15 +257,18 @@ def forward(self, x, det, mask=None, cross_attn=False, cross_attn_mask=None): if cross_attn: # reconstruct the spatial form of [PATCH] tokens for global [DET x PATCH] attention - cross_patch_qkv = cross_patch_qkv.view(B, ori_H, ori_W, 3, self.num_heads, C // self.num_heads) - patch_kv = cross_patch_qkv[:, :, :, 1:, :, :].permute(3, 0, 4, 1, 2, 5).contiguous() + cross_patch_qkv = cross_patch_qkv.view( + B, ori_H, ori_W, 3, self.num_heads, C // self.num_heads) + patch_kv = cross_patch_qkv[:, :, :, 1:, :, :].permute( + 3, 0, 4, 1, 2, 5).contiguous() patch_kv = patch_kv.view(2, B, self.num_heads, ori_H * ori_W, -1) # extract "key and value" of [PATCH] tokens for cross-attention cross_patch_k, cross_patch_v = patch_kv[0], patch_kv[1] # bind key and value of [PATCH] and [DET] tokens for [DET X [PATCH, DET]] attention - det_k, det_v = torch.cat([cross_patch_k, det_k], dim=2), torch.cat([cross_patch_v, det_v], dim=2) + det_k, det_v = torch.cat([cross_patch_k, det_k], dim=2), torch.cat( + [cross_patch_v, det_v], dim=2) # [DET x DET] self-attention or binded [DET x [PATCH, DET]] attention det_q = det_q * self.scale @@ -309,10 +329,12 @@ def __init__(self, dim, num_heads, window_size=7, shift_size=0, dim, window_size=to_2tuple(self.window_size), num_heads=num_heads, qkv_bias=qkv_bias, qk_scale=qk_scale, attn_drop=attn_drop, proj_drop=drop) - self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity() + self.drop_path = DropPath( + drop_path) if drop_path > 0. else nn.Identity() self.norm2 = norm_layer(dim) mlp_hidden_dim = int(dim * mlp_ratio) - self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop) + self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, + act_layer=act_layer, drop=drop) self.H = None self.W = None @@ -358,7 +380,8 @@ def forward(self, x, mask_matrix, pos, cross_attn, cross_attn_mask): # cyclic shift if self.shift_size > 0: - shifted_x = torch.roll(x, shifts=(-self.shift_size, -self.shift_size), dims=(1, 2)) + shifted_x = torch.roll( + x, shifts=(-self.shift_size, -self.shift_size), dims=(1, 2)) attn_mask = mask_matrix else: shifted_x = x @@ -385,7 +408,8 @@ def forward(self, x, mask_matrix, pos, cross_attn, cross_attn_mask): # reverse cyclic shift if self.shift_size > 0: - x = torch.roll(shifted_x, shifts=(self.shift_size, self.shift_size), dims=(1, 2)) + x = torch.roll(shifted_x, shifts=( + self.shift_size, self.shift_size), dims=(1, 2)) else: x = shifted_x @@ -518,17 +542,18 @@ def __init__(self, qk_scale=qk_scale, drop=drop, attn_drop=attn_drop, - drop_path=drop_path[i] if isinstance(drop_path, list) else drop_path, + drop_path=drop_path[i] if isinstance( + drop_path, list) else drop_path, norm_layer=norm_layer) for i in range(depth)]) # patch merging layer if downsample is not None: - self.downsample = downsample(dim=dim, norm_layer=norm_layer, expand=(not last)) + self.downsample = downsample( + dim=dim, norm_layer=norm_layer, expand=(not last)) else: self.downsample = None - def forward(self, x, H, W, det_pos, input_mask, cross_attn=False): """ Forward function. @@ -560,16 +585,19 @@ def forward(self, x, H, W, det_pos, input_mask, cross_attn=False): # mask for cyclic shift mask_windows = window_partition(img_mask, self.window_size) - mask_windows = mask_windows.view(-1, self.window_size * self.window_size) + mask_windows = mask_windows.view(-1, + self.window_size * self.window_size) attn_mask = mask_windows.unsqueeze(1) - mask_windows.unsqueeze(2) - attn_mask = attn_mask.masked_fill(attn_mask != 0, float(-100.0)).masked_fill(attn_mask == 0, float(0.0)) + attn_mask = attn_mask.masked_fill( + attn_mask != 0, float(-100.0)).masked_fill(attn_mask == 0, float(0.0)) # compute sinusoidal pos encoding and cross-attn mask here to avoid redundant computation if cross_attn: _H, _W = input_mask.shape[1:] if not (_H == H and _W == W): - input_mask = F.interpolate(input_mask[None].float(), size=(H, W)).to(torch.bool)[0] + input_mask = F.interpolate( + input_mask[None].float(), size=(H, W)).to(torch.bool)[0] # sinusoidal pos encoding for [PATCH] tokens used in cross-attention patch_pos = masked_sin_pos_encoding(x, input_mask, self.dim) @@ -581,8 +609,10 @@ def forward(self, x, H, W, det_pos, input_mask, cross_attn=False): masked_fill(cross_attn_mask == 0.0, float(0.0)) # pad for detection token (this padding is required to process the binded [PATCH, DET] attention - cross_attn_mask = cross_attn_mask.view(B, H * W).unsqueeze(1).unsqueeze(2) - cross_attn_mask = F.pad(cross_attn_mask, (0, self.det_token_num), value=0) + cross_attn_mask = cross_attn_mask.view( + B, H * W).unsqueeze(1).unsqueeze(2) + cross_attn_mask = F.pad( + cross_attn_mask, (0, self.det_token_num), value=0) else: patch_pos = None @@ -598,7 +628,7 @@ def forward(self, x, H, W, det_pos, input_mask, cross_attn=False): if cross_attn: _cross_attn = True _cross_attn_mask = cross_attn_mask - _pos = pos # i.e., (patch_pos, det_pos) + _pos = pos # i.e., (patch_pos, det_pos) else: _cross_attn = False _cross_attn_mask = None @@ -645,7 +675,8 @@ def __init__(self, patch_size=4, in_chans=3, embed_dim=96, norm_layer=None): self.in_chans = in_chans self.embed_dim = embed_dim - self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size) + self.proj = nn.Conv2d(in_chans, embed_dim, + kernel_size=patch_size, stride=patch_size) if norm_layer is not None: self.norm = norm_layer(embed_dim) else: @@ -659,7 +690,8 @@ def forward(self, x): if W % self.patch_size[1] != 0: x = F.pad(x, (0, self.patch_size[1] - W % self.patch_size[1])) if H % self.patch_size[0] != 0: - x = F.pad(x, (0, 0, 0, self.patch_size[0] - H % self.patch_size[0])) + x = F.pad( + x, (0, 0, 0, self.patch_size[0] - H % self.patch_size[0])) x = self.proj(x) # B C Wh Ww if self.norm is not None: @@ -717,7 +749,8 @@ def __init__(self, norm_layer=nn.LayerNorm, ape=False, patch_norm=True, - out_indices=[1, 2, 3], # not used in the current version, please ignore. + # not used in the current version, please ignore. + out_indices=[1, 2, 3], frozen_stages=-1, use_checkpoint=False): super().__init__() @@ -739,15 +772,18 @@ def __init__(self, if self.ape: pretrain_img_size = to_2tuple(pretrain_img_size) patch_size = to_2tuple(patch_size) - patches_resolution = [pretrain_img_size[0] // patch_size[0], pretrain_img_size[1] // patch_size[1]] + patches_resolution = [ + pretrain_img_size[0] // patch_size[0], pretrain_img_size[1] // patch_size[1]] - self.absolute_pos_embed = nn.Parameter(torch.zeros(1, embed_dim, patches_resolution[0], patches_resolution[1])) + self.absolute_pos_embed = nn.Parameter(torch.zeros( + 1, embed_dim, patches_resolution[0], patches_resolution[1])) trunc_normal_(self.absolute_pos_embed, std=.02) self.pos_drop = nn.Dropout(p=drop_rate) # stochastic depth - dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] # stochastic depth decay rule + dpr = [x.item() for x in torch.linspace(0, drop_path_rate, + sum(depths))] # stochastic depth decay rule # build layers self.layers = nn.ModuleList() @@ -765,13 +801,15 @@ def __init__(self, drop_path=dpr[sum(depths[:i_layer]):sum(depths[:i_layer + 1])], norm_layer=norm_layer, # modified by ViDT - downsample=PatchMerging if (i_layer < self.num_layers) else None, + downsample=PatchMerging if ( + i_layer < self.num_layers) else None, last=None if (i_layer < self.num_layers-1) else True, # use_checkpoint=use_checkpoint) self.layers.append(layer) - num_features = [int(embed_dim * 2 ** i) for i in range(self.num_layers)] + num_features = [int(embed_dim * 2 ** i) + for i in range(self.num_layers)] self.num_features = num_features # add a norm layer for each output @@ -821,7 +859,8 @@ def finetune_det(self, method, det_token_num=100, pos_dim=256, cross_indices=[3] # how many object we detect? self.det_token_num = det_token_num - self.det_token = nn.Parameter(torch.zeros(1, det_token_num, self.num_features[0])) + self.det_token = nn.Parameter(torch.zeros( + 1, det_token_num, self.num_features[0])) self.det_token = trunc_normal_(self.det_token, std=.02) # dim size of pos encoding @@ -833,9 +872,11 @@ def finetune_det(self, method, det_token_num=100, pos_dim=256, cross_indices=[3] self.det_pos_embed = torch.nn.Parameter(det_pos_embed) # info for detection - self.num_channels = [self.num_features[i+1] for i in range(len(self.num_features)-1)] + self.num_channels = [self.num_features[i+1] + for i in range(len(self.num_features)-1)] if method == 'vidt': - self.num_channels.append(self.pos_dim) # default: 256 (same to the default pos_dim) + # default: 256 (same to the default pos_dim) + self.num_channels.append(self.pos_dim) self.cross_indices = cross_indices # divisor to reduce the spatial size of the mask self.mask_divisor = 2 ** (len(self.layers) - len(self.cross_indices)) @@ -887,7 +928,7 @@ def forward(self, x, mask): # prepare a mask for cross attention mask = F.interpolate(mask[None].float(), - size=(Wh // self.mask_divisor, Ww // self.mask_divisor)).to(torch.bool)[0] + size=(Wh // self.mask_divisor, Ww // self.mask_divisor)).to(torch.bool)[0] patch_outs = [] for stage in range(self.num_layers): @@ -906,11 +947,13 @@ def forward(self, x, mask): det_pos=det_pos, cross_attn=cross_attn) - x, det_token = x[:, :-self.det_token_num, :], x[:, -self.det_token_num:, :] + x, det_token = x[:, :-self.det_token_num, + :], x[:, -self.det_token_num:, :] # Aggregate intermediate outputs if stage > 0: - patch_out = x_out[:, :-self.det_token_num, :].view(B, H, W, -1).permute(0, 3, 1, 2) + patch_out = x_out[:, :-self.det_token_num, + :].view(B, H, W, -1).permute(0, 3, 1, 2) patch_outs.append(patch_out) # patch token reduced from last stage output @@ -935,11 +978,26 @@ def flops(self): flops += self.patch_embed.flops() for i, layer in enumerate(self.layers): flops += layer.flops() - flops += self.num_features * self.patches_resolution[0] * self.patches_resolution[1] // (2 ** self.num_layers) + flops += self.num_features * \ + self.patches_resolution[0] * \ + self.patches_resolution[1] // (2 ** self.num_layers) flops += self.num_features * self.num_classes return flops +def load_state_dict_from_url(url, model_dir, map_location): + """ + for pytorch 1.6, the torch.hub.load_state_dict_from_url not work, + view https://github.com/open-mmlab/mmsegmentation/issues/687 for details + """ + filename = os.path.basename(url) + filepath = os.path.join(model_dir, filename) + + if not os.path.exists(filepath): + torch.hub.download_url_to_file(url=url, dst=filepath) + return torch.load(filepath, map_location=map_location) + + def swin_nano(pretrained=None, **kwargs): model = SwinTransformer(pretrain_img_size=[224, 224], embed_dim=48, depths=[2, 2, 6, 2], num_heads=[3, 6, 12, 24], window_size=7, drop_path_rate=0.0, **kwargs) @@ -949,11 +1007,11 @@ def swin_nano(pretrained=None, **kwargs): if pretrained is not None: if pretrained == 'imagenet': - torch.hub.download_url_to_file( - url="https://github.com/naver-ai/vidt/releases/download/v0.1-swin/swin_nano_patch4_window7_224.pth", - dst="checkpoint.pth" - ) - checkpoint = torch.load("checkpoint.pth", map_location="cpu") + url = "https://github.com/naver-ai/vidt/releases/download/v0.1-swin/swin_nano_patch4_window7_224.pth" + checkpoint = load_state_dict_from_url(url=url, + model_dir="/out", + map_location="cpu") + model.load_state_dict(checkpoint["model"], strict=False) print('Load the backbone pretrained on ImageNet 1K') @@ -974,11 +1032,11 @@ def swin_tiny(pretrained=None, **kwargs): if pretrained is not None: if pretrained == 'imagenet': - torch.hub.download_url_to_file( - url="https://github.com/SwinTransformer/storage/releases/download/v1.0.0/swin_tiny_patch4_window7_224.pth", - dst="checkpoint.pth" - ) - checkpoint = torch.load("checkpoint.pth", map_location="cpu") + url = "https://github.com/SwinTransformer/storage/releases/download/v1.0.0/swin_tiny_patch4_window7_224.pth" + + checkpoint = load_state_dict_from_url(url=url, + model_dir="/out", + map_location="cpu") model.load_state_dict(checkpoint["model"], strict=False) print('Load the backbone pretrained on ImageNet 1K') else: @@ -997,11 +1055,10 @@ def swin_small(pretrained=None, **kwargs): if pretrained is not None: if pretrained == 'imagenet': - torch.hub.download_url_to_file( - url="https://github.com/SwinTransformer/storage/releases/download/v1.0.0/swin_small_patch4_window7_224.pth", - dst="checkpoint.pth" - ) - checkpoint = torch.load("checkpoint.pth", map_location="cpu") + url = "https://github.com/SwinTransformer/storage/releases/download/v1.0.0/swin_small_patch4_window7_224.pth" + checkpoint = load_state_dict_from_url(url=url, + model_dir="/out", + map_location="cpu") model.load_state_dict(checkpoint["model"], strict=False) print('Load the backbone pretrained on ImageNet 1K') else: @@ -1020,11 +1077,10 @@ def swin_base_win7(pretrained=None, **kwargs): if pretrained is not None: if pretrained == 'imagenet': - torch.hub.download_url_to_file( - url="https://github.com/SwinTransformer/storage/releases/download/v1.0.0/swin_base_patch4_window7_224_22k.pth", - dst="checkpoint.pth" - ) - checkpoint = torch.load("checkpoint.pth", map_location="cpu") + url = "https://github.com/SwinTransformer/storage/releases/download/v1.0.0/swin_base_patch4_window7_224_22k.pth" + checkpoint = load_state_dict_from_url(url=url, + model_dir="/out", + map_location="cpu") model.load_state_dict(checkpoint["model"], strict=False) print('Load the backbone pretrained on ImageNet 22K') else: @@ -1043,11 +1099,11 @@ def swin_large_win7(pretrained=None, **kwargs): if pretrained is not None: if pretrained == 'imagenet': - torch.hub.download_url_to_file( - url="https://github.com/SwinTransformer/storage/releases/download/v1.0.0/swin_large_patch4_window7_224_22k.pth", - dst="checkpoint.pth" - ) - checkpoint = torch.load("checkpoint.pth", map_location="cpu") + url = "https://github.com/SwinTransformer/storage/releases/download/v1.0.0/swin_large_patch4_window7_224_22k.pth" + + checkpoint = load_state_dict_from_url(url=url, + model_dir="/out", + map_location="cpu") model.load_state_dict(checkpoint["model"], strict=False) print('Load the backbone pretrained on ImageNet 22K') else: diff --git a/methods/vidt/detector.py b/methods/vidt/detector.py index 9e75010..270bac4 100644 --- a/methods/vidt/detector.py +++ b/methods/vidt/detector.py @@ -394,6 +394,9 @@ def build_teacher(args): if args.dataset_file == 'coco': num_classes = 91 + if args.dataset_file == 'ymir': + num_classes = args.num_classes + if args.dataset_file == "coco_panoptic": num_classes = 250 diff --git a/methods/vidt/postprocessor.py b/methods/vidt/postprocessor.py index d0cd8f0..11a526f 100644 --- a/methods/vidt/postprocessor.py +++ b/methods/vidt/postprocessor.py @@ -36,7 +36,7 @@ def forward(self, outputs, target_sizes, target_boxes=None): assert len(out_logits) == len(target_sizes) assert target_sizes.shape[1] == 2 - if self.dataset_file == 'coco': + if self.dataset_file in ['coco', 'ymir']: prob = out_logits.sigmoid() topk_values, topk_indexes = torch.topk(prob.view(out_logits.shape[0], -1), 100, dim=1) scores = topk_values @@ -75,4 +75,6 @@ def forward(self, outputs, target_sizes, target_boxes=None): true_boxes.append(true_box * scale_fct) return results, true_boxes + else: + raise Exception(f"Invalid dataset_file format {self.dataset_file_format}") diff --git a/methods/vidt_wo_neck/detector.py b/methods/vidt_wo_neck/detector.py index c3b223a..f130a12 100644 --- a/methods/vidt_wo_neck/detector.py +++ b/methods/vidt_wo_neck/detector.py @@ -110,6 +110,9 @@ def build(args): if args.dataset_file == 'coco': num_classes = 91 + if args.dataset_file == 'ymir': + num_classes = args.num_classes + if args.dataset_file == "coco_panoptic": num_classes = 250 device = torch.device(args.device) diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..55206f5 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,4 @@ +[mypy] +allow_untyped_defs = True +warn_unused_configs = True +ignore_missing_imports = True diff --git a/ymir/docker/cuda102.dockerfile b/ymir/docker/cuda102.dockerfile index ff40015..8203773 100644 --- a/ymir/docker/cuda102.dockerfile +++ b/ymir/docker/cuda102.dockerfile @@ -12,4 +12,5 @@ apt-key adv --keyserver keyserver.ubuntu.com --recv-keys A4B469963BF863CC apt update && apt install -y git cd /app/ops && bash make.sh cd /app && pip install -r requirements.txt +pip install "git+https://github.com/yzbx/ymir-executor-sdk.git@ymir1.0.0" EOF diff --git a/ymir/img-man/infer-template.yaml b/ymir/img-man/infer-template.yaml new file mode 100644 index 0000000..30694a1 --- /dev/null +++ b/ymir/img-man/infer-template.yaml @@ -0,0 +1,9 @@ +# infer template for your executor app +# after build image, it should at /img-man/infer-template.yaml +# key: gpu_id, task_id, model_params_path, class_names should be preserved + +# gpu_id: '0' +# task_id: 'default-infer-task' +# model_params_path: [] +# class_names: [] +conf_threshold: 0.2 diff --git a/ymir/img-man/mining-template.yaml b/ymir/img-man/mining-template.yaml new file mode 100644 index 0000000..a82c8f3 --- /dev/null +++ b/ymir/img-man/mining-template.yaml @@ -0,0 +1,10 @@ +# mining template for your executor app +# after build image, it should at /img-man/mining-template.yaml +# key: gpu_id, task_id, model_params_path, class_names should be preserved + +# gpu_id: '0' +# task_id: 'default-mining-task' +# model_params_path: [] +# class_names: [] + +conf_threshold: 0.2 diff --git a/ymir/img-man/training-template.yaml b/ymir/img-man/training-template.yaml new file mode 100644 index 0000000..66d8fbe --- /dev/null +++ b/ymir/img-man/training-template.yaml @@ -0,0 +1,16 @@ +# training template for your executor app +# after build image, it should at /img-man/training-template.yaml +# key: gpu_id, task_id, pretrained_model_params, class_names should be preserved + +# gpu_id: '0' +# task_id: 'default-training-task' +# pretrained_model_params: [] +# class_names: [] + +batch_size: 2 +num_workers: 2 +epochs: 50 +learning_rate: 0.0001 +eval_size: 640 +backbone_name: 'swin_nano' +args_options: '' diff --git a/ymir/readme.md b/ymir/readme.md index f8dddc1..0d3b821 100644 --- a/ymir/readme.md +++ b/ymir/readme.md @@ -4,3 +4,8 @@ ``` onnxruntime 1.12.1 has requirement numpy>=1.21.0 ``` + +## thanks to + +- [detr](https://github.com/facebookresearch/detr) +- [vidt](https://github.com/naver-ai/vidt) diff --git a/ymir/start.py b/ymir/start.py new file mode 100644 index 0000000..ad864ab --- /dev/null +++ b/ymir/start.py @@ -0,0 +1,67 @@ +import logging +import os +import subprocess +import sys + +from ymir_exc import monitor +from ymir_exc.util import get_merged_config, get_ymir_process, YmirStage + + +def start() -> int: + cfg = get_merged_config() + + logging.info(f'merged config: {cfg}') + + if cfg.ymir.run_training: + _run_training() + elif cfg.ymir.run_mining or cfg.ymir.run_infer: + # for multiple tasks, run mining first, infer later. + if cfg.ymir.run_mining and cfg.ymir.run_infer: + task_num = 2 + mining_task_idx = 0 + infer_task_idx = 1 + else: + task_num = 1 + mining_task_idx = 0 + infer_task_idx = 0 + + if cfg.ymir.run_mining: + _run_mining(task_idx=mining_task_idx, task_num=task_num) + if cfg.ymir.run_infer: + _run_infer(task_idx=infer_task_idx, task_num=task_num) + else: + logging.warning('no task running') + + return 0 + + +def _run_training() -> None: + command = 'python3 ymir/ymir_training.py' + logging.info(f'training: {command}') + subprocess.run(command.split(), check=True) + monitor.write_monitor_logger(percent=1.0) + + +def _run_mining(task_idx: int = 0, task_num: int = 1) -> None: + command = f'python3 ymir/ymir_mining.py' + logging.info(f'mining: {command}') + subprocess.run(command.split(), check=True) + monitor.write_monitor_logger(percent=get_ymir_process( + stage=YmirStage.POSTPROCESS, p=1, task_idx=task_idx, task_num=task_num)) + + +def _run_infer(task_idx: int = 0, task_num: int = 1) -> None: + command = f'python3 ymir/ymir_infer.py' + logging.info(f'infer: {command}') + subprocess.run(command.split(), check=True) + monitor.write_monitor_logger(percent=get_ymir_process( + stage=YmirStage.POSTPROCESS, p=1, task_idx=task_idx, task_num=task_num)) + + +if __name__ == '__main__': + logging.basicConfig(stream=sys.stdout, + format='%(levelname)-8s: [%(asctime)s] %(message)s', + datefmt='%Y%m%d-%H:%M:%S', + level=logging.INFO) + + sys.exit(start()) diff --git a/ymir/ymir_training.py b/ymir/ymir_training.py new file mode 100644 index 0000000..c75aeaf --- /dev/null +++ b/ymir/ymir_training.py @@ -0,0 +1,58 @@ +import logging +import sys +from easydict import EasyDict as edict +import os +from ymir_exc.util import get_merged_config +import os.path as osp +import subprocess + +def main(cfg: edict) -> int: + num_gpus = len(cfg.param.gpu_id.split(',')) + models_dir = cfg.ymir.output.models_dir + args_options = cfg.param.get('args_options','') + num_classes = len(cfg.param.class_names) + batch_size = int(cfg.param.batch_size) + eval_size = int(cfg.param.eval_size) + epochs = int(cfg.param.epochs) + num_workers = int(cfg.param.num_workers) + learning_rate = float(cfg.param.learning_rate) + backbone_name = str(cfg.param.backbone_name) + + cmd = f""" + python -m torch.distributed.launch \ + --nproc_per_node {num_gpus} \ + --nnodes 1 \ + --use_env main.py \ + --method vidt \ + --backbone_name {backbone_name} \ + --epochs {epochs} \ + --lr {learning_rate} \ + --min-lr 1e-7 \ + --batch_size {batch_size} \ + --num_workers {num_workers} \ + --eval_size {eval_size} \ + --aux_loss True \ + --with_box_refine True \ + --dataset_file ymir \ + --num_classes {num_classes} \ + --coco_path /in \ + --output_dir {models_dir} + """ + + if args_options: + cmd = cmd + " " + args_options + + logging.info(f"Running command: {cmd}") + subprocess.run(cmd.split(), check=True) + + write_ymir_training_result(last=True) + + return 0 + +if __name__ == '__main__': + cfg = get_merged_config() + os.environ.setdefault('PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION', 'python') + os.environ.setdefault('EVAL_TMP_FILE', osp.join(cfg.ymir.output.models_dir, 'eval_tmp.json')) + os.environ.setdefault('YMIR_MODELS_DIR', cfg.ymir.output.models_dir) + os.environ.setdefault('TENSORBOARD_DIR', cfg.ymir.output.tensorboard_dir) + sys.exit(main(cfg)) From 9e0296ef33daca9e21bc69e6088074d5b9c1ce94 Mon Sep 17 00:00:00 2001 From: youdaoyzbx Date: Mon, 15 Aug 2022 09:22:06 +0800 Subject: [PATCH 19/26] zzz --- arguments.py | 6 +- main.py | 93 ++++++++++++---- ops/setup.py | 14 +-- requirements.txt | 6 +- ymir/docker/cuda102.dockerfile | 38 +++++-- ymir/img-man/training-template.yaml | 1 + ymir/readme.md | 82 ++++++++++++++ ymir/start.py | 2 +- ymir/ymir_infer.py | 161 ++++++++++++++++++++++++++++ ymir/ymir_training.py | 34 +++--- 10 files changed, 382 insertions(+), 55 deletions(-) create mode 100644 ymir/ymir_infer.py diff --git a/arguments.py b/arguments.py index a23cc2a..9fb44c6 100644 --- a/arguments.py +++ b/arguments.py @@ -7,6 +7,7 @@ import argparse + def str2bool(v, bool): if isinstance(v, bool): @@ -77,8 +78,9 @@ def get_args_parser(): parser.add_argument('--remove_difficult', action='store_true') # * Device and Log - parser.add_argument('--output_dir', default='', + parser.add_argument('--output_dir', default='/out/models', help='path where to save, empty for no saving') + parser.add_argument('--tensorboard_dir', default='/out/tensorboard', help='path to save tensorboard logs') parser.add_argument('--device', default='cuda', help='device to use for training / testing') parser.add_argument('--seed', default=42, type=int) @@ -86,6 +88,8 @@ def get_args_parser(): help='start epoch') parser.add_argument('--eval', default=False, type=lambda x: (str(x).lower() == 'true'), help='eval mode') parser.add_argument('--resume', default='', help='resume from checkpoint') + parser.add_argument('--load_from', default='', help='load from checkpoint, not work with --resume') + parser.add_argument('--save_interval', default=100, type=int, help='the interval to save the checkpoint') # * Training setup parser.add_argument('--dist-url', default='tcp://127.0.0.1:3457', type=str, diff --git a/main.py b/main.py index 30f8c54..f127695 100644 --- a/main.py +++ b/main.py @@ -16,7 +16,11 @@ import numpy as np import torch +from tensorboardX import SummaryWriter from torch.utils.data import DataLoader, DistributedSampler +from ymir_exc import monitor +from ymir_exc.util import (YmirStage, get_merged_config, get_ymir_process, + write_ymir_training_result) import datasets import util.misc as utils @@ -29,11 +33,14 @@ def build_distil_model(args): """ build a teacher model """ - assert args.distil_model in ['vidt_nano', 'vidt_tiny', 'vidt_small', 'vidt_base'] + assert args.distil_model in ['vidt_nano', + 'vidt_tiny', 'vidt_small', 'vidt_base'] return build_model(args, is_teacher=True) + def main(args): """ main function to train a ViDT model """ + cfg = get_merged_config() rlimit = resource.getrlimit(resource.RLIMIT_NOFILE) resource.setrlimit(resource.RLIMIT_NOFILE, (4096, rlimit[1])) @@ -41,7 +48,8 @@ def main(args): # Gradient accumulation setup if args.n_iter_to_acc > 1: if args.batch_size % args.n_iter_to_acc != 0: - raise Exception(f"Not supported divisor for acc grade with batch size {args.batch_size} and n_iter_to_acc {args.n_iter_to_acc}") + raise Exception( + f"Not supported divisor for acc grade with batch size {args.batch_size} and n_iter_to_acc {args.n_iter_to_acc}") print("Gradient Accumulation is applied.") print("The batch: ", args.batch_size, "->", int(args.batch_size / args.n_iter_to_acc), @@ -69,12 +77,13 @@ def main(args): # if distil_mode is specified, load a teacher model fron a url or local path teacher_model = None if args.distil_model is not None: - print("Distillation On -- Model:", args.distil_model, "Path:", args.distil_model_path) + print("Distillation On -- Model:", args.distil_model, + "Path:", args.distil_model_path) teacher_model = build_distil_model(args) if 'http' in args.distil_model_path or 'https' in args.distil_model_path: # load from a url - url=args.distil_model_path + url = args.distil_model_path filename = os.path.basename(url) model_dir = '/out' filepath = os.path.join(model_dir, filename) @@ -94,20 +103,24 @@ def main(args): model_without_ddp = model if args.distributed: model = torch.nn.parallel.DistributedDataParallel(model, - device_ids=[args.gpu], + device_ids=[ + args.gpu], find_unused_parameters=True) model_without_ddp = model.module if teacher_model is not None: teacher_model = torch.nn.parallel.DistributedDataParallel(teacher_model, - device_ids=[args.gpu], + device_ids=[ + args.gpu], find_unused_parameters=True) # print parameter info. - n_parameters = sum(p.numel() for p in model.parameters() if p.requires_grad) + n_parameters = sum(p.numel() + for p in model.parameters() if p.requires_grad) print('number of params:', n_parameters) if teacher_model is not None: - n_parameters = sum(p.numel() for p in teacher_model.parameters() if p.requires_grad) + n_parameters = sum(p.numel() + for p in teacher_model.parameters() if p.requires_grad) print('number of params for teacher:', n_parameters) # optimizer setup @@ -132,10 +145,12 @@ def build_optimizer(model, args): ] # print the total number of trainable params. - n_parameters = sum(p.numel() for p in model.parameters() if p.requires_grad) + n_parameters = sum(p.numel() + for p in model.parameters() if p.requires_grad) print('num of total trainable prams:' + str(n_parameters)) - optimizer = torch.optim.AdamW(param_dicts, lr=args.lr, weight_decay=args.weight_decay) + optimizer = torch.optim.AdamW( + param_dicts, lr=args.lr, weight_decay=args.weight_decay) return optimizer # build an optiimzer along with a learning scheduler @@ -143,7 +158,8 @@ def build_optimizer(model, args): lr_scheduler, _ = create_scheduler(args, optimizer) # build data loader - dataset_train, dataset_val = build_dataset(image_set=['train', 'val'], args=args) + dataset_train, dataset_val = build_dataset( + image_set=['train', 'val'], args=args) print("# train:", len(dataset_train), ", # val", len(dataset_val)) # data samplers @@ -171,8 +187,20 @@ def build_optimizer(model, args): output_dir = Path(args.output_dir) + # finetune from a checkpoint + if args.load_from: + if args.resume: + raise Exception("cannot load from and resume at the same time") + + if args.load_from.startswith('https'): + checkpoint = torch.hub.load_state_dict_from_url( + args.load_from, map_location='cpu', check_hash=True) + else: + checkpoint = torch.load(args.load_from, map_location='cpu') + model_without_ddp.load_state_dict(checkpoint['model'], strict=False) + print('load a checkpoint from', args.load_from) # resume from a checkpoint or eval with a checkpoint - if args.resume: + elif args.resume: if args.resume.startswith('https'): checkpoint = torch.hub.load_state_dict_from_url( args.resume, map_location='cpu', check_hash=True) @@ -191,13 +219,15 @@ def build_optimizer(model, args): data_loader_val, base_ds, device) if args.output_dir: - utils.save_on_master(coco_evaluator.coco_eval["bbox"].eval, output_dir / "eval.pth") + utils.save_on_master( + coco_evaluator.coco_eval["bbox"].eval, output_dir / "eval.pth") return print("Start training") start_time = time.time() + if args.output_dir and utils.is_main_process(): + tb_writer = SummaryWriter(args.tensorboard_dir) for epoch in range(args.start_epoch, args.epochs): - # specify the current epoch number for samplers if args.distributed: sampler_train.set_epoch(epoch) @@ -219,7 +249,8 @@ def build_optimizer(model, args): checkpoint_paths = [output_dir / 'checkpoint.pth'] # extra checkpoint before LR drop and every 100 epochs if (epoch + 1) % args.lr_drop == 0 or (epoch + 1) % 100 == 0: - checkpoint_paths.append(output_dir / f'checkpoint{epoch:04}.pth') + checkpoint_paths.append( + output_dir / f'checkpoint{epoch:04}.pth') for checkpoint_path in checkpoint_paths: utils.save_on_master({ 'model': model_without_ddp.state_dict(), @@ -239,6 +270,29 @@ def build_optimizer(model, args): 'epoch': epoch, 'n_parameters': n_parameters} if args.output_dir and utils.is_main_process(): + percent = get_ymir_process(stage=YmirStage.TASK, p=( + epoch - args.start_epoch + 1) / (args.epochs - args.start_epoch + 1)) + monitor.write_monitor_logger(percent=percent) + + map50 = test_stats['coco_eval_bbox'][1] + write_ymir_training_result( + cfg, map50, files=[str(checkpoint_paths[-1])], id=f'epoch{epoch}') + + for tag in ['lr', 'loss', 'class_error', 'loss_ce', 'loss_bbox', 'loss_giou']: + if tag in train_stats: + tb_writer.add_scalar( + tag=f'train/{tag}', scalar_value=train_stats[tag], global_step=epoch) + if tag in test_stats: + tb_writer.add_scalar( + tag=f'test/{tag}', scalar_value=test_stats[tag], global_step=epoch) + for tag, value in zip(['map', 'map50', 'map75', + 'small-map', 'medium-map', 'large-map', + 'mar-1', 'mar-10', 'mar-100', + 'small-mar', 'medium-mar', 'large-mar' + ], test_stats['coco_eval_bbox']): + tb_writer.add_scalar( + tag=f'test/{tag}', scalar_value=value, global_step=epoch) + with (output_dir / "log.txt").open("a") as f: f.write(json.dumps(log_stats) + "\n") @@ -253,6 +307,8 @@ def build_optimizer(model, args): torch.save(coco_evaluator.coco_eval["bbox"].eval, output_dir / "eval" / name) + if args.output_dir and utils.is_main_process(): + tb_writer.close() total_time = time.time() - start_time total_time_str = str(datetime.timedelta(seconds=int(total_time))) print('Training time {}'.format(total_time_str)) @@ -260,7 +316,8 @@ def build_optimizer(model, args): if __name__ == '__main__': - parser = argparse.ArgumentParser('ViDT training and evaluation script', parents=[get_args_parser()]) + parser = argparse.ArgumentParser( + 'ViDT training and evaluation script', parents=[get_args_parser()]) args = parser.parse_args() ''' for testing @@ -289,7 +346,7 @@ def build_optimizer(model, args): args.output_dir += str(args.epochs) + '-' args.output_dir += str(args.batch_size) args.output_dir = args.method + '-' + args.backbone_name.upper() + '-batch-' + \ - str(args.batch_size) + '-epoch-' + str(args.epochs) + str(args.batch_size) + '-epoch-' + str(args.epochs) # make log directories if args.output_dir: @@ -299,5 +356,3 @@ def build_optimizer(model, args): print('log', args.output_dir) main(args) - - diff --git a/ops/setup.py b/ops/setup.py index a0131bc..a084995 100644 --- a/ops/setup.py +++ b/ops/setup.py @@ -6,17 +6,12 @@ # Modified from https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch/tree/pytorch_1.0.0 # ------------------------------------------------------------------------------------------------ -import os import glob +import os import torch - -from torch.utils.cpp_extension import CUDA_HOME -from torch.utils.cpp_extension import CppExtension -from torch.utils.cpp_extension import CUDAExtension - -from setuptools import find_packages -from setuptools import setup +from setuptools import find_packages, setup +from torch.utils.cpp_extension import CUDA_HOME, CppExtension, CUDAExtension requirements = ["torch", "torchvision"] @@ -33,7 +28,8 @@ def get_extensions(): extra_compile_args = {"cxx": []} define_macros = [] - if torch.cuda.is_available() and CUDA_HOME is not None: + # remove torch.cuda.is_available() for docker build + if CUDA_HOME is not None: extension = CUDAExtension sources += source_cuda define_macros += [("WITH_CUDA", None)] diff --git a/requirements.txt b/requirements.txt index 3f24c42..990fbb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ scipy pycocotools cython -onnx -onnxruntime +# onnx +# onnxruntime timm -einops \ No newline at end of file +einops diff --git a/ymir/docker/cuda102.dockerfile b/ymir/docker/cuda102.dockerfile index 8203773..a049d44 100644 --- a/ymir/docker/cuda102.dockerfile +++ b/ymir/docker/cuda102.dockerfile @@ -1,16 +1,34 @@ FROM pytorch/pytorch:1.6.0-cuda10.1-cudnn7-devel -ENV CUDA_HOME=/usr/local/cuda + +ENV TORCH_CUDA_ARCH_LIST="6.0 6.1 7.0+PTX" +ENV TORCH_NVCC_FLAGS="-Xfatbin -compress-all" +ENV CMAKE_PREFIX_PATH="$(dirname $(which conda))/../" +ENV PYTHONPATH=. +ENV FORCE_CUDA="1" +ENV MKL_SERVICE_FORCE_INTEL="1" +ENV MKL_THREADING_LAYER="GNU" + +LABEL pytorch="1.6.0" +LABEL cuda="10.1" +LABEL cudnn="7" + +# To fix GPG key error when running apt-get update +# apt-key adv --keyserver keyserver.ubuntu.com --recv-keys A4B469963BF863CC +RUN apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/3bf863cc.pub +RUN apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu1804/x86_64/7fa2af80.pub + +RUN sed -i 's/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list && \ +pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple && \ +apt update && apt install -y git vim && rm -rf /var/lib/apt/lists/* && \ +mkdir -p /img-man && echo "python3 ymir/start.py" > /usr/bin/start.sh COPY . /app WORKDIR /app -RUN <=1.21.0 ``` +## ymir supports + +### ymir dataset supports +add new option dataset_file=ymir and num_classes for args, modify related files. + +### ymir training supports + +- ymir/ymir_training.py, entry point for ymir training task, +- ymir/img-man/training-template.py hyper-param template +``` +cmd = f""" + python -m torch.distributed.launch \ + --nproc_per_node {num_gpus} \ + --nnodes 1 \ + --use_env main.py \ + --method vidt \ + --backbone_name {backbone_name} \ + --epochs {epochs} \ + --lr {learning_rate} \ + --min-lr 1e-7 \ + --batch_size {batch_size} \ + --num_workers {num_workers} \ + --eval_size {eval_size} \ + --aux_loss True \ + --with_box_refine True \ + --dataset_file ymir \ + --num_classes {num_classes} \ + --coco_path /in \ + --output_dir {models_dir} + """ +``` + +- save weight and map50, monitor process write tensorboard logs + +``` +from tensorboardX import SummaryWriter +from ymir_exc import monitor +from ymir_exc.util import (YmirStage, get_merged_config, get_ymir_process, + write_ymir_training_result) + +# save weight, map and monitor process for main process. +tb_writer = SummaryWriter(args.tensorboard_dir) +for epoch in range(args.start_epoch, args.epochs): + # monitor process + percent = get_ymir_process(stage=YmirStage.TASK, p=(epoch - args.start_epoch + 1) / (args.epochs - args.start_epoch + 1)) + monitor.write_monitor_logger(percent=percent) + + # save weight and map + map50 = test_stats['coco_eval_bbox'][1] + write_ymir_training_result(cfg, map50, files=checkpoint_paths, id=str(epoch)) + + # writer tensorboard logs + tb_writer_add_scaler(tag='train/loss', scalar_value=0.1, global_step=epoch) +``` + +- finetune + +``` +# finetune from offered weight file +weight_files = get_weight_files(cfg, suffix=('.pth')) +if weight_files: + latest_weight_file = max(weight_files, key=osp.getctime) + + # auto finetune if not designed by user. + if args_options.find('--load_from')== -1 and args_options.find('--resume')==-1: + cmd = cmd + " --load_from " + latest_weight_file +logging.info(f"Running command: {cmd}") +subprocess.run(cmd.split(), check=True) + +if args.load_from: + if args.resume: + raise Exception("cannot load from and resume at the same time") + + if args.load_from.startswith('https'): + checkpoint = torch.hub.load_state_dict_from_url( + args.load_from, map_location='cpu', check_hash=True) + else: + checkpoint = torch.load(args.load_from, map_location='cpu') + model_without_ddp.load_state_dict(checkpoint['model'], strict=False) + print('load a checkpoint from', args.load_from) +``` + ## thanks to - [detr](https://github.com/facebookresearch/detr) diff --git a/ymir/start.py b/ymir/start.py index ad864ab..bfb176f 100644 --- a/ymir/start.py +++ b/ymir/start.py @@ -4,7 +4,7 @@ import sys from ymir_exc import monitor -from ymir_exc.util import get_merged_config, get_ymir_process, YmirStage +from ymir_exc.util import YmirStage, get_merged_config, get_ymir_process def start() -> int: diff --git a/ymir/ymir_infer.py b/ymir/ymir_infer.py new file mode 100644 index 0000000..3beaa8f --- /dev/null +++ b/ymir/ymir_infer.py @@ -0,0 +1,161 @@ +""" +ymir infer task entry point +""" +import os.path as osp + +import torch +from easydict import Easydict as edict +from ymir_exc.util import get_weight_files + +from methods.coat_w_ram import coat_lite_mini, coat_lite_small, coat_lite_tiny +from methods.swin_w_ram import (swin_base_win7, swin_large_win7, swin_nano, + swin_small, swin_tiny) + + +def build_model(args): + if args.backbone_name == 'swin_nano': + backbone, hidden_dim = swin_nano(pretrained=False) + elif args.backbone_name == 'swin_tiny': + backbone, hidden_dim = swin_tiny(pretrained=False) + elif args.backbone_name == 'swin_small': + backbone, hidden_dim = swin_small(pretrained=False) + elif args.backbone_name == 'swin_base_win7_22k': + backbone, hidden_dim = swin_base_win7(pretrained=False) + elif args.backbone_name == 'swin_large_win7_22k': + backbone, hidden_dim = swin_large_win7(pretrained=False) + elif args.backbone_name == 'coat_lite_tiny': + backbone, hidden_dim = coat_lite_tiny(pretrained=False) + elif args.backbone_name == 'coat_lite_mini': + backbone, hidden_dim = coat_lite_mini(pretrained=False) + elif args.backbone_name == 'coat_lite_small': + backbone, hidden_dim = coat_lite_small(pretrained=False) + else: + raise ValueError(f'backbone {args.backbone_name} not supported') + + if args.method == 'vidt': + return build_vidt_model(args, backbone) + elif args.method == 'vidt_wo_neck': + return build_vidt_wo_neck_model(args, backbone) + else: + available_methods = ['vidt_wo_neck', 'vidt'] + raise ValueError(f'method {args.method} is not in {available_methods}') + +def build_vidt_model(args, backbone): + from methods.vidt.deformable_transformer import \ + build_deforamble_transformer + from methods.vidt.detector import Detector + from methods.vidt.fpn_fusion import FPNFusionModule + + backbone.finetune_det(method=args.method, + det_token_num=args.det_token_num, + pos_dim=args.reduced_dim, + cross_indices=args.cross_indices) + + cross_scale_fusion = None + if args.cross_scale_fusion: + cross_scale_fusion = FPNFusionModule(backbone.num_channels, fuse_dim=args.reduced_dim) + + deform_transformers = build_deforamble_transformer(args) + + model = Detector( + backbone, + deform_transformers, + num_classes=args.num_classes, + num_queries=args.det_token_num, + # two essential techniques used in ViDT + aux_loss=args.aux_loss, + with_box_refine=args.with_box_refine, + # three additional techniques (optionally) + cross_scale_fusion=cross_scale_fusion, + iou_aware=args.iou_aware, + token_label=args.token_label, + # distil + distil=False if args.distil_model is None else True, + ) + + return model + +def build_vidt_wo_neck_model(args, backbone): + from methods.vidt_wo_neck.detector import Detector + + backbone.finetune_det(method=args.method, + det_token_num=args.det_token_num, + pos_dim=args.pos_dim, + cross_indices=args.cross_indices) + + model = Detector( + backbone, + reduced_dim=args.reduced_dim, + num_classes=args.num_classes, + ) + + return model + +def get_postprocessor(args): + if args.method == 'vidt': + from methods.vidt.postprocessor import PostProcess as postprocess_vidt + return postprocess_vidt(args.dataset_file) + elif args.method == 'vidt_wo_neck': + from methods.vidt_wo_neck.postprocessor import \ + PostProcess as postprocess_vidt_wo_neck + return postprocess_vidt_wo_neck() + else: + available_methods = ['vidt_wo_neck', 'vidt'] + raise ValueError(f'method {args.method} is not in {available_methods}') + +class YmirModel(object): + def __init__(self, cfg: edict): + """model for ymir infer task + + build model and load weight, inference + + Args: + cfg: the ymir merged config, view get_merged_config() + + Raises: + Exception: No weight files specified + """ + self.cfg = cfg + self.init_detector() + + # for multiple tasks, mining first, then infer + if cfg.ymir.run_mining and cfg.ymir.run_infer: + infer_task_idx = 1 + task_num = 2 + else: + infer_task_idx = 0 + task_num = 1 + + self.task_idx = infer_task_idx + self.task_num = task_num + + def init_detector(self): + """ + build model and load weight + """ + weight_files = get_weight_files(self.cfg, suffix=('.pth')) + weight_files = [f for f in weight_files if osp.basename(f).startswith('checkpoint')] + + if len(weight_files) == 0: + raise Exception('No weight files specified') + + latest_weight_file = max(weight_files, key=osp.getctime) + + # save "model", "optimizer", "lr_scheduler", "epoch" and "args" in weight_file + load_data = torch.load(latest_weight_file, map_location='cpu') + + args = load_data['args'] + self.model = build_model(args) + self.model.load_state_dict(load_data['model'], strict=False) + self.device = torch.device(args.device) + self.model.to(self.device) + self.model.eval() + + self.postprocess = get_postprocessor(args) + + def infer(self, img): + sample = img + img_h, img_w = img.shape[0:2] + orig_target_sizes = torch.tensor([[img_h, img_w]]) + outputs = self.model(sample) + results = self.postprocess(outputs, orig_target_sizes) diff --git a/ymir/ymir_training.py b/ymir/ymir_training.py index c75aeaf..361f455 100644 --- a/ymir/ymir_training.py +++ b/ymir/ymir_training.py @@ -1,14 +1,18 @@ import logging -import sys -from easydict import EasyDict as edict import os -from ymir_exc.util import get_merged_config import os.path as osp import subprocess +import sys + +from easydict import EasyDict as edict +from ymir_exc.util import (get_merged_config, get_weight_files, + write_ymir_training_result) + def main(cfg: edict) -> int: num_gpus = len(cfg.param.gpu_id.split(',')) models_dir = cfg.ymir.output.models_dir + tensorboard_dir = cfg.ymir.output.tensorboard_dir args_options = cfg.param.get('args_options','') num_classes = len(cfg.param.class_names) batch_size = int(cfg.param.batch_size) @@ -17,42 +21,48 @@ def main(cfg: edict) -> int: num_workers = int(cfg.param.num_workers) learning_rate = float(cfg.param.learning_rate) backbone_name = str(cfg.param.backbone_name) + weight_save_interval = int(cfg.param.weight_save_interval) cmd = f""" python -m torch.distributed.launch \ --nproc_per_node {num_gpus} \ --nnodes 1 \ --use_env main.py \ - --method vidt \ --backbone_name {backbone_name} \ --epochs {epochs} \ --lr {learning_rate} \ - --min-lr 1e-7 \ --batch_size {batch_size} \ --num_workers {num_workers} \ --eval_size {eval_size} \ - --aux_loss True \ - --with_box_refine True \ --dataset_file ymir \ --num_classes {num_classes} \ --coco_path /in \ - --output_dir {models_dir} + --output_dir {models_dir} \ + --save_interval {weight_save_interval} \ + --tensorboard_dir {tensorboard_dir} """ if args_options: cmd = cmd + " " + args_options + # finetune from offered weight file + weight_files = get_weight_files(cfg, suffix=('.pth')) + weight_files = [f for f in weight_files if osp.basename(f).startswith('checkpoint')] + if weight_files: + latest_weight_file = max(weight_files, key=osp.getctime) + + # auto finetune if not designed by user. + if args_options.find('--load_from')== -1 and args_options.find('--resume')==-1: + cmd = cmd + " --load_from " + latest_weight_file + logging.info(f"Running command: {cmd}") subprocess.run(cmd.split(), check=True) - write_ymir_training_result(last=True) + write_ymir_training_result(cfg, 0, [], 'last') return 0 if __name__ == '__main__': cfg = get_merged_config() os.environ.setdefault('PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION', 'python') - os.environ.setdefault('EVAL_TMP_FILE', osp.join(cfg.ymir.output.models_dir, 'eval_tmp.json')) - os.environ.setdefault('YMIR_MODELS_DIR', cfg.ymir.output.models_dir) - os.environ.setdefault('TENSORBOARD_DIR', cfg.ymir.output.tensorboard_dir) sys.exit(main(cfg)) From 59f53fed8302059b858aae74a65486eae57cd189 Mon Sep 17 00:00:00 2001 From: youdaoyzbx Date: Mon, 15 Aug 2022 17:48:37 +0800 Subject: [PATCH 20/26] tmi --- datasets/transforms.py | 8 +- datasets/voc.py | 4 +- datasets/ymir.py | 2 +- requirements.txt | 2 + ymir/docker/cuda102.dockerfile | 2 +- ymir/readme.md | 4 + ymir/ymir_infer.py | 97 +++++++-- ymir/ymir_mining.py | 378 +++++++++++++++++++++++++++++++++ 8 files changed, 475 insertions(+), 22 deletions(-) create mode 100644 ymir/ymir_mining.py diff --git a/datasets/transforms.py b/datasets/transforms.py index 1477a84..21a453b 100644 --- a/datasets/transforms.py +++ b/datasets/transforms.py @@ -4,6 +4,7 @@ """ import random +import numpy as np import PIL import torch import torchvision.transforms as T @@ -11,7 +12,7 @@ from util.box_ops import box_xyxy_to_cxcywh from util.misc import interpolate -import numpy as np + def crop(image, target, region): cropped_image = F.crop(image, *region) @@ -74,6 +75,9 @@ def hflip(image, target): def resize(image, target, size, max_size=None): + """ + PIL image input + """ # size can be min_size (scalar) or (w, h) tuple # import pdb;pdb.set_trace() maxs = size @@ -142,7 +146,7 @@ def get_size(image_size, size, max_size=None): if "masks" in target: target['masks'] = interpolate( target['masks'][:, None].float(), size, mode="nearest")[:, 0] > 0.5 - + # max_size = max(rescaled_image.size) # maxs = max(size) # padding_size = [maxs-rescaled_image.size[0], maxs-rescaled_image.size[1]] diff --git a/datasets/voc.py b/datasets/voc.py index 5614265..7634ba0 100644 --- a/datasets/voc.py +++ b/datasets/voc.py @@ -126,7 +126,7 @@ def __call__(self, image, target): return image, target -def make_coco_transforms(image_set): +def make_coco_transforms(image_set, args): normalize = T.Compose([ T.ToTensor(), @@ -171,7 +171,7 @@ def make_coco_transforms(image_set): if image_set == 'val': return T.Compose([ - T.RandomResize([512], max_size=800), + T.RandomResize([args.eval_size], max_size=max(args.eval_size, 800)), normalize, ]) diff --git a/datasets/ymir.py b/datasets/ymir.py index 427a9f2..45b85f7 100644 --- a/datasets/ymir.py +++ b/datasets/ymir.py @@ -16,6 +16,6 @@ def build(image_set, args): img_folder = data_info[split]['img_dir'] ann_file = data_info[split]['ann_file'] dataset = CocoDetection(split, img_folder, ann_file, - transforms=make_coco_transforms(split), return_masks=False) + transforms=make_coco_transforms(split, args), return_masks=False) datasets.append(dataset) return datasets diff --git a/requirements.txt b/requirements.txt index 990fbb2..9970147 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,5 @@ cython # onnxruntime timm einops +opencv_python +nptyping diff --git a/ymir/docker/cuda102.dockerfile b/ymir/docker/cuda102.dockerfile index a049d44..c282f86 100644 --- a/ymir/docker/cuda102.dockerfile +++ b/ymir/docker/cuda102.dockerfile @@ -20,7 +20,7 @@ RUN apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/machi RUN sed -i 's/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list && \ pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple && \ -apt update && apt install -y git vim && rm -rf /var/lib/apt/lists/* && \ +apt update && apt install -y git vim libgl1-mesa-glx ffmpeg libsm6 libxext6 ninja-build libglib2.0-0 libsm6 libxrender-dev libxext6 && rm -rf /var/lib/apt/lists/* && \ mkdir -p /img-man && echo "python3 ymir/start.py" > /usr/bin/start.sh COPY . /app diff --git a/ymir/readme.md b/ymir/readme.md index 5d029ef..9af7232 100644 --- a/ymir/readme.md +++ b/ymir/readme.md @@ -87,6 +87,10 @@ if args.load_from: print('load a checkpoint from', args.load_from) ``` +### ymir infer support + + + ## thanks to - [detr](https://github.com/facebookresearch/detr) diff --git a/ymir/ymir_infer.py b/ymir/ymir_infer.py index 3beaa8f..75104dd 100644 --- a/ymir/ymir_infer.py +++ b/ymir/ymir_infer.py @@ -2,11 +2,19 @@ ymir infer task entry point """ import os.path as osp +import sys +from typing import List import torch -from easydict import Easydict as edict -from ymir_exc.util import get_weight_files - +from easydict import EasyDict as edict +from PIL import Image +from ymir_exc import dataset_reader as dr +from ymir_exc import env, monitor +from ymir_exc import result_writer as rw +from ymir_exc.util import (YmirStage, get_merged_config, get_weight_files, + get_ymir_process) + +import datasets.transforms as T from methods.coat_w_ram import coat_lite_mini, coat_lite_small, coat_lite_tiny from methods.swin_w_ram import (swin_base_win7, swin_large_win7, swin_nano, swin_small, swin_tiny) @@ -14,21 +22,21 @@ def build_model(args): if args.backbone_name == 'swin_nano': - backbone, hidden_dim = swin_nano(pretrained=False) + backbone, hidden_dim = swin_nano(pretrained=None) elif args.backbone_name == 'swin_tiny': - backbone, hidden_dim = swin_tiny(pretrained=False) + backbone, hidden_dim = swin_tiny(pretrained=None) elif args.backbone_name == 'swin_small': - backbone, hidden_dim = swin_small(pretrained=False) + backbone, hidden_dim = swin_small(pretrained=None) elif args.backbone_name == 'swin_base_win7_22k': - backbone, hidden_dim = swin_base_win7(pretrained=False) + backbone, hidden_dim = swin_base_win7(pretrained=None) elif args.backbone_name == 'swin_large_win7_22k': - backbone, hidden_dim = swin_large_win7(pretrained=False) + backbone, hidden_dim = swin_large_win7(pretrained=None) elif args.backbone_name == 'coat_lite_tiny': - backbone, hidden_dim = coat_lite_tiny(pretrained=False) + backbone, hidden_dim = coat_lite_tiny(pretrained=None) elif args.backbone_name == 'coat_lite_mini': - backbone, hidden_dim = coat_lite_mini(pretrained=False) + backbone, hidden_dim = coat_lite_mini(pretrained=None) elif args.backbone_name == 'coat_lite_small': - backbone, hidden_dim = coat_lite_small(pretrained=False) + backbone, hidden_dim = coat_lite_small(pretrained=None) else: raise ValueError(f'backbone {args.backbone_name} not supported') @@ -129,6 +137,9 @@ def __init__(self, cfg: edict): self.task_idx = infer_task_idx self.task_num = task_num + self.conf_threshold = self.cfg.param.conf_threshold + self.class_names= self.cfg.param.class_names + def init_detector(self): """ build model and load weight @@ -151,11 +162,65 @@ def init_detector(self): self.model.to(self.device) self.model.eval() + self.preprocess = T.Compose([ + T.RandomResize([512], max_size=800), + T.ToTensor(), + T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + ]) self.postprocess = get_postprocessor(args) - def infer(self, img): - sample = img - img_h, img_w = img.shape[0:2] - orig_target_sizes = torch.tensor([[img_h, img_w]]) - outputs = self.model(sample) + def infer(self, img: Image) -> List[rw.Annotation]: + sample, _ = self.preprocess(img, None) + img_w, img_h = img.size + orig_target_sizes = torch.tensor([[img_h, img_w]], device=self.device) + outputs = self.model([sample.to(self.device)]) + # results = [{'scores': s, 'labels': l, 'boxes': b} for s, l, b in zip(scores, labels, boxes)] results = self.postprocess(outputs, orig_target_sizes) + + anns=[] + # for batch + for r in results: + # for bbox + for conf, cls, (xmin, ymin, xmax, ymax) in zip(r['scores'], r['labels'], r['boxes']): + if conf < self.conf_threshold: + continue + + ann = rw.Annotation(class_name=self.class_names[int(cls)], score=conf, box=rw.Box( + x=int(xmin), y=int(ymin), w=int(xmax - xmin), h=int(ymax - ymin))) + + anns.append(ann) + return anns + +def main() -> int: + cfg = get_merged_config() + model = YmirModel(cfg) + task_idx = model.task_idx + task_num = model.task_num + monitor.write_monitor_logger(percent=get_ymir_process( + stage=YmirStage.PREPROCESS, p=1.0, task_idx=task_idx, task_num=task_num)) + + N = dr.items_count(env.DatasetType.CANDIDATE) + infer_result = {} + + idx = -1 + + monitor_gap = max(1, N // 1000) + for asset_path, _ in dr.item_paths(dataset_type=env.DatasetType.CANDIDATE): + # img = cv2.imread(asset_path) + img = Image.open(asset_path).convert('RGB') + result = model.infer(img) + infer_result[asset_path] = result + idx += 1 + + if idx % monitor_gap == 0: + percent = get_ymir_process( + stage=YmirStage.TASK, p=idx / N, task_idx=task_idx, task_num=task_num) + monitor.write_monitor_logger(percent=percent) + + rw.write_infer_result(infer_result=infer_result) + monitor.write_monitor_logger(percent=get_ymir_process( + stage=YmirStage.PREPROCESS, p=1.0, task_idx=task_idx, task_num=task_num)) + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/ymir/ymir_mining.py b/ymir/ymir_mining.py new file mode 100644 index 0000000..a4a4f6e --- /dev/null +++ b/ymir/ymir_mining.py @@ -0,0 +1,378 @@ +""" +data augmentations for CALD method, including horizontal_flip, rotate(5'), cutout +official code: https://github.com/we1pingyu/CALD/blob/master/cald/cald_helper.py +""" +import random +import sys +from typing import Any, Dict, List, Tuple + +import cv2 +import numpy as np +from easydict import EasyDict as edict +from nptyping import NDArray, Shape, UInt8 +from PIL import Image +from scipy.stats import entropy +from tqdm import tqdm +from ymir_exc import dataset_reader as dr +from ymir_exc import env, monitor +from ymir_exc import result_writer as rw +from ymir_exc.util import YmirStage, get_merged_config, get_ymir_process + +from ymir.ymir_infer import YmirModel + +BBOX = NDArray[Shape['*,4'], Any] +CV_IMAGE = NDArray[Shape['*,*,3'], UInt8] + +def intersect(boxes1: BBOX, boxes2: BBOX) -> NDArray: + ''' + Find intersection of every box combination between two sets of box + boxes1: bounding boxes 1, a tensor of dimensions (n1, 4) + boxes2: bounding boxes 2, a tensor of dimensions (n2, 4) + + Out: Intersection each of boxes1 with respect to each of boxes2, + a tensor of dimensions (n1, n2) + ''' + n1 = boxes1.shape[0] + n2 = boxes2.shape[0] + max_xy = np.minimum(np.expand_dims(boxes1[:, 2:], axis=1).repeat(n2, axis=1), + np.expand_dims(boxes2[:, 2:], axis=0).repeat(n1, axis=0)) + + min_xy = np.maximum(np.expand_dims(boxes1[:, :2], axis=1).repeat(n2, axis=1), + np.expand_dims(boxes2[:, :2], axis=0).repeat(n1, axis=0)) + inter = np.clip(max_xy - min_xy, a_min=0, a_max=None) # (n1, n2, 2) + return inter[:, :, 0] * inter[:, :, 1] # (n1, n2) + + +def horizontal_flip(image: CV_IMAGE, bbox: BBOX) \ + -> Tuple[CV_IMAGE, BBOX]: + """ + image: opencv image, [height,width,channels] + bbox: numpy.ndarray, [N,4] --> [x1,y1,x2,y2] + """ + image = image.copy() + + width = image.shape[1] + # Flip image horizontally + image = image[:, ::-1, :] + if len(bbox) > 0: + bbox = bbox.copy() + # Flip bbox horizontally + bbox[:, [0, 2]] = width - bbox[:, [2, 0]] + return image, bbox + + +def cutout(image: CV_IMAGE, bbox: BBOX, cut_num: int = 2, fill_val: int = 0, + bbox_remove_thres: float = 0.4, bbox_min_thres: float = 0.1) -> Tuple[CV_IMAGE, BBOX]: + ''' + Cutout augmentation + image: A PIL image + boxes: bounding boxes, a tensor of dimensions (#objects, 4) + labels: labels of object, a tensor of dimensions (#objects) + fill_val: Value filled in cut out + bbox_remove_thres: Theshold to remove bbox cut by cutout + + Out: new image, new_boxes, new_labels + ''' + image = image.copy() + bbox = bbox.copy() + + if len(bbox) == 0: + return image, bbox + + original_h, original_w, original_channel = image.shape + count = 0 + for _ in range(50): + # Random cutout size: [0.15, 0.5] of original dimension + cutout_size_h = random.uniform(0.05 * original_h, 0.2 * original_h) + cutout_size_w = random.uniform(0.05 * original_w, 0.2 * original_w) + + # Random position for cutout + left = random.uniform(0, original_w - cutout_size_w) + right = left + cutout_size_w + top = random.uniform(0, original_h - cutout_size_h) + bottom = top + cutout_size_h + cutout = np.array( + [[float(left), float(top), float(right), float(bottom)]]) + + # Calculate intersect between cutout and bounding boxes + overlap_size = intersect(cutout, bbox) + area_boxes = (bbox[:, 2] - bbox[:, 0]) * (bbox[:, 3] - bbox[:, 1]) + ratio = overlap_size / (area_boxes + 1e-14) + # If all boxes have Iou greater than bbox_remove_thres, try again + if ratio.max() > bbox_remove_thres or ratio.max() < bbox_min_thres: + continue + + image[int(top):int(bottom), int(left):int(right), :] = fill_val + count += 1 + if count >= cut_num: + break + return image, bbox + + +def rotate(image: CV_IMAGE, bbox: BBOX, rot: float = 5) -> Tuple[CV_IMAGE, BBOX]: + image = image.copy() + bbox = bbox.copy() + h, w, c = image.shape + center = np.array([w / 2.0, h / 2.0]) + s = max(h, w) * 1.0 + trans = get_affine_transform(center, s, rot, [w, h]) + if len(bbox) > 0: + for i in range(bbox.shape[0]): + x1, y1 = affine_transform(bbox[i, :2], trans) + x2, y2 = affine_transform(bbox[i, 2:], trans) + x3, y3 = affine_transform(bbox[i, [2, 1]], trans) + x4, y4 = affine_transform(bbox[i, [0, 3]], trans) + bbox[i, :2] = [min(x1, x2, x3, x4), min(y1, y2, y3, y4)] + bbox[i, 2:] = [max(x1, x2, x3, x4), max(y1, y2, y3, y4)] + image = cv2.warpAffine(image, trans, (w, h), flags=cv2.INTER_LINEAR) + return image, bbox + + +def get_3rd_point(a: NDArray, b: NDArray) -> NDArray: + direct = a - b + return b + np.array([-direct[1], direct[0]], dtype=np.float32) + + +def get_dir(src_point: NDArray, rot_rad: float) -> List: + sn, cs = np.sin(rot_rad), np.cos(rot_rad) + + src_result = [0, 0] + src_result[0] = src_point[0] * cs - src_point[1] * sn + src_result[1] = src_point[0] * sn + src_point[1] * cs + + return src_result + + +def transform_preds(coords: NDArray, center: NDArray, scale: Any, rot: float, output_size: List) -> NDArray: + trans = get_affine_transform(center, scale, rot, output_size, inv=True) + target_coords = affine_transform(coords, trans) + return target_coords + + +def get_affine_transform(center: NDArray, + scale: Any, + rot: float, + output_size: List, + shift: NDArray = np.array([0, 0], dtype=np.float32), + inv: bool = False) -> NDArray: + if not isinstance(scale, np.ndarray) and not isinstance(scale, list): + scale = np.array([scale, scale], dtype=np.float32) + + scale_tmp = scale + src_w = scale_tmp[0] + dst_w = output_size[0] + dst_h = output_size[1] + + rot_rad = np.pi * rot / 180 + src_dir = get_dir([0, src_w * -0.5], rot_rad) + dst_dir = np.array([0, dst_w * -0.5], np.float32) + + src = np.zeros((3, 2), dtype=np.float32) + dst = np.zeros((3, 2), dtype=np.float32) + src[0, :] = center + scale_tmp * shift + src[1, :] = center + src_dir + scale_tmp * shift + dst[0, :] = [dst_w * 0.5, dst_h * 0.5] + dst[1, :] = np.array([dst_w * 0.5, dst_h * 0.5], np.float32) + dst_dir + + src[2:, :] = get_3rd_point(src[0, :], src[1, :]) + dst[2:, :] = get_3rd_point(dst[0, :], dst[1, :]) + + if inv: + trans = cv2.getAffineTransform(np.float32(dst), np.float32(src)) + else: + trans = cv2.getAffineTransform(np.float32(src), np.float32(dst)) + + return trans + + +def affine_transform(pt: NDArray, t: NDArray) -> NDArray: + new_pt = np.array([pt[0], pt[1], 1.], dtype=np.float32).T + new_pt = np.dot(t, new_pt) + return new_pt[:2] + + +def resize(img: CV_IMAGE, boxes: BBOX, ratio: float = 0.8) -> Tuple[CV_IMAGE, BBOX]: + """ + ratio: <= 1.0 + """ + assert ratio <= 1.0, f'resize ratio {ratio} must <= 1.0' + + h, w, _ = img.shape + ow = int(w * ratio) + oh = int(h * ratio) + resize_img = cv2.resize(img, (ow, oh)) + new_img = np.zeros_like(img) + new_img[:oh, :ow] = resize_img + + if len(boxes) == 0: + return new_img, boxes + else: + return new_img, boxes * ratio + + +def get_ious(boxes1: BBOX, boxes2: BBOX) -> NDArray: + """ + args: + boxes1: np.array, (N, 4), xyxy + boxes2: np.array, (M, 4), xyxy + return: + iou: np.array, (N, M) + """ + area1 = (boxes1[:, 2] - boxes1[:, 0]) * (boxes1[:, 3] - boxes1[:, 1]) + area2 = (boxes2[:, 2] - boxes2[:, 0]) * (boxes2[:, 3] - boxes2[:, 1]) + iner_area = intersect(boxes1, boxes2) + area1 = area1.reshape(-1, 1).repeat(area2.shape[0], axis=1) + area2 = area2.reshape(1, -1).repeat(area1.shape[0], axis=0) + iou = iner_area / (area1 + area2 - iner_area + 1e-14) + return iou + + +def split_result(result: NDArray) -> Tuple[BBOX, NDArray, NDArray]: + if len(result) > 0: + bboxes = result[:, :4].astype(np.int32) + conf = result[:, 4] + class_id = result[:, 5] + else: + bboxes = np.zeros(shape=(0, 4), dtype=np.int32) + conf = np.zeros(shape=(0, 1), dtype=np.float32) + class_id = np.zeros(shape=(0, 1), dtype=np.int32) + + return bboxes, conf, class_id + + +class YmirMining(YmirModel): + def __init__(self, cfg: edict): + super().__init__(cfg) + # for multiple tasks, mining first, then infer + if cfg.ymir.run_mining and cfg.ymir.run_infer: + mining_task_idx = 0 + task_num = 2 + else: + mining_task_idx = 0 + task_num = 1 + self.task_idx = mining_task_idx + self.task_num = task_num + + def mining(self): + N = dr.items_count(env.DatasetType.CANDIDATE) + monitor_gap = max(1, N // 1000) + idx = -1 + beta = 1.3 + mining_result = [] + for asset_path, _ in tqdm(dr.item_paths(dataset_type=env.DatasetType.CANDIDATE)): + # img = cv2.imread(asset_path) + img = cv2.imread(asset_path) + # xyxy,conf,cls + result = self.predict(img) + bboxes, conf, _ = split_result(result) + if len(result) == 0: + # no result for the image without augmentation + mining_result.append((asset_path, -beta)) + continue + + consistency = 0.0 + aug_bboxes_dict, aug_results_dict = self.aug_predict(img, bboxes) + for key in aug_results_dict: + # no result for the image with augmentation f'{key}' + if len(aug_results_dict[key]) == 0: + consistency += beta + continue + + bboxes_key, conf_key, _ = split_result(aug_results_dict[key]) + cls_scores_aug = 1 - conf_key + cls_scores = 1 - conf + + consistency_per_aug = 2.0 + ious = get_ious(bboxes_key, aug_bboxes_dict[key]) + aug_idxs = np.argmax(ious, axis=0) + for origin_idx, aug_idx in enumerate(aug_idxs): + max_iou = ious[aug_idx, origin_idx] + if max_iou == 0: + consistency_per_aug = min(consistency_per_aug, beta) + p = cls_scores_aug[aug_idx] + q = cls_scores[origin_idx] + m = (p + q) / 2. + js = 0.5 * entropy(p, m) + 0.5 * entropy(q, m) + if js < 0: + js = 0 + consistency_box = max_iou + consistency_cls = 0.5 * \ + (conf[origin_idx] + conf_key[aug_idx]) * (1 - js) + consistency_per_inst = abs( + consistency_box + consistency_cls - beta) + consistency_per_aug = min( + consistency_per_aug, consistency_per_inst.item()) + + consistency += consistency_per_aug + + consistency /= len(aug_results_dict) + + mining_result.append((asset_path, consistency)) + idx += 1 + + if idx % monitor_gap == 0: + percent = get_ymir_process( + stage=YmirStage.TASK, p=idx / N, task_idx=self.task_idx, task_num=self.task_num) + monitor.write_monitor_logger(percent=percent) + + return mining_result + + def predict(self, img: CV_IMAGE) -> NDArray: + """ + predict single image and return bbox information + img: opencv BGR, uint8 format + """ + pillow_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) + anns = self.infer(pillow_img) + + xyxy_conf_idx_list = [] + for idx, ann in enumerate(anns): + bbox = ann.box + score = ann.score + x1, y1, x2, y2 = bbox.x, bbox.y, bbox.x + bbox.w, bbox.y + bbox.h + idx = self.class_names.index(ann.class_name) + xyxy_conf_idx_list.append([x1, y1, x2, y2, score, idx]) + + if len(xyxy_conf_idx_list) == 0: + return np.zeros(shape=(0, 6), dtype=np.float32) + else: + return np.array(xyxy_conf_idx_list, dtype=np.float32) + + def aug_predict(self, image: CV_IMAGE, bboxes: BBOX) -> Tuple[Dict[str, BBOX], Dict[str, NDArray]]: + """ + for different augmentation methods: flip, cutout, rotate and resize + augment the image and bbox and use model to predict them. + + return the predict result and augment bbox. + """ + aug_dict = dict(flip=horizontal_flip, + cutout=cutout, + rotate=rotate, + resize=resize) + + aug_bboxes = dict() + aug_results = dict() + for key in aug_dict: + aug_img, aug_bbox = aug_dict[key](image, bboxes) + + aug_result = self.predict(aug_img) + aug_bboxes[key] = aug_bbox + aug_results[key] = aug_result + + return aug_bboxes, aug_results + + +def main(): + cfg = get_merged_config() + miner = YmirMining(cfg) + mining_result = miner.mining() + rw.write_mining_result(mining_result=mining_result) + + percent = get_ymir_process(stage=YmirStage.POSTPROCESS, + p=1, task_idx=miner.task_idx, task_num=miner.task_num) + monitor.write_monitor_logger(percent=percent) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 65e6e333febd613e43be44fdc9880de60a15229d Mon Sep 17 00:00:00 2001 From: youdaoyzbx Date: Tue, 16 Aug 2022 18:41:36 +0800 Subject: [PATCH 21/26] add cuda111.dockerfile --- ymir/docker/cuda102.dockerfile | 6 ++++- ymir/docker/cuda111.dockerfile | 44 ++++++++++++++++++++++++++++++++++ ymir/readme.md | 18 ++++++++++++++ ymir/ymir_training.py | 2 +- 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 ymir/docker/cuda111.dockerfile diff --git a/ymir/docker/cuda102.dockerfile b/ymir/docker/cuda102.dockerfile index c282f86..a944931 100644 --- a/ymir/docker/cuda102.dockerfile +++ b/ymir/docker/cuda102.dockerfile @@ -1,5 +1,8 @@ -FROM pytorch/pytorch:1.6.0-cuda10.1-cudnn7-devel +ARG PYTORCH="1.6.0" +ARG CUDA="10.1" +ARG CUDNN="7" +FROM pytorch/pytorch:${PYTORCH}-cuda${CUDA}-cudnn${CUDNN}-devel ENV TORCH_CUDA_ARCH_LIST="6.0 6.1 7.0+PTX" ENV TORCH_NVCC_FLAGS="-Xfatbin -compress-all" @@ -12,6 +15,7 @@ ENV MKL_THREADING_LAYER="GNU" LABEL pytorch="1.6.0" LABEL cuda="10.1" LABEL cudnn="7" +LABEL ymir="1.1.0" # To fix GPG key error when running apt-get update # apt-key adv --keyserver keyserver.ubuntu.com --recv-keys A4B469963BF863CC diff --git a/ymir/docker/cuda111.dockerfile b/ymir/docker/cuda111.dockerfile new file mode 100644 index 0000000..0ecb238 --- /dev/null +++ b/ymir/docker/cuda111.dockerfile @@ -0,0 +1,44 @@ +ARG PYTORCH="1.8.0" +ARG CUDA="11.1" +ARG CUDNN="8" + +FROM pytorch/pytorch:${PYTORCH}-cuda${CUDA}-cudnn${CUDNN}-devel + + +ENV TORCH_CUDA_ARCH_LIST="6.0 6.1 7.0+PTX" +ENV TORCH_NVCC_FLAGS="-Xfatbin -compress-all" +ENV CMAKE_PREFIX_PATH="$(dirname $(which conda))/../" +ENV PYTHONPATH=. +ENV FORCE_CUDA="1" +ENV MKL_SERVICE_FORCE_INTEL="1" +ENV MKL_THREADING_LAYER="GNU" + +LABEL pytorch="1.8.0" +LABEL cuda="11.1" +LABEL cudnn="8" +LABEL ymir="1.1.0" + + +RUN sed -i 's/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list && \ +pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple + +# To fix GPG key error when running apt-get update +# apt-key adv --keyserver keyserver.ubuntu.com --recv-keys A4B469963BF863CC +RUN apt install -y gnupg2 && \ + apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/3bf863cc.pub && \ + apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu1804/x86_64/7fa2af80.pub + +RUN apt update && apt install -y git vim libgl1-mesa-glx ffmpeg libsm6 libxext6 ninja-build libglib2.0-0 libsm6 libxrender-dev libxext6 && rm -rf /var/lib/apt/lists/* && \ +mkdir -p /img-man && echo "python3 ymir/start.py" > /usr/bin/start.sh + +COPY ./requirements.txt /workspace +RUN pip install -r /workspace/requirements.txt + +COPY . /app +WORKDIR /app + +RUN cd /app/ops && bash make.sh && \ + mv /app/ymir/img-man/*.yaml /img-man && \ + pip install "git+https://github.com/yzbx/ymir-executor-sdk.git@ymir1.0.0" + +CMD bash /usr/bin/start.sh diff --git a/ymir/readme.md b/ymir/readme.md index 9af7232..b2c4c4e 100644 --- a/ymir/readme.md +++ b/ymir/readme.md @@ -89,7 +89,25 @@ if args.load_from: ### ymir infer support +- view `ymir/ymir_infer.py` for detail +- init model (view class `YmirModel.init_detector()`) + - build backbone + - build detector + - load weight file + - preprocess + - postprocess + +- infer (view class `YmirModel.infer()`) + - read image with `PIL.Image` + - obtain `conf_threshold` and `class_names` from `get_merged_config()` + - convert result to ymir format `rw.Anotation` and save infer result with `rw.write_infer_result()` + +### ymir mining support + +- view `ymir/ymir_mining.py` for detail + +- save mining result with `rw.write_mining_result()` ## thanks to diff --git a/ymir/ymir_training.py b/ymir/ymir_training.py index 361f455..30eef74 100644 --- a/ymir/ymir_training.py +++ b/ymir/ymir_training.py @@ -51,7 +51,7 @@ def main(cfg: edict) -> int: if weight_files: latest_weight_file = max(weight_files, key=osp.getctime) - # auto finetune if not designed by user. + # auto finetune if not specified by user. if args_options.find('--load_from')== -1 and args_options.find('--resume')==-1: cmd = cmd + " --load_from " + latest_weight_file From 394c88ec0dbd42c47c268a3b6d3d11b1dd3f3cd9 Mon Sep 17 00:00:00 2001 From: youdaoyzbx Date: Tue, 16 Aug 2022 18:45:32 +0800 Subject: [PATCH 22/26] format with yapf --- ymir/start.py | 7 ++--- ymir/ymir_infer.py | 49 +++++++++++++++++++++++++---------- ymir/ymir_mining.py | 60 +++++++++++++++++++++++++++++-------------- ymir/ymir_training.py | 10 +++++--- 4 files changed, 88 insertions(+), 38 deletions(-) diff --git a/ymir/start.py b/ymir/start.py index bfb176f..f025322 100644 --- a/ymir/start.py +++ b/ymir/start.py @@ -1,5 +1,4 @@ import logging -import os import subprocess import sys @@ -47,7 +46,8 @@ def _run_mining(task_idx: int = 0, task_num: int = 1) -> None: logging.info(f'mining: {command}') subprocess.run(command.split(), check=True) monitor.write_monitor_logger(percent=get_ymir_process( - stage=YmirStage.POSTPROCESS, p=1, task_idx=task_idx, task_num=task_num)) + stage=YmirStage.POSTPROCESS, p=1, task_idx=task_idx, + task_num=task_num)) def _run_infer(task_idx: int = 0, task_num: int = 1) -> None: @@ -55,7 +55,8 @@ def _run_infer(task_idx: int = 0, task_num: int = 1) -> None: logging.info(f'infer: {command}') subprocess.run(command.split(), check=True) monitor.write_monitor_logger(percent=get_ymir_process( - stage=YmirStage.POSTPROCESS, p=1, task_idx=task_idx, task_num=task_num)) + stage=YmirStage.POSTPROCESS, p=1, task_idx=task_idx, + task_num=task_num)) if __name__ == '__main__': diff --git a/ymir/ymir_infer.py b/ymir/ymir_infer.py index 75104dd..dcf28b1 100644 --- a/ymir/ymir_infer.py +++ b/ymir/ymir_infer.py @@ -48,6 +48,7 @@ def build_model(args): available_methods = ['vidt_wo_neck', 'vidt'] raise ValueError(f'method {args.method} is not in {available_methods}') + def build_vidt_model(args, backbone): from methods.vidt.deformable_transformer import \ build_deforamble_transformer @@ -61,7 +62,8 @@ def build_vidt_model(args, backbone): cross_scale_fusion = None if args.cross_scale_fusion: - cross_scale_fusion = FPNFusionModule(backbone.num_channels, fuse_dim=args.reduced_dim) + cross_scale_fusion = FPNFusionModule(backbone.num_channels, + fuse_dim=args.reduced_dim) deform_transformers = build_deforamble_transformer(args) @@ -83,6 +85,7 @@ def build_vidt_model(args, backbone): return model + def build_vidt_wo_neck_model(args, backbone): from methods.vidt_wo_neck.detector import Detector @@ -99,6 +102,7 @@ def build_vidt_wo_neck_model(args, backbone): return model + def get_postprocessor(args): if args.method == 'vidt': from methods.vidt.postprocessor import PostProcess as postprocess_vidt @@ -111,7 +115,9 @@ def get_postprocessor(args): available_methods = ['vidt_wo_neck', 'vidt'] raise ValueError(f'method {args.method} is not in {available_methods}') + class YmirModel(object): + def __init__(self, cfg: edict): """model for ymir infer task @@ -138,14 +144,16 @@ def __init__(self, cfg: edict): self.task_num = task_num self.conf_threshold = self.cfg.param.conf_threshold - self.class_names= self.cfg.param.class_names + self.class_names = self.cfg.param.class_names def init_detector(self): """ build model and load weight """ weight_files = get_weight_files(self.cfg, suffix=('.pth')) - weight_files = [f for f in weight_files if osp.basename(f).startswith('checkpoint')] + weight_files = [ + f for f in weight_files if osp.basename(f).startswith('checkpoint') + ] if len(weight_files) == 0: raise Exception('No weight files specified') @@ -177,27 +185,36 @@ def infer(self, img: Image) -> List[rw.Annotation]: # results = [{'scores': s, 'labels': l, 'boxes': b} for s, l, b in zip(scores, labels, boxes)] results = self.postprocess(outputs, orig_target_sizes) - anns=[] + anns = [] # for batch for r in results: # for bbox - for conf, cls, (xmin, ymin, xmax, ymax) in zip(r['scores'], r['labels'], r['boxes']): + for conf, cls, (xmin, ymin, xmax, + ymax) in zip(r['scores'], r['labels'], r['boxes']): if conf < self.conf_threshold: continue - ann = rw.Annotation(class_name=self.class_names[int(cls)], score=conf, box=rw.Box( - x=int(xmin), y=int(ymin), w=int(xmax - xmin), h=int(ymax - ymin))) + ann = rw.Annotation(class_name=self.class_names[int(cls)], + score=conf, + box=rw.Box(x=int(xmin), + y=int(ymin), + w=int(xmax - xmin), + h=int(ymax - ymin))) anns.append(ann) return anns + def main() -> int: cfg = get_merged_config() model = YmirModel(cfg) task_idx = model.task_idx task_num = model.task_num - monitor.write_monitor_logger(percent=get_ymir_process( - stage=YmirStage.PREPROCESS, p=1.0, task_idx=task_idx, task_num=task_num)) + monitor.write_monitor_logger( + percent=get_ymir_process(stage=YmirStage.PREPROCESS, + p=1.0, + task_idx=task_idx, + task_num=task_num)) N = dr.items_count(env.DatasetType.CANDIDATE) infer_result = {} @@ -213,14 +230,20 @@ def main() -> int: idx += 1 if idx % monitor_gap == 0: - percent = get_ymir_process( - stage=YmirStage.TASK, p=idx / N, task_idx=task_idx, task_num=task_num) + percent = get_ymir_process(stage=YmirStage.TASK, + p=idx / N, + task_idx=task_idx, + task_num=task_num) monitor.write_monitor_logger(percent=percent) rw.write_infer_result(infer_result=infer_result) - monitor.write_monitor_logger(percent=get_ymir_process( - stage=YmirStage.PREPROCESS, p=1.0, task_idx=task_idx, task_num=task_num)) + monitor.write_monitor_logger( + percent=get_ymir_process(stage=YmirStage.PREPROCESS, + p=1.0, + task_idx=task_idx, + task_num=task_num)) return 0 + if __name__ == '__main__': sys.exit(main()) diff --git a/ymir/ymir_mining.py b/ymir/ymir_mining.py index a4a4f6e..71f3f55 100644 --- a/ymir/ymir_mining.py +++ b/ymir/ymir_mining.py @@ -23,6 +23,7 @@ BBOX = NDArray[Shape['*,4'], Any] CV_IMAGE = NDArray[Shape['*,*,3'], UInt8] + def intersect(boxes1: BBOX, boxes2: BBOX) -> NDArray: ''' Find intersection of every box combination between two sets of box @@ -34,11 +35,13 @@ def intersect(boxes1: BBOX, boxes2: BBOX) -> NDArray: ''' n1 = boxes1.shape[0] n2 = boxes2.shape[0] - max_xy = np.minimum(np.expand_dims(boxes1[:, 2:], axis=1).repeat(n2, axis=1), - np.expand_dims(boxes2[:, 2:], axis=0).repeat(n1, axis=0)) + max_xy = np.minimum( + np.expand_dims(boxes1[:, 2:], axis=1).repeat(n2, axis=1), + np.expand_dims(boxes2[:, 2:], axis=0).repeat(n1, axis=0)) - min_xy = np.maximum(np.expand_dims(boxes1[:, :2], axis=1).repeat(n2, axis=1), - np.expand_dims(boxes2[:, :2], axis=0).repeat(n1, axis=0)) + min_xy = np.maximum( + np.expand_dims(boxes1[:, :2], axis=1).repeat(n2, axis=1), + np.expand_dims(boxes2[:, :2], axis=0).repeat(n1, axis=0)) inter = np.clip(max_xy - min_xy, a_min=0, a_max=None) # (n1, n2, 2) return inter[:, :, 0] * inter[:, :, 1] # (n1, n2) @@ -61,8 +64,12 @@ def horizontal_flip(image: CV_IMAGE, bbox: BBOX) \ return image, bbox -def cutout(image: CV_IMAGE, bbox: BBOX, cut_num: int = 2, fill_val: int = 0, - bbox_remove_thres: float = 0.4, bbox_min_thres: float = 0.1) -> Tuple[CV_IMAGE, BBOX]: +def cutout(image: CV_IMAGE, + bbox: BBOX, + cut_num: int = 2, + fill_val: int = 0, + bbox_remove_thres: float = 0.4, + bbox_min_thres: float = 0.1) -> Tuple[CV_IMAGE, BBOX]: ''' Cutout augmentation image: A PIL image @@ -92,7 +99,9 @@ def cutout(image: CV_IMAGE, bbox: BBOX, cut_num: int = 2, fill_val: int = 0, top = random.uniform(0, original_h - cutout_size_h) bottom = top + cutout_size_h cutout = np.array( - [[float(left), float(top), float(right), float(bottom)]]) + [[float(left), + float(top), float(right), + float(bottom)]]) # Calculate intersect between cutout and bounding boxes overlap_size = intersect(cutout, bbox) @@ -109,7 +118,9 @@ def cutout(image: CV_IMAGE, bbox: BBOX, cut_num: int = 2, fill_val: int = 0, return image, bbox -def rotate(image: CV_IMAGE, bbox: BBOX, rot: float = 5) -> Tuple[CV_IMAGE, BBOX]: +def rotate(image: CV_IMAGE, + bbox: BBOX, + rot: float = 5) -> Tuple[CV_IMAGE, BBOX]: image = image.copy() bbox = bbox.copy() h, w, c = image.shape @@ -143,7 +154,8 @@ def get_dir(src_point: NDArray, rot_rad: float) -> List: return src_result -def transform_preds(coords: NDArray, center: NDArray, scale: Any, rot: float, output_size: List) -> NDArray: +def transform_preds(coords: NDArray, center: NDArray, scale: Any, rot: float, + output_size: List) -> NDArray: trans = get_affine_transform(center, scale, rot, output_size, inv=True) target_coords = affine_transform(coords, trans) return target_coords @@ -191,7 +203,9 @@ def affine_transform(pt: NDArray, t: NDArray) -> NDArray: return new_pt[:2] -def resize(img: CV_IMAGE, boxes: BBOX, ratio: float = 0.8) -> Tuple[CV_IMAGE, BBOX]: +def resize(img: CV_IMAGE, + boxes: BBOX, + ratio: float = 0.8) -> Tuple[CV_IMAGE, BBOX]: """ ratio: <= 1.0 """ @@ -241,6 +255,7 @@ def split_result(result: NDArray) -> Tuple[BBOX, NDArray, NDArray]: class YmirMining(YmirModel): + def __init__(self, cfg: edict): super().__init__(cfg) # for multiple tasks, mining first, then infer @@ -259,7 +274,8 @@ def mining(self): idx = -1 beta = 1.3 mining_result = [] - for asset_path, _ in tqdm(dr.item_paths(dataset_type=env.DatasetType.CANDIDATE)): + for asset_path, _ in tqdm( + dr.item_paths(dataset_type=env.DatasetType.CANDIDATE)): # img = cv2.imread(asset_path) img = cv2.imread(asset_path) # xyxy,conf,cls @@ -298,10 +314,10 @@ def mining(self): consistency_box = max_iou consistency_cls = 0.5 * \ (conf[origin_idx] + conf_key[aug_idx]) * (1 - js) - consistency_per_inst = abs( - consistency_box + consistency_cls - beta) - consistency_per_aug = min( - consistency_per_aug, consistency_per_inst.item()) + consistency_per_inst = abs(consistency_box + + consistency_cls - beta) + consistency_per_aug = min(consistency_per_aug, + consistency_per_inst.item()) consistency += consistency_per_aug @@ -311,8 +327,10 @@ def mining(self): idx += 1 if idx % monitor_gap == 0: - percent = get_ymir_process( - stage=YmirStage.TASK, p=idx / N, task_idx=self.task_idx, task_num=self.task_num) + percent = get_ymir_process(stage=YmirStage.TASK, + p=idx / N, + task_idx=self.task_idx, + task_num=self.task_num) monitor.write_monitor_logger(percent=percent) return mining_result @@ -338,7 +356,9 @@ def predict(self, img: CV_IMAGE) -> NDArray: else: return np.array(xyxy_conf_idx_list, dtype=np.float32) - def aug_predict(self, image: CV_IMAGE, bboxes: BBOX) -> Tuple[Dict[str, BBOX], Dict[str, NDArray]]: + def aug_predict( + self, image: CV_IMAGE, + bboxes: BBOX) -> Tuple[Dict[str, BBOX], Dict[str, NDArray]]: """ for different augmentation methods: flip, cutout, rotate and resize augment the image and bbox and use model to predict them. @@ -369,7 +389,9 @@ def main(): rw.write_mining_result(mining_result=mining_result) percent = get_ymir_process(stage=YmirStage.POSTPROCESS, - p=1, task_idx=miner.task_idx, task_num=miner.task_num) + p=1, + task_idx=miner.task_idx, + task_num=miner.task_num) monitor.write_monitor_logger(percent=percent) return 0 diff --git a/ymir/ymir_training.py b/ymir/ymir_training.py index 30eef74..75e9a4e 100644 --- a/ymir/ymir_training.py +++ b/ymir/ymir_training.py @@ -13,7 +13,7 @@ def main(cfg: edict) -> int: num_gpus = len(cfg.param.gpu_id.split(',')) models_dir = cfg.ymir.output.models_dir tensorboard_dir = cfg.ymir.output.tensorboard_dir - args_options = cfg.param.get('args_options','') + args_options = cfg.param.get('args_options', '') num_classes = len(cfg.param.class_names) batch_size = int(cfg.param.batch_size) eval_size = int(cfg.param.eval_size) @@ -47,12 +47,15 @@ def main(cfg: edict) -> int: # finetune from offered weight file weight_files = get_weight_files(cfg, suffix=('.pth')) - weight_files = [f for f in weight_files if osp.basename(f).startswith('checkpoint')] + weight_files = [ + f for f in weight_files if osp.basename(f).startswith('checkpoint') + ] if weight_files: latest_weight_file = max(weight_files, key=osp.getctime) # auto finetune if not specified by user. - if args_options.find('--load_from')== -1 and args_options.find('--resume')==-1: + if args_options.find('--load_from') == -1 and args_options.find( + '--resume') == -1: cmd = cmd + " --load_from " + latest_weight_file logging.info(f"Running command: {cmd}") @@ -62,6 +65,7 @@ def main(cfg: edict) -> int: return 0 + if __name__ == '__main__': cfg = get_merged_config() os.environ.setdefault('PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION', 'python') From 250ba63f25ff64b42256933310385738517ab518 Mon Sep 17 00:00:00 2001 From: youdaoyzbx Date: Thu, 18 Aug 2022 11:06:22 +0800 Subject: [PATCH 23/26] reformat --- .flake8 | 24 ++++++++++++++++++++++++ .style.yapf | 3 +++ mypy.ini | 11 +++++++++++ ymir/start.py | 4 ++-- ymir/ymir_mining.py | 9 +++------ ymir/ymir_training.py | 2 +- 6 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 .flake8 create mode 100644 .style.yapf diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..65a07cd --- /dev/null +++ b/.flake8 @@ -0,0 +1,24 @@ +[flake8] +ignore = + # 'toml' imported but unused + # F401, + # closing bracket does not match visual indentation + # E124, + # continuation line over-indented for hanging indent + # E126, + # visually indented line with same indent as next logical line, + # E129, + # line break before binary operator + # W503, + # line break after binary operator + W504, + # camelcase 'PostProcess' imported as lowercase 'postprocess_vidt' + N813, + # lowercase 'datasets.transforms' imported as non lowercase 'T' + N812, + # N806 variable 'N' in function should be lowercase + N806 + +disable-noqa +max-line-length = 121 +exclude = methods/**, datasets/*, util/*, ops/*, arguments.py, engine.py, fps_calculator.py, main.py diff --git a/.style.yapf b/.style.yapf new file mode 100644 index 0000000..c9a88d5 --- /dev/null +++ b/.style.yapf @@ -0,0 +1,3 @@ +[style] +based_on_style = pep8 +column_limit = 120 diff --git a/mypy.ini b/mypy.ini index 55206f5..1e256fc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2,3 +2,14 @@ allow_untyped_defs = True warn_unused_configs = True ignore_missing_imports = True + +exclude = (?x)( + ^methods + | ^ops + | ^util + | ^datasets + | main.py + | engine.py + | arguments.py + | fps_calculator.py + ) diff --git a/ymir/start.py b/ymir/start.py index f025322..449aa4e 100644 --- a/ymir/start.py +++ b/ymir/start.py @@ -42,7 +42,7 @@ def _run_training() -> None: def _run_mining(task_idx: int = 0, task_num: int = 1) -> None: - command = f'python3 ymir/ymir_mining.py' + command = 'python3 ymir/ymir_mining.py' logging.info(f'mining: {command}') subprocess.run(command.split(), check=True) monitor.write_monitor_logger(percent=get_ymir_process( @@ -51,7 +51,7 @@ def _run_mining(task_idx: int = 0, task_num: int = 1) -> None: def _run_infer(task_idx: int = 0, task_num: int = 1) -> None: - command = f'python3 ymir/ymir_infer.py' + command = 'python3 ymir/ymir_infer.py' logging.info(f'infer: {command}') subprocess.run(command.split(), check=True) monitor.write_monitor_logger(percent=get_ymir_process( diff --git a/ymir/ymir_mining.py b/ymir/ymir_mining.py index 71f3f55..2224740 100644 --- a/ymir/ymir_mining.py +++ b/ymir/ymir_mining.py @@ -4,7 +4,7 @@ """ import random import sys -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Tuple, Callable import cv2 import numpy as np @@ -176,7 +176,7 @@ def get_affine_transform(center: NDArray, dst_h = output_size[1] rot_rad = np.pi * rot / 180 - src_dir = get_dir([0, src_w * -0.5], rot_rad) + src_dir = get_dir(np.array([0, src_w * -0.5], np.float32), rot_rad) dst_dir = np.array([0, dst_w * -0.5], np.float32) src = np.zeros((3, 2), dtype=np.float32) @@ -365,10 +365,7 @@ def aug_predict( return the predict result and augment bbox. """ - aug_dict = dict(flip=horizontal_flip, - cutout=cutout, - rotate=rotate, - resize=resize) + aug_dict: Dict[str, Callable] = dict(flip=horizontal_flip, cutout=cutout, rotate=rotate, resize=resize) aug_bboxes = dict() aug_results = dict() diff --git a/ymir/ymir_training.py b/ymir/ymir_training.py index 75e9a4e..d2a6d45 100644 --- a/ymir/ymir_training.py +++ b/ymir/ymir_training.py @@ -61,7 +61,7 @@ def main(cfg: edict) -> int: logging.info(f"Running command: {cmd}") subprocess.run(cmd.split(), check=True) - write_ymir_training_result(cfg, 0, [], 'last') + write_ymir_training_result(cfg, map50=0, files=[], id='last') return 0 From 92351515491fd17cd24bf318806e3928a792e0d7 Mon Sep 17 00:00:00 2001 From: youdaoyzbx Date: Mon, 22 Aug 2022 13:56:59 +0800 Subject: [PATCH 24/26] format ++ --- .flake8 | 2 ++ ymir/docker/cuda111.dockerfile | 1 - ymir/ymir_mining.py | 36 ++++++++++------------------------ ymir/ymir_training.py | 3 +-- 4 files changed, 13 insertions(+), 29 deletions(-) diff --git a/.flake8 b/.flake8 index 65a07cd..92bd47c 100644 --- a/.flake8 +++ b/.flake8 @@ -18,6 +18,8 @@ ignore = N812, # N806 variable 'N' in function should be lowercase N806 + N803 + N813 disable-noqa max-line-length = 121 diff --git a/ymir/docker/cuda111.dockerfile b/ymir/docker/cuda111.dockerfile index 0ecb238..b8b7dc2 100644 --- a/ymir/docker/cuda111.dockerfile +++ b/ymir/docker/cuda111.dockerfile @@ -4,7 +4,6 @@ ARG CUDNN="8" FROM pytorch/pytorch:${PYTORCH}-cuda${CUDA}-cudnn${CUDNN}-devel - ENV TORCH_CUDA_ARCH_LIST="6.0 6.1 7.0+PTX" ENV TORCH_NVCC_FLAGS="-Xfatbin -compress-all" ENV CMAKE_PREFIX_PATH="$(dirname $(which conda))/../" diff --git a/ymir/ymir_mining.py b/ymir/ymir_mining.py index 2224740..ac1def5 100644 --- a/ymir/ymir_mining.py +++ b/ymir/ymir_mining.py @@ -4,7 +4,7 @@ """ import random import sys -from typing import Any, Dict, List, Tuple, Callable +from typing import Any, Callable, Dict, List, Tuple import cv2 import numpy as np @@ -98,10 +98,7 @@ def cutout(image: CV_IMAGE, right = left + cutout_size_w top = random.uniform(0, original_h - cutout_size_h) bottom = top + cutout_size_h - cutout = np.array( - [[float(left), - float(top), float(right), - float(bottom)]]) + cutout = np.array([[float(left), float(top), float(right), float(bottom)]]) # Calculate intersect between cutout and bounding boxes overlap_size = intersect(cutout, bbox) @@ -118,9 +115,7 @@ def cutout(image: CV_IMAGE, return image, bbox -def rotate(image: CV_IMAGE, - bbox: BBOX, - rot: float = 5) -> Tuple[CV_IMAGE, BBOX]: +def rotate(image: CV_IMAGE, bbox: BBOX, rot: float = 5) -> Tuple[CV_IMAGE, BBOX]: image = image.copy() bbox = bbox.copy() h, w, c = image.shape @@ -154,8 +149,7 @@ def get_dir(src_point: NDArray, rot_rad: float) -> List: return src_result -def transform_preds(coords: NDArray, center: NDArray, scale: Any, rot: float, - output_size: List) -> NDArray: +def transform_preds(coords: NDArray, center: NDArray, scale: Any, rot: float, output_size: List) -> NDArray: trans = get_affine_transform(center, scale, rot, output_size, inv=True) target_coords = affine_transform(coords, trans) return target_coords @@ -203,9 +197,7 @@ def affine_transform(pt: NDArray, t: NDArray) -> NDArray: return new_pt[:2] -def resize(img: CV_IMAGE, - boxes: BBOX, - ratio: float = 0.8) -> Tuple[CV_IMAGE, BBOX]: +def resize(img: CV_IMAGE, boxes: BBOX, ratio: float = 0.8) -> Tuple[CV_IMAGE, BBOX]: """ ratio: <= 1.0 """ @@ -274,8 +266,7 @@ def mining(self): idx = -1 beta = 1.3 mining_result = [] - for asset_path, _ in tqdm( - dr.item_paths(dataset_type=env.DatasetType.CANDIDATE)): + for asset_path, _ in tqdm(dr.item_paths(dataset_type=env.DatasetType.CANDIDATE)): # img = cv2.imread(asset_path) img = cv2.imread(asset_path) # xyxy,conf,cls @@ -314,10 +305,8 @@ def mining(self): consistency_box = max_iou consistency_cls = 0.5 * \ (conf[origin_idx] + conf_key[aug_idx]) * (1 - js) - consistency_per_inst = abs(consistency_box + - consistency_cls - beta) - consistency_per_aug = min(consistency_per_aug, - consistency_per_inst.item()) + consistency_per_inst = abs(consistency_box + consistency_cls - beta) + consistency_per_aug = min(consistency_per_aug, consistency_per_inst.item()) consistency += consistency_per_aug @@ -356,9 +345,7 @@ def predict(self, img: CV_IMAGE) -> NDArray: else: return np.array(xyxy_conf_idx_list, dtype=np.float32) - def aug_predict( - self, image: CV_IMAGE, - bboxes: BBOX) -> Tuple[Dict[str, BBOX], Dict[str, NDArray]]: + def aug_predict(self, image: CV_IMAGE, bboxes: BBOX) -> Tuple[Dict[str, BBOX], Dict[str, NDArray]]: """ for different augmentation methods: flip, cutout, rotate and resize augment the image and bbox and use model to predict them. @@ -385,10 +372,7 @@ def main(): mining_result = miner.mining() rw.write_mining_result(mining_result=mining_result) - percent = get_ymir_process(stage=YmirStage.POSTPROCESS, - p=1, - task_idx=miner.task_idx, - task_num=miner.task_num) + percent = get_ymir_process(stage=YmirStage.POSTPROCESS, p=1, task_idx=miner.task_idx, task_num=miner.task_num) monitor.write_monitor_logger(percent=percent) return 0 diff --git a/ymir/ymir_training.py b/ymir/ymir_training.py index d2a6d45..965d49b 100644 --- a/ymir/ymir_training.py +++ b/ymir/ymir_training.py @@ -5,8 +5,7 @@ import sys from easydict import EasyDict as edict -from ymir_exc.util import (get_merged_config, get_weight_files, - write_ymir_training_result) +from ymir_exc.util import get_merged_config, get_weight_files, write_ymir_training_result def main(cfg: edict) -> int: From f7e327bb3a301d1e1bab0faff37be4595254b9b8 Mon Sep 17 00:00:00 2001 From: youdaoyzbx Date: Fri, 21 Oct 2022 15:35:07 +0800 Subject: [PATCH 25/26] update entropy --- ymir/ymir_infer.py | 17 ++++++----------- ymir/ymir_mining.py | 7 +++---- ymir/ymir_training.py | 3 +-- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/ymir/ymir_infer.py b/ymir/ymir_infer.py index dcf28b1..818ff90 100644 --- a/ymir/ymir_infer.py +++ b/ymir/ymir_infer.py @@ -5,19 +5,16 @@ import sys from typing import List +import datasets.transforms as T import torch from easydict import EasyDict as edict +from methods.coat_w_ram import coat_lite_mini, coat_lite_small, coat_lite_tiny +from methods.swin_w_ram import swin_base_win7, swin_large_win7, swin_nano, swin_small, swin_tiny from PIL import Image from ymir_exc import dataset_reader as dr from ymir_exc import env, monitor from ymir_exc import result_writer as rw -from ymir_exc.util import (YmirStage, get_merged_config, get_weight_files, - get_ymir_process) - -import datasets.transforms as T -from methods.coat_w_ram import coat_lite_mini, coat_lite_small, coat_lite_tiny -from methods.swin_w_ram import (swin_base_win7, swin_large_win7, swin_nano, - swin_small, swin_tiny) +from ymir_exc.util import YmirStage, get_merged_config, get_weight_files, get_ymir_process def build_model(args): @@ -50,8 +47,7 @@ def build_model(args): def build_vidt_model(args, backbone): - from methods.vidt.deformable_transformer import \ - build_deforamble_transformer + from methods.vidt.deformable_transformer import build_deforamble_transformer from methods.vidt.detector import Detector from methods.vidt.fpn_fusion import FPNFusionModule @@ -108,8 +104,7 @@ def get_postprocessor(args): from methods.vidt.postprocessor import PostProcess as postprocess_vidt return postprocess_vidt(args.dataset_file) elif args.method == 'vidt_wo_neck': - from methods.vidt_wo_neck.postprocessor import \ - PostProcess as postprocess_vidt_wo_neck + from methods.vidt_wo_neck.postprocessor import PostProcess as postprocess_vidt_wo_neck return postprocess_vidt_wo_neck() else: available_methods = ['vidt_wo_neck', 'vidt'] diff --git a/ymir/ymir_mining.py b/ymir/ymir_mining.py index 2224740..7b22a35 100644 --- a/ymir/ymir_mining.py +++ b/ymir/ymir_mining.py @@ -4,7 +4,7 @@ """ import random import sys -from typing import Any, Dict, List, Tuple, Callable +from typing import Any, Callable, Dict, List, Tuple import cv2 import numpy as np @@ -13,13 +13,12 @@ from PIL import Image from scipy.stats import entropy from tqdm import tqdm +from ymir.ymir_infer import YmirModel from ymir_exc import dataset_reader as dr from ymir_exc import env, monitor from ymir_exc import result_writer as rw from ymir_exc.util import YmirStage, get_merged_config, get_ymir_process -from ymir.ymir_infer import YmirModel - BBOX = NDArray[Shape['*,4'], Any] CV_IMAGE = NDArray[Shape['*,*,3'], UInt8] @@ -308,7 +307,7 @@ def mining(self): p = cls_scores_aug[aug_idx] q = cls_scores[origin_idx] m = (p + q) / 2. - js = 0.5 * entropy(p, m) + 0.5 * entropy(q, m) + js = 0.5 * entropy([p, 1 - p], [m, 1 - m]) + 0.5 * entropy([q, 1 - q], [m, 1 - m]) if js < 0: js = 0 consistency_box = max_iou diff --git a/ymir/ymir_training.py b/ymir/ymir_training.py index d2a6d45..965d49b 100644 --- a/ymir/ymir_training.py +++ b/ymir/ymir_training.py @@ -5,8 +5,7 @@ import sys from easydict import EasyDict as edict -from ymir_exc.util import (get_merged_config, get_weight_files, - write_ymir_training_result) +from ymir_exc.util import get_merged_config, get_weight_files, write_ymir_training_result def main(cfg: edict) -> int: From 909e08cc095c2cdfc369c5dca22ddf3ef5091715 Mon Sep 17 00:00:00 2001 From: youdaoyzbx Date: Tue, 25 Oct 2022 19:04:17 +0800 Subject: [PATCH 26/26] update vidt to ymir2.0.0 --- main.py | 99 +++++++++++++++-------------- ymir/docker/cuda102.dockerfile | 2 +- ymir/docker/cuda111.dockerfile | 2 +- ymir/img-man/training-template.yaml | 5 +- ymir/start.py | 27 +++----- ymir/ymir_infer.py | 35 ++-------- ymir/ymir_mining.py | 14 ++-- ymir/ymir_training.py | 5 +- 8 files changed, 76 insertions(+), 113 deletions(-) diff --git a/main.py b/main.py index aaca371..dd39a69 100644 --- a/main.py +++ b/main.py @@ -19,8 +19,7 @@ from tensorboardX import SummaryWriter from torch.utils.data import DataLoader, DistributedSampler from ymir_exc import monitor -from ymir_exc.util import (YmirStage, get_merged_config, get_ymir_process, - write_ymir_training_result) +from ymir_exc.util import (YmirStage, get_merged_config, write_ymir_training_result, write_ymir_monitor_process) import datasets import util.misc as utils @@ -33,8 +32,7 @@ def build_distil_model(args): """ build a teacher model """ - assert args.distil_model in ['vidt_nano', - 'vidt_tiny', 'vidt_small', 'vidt_base'] + assert args.distil_model in ['vidt_nano', 'vidt_tiny', 'vidt_small', 'vidt_base'] return build_model(args, is_teacher=True) @@ -49,11 +47,12 @@ def main(args): if args.n_iter_to_acc > 1: if args.batch_size % args.n_iter_to_acc != 0: raise Exception( - f"Not supported divisor for acc grade with batch size {args.batch_size} and n_iter_to_acc {args.n_iter_to_acc}") + f"Not supported divisor for acc grade with batch size {args.batch_size} and n_iter_to_acc {args.n_iter_to_acc}" + ) print("Gradient Accumulation is applied.") - print("The batch: ", args.batch_size, "->", int(args.batch_size / args.n_iter_to_acc), - 'but updated every ', args.n_iter_to_acc, 'steps.') + print("The batch: ", args.batch_size, "->", int(args.batch_size / args.n_iter_to_acc), 'but updated every ', + args.n_iter_to_acc, 'steps.') args.batch_size = args.batch_size // args.n_iter_to_acc ## @@ -77,8 +76,7 @@ def main(args): # if distil_mode is specified, load a teacher model fron a url or local path teacher_model = None if args.distil_model is not None: - print("Distillation On -- Model:", args.distil_model, - "Path:", args.distil_model_path) + print("Distillation On -- Model:", args.distil_model, "Path:", args.distil_model_path) teacher_model = build_distil_model(args) if 'http' in args.distil_model_path or 'https' in args.distil_model_path: @@ -102,25 +100,19 @@ def main(args): # parallel model setup model_without_ddp = model if args.distributed: - model = torch.nn.parallel.DistributedDataParallel(model, - device_ids=[ - args.gpu], - find_unused_parameters=True) + model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.gpu], find_unused_parameters=True) model_without_ddp = model.module if teacher_model is not None: teacher_model = torch.nn.parallel.DistributedDataParallel(teacher_model, - device_ids=[ - args.gpu], + device_ids=[args.gpu], find_unused_parameters=True) # print parameter info. - n_parameters = sum(p.numel() - for p in model.parameters() if p.requires_grad) + n_parameters = sum(p.numel() for p in model.parameters() if p.requires_grad) print('number of params:', n_parameters) if teacher_model is not None: - n_parameters = sum(p.numel() - for p in teacher_model.parameters() if p.requires_grad) + n_parameters = sum(p.numel() for p in teacher_model.parameters() if p.requires_grad) print('number of params for teacher:', n_parameters) # optimizer setup @@ -139,18 +131,25 @@ def build_optimizer(model, args): else: backbone_decay.append(param) param_dicts = [ - {"params": head}, - {"params": backbone_no_decay, "weight_decay": 0., "lr": args.lr}, - {"params": backbone_decay, "lr": args.lr}, + { + "params": head + }, + { + "params": backbone_no_decay, + "weight_decay": 0., + "lr": args.lr + }, + { + "params": backbone_decay, + "lr": args.lr + }, ] # print the total number of trainable params. - n_parameters = sum(p.numel() - for p in model.parameters() if p.requires_grad) + n_parameters = sum(p.numel() for p in model.parameters() if p.requires_grad) print('num of total trainable prams:' + str(n_parameters)) - optimizer = torch.optim.AdamW( - param_dicts, lr=args.lr, weight_decay=args.weight_decay) + optimizer = torch.optim.AdamW(param_dicts, lr=args.lr, weight_decay=args.weight_decay) return optimizer # build an optiimzer along with a learning scheduler @@ -158,8 +157,7 @@ def build_optimizer(model, args): lr_scheduler, _ = create_scheduler(args, optimizer) # build data loader - dataset_train, dataset_val = build_dataset( - image_set=['train', 'val'], args=args) + dataset_train, dataset_val = build_dataset(image_set=['train', 'val'], args=args) print("# train:", len(dataset_train), ", # val", len(dataset_val)) # data samplers @@ -170,13 +168,18 @@ def build_optimizer(model, args): sampler_train = torch.utils.data.RandomSampler(dataset_train) sampler_val = torch.utils.data.SequentialSampler(dataset_val) - batch_sampler_train = torch.utils.data.BatchSampler( - sampler_train, args.batch_size, drop_last=True) + batch_sampler_train = torch.utils.data.BatchSampler(sampler_train, args.batch_size, drop_last=True) - data_loader_train = DataLoader(dataset_train, batch_sampler=batch_sampler_train, - collate_fn=utils.collate_fn, num_workers=args.num_workers) - data_loader_val = DataLoader(dataset_val, args.batch_size, sampler=sampler_val, - drop_last=False, collate_fn=utils.collate_fn, num_workers=args.num_workers) + data_loader_train = DataLoader(dataset_train, + batch_sampler=batch_sampler_train, + collate_fn=utils.collate_fn, + num_workers=args.num_workers) + data_loader_val = DataLoader(dataset_val, + args.batch_size, + sampler=sampler_val, + drop_last=False, + collate_fn=utils.collate_fn, + num_workers=args.num_workers) if args.dataset_file == "coco_panoptic": # We also evaluate AP during panoptic training, on original coco DS @@ -193,8 +196,7 @@ def build_optimizer(model, args): raise Exception("cannot load from and resume at the same time") if args.load_from.startswith('https'): - checkpoint = torch.hub.load_state_dict_from_url( - args.load_from, map_location='cpu', check_hash=True) + checkpoint = torch.hub.load_state_dict_from_url(args.load_from, map_location='cpu', check_hash=True) else: checkpoint = torch.load(args.load_from, map_location='cpu') model_without_ddp.load_state_dict(checkpoint['model'], strict=False) @@ -202,8 +204,7 @@ def build_optimizer(model, args): # resume from a checkpoint or eval with a checkpoint elif args.resume: if args.resume.startswith('https'): - checkpoint = torch.hub.load_state_dict_from_url( - args.resume, map_location='cpu', check_hash=True) + checkpoint = torch.hub.load_state_dict_from_url(args.resume, map_location='cpu', check_hash=True) else: checkpoint = torch.load(args.resume, map_location='cpu') model_without_ddp.load_state_dict(checkpoint['model'], strict=False) @@ -215,18 +216,17 @@ def build_optimizer(model, args): # only evaluation purpose if args.eval: - test_stats, coco_evaluator = evaluate(model, criterion, postprocessors, - data_loader_val, base_ds, device) + test_stats, coco_evaluator = evaluate(model, criterion, postprocessors, data_loader_val, base_ds, device) if args.output_dir: - utils.save_on_master( - coco_evaluator.coco_eval["bbox"].eval, output_dir / "eval.pth") + utils.save_on_master(coco_evaluator.coco_eval["bbox"].eval, output_dir / "eval.pth") return print("Start training") start_time = time.time() if args.output_dir and utils.is_main_process(): tb_writer = SummaryWriter(args.tensorboard_dir) + for epoch in range(args.start_epoch, args.epochs): # specify the current epoch number for samplers if args.distributed: @@ -248,7 +248,7 @@ def build_optimizer(model, args): if args.output_dir: checkpoint_paths = [output_dir / 'checkpoint.pth'] # extra checkpoint before LR drop and every 100 epochs - if (epoch + 1) % args.lr_drop == 0 or (epoch + 1) % 100 == 0: + if (epoch + 1) % args.lr_drop == 0 or (args.save_interval > 0 and (epoch + 1) % args.save_interval) == 0: checkpoint_paths.append( output_dir / f'checkpoint{epoch:04}.pth') for checkpoint_path in checkpoint_paths: @@ -270,13 +270,16 @@ def build_optimizer(model, args): 'epoch': epoch, 'n_parameters': n_parameters} if args.output_dir and utils.is_main_process(): - percent = get_ymir_process(stage=YmirStage.TASK, p=( - epoch - args.start_epoch + 1) / (args.epochs - args.start_epoch + 1)) - monitor.write_monitor_logger(percent=percent) + percent = (epoch - args.start_epoch + 1) / (args.epochs - args.start_epoch + 1) + write_ymir_monitor_process(cfg, task='training', naive_stage_percent=percent, stage=YmirStage.TASK) map50 = test_stats['coco_eval_bbox'][1] - write_ymir_training_result( - cfg, map50, files=[str(checkpoint_paths[-1])], id=f'epoch{epoch}') + if len(checkpoint_paths) == 1: + stage_id = 'vidt_last' + else: + stage_id = f'epoch_{epoch}' + + write_ymir_training_result(cfg, map50, files=[str(checkpoint_paths[-1])], id=stage_id) for tag in ['lr', 'loss', 'class_error', 'loss_ce', 'loss_bbox', 'loss_giou']: if tag in train_stats: diff --git a/ymir/docker/cuda102.dockerfile b/ymir/docker/cuda102.dockerfile index a944931..d7c5568 100644 --- a/ymir/docker/cuda102.dockerfile +++ b/ymir/docker/cuda102.dockerfile @@ -33,6 +33,6 @@ WORKDIR /app RUN cd /app/ops && bash make.sh && \ cd /app && pip install -r requirements.txt && \ mv /app/ymir/img-man/*.yaml /img-man && \ - pip install "git+https://github.com/yzbx/ymir-executor-sdk.git@ymir1.0.0" + pip install "git+https://github.com/modelai/ymir-executor-sdk.git@ymir1.3.0" CMD bash /usr/bin/start.sh diff --git a/ymir/docker/cuda111.dockerfile b/ymir/docker/cuda111.dockerfile index b8b7dc2..bf18028 100644 --- a/ymir/docker/cuda111.dockerfile +++ b/ymir/docker/cuda111.dockerfile @@ -38,6 +38,6 @@ WORKDIR /app RUN cd /app/ops && bash make.sh && \ mv /app/ymir/img-man/*.yaml /img-man && \ - pip install "git+https://github.com/yzbx/ymir-executor-sdk.git@ymir1.0.0" + pip install "git+https://github.com/modelai/ymir-executor-sdk.git@ymir1.3.0" CMD bash /usr/bin/start.sh diff --git a/ymir/img-man/training-template.yaml b/ymir/img-man/training-template.yaml index fc338ae..8e3ce8b 100644 --- a/ymir/img-man/training-template.yaml +++ b/ymir/img-man/training-template.yaml @@ -7,11 +7,12 @@ # pretrained_model_params: [] # class_names: [] -batch_size: 2 -num_workers: 2 +batch_size_per_gpu: 2 +num_workers_per_gpu: 2 epochs: 50 learning_rate: 0.0001 eval_size: 640 backbone_name: 'swin_nano' weight_save_interval: 100 args_options: '' +export_format: 'ark:raw' diff --git a/ymir/start.py b/ymir/start.py index 449aa4e..c142f9b 100644 --- a/ymir/start.py +++ b/ymir/start.py @@ -3,7 +3,8 @@ import sys from ymir_exc import monitor -from ymir_exc.util import YmirStage, get_merged_config, get_ymir_process +from easydict import EasyDict as edict +from ymir_exc.util import YmirStage, get_merged_config, write_ymir_monitor_process def start() -> int: @@ -15,19 +16,11 @@ def start() -> int: _run_training() elif cfg.ymir.run_mining or cfg.ymir.run_infer: # for multiple tasks, run mining first, infer later. - if cfg.ymir.run_mining and cfg.ymir.run_infer: - task_num = 2 - mining_task_idx = 0 - infer_task_idx = 1 - else: - task_num = 1 - mining_task_idx = 0 - infer_task_idx = 0 if cfg.ymir.run_mining: - _run_mining(task_idx=mining_task_idx, task_num=task_num) + _run_mining(cfg) if cfg.ymir.run_infer: - _run_infer(task_idx=infer_task_idx, task_num=task_num) + _run_infer(cfg) else: logging.warning('no task running') @@ -41,22 +34,18 @@ def _run_training() -> None: monitor.write_monitor_logger(percent=1.0) -def _run_mining(task_idx: int = 0, task_num: int = 1) -> None: +def _run_mining(cfg: edict) -> None: command = 'python3 ymir/ymir_mining.py' logging.info(f'mining: {command}') subprocess.run(command.split(), check=True) - monitor.write_monitor_logger(percent=get_ymir_process( - stage=YmirStage.POSTPROCESS, p=1, task_idx=task_idx, - task_num=task_num)) + write_ymir_monitor_process(cfg, task='mining', naive_stage_percent=1.0, stage=YmirStage.POSTPROCESS) -def _run_infer(task_idx: int = 0, task_num: int = 1) -> None: +def _run_infer(cfg: edict) -> None: command = 'python3 ymir/ymir_infer.py' logging.info(f'infer: {command}') subprocess.run(command.split(), check=True) - monitor.write_monitor_logger(percent=get_ymir_process( - stage=YmirStage.POSTPROCESS, p=1, task_idx=task_idx, - task_num=task_num)) + write_ymir_monitor_process(cfg, task='infer', naive_stage_percent=1.0, stage=YmirStage.POSTPROCESS) if __name__ == '__main__': diff --git a/ymir/ymir_infer.py b/ymir/ymir_infer.py index 818ff90..4e2a833 100644 --- a/ymir/ymir_infer.py +++ b/ymir/ymir_infer.py @@ -12,9 +12,9 @@ from methods.swin_w_ram import swin_base_win7, swin_large_win7, swin_nano, swin_small, swin_tiny from PIL import Image from ymir_exc import dataset_reader as dr -from ymir_exc import env, monitor +from ymir_exc import env from ymir_exc import result_writer as rw -from ymir_exc.util import YmirStage, get_merged_config, get_weight_files, get_ymir_process +from ymir_exc.util import YmirStage, get_merged_config, get_weight_files, write_ymir_monitor_process def build_model(args): @@ -127,17 +127,6 @@ def __init__(self, cfg: edict): self.cfg = cfg self.init_detector() - # for multiple tasks, mining first, then infer - if cfg.ymir.run_mining and cfg.ymir.run_infer: - infer_task_idx = 1 - task_num = 2 - else: - infer_task_idx = 0 - task_num = 1 - - self.task_idx = infer_task_idx - self.task_num = task_num - self.conf_threshold = self.cfg.param.conf_threshold self.class_names = self.cfg.param.class_names @@ -203,13 +192,7 @@ def infer(self, img: Image) -> List[rw.Annotation]: def main() -> int: cfg = get_merged_config() model = YmirModel(cfg) - task_idx = model.task_idx - task_num = model.task_num - monitor.write_monitor_logger( - percent=get_ymir_process(stage=YmirStage.PREPROCESS, - p=1.0, - task_idx=task_idx, - task_num=task_num)) + write_ymir_monitor_process(cfg, task='infer', naive_stage_percent=1.0, stage=YmirStage.PREPROCESS) N = dr.items_count(env.DatasetType.CANDIDATE) infer_result = {} @@ -225,18 +208,10 @@ def main() -> int: idx += 1 if idx % monitor_gap == 0: - percent = get_ymir_process(stage=YmirStage.TASK, - p=idx / N, - task_idx=task_idx, - task_num=task_num) - monitor.write_monitor_logger(percent=percent) + write_ymir_monitor_process(cfg, task='infer', naive_stage_percent=idx / N, stage=YmirStage.TASK) rw.write_infer_result(infer_result=infer_result) - monitor.write_monitor_logger( - percent=get_ymir_process(stage=YmirStage.PREPROCESS, - p=1.0, - task_idx=task_idx, - task_num=task_num)) + write_ymir_monitor_process(cfg, task='infer', naive_stage_percent=1.0, stage=YmirStage.POSTPROCESS) return 0 diff --git a/ymir/ymir_mining.py b/ymir/ymir_mining.py index 4051128..9f68276 100644 --- a/ymir/ymir_mining.py +++ b/ymir/ymir_mining.py @@ -15,9 +15,9 @@ from tqdm import tqdm from ymir.ymir_infer import YmirModel from ymir_exc import dataset_reader as dr -from ymir_exc import env, monitor +from ymir_exc import env from ymir_exc import result_writer as rw -from ymir_exc.util import YmirStage, get_merged_config, get_ymir_process +from ymir_exc.util import YmirStage, get_merged_config, write_ymir_monitor_process BBOX = NDArray[Shape['*,4'], Any] CV_IMAGE = NDArray[Shape['*,*,3'], UInt8] @@ -315,11 +315,7 @@ def mining(self): idx += 1 if idx % monitor_gap == 0: - percent = get_ymir_process(stage=YmirStage.TASK, - p=idx / N, - task_idx=self.task_idx, - task_num=self.task_num) - monitor.write_monitor_logger(percent=percent) + write_ymir_monitor_process(self.cfg, task='mining', naive_stage_percent=idx / N, stage=YmirStage.TASK) return mining_result @@ -370,9 +366,7 @@ def main(): miner = YmirMining(cfg) mining_result = miner.mining() rw.write_mining_result(mining_result=mining_result) - - percent = get_ymir_process(stage=YmirStage.POSTPROCESS, p=1, task_idx=miner.task_idx, task_num=miner.task_num) - monitor.write_monitor_logger(percent=percent) + write_ymir_monitor_process(cfg, task='mining', naive_stage_percent=1, stage=YmirStage.POSTPROCESS) return 0 diff --git a/ymir/ymir_training.py b/ymir/ymir_training.py index 965d49b..d428b91 100644 --- a/ymir/ymir_training.py +++ b/ymir/ymir_training.py @@ -14,10 +14,11 @@ def main(cfg: edict) -> int: tensorboard_dir = cfg.ymir.output.tensorboard_dir args_options = cfg.param.get('args_options', '') num_classes = len(cfg.param.class_names) - batch_size = int(cfg.param.batch_size) + # gpu_count = len(cfg.param.get('gpu_id', '0').split(',')) + batch_size = int(cfg.param.batch_size_per_gpu) eval_size = int(cfg.param.eval_size) epochs = int(cfg.param.epochs) - num_workers = int(cfg.param.num_workers) + num_workers = int(cfg.param.num_workers_per_gpu) learning_rate = float(cfg.param.learning_rate) backbone_name = str(cfg.param.backbone_name) weight_save_interval = int(cfg.param.weight_save_interval)