From ab96149f4a7ecaad36092db6d5881717c5e1de6c Mon Sep 17 00:00:00 2001 From: Carlo Alberto Barbano Date: Tue, 21 Sep 2021 22:35:37 +0200 Subject: [PATCH] add training scripts --- Dockerfile | 8 ++ data/target.jpg | Bin 0 -> 54129 bytes train.py | 265 ++++++++++++++++++++++++++++++++++++++++++++++++ utils.py | 164 ++++++++++++++++++++++++++++++ 4 files changed, 437 insertions(+) create mode 100644 Dockerfile create mode 100644 data/target.jpg create mode 100755 train.py create mode 100644 utils.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e103a9e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM carlduke/eidos-base:latest +RUN pip3 install gdown albumentations torchstain sklearn tqdm +WORKDIR /app +COPY train.py /app/train.py +COPY utils.py /app/utils.py +COPY unitopatho.py /app/unitopatho.py +COPY data /app/data +ENTRYPOINT ["/app/train.py"] \ No newline at end of file diff --git a/data/target.jpg b/data/target.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ebc38dc3aad0aa4bd185c5105810acaecbc79d07 GIT binary patch literal 54129 zcmbSybx<79*X7_EG{6uX2AAOO1R30&1Q^_cYw$nw;>i(x+cfIqvy6!#a-1~3w-zI=SSplp7KtcilkX{bJzZHNS023V@104+$ z0|NsK3lkgn^($N)99(iDV*J-sZ)m8g-cVA~g1DGz={XoEDOrSAIkMI?lI z#ku(SdH?egBrGf}TpV2Bt5-l?I!Zd;|L5_q3qXj8)P>ZAf~m34IW z^bHJ+j3HLmHnw*54j!If-afv5{^6e^BENi%icUyOf+v4ZNlnYkFDNW3E-5Xmt8YLw zHZ`}j_Vo7k4-5_sk4(?Z&do0@E-i0u@9ggFAN)Ez`g3u4b$xUD_wN2bxR3xS{|)Qq z`rp9*4_t&VxR6m%QBX1dg9{1S=jB8pL`9?HK_`;Y!mw~7rsoaCd@T#ht?kBQ;M4w1 zV(I=9n-s*q#rWqx(EbbA{~fTf|1V_!3)uh0wG6;TL3;UkD1-nhK-$gasN?6jsq9a4 z$M)L~^gnLw1rzE20X)qiB$=FF^^@(@N#YKk{VG}buc6Ce3Y0Oxj`-zNkZjvpg?}rY zv$`Ny6wII}KY*M#doymDuuq!ul(D->lTJ=Om&&*jH72hov&#a+X1y7@|72reHZ9UM zX(1;ai{ep_ZnM1B;SiII@u2mQ^S#|t5|3M0T=>*2#(Ji}ShmwUD5LX6pMu9Ti8TT! zzC$45v=gzz-?-@WV^8G(#$nUm5h4=fWH)*?#2nt%WIS4DL|YGcstTuNcTH~77%6ba z|AF~fp4-j`)Me~k7UI^Ii)-7cHXdo7sbZMNLg+!s!*OCeq`N0Hil@`8h8;ak-a%2r zSae1dHC3H_cRlYfb@FjCVL8ET3G^|IynGp#GZ_cVb%jo*Cd>Sip4947r?=q{jI1?` z17odrZjnYKy|I9MA4tXCyj-NbI@YLpo^F|eK#xQUQgRJ07$(j< zR(@CcAXWs!w1NT$j~C)BiV+jp?BX#RXYN!6vyoJ_f9LdD9|W>jY^ZI|L)6Y8s;s5H~@#)^2^lxleNdYW5*;-wifo`Q{=ja788!2J!W zNeeUL$#sh>Sd}&(Ge#e&TF7_*Y)G&x+kNWAZSFZ;V-HBpaSoG3vghltwP&fla~m7us7%vs+huL`rwkj8@Og((8}}=*_-4Y8^Ne zi)>P6?`cmulZ+{t#l=_$2EnuFy<#fpzTLhZrcpjz?Or2RucrkUTVanO)9+;eF86mz z5O?>R_WNv(-gUS^9iCGr#4WB~PA78_@gcpqni@e!kAckisYv(Jn!Ep1-U7*ZGZS&o zBoQ~Z^6u?F0DYn6XY`deS6cL;gV|@V)K^!@yj^GM7ve2sJ}B85#t4DgX|wLv<^>cg zN^ZAN%yD~nx(#~keG5o0e}szeCpLf*<^5(5{Cj}^t$T^x#TQ73qOjC+YbRg6amAD} zwqJy#%>d7!az0P;nUSU5;-8FCss#i;U29;yihDYg0aSBP%^Wx1N|Bx($TlsqAN#4K zW`!>-5K!gIcbMk%J;4etCTmO1RK2EjJ3p@wVEUc)fXVkTaS#&1r=?YXT|#_IH& z4u5|bL%v6l z+k|qTxJuuBUvE%V=I;w*va#!_0YMmCC4!anZ~Q>D9-KXXWj=U9B*x&?R{@6aao#B{ z)+`DVHKS(y16-E|{p9R(1-teQH(|tl>bj7=Dw}eUw$HUt+D#S!lwiPAyr^a`>P&_k zWl0)I2iE!GilpFbh}l;W5VpKzp4)CJd0S)IT8C-|6AmFm+OGrJR@da`r<&bLyU8)&48fC1ljGOBmSvJhv)Z?XMZ1Dy7b}HB znqPd>2dUvfftM|gk>-mJy%9Qp^+=~bqN?Dee0EHHC>CLOnieUmIXn=ntSpr$kdcY8A;Utj(Lp2#m2@l}7t6b!6kv^0)DFP8hqT`Q3#X`N28i%vvn=czRE+ESNvd(XaR8Ha(?E!5jvA&<> z#CJFMdd}MGcGY_cjF;j+V=PNnSBBr@5Tcs3ICmhO8vqAJ<+5DC4ar};wpJd@WO9Fm z>#CR>;xW}??0Qpu$&m=Su7V5rF=PR&PYMy2zC?k$YlpDuc3q^`Ax3o#pCg&Q@d7*8 zt7oob?N1@mDt880h){;m^JBx=yr!1yIuGyFEB4X0l>G6+^wo2qLTo7cL3Pe;xdvM| z-rcTJ7Y!dz7kYcx4Ne<{)5`nRQ88FJ!|(hl@7JTTiaLq1RzH&MccgJxMOG%z)s%D6 z^sO}TwomTgRN)mnF?>k+qMG3i*nLJ==fZY35)ovb=E3)KK+i^YTt8k_fBZfDy=j|l z$Miw)Yh(xjb@NK%AK=Yg8^wsSSn7Ad+KOZTa7;t$;BQoD2JEg2>4hKRE1xHU*OKr; zdd{($`oKAI^+f>PKTtld&n14;ZV{=t;m_7fFcJG^iw${2F;IYX0dVJufn{C4mm=JFKg7YA z#zM#9t>OaE#`v8e8bp$0eF4+{W4-8sU6z58m_3r8e6Bp>L{~G@WU`u90^fLh$vwP@ zw^Vp?&uN&=wnnPf|8ur#x7}`<`?$`C<{UOm`xFc}n4jO`aG6r5C8K*|xoDQnDiuW= zq)_ZEve%5aFV@1Ku@;FWLu-{ioj~Eo#kurWyhSCWg~kWR|+&Xq?2P5%K3$|Bv-wd&*Xq@?`yWg%;L2b#RQMJM+M9gs-@??yqb zZ_#F|$7`%Mp5I+D&RH=K`rnijI8S4$q}q8~DyMQph)TkDDU8Ux@ujg7!?;CC(zbi% zP|Qq)!2oRwX=xGhcnx(ieORSXZTe^ZUp;Zk>s1hQkizc$gRajZ?;R7b9Ka)boliv9 zQ4h>;wDOmOZMT&GhBk+gVpXFcSMdhaUVPA zm&k;?Sxs%TJL-s@o{0M*GZoLhj*VzUMH3M!ncF~j4#ut3^$;!foR7x5vrGDLGT-aZ zucjLHx^G?ri4I)D`&0C`lCNSzV(?HLSi3e^LRYgH<+>=|I^yi=-k;J%7FVJYjM-nG1=f}y{(p_^}B3O(RdgWBivVBD}gugZ&k2<3-rh@hdefOv}8auBP3Y#7nd_fg)^gEfJEOyEjviY zJFm(IIe-XIC8_I|S=7u@kQkygA%;Ew*~@9bV}H*1$<#`_I*mRa?$N~@#%?n`!GfZ? zaV(YueEzVd>zw>FA`(UxF4ZSz9^_ZqY?*LG%2T#y5}Lt%e)Z&-r#h)(7DGUmO7FE7 zM{PGDnRJ)uOTGhFQ2ulH>_4}}rGGJ75W~}w0<>#a7PI87*6sYUCk z2wU+@vSqfrCV$zwIkFcHN2C!}3+PXzlB3qxif&wp92iirq5j}Kpo9Ts8#5sN{&Q)d z4Esc!`_3c=azAI5_YG@I`oeR#5UgpLO`jLgc3WNM=>(OxCsk{^D}$278U+jH6q| zLGfj&045W3Iv|(zK?W@vtxMdqqNVC1>GZZ@svspGM)FYnvL8oNjo%<;pswAuuAJ_Y z!gs>s&{%WGD)mzxxd}zciKC#>`)y53eIo)gb5ZKfiA={`KVm|j*z{d#zu`eDiK8^C z6Z;K6dt8rnh&YU}25D%&!pyI}`ZepZ~Nrbchn81XfiP51t{U->+-8hwM#@-Ss>mPv`*aclDS9Na3NM3|yRLTsMT z;X>}sNP0Sc12ko)L$4)1QA<=Y$_8ENtEd;W{Deqd)h0{z7|9Prj=7-S^RB3?= z$Gv#tPA}Ah*f`a~g2i)H@b;>rtx`%Yq@jMLCe2dYB24Ct?{I?c`wFAo_7|0iWz1F9 zu5RRP`Tz`s+lj!>Yo*Pnt~+LPWx-Wkl<8Iq6L9!uVzrWQj0dX=Z zJafj_+Ps1W#r_8%IN~$W`7(0?$y`@3qH$Ehz@U=8t$d*38USVqTQx`Q-Zhb+5(qPd zpLUn6GZXga%cU+mQrQ1c`hYzsGD%e%J+u5?|CisLgu`YDpP7U4lXR~(KAzics_h6l8 z0r8Bg+d6))mW8*RD@XRr<)an(aV-?6+KTFqh0XCY&J08TVMrLAg^;8n?Qk@Ov&?>h zgRD>NO+&@B#Bv5cBFy07oLHq;U8_y=#O2~>In*#+q-MILTbf-Rq~*^@8MozWLD`&_ zq?2%$F9%6igYQ|v1qgn4!fhi{By%gheb+9AQdgJO;*&!}JN@>NtYL+G6v&4WOgtmzQU48be~E#s`XCQtIKj)*0HY+LDcamS240(O8aF@b@x##MYj4?<3g%U2$(*K( z-u4_>c=*&H-%F9Vg1f}&`OsibGY~&`pgaMb_G=r_bt>`wvmyKSZ;hG#kCR~x8*h+uEm~>O& zMo7G@rNp{q*keCWtdE`ke$8{D7^!fs)_w%V%TdN-P+^dyWw#f^A2a3J9W6z9*)yJHx?6IDyUe?`Sc}sf z-|GQpmXzlklVjh0C;e5-ITjlucieHe-o5KD35^Khmor;KC%>xyJ)F!d*HqI_M==_Ywv z9>)c);{1t7q~FOz>7x^2ann2NsG`^lA1+KFL$Qcdq4kmADNoG6=1_R(sd3K=g9y{w zFvA?@8)0+bH4~s1g~P$2F}I?7OBzWJ6l>_<_qmbf2z?X>EGORU_L1F~yfE~e3}wGb zLM(ZgQ_;ec`@{vkAmhK}h5AA_RlRtj$#3wb<;dvwo3lvnCU~?<#sILGmKeZD`_-m%i zO%2XmEg(J$+6zhjhD~8lM~eksP?c5Ejk?`758mC+W|;_E17Klz3Ory2@$r#zZh&o( zoy!i`Nj4s2=N83qu#rZ$C~4TG=_lrjCQ`eX0%b6F#S`@@VQz#n+Q-zEz2#6b5128I z`elkOil)Ezl3_<%^4qib)Db)&$K$z5X8!0j4qM+M!@?~w`Rg`G^PI2t+}|J?cax0w zdqo9VHo+~#kAz62w87@=yR1T!cgsJ3Srcd%B>}@9#yKs~XG`4&2o^=~so*+&v1~9u z-}M|+^{Lo66;2zSG+aa(UI)DFDE8+Jcx=lKnOhoS^0qWx#v|a8>?Cj;gW-%31yQt+a2lD&REZ;@GyyK2HA_c+``O15r z-BGC_T^4!gHdNj9$CuSPiZ%ajYYXHQgNMGouA38fPFvY7srw~-eWzr8md3N;-HbiI z)5Q*?#%N$ce8IG!Vluj+TNiTqC}mCg$W5+)W=?ptS4DK?UEEU3Pi!L`>>u}|fU0NW?NBWV-(MO)8sm3x=g|`} zYr92q6A=_DbWvNT7X3IKu!nF%gv@~w$UCN7#CEwwQ=&vPTDjW(RheulEib?$!yop} zZ4d~d$QxK>Ob_Y19;E7Ui*~QDOE|~n_3nqo4lb+)y?(#rAhxk9BOY%o&rnv?#ap%` zrRp_l?D+21Tjnj}r=d^1wb4&V5)MwR6d_1myFMhD{O3M6AXU__?%e*{trj@s>M9Vm zcWzs{F*(clrq}1|^$1ob%)}KTOgH*D#^im&H+;>)zZ^`k(roPZM=e}U7HZKOv>Jx$8@1x*l_@Ry8*U`@}6;gH*zO^t(d@Q786OQ*f#3f zotj1_zD;=^u?$ML4VvfH<;V+>O(Ouy=F{rDWF}T~29bAP2JTgP)4fe1)vPSVN+d6n zAu?~}p|ZY9SJ4drW_z^Zdy1m8ZO)OAEEilS(H#-spu^<+$=-urz6=xbxPpF+Ga7z% z^RM7HdNEx&iOnp#w~r31hO{MnA7E24x4>JH&$%Oaum)}OKg;R9g{hkH`}`azuY)Ad z6#awJZ3X65Kbvw)r(|OehhUOxwdH`e%N*ttKA5b?*6}+NpHYMWX|taH0WulSO6{pj z(#(3>_?x?9ek%e&fgW16qn;w1^W)c?wRRxiDq6dJT2M#`PxxSysXKc{+V-B&v=OPK zl0Ma4)g05z$8~m!|K4_LZr?}VD%TcfsZ3Ahc~S=- zb*A{x9;<`HxC7%6wp;Fmubu6UZ655Ui7x8sCy6tMI?L}haPwXCEra$~t^A{lQAVcg zdpddcTDAhw?ML~et=+LQ{p%?=Z?+5b(xwOVjoZM^6rJmASQW7*iecNnV?kEL!xt4L z>G2QT&$`EI>h`BjOUUQT;)_WTkwFa?g!lIS8k36Ao?$^eRhh>4fXGgObnFx5iCCbxmW!o3f8pXDYJR z7vHIGtE>12=*@pMCxMwMM_zg9%(!2Z*mLGUC*9!fr(LLnXhDc-!DLaFtRj+Guv6V? zfr}ZmE5=vy8Woo_pADANl9)mLpYUCt1|MJLQ`|GI@9|rGo&67>XJ|;zTW}5}A_3l% z{RpQsN&M)7Hd|QyDV}e+tl;*0{i#m^=shnUptOp4hUip#Usb)&c_cIZ&$;?6Cn(FV zp3d;K3thl|sEbKQ<|${mTthtpiiC{7_<1Ig+?$4XtTxZS0)!ni*BmnY?BF&!FvC?b zqBGOyRabmjUydInMC0yRx6-+HYWnW0R@=E3`LpM&Z1mouGe*hv3sbA4{v&?T)(8UP zWVabriqu_lZeMbdPmWFaQg>D=p=0^A;g>Cbd-f8qI?o=+1)Gi1EVo1;{% zwzO@MDg;;$9WDJf%W|%_+N(o%s9`PSAg^D6r8k;m;loX~3HH3NQ4%LRjpv$UQVY^3 znz;nGloWeEm0`#&JEt{(3`H|-g)ucy zXwd!%oi3#;JFsUiCTU&Vi%6I%bz=?FMN{$ng0i?@7@*?jS+Ck}@;As_q#$|ALT8^# z!JTipzWUR*Z7z}x9$XR>5$ZX6Jk@%ilCVi9AqLh7;od-p0;ZdDKo2S;OA$2tY}%x$ z$R-^<7}n=;I$tQ%IDaV*0j(5U12=0$a@#lw653|ltR2h=$B@|-wqQafmq6<0-sNm; z3C63h^cUSdF+kd_3auqM944B-c0JaOI)rTR?ea zPgZNIFXFlVGVXCBV3S1mo-m4>^NPA*)E-Muh_FIww1s3yHx z8z|iG+?=;oHLO-J-?Ukz%1b>cT<#zAS>NdG^9+3y4vD;Jj5qUqvA6E$hHoJN^Ij3) zf#kK1T&tRFUxBJLZF@lT&>6J9C7WP!q}S0tYx~gXP!4kb53#tO(B_cMPb2x^ zvryskrqPF|_Q*wE9_W7AK<}9NuAs-gW>!S^IxB3XXJ2|tdgOG1UE%~ct-$?^TANwq zdLNOa%3u}d273;kG`6yMH)JFuZ_$=DJ5#IhY1wOp!~I>TGt2~F%b_GbOG?ViQ7Jwo zN87b_Z07tY#8~+uHrt=$9Q_Ma0(s&a`^Mt^s5*=r91s}Eg%*-m(u`c-V zeHhzQXQn{XAB8Q(hq9d*N;iE(xuH{tA&5%}6x)tulm=*7J}~y|u8cLq zWCo_CpTu&0kI8qK$&Xz%)7ZDQqzRtEk+L*;Iy?^0^2GM5swIJVb}@!HT0xM?y5!jLPB zK@!0M9XcfS0blG)87MD!pr6(*50qOMl@GO`WG7vFOzDFY50dRosEpz)o_#Ckl2ufE zIaITGXm>=G@r|>d`GgYffkTP=u%v@f?R-nxiWj+TiQJ}HKGKOTw!8KcU!zcPT}tvv zbzcaRq!S51@^yY?X9ldJI=`^vU^{yu4%CXw2&! z3Hk$-%2eUX72gY!f>QvLnF04R3{#Z_C{rNQy*k0T`n6$TF+B!Df0z=$Y0gEOUY*FDLc!6or z>Z9Esm!YyF;Wy!C4Boc7T;I?`MEec^JT-&mwC}#lnk1Q3=p>$a(7k;lYP+Y3{=~*Y z#-$rl*@QFTW-xvpIZCR+o)oyzRlzIae|1yz56~|msJtjv&Z$8-yy=9Wy-N3fm8bjzAkw6RE)^msB%8MjR05G8ub4`^DYyOs zdLLOYO`2uxT_Cuuj9)1a1Sr0)HD&$C(LWm8I$ua^A-9IPh?aI!HQZpPX= zx1|)ENZ)&CWDiw45~n+27F%xC!G;-sOBusG5ip#gXH%7!%h~unwj?GnnLT0>tw=;;rvvapC$!{mGK8kusf+&0ET^Y_O2FfWhK>kKb*l z%}x`!`N$u)9bV_o&GddFUb3#6csAWu%4%*r4TJ)l=go1^Yn?JBLf|5{H~}}9yq(W z5q46Qy>XYJYfREQ+L&nS8z$!Fu?Pds-MT-gVuYSY8#-_Ei9egd*0}Au<6^3{CF0&^ zmb1*wKdCXRH7^nI_1xIyeo28C?q|Yl&O?9^w}ZvimA2{ZCS*}WAL$t5Yp_M)?-Ts` z-#0dPB<~xIp`GrqrPHC<0mT!KwL0OPyRLxgp>X!*@V%_vjO`?$n#`QaKm*qY?NJ6f zV1mB1(x9~4)V-U|FaHF@YI2j(Phr|q=9v3~PKUIgIvTdoCawv5q`WR$7lDED)(*tI zK2=@x`wG7lrLt=)Ata8TA_rQt-T@S8h(6aPv4JBlZdEZum&v9e_*i=CnWdEdCshR{ z$S8_IdT(8~>vwRQOVF=f=vAVTjMyNb2ivE7jOdb!!d(9C@7nN;I#VN46L|)PYBrf) zjw`EQ7;8r`L56lO!)Y0C-kT^gPKx7*3GIegLECW>rH8AXTFmw z7SR&DOm%PCS?u)6hA0b!RvyG4j1ThshH@Emkw5miv5IP|gD~I65tJg;z z3AJAv$Hj+n5~{FXWpDLs1Y3}Flq3szxrBTfh8*e(0BxduS_s6g4L{nb{~b^)VL;%N zm?$>t@JGtOuj-J@&@=lrB{IB}pWPlWZ|5U9wv&v3@kan|amk%5qPz9%qG5daR@ML8 zwpL2go&nX9D|>cpgww%#U1Gyy85tl@-jS>@*5x#SBAGX;qP|OKBCw1-P(@Q|j|ji6 zr{ZUYQidRyu9d(8+F+0O{2qjjj|MQsgFBAtI62>J^OAI-`6&E-rd!C!o~|u*Tpp+e z7kVI@X&)Fi897%Jrwr{XSCJU((gZ`p$sZjZrU~(KDapvhIL0u167mTj3$CBJ{2m=M z+duyUI2f%T-u5xnESue2XvmzN1UeRV7*&pW*U)fUSlWI<{!UJtMD?35Cg2TIgO)LQ z(M)Z|n_;gnGGn_oBDHY>iXtdY)QI={>u;M}DZRB^M=}#8fy5?|3y+NxHz*ZqdG)gFK_j@T|i zz9aCnVZ}n+P_nMx$NBSd7F(?VSS>Z_!STlb{YNUB&= zW)+s%XDgvRDBr#QYgJXg@{G{#$@!{edna7BO|%d;*!6JX2A49g4s4-GD~lU@In3z zx$!sp9)%f`?bhI}uH(P6+Rc8MF!XsBz;*wN1;dy@U^ED9wkOe3-*7>=?;X^C0V&ic zp+O+Gr6!stt8Kd`9dT*ok+YMWj~_HD<(+Zg`+{d6O{g(5*YD%jl(Yi76K-2{PokGeAeeYTqRwA51JfoX1oE?y-)HX|t?|up|LC}YkYWjK; zotCC}i3XMe(xD0N%=#?W?__o2m<)?n7AcG(cNWGwVa1&ICuuzH0pjH>0fGcriA^y*P{_SfUu<|vBo_+mpv34&;6(ozC`#vjg2N;=G6eT%Di!_Qf}K5sCapmFl#M5ly z?uzd64{G+lwMN^%mwOd{u0buEFQUP*Zet^g!E_~sChBOD2iGI5A&@oPV86%)hu z0c$B11|&u1;;rc99qNekVY4;{YAUV|Omtt_m(-0U-5bj%KQQDNk- z^>1H4lMxPlv4=m)%_N9j>}I{!ZzSt?jV0%A7*`#wfc}-6Ha=m(X!nP-S+{iJM|OcP`*O#Vb)^oF-%>0xE9aU}-3{b9D_ril-_{}aN>!1m$fm<;}gNXp$1ow;TB$)PGyFyF&jWOgT- zH}0Y1MJ#j6dSYA4Ds_@=e_yr0Ulla^(HcYuI{m(Glq<^KQ>`J6pBDR-FQCe zL7{-lK7rX%&Eleh5|y;T*n5q)k0AEf?CBrd$5$kVr9K=Oqf_Lh=zX^*A52|2d}52a z;dQ{Ll(HwC@33;pXnLT}(3bC^Z1Mr6@p*~K5Thp%7z>*I4#@uayPVV;yXn0$eQ)?B zZw!<>xTIYFHXDKp!rqI+Ri_{yLY*;hBT&8=D+nprQ&jhmUtGy5be88j*+*mnY@$GS zKNUF&2;2;+s!!5ZW%gNYf!Kqn31TxY%n4h1JCPsgxc6=dM0TW}{7Y>MRwhfqzMCz; z&8xLzh2%D-1=DHfT0~t2_&}+Wtw}b(yKI1)DS_WzmnsLk^O!c@T-TJr2PPP%k&CM~ z?vLKbM-1CiwiSLW+o_f8j3D%xEzw9*ot~a@P6;NY4FZk}>1tX=sEY(2>dt$IZ8oUm z`~whCKWdNjFA%YhhAsf!h*WS>$)R_2VXx)qQ{TYdVl@0OSt-=U1ywUZP&PM-Tbr@rOC9> zjx@lIx377dz*lu06hb-w0|ED&c8@d-j|ojHEt2kk{>iHd3Y?`Rknk`Wly^V(a$l?H z9f=tlX%UX0=F?d}j4L^G_tW>%|+BqBP&HORw9PAGkCvsU%6#zgU#zaH-OtMR%{=Lc!Cu6;F&V zFHDa@v<(-xqj(>_5kE+9N1{S!cdisr6QuKTmY7*uW%~-1UThv7!Z@BpNb{#;fvdtA z5?8rw(2_{Bej!atcxb&DE=su*(B@EcqgRMxNadCt_@U*c^|TXL$uVUbp_vcb<&F)~ z9h7=|c_85sk+_QAP?4Md@U=ZYTKJJ&T?|pHv;)PoijCr;YD{)HxT@ImZOeS4Vu@%M zg~MZRmZ6RR05(~~dnWG+1Wj5bc#e&{M?G*RohTbb3=9r3Ak{&0W&0?sDtvT4Pdf^j zmc`=cp~rdxj=vg`BF-3^FNNa&qM$HKWsA?m6l}E0ex|Evg&qDpVlsMv4#d~?cV1o@nQX#=!l0wOVc-#e{nEF-;AaT9XW1FfG`wq_3NaqQ;7L-=@aa! zkbHg1!jpmai%-AI?1=dlzZI$M^3me^-uy)=t z9e(9+7*|>vo3X_GWQ_s-T=>9Ee?8_I7X+szKXkwzW-vya55MaeRNc=OPY>i!OHfH^ zLQBL?K=tE@MoOkm|&c@(GWt{D2@sR^`&>7t74B#T9j1&w?}&4$SXOj^I@B zBtD%G`BpqNz^XwXA><9V!3}Jsj8WnqamLP#=^RW-yW||{x_GNz{Z8aWv@_!0fRj2C zKMx-Soxn+462iTjulr7>c@nt$w8?L$bp_y8L@lSij#@Z9*0@EZi zt;Zjwa?FWlXYgcb?~FoWaMqg~37nLc5-obB_-UcTYibgg_{k~N@<4)Tq55?|(5|rJ zNxk3&GdR~w98EfBn2-9i*J;~=*(L>keBL-1R z+`<##ih9%N6+k9TjOq_VYro76h7^@SwLY|(x_eH2P73Z`5to@4TPdpNv z-AyIs?zktymk4AGS-ILXWFy@)(%UDLSm}Nl&NF0mUf@{)8=-) z=X;c#Gb<|#&dO9P2G7j{Ais-#-G5Glrv`Z1d{&aiMRJOv#`-1EQJ%@lIA)J}HWP5g zVo!PxZ~J;a6gG{{a;b93^ABJh7+mNA)Yc~&s8IZkez;I2=B7F7@ct*~vc7r61SUgf zAMyDLNq#o{2CWO5FiL5qZbs&?>#lJ<`py0J^6{wrmNdwDFRl>$x_)u&g~HH-{dh)J z;|lH8Ii~vz-;wnERvIGF7NbG*buLo?44rZe3^G3jt5&R;*o8s}p)%0+Z_JH^+&~G+RV`4l*!Du)B??SS# zyM@mWjX7#SjIY0dP9VoG{e3uUqMY~>;`KEq0hlzc_Q*MW^z)rK32NCU2v`}s+@a_18ZQ3udf=hmiTpAf_1qlp+5#!8s z!|Zgxba91QPmJG8i;GFMKzqtXL~HVKoW-#bzO}7&MaOWW(=9fkZU!!Ha0F)-93&e4 zUQPH#sneyI%~mciH5NEM?O1CFlh^O%K8;ge0%ax&ekNLa^U8^fwk4Thl*2q^Of)Uu z<=nXl%sJ%TT}LJ!y{G~YC~n}T5rC}z{l(UPoBR!Zr3E5yaqFM>S6l5wZ?sWpYR!m2 zk=96hmuCBxL{4iqx86EI3`W`K9GLH|XeqA~W84m$VOR}q;f>Kw*Mm?duF%1xA~aAn z^Ah=Ply6y#4z~mHgG)>iJwpn<1M(j=sM42PyBO9nhWB<*E{Y^`aM5`%H5D1CIyqjdD&LvaJ5|s zt2WiQv95N0`IVlXyq{3A?rgr;grlJ2cisO3WL+)awhxZAINe8mEN47DvBXLiitSS( zaV-sxK{|XUX%qYcFbO9aFCQhqo$^Z|Z2I)zwe!15S)e`g?C8dT6@AX`r|=n`o6hpl z&kyeP1n~-!hkce%cknL3R_Kgu2NVrf107-Nf)?{ z*mZr2kg~Bvl~Fdc9qJDCV=!CUEukDOxWH_&Yh?&yzV2noenOBNt0ZF;R!&5k^aj;_i%K7oYj1WQEE1-P+fpb-&)zLJTCeO;y8yol!(;$<9UJMi z-;kbZz!_ZW6x4DRJ3CaKay^O9#6$Z`NhW8(&B+IZfrq@4a(uxQu~>FPAR)eluySO9*&qi-@7& zs)s^m%IzLxspu{7D_iW2M6GN=0T`<0+T0LoJ$UOj#W zt$A}jt@CF{^}%|}@S6`z8mp2f@Ks-iSp%ajvDE#Vwi%b#YLrU-3|_Q^zrd$PIt~P; zT^!$4lexbHEmqamb}k7^u@;jfN(V7OYl78~@2zPj;M239zFVR&7%g%bF^9dW0Tkd!_VgKlJkb4w~eNLb=?$k@dOm1 z3x6;=r<>fhn*_u6>RaD-`w-WyNLq(tBr8m&;4O-K=sP=I9ooc=%`zK7^(97Q`(D_M zBiq7BxizuL>RmrpOlDW))y~RvH7k(<O_={aqCX9mGzbYmkX)J?q27YC}` zP7Z$LFp!dXk(H|k=p?-X7ksnPoo;R<{SC3GngwM%z(~`ufIm>SSJeCW6kxnu zsv6iZ-a8|<=1r8Ttd3aumDo}JAItFE{{gN*QNL@8dnk5J%P3NS$WLDTy}9D8Tf(wh z%({iw@8gU&$gP4)0m;W0&*4tpbspA)M1v;`S9X6abBd1pQM)@nKE<8+ByM%Gg+D(Ht_0KcNZ|Poh#uCfRI4RJOVuhN`pzZKWP^2 z4PW)t-L;HI0)Wm=eq#M<)axTa!rYN2Fd2csqygGhCN#H?0IX^_9{#+X)bYtYj>;oY z2LXe1_p6I`yC!z%V%tHiM>zAGG)=>j;EJ>lTf(Pa8T9DeMx?(ud3F;T0)Ygr@rdo6>M_};WndG>6TbwRN-hP>+ke+vK$DylI zFcJkqPCDd@f+-^Nm`}+a1}R;R)>kfATj@;AJIgTguyEW1o|wg0S*_$}K598Fz|L`5 z&$d&IfFSkus7NgT03aaej1FmX+{M%8)w60G2Qjf>pSl3a0~n{Zy3KA;!I5%y61nfv zp|xowiOcZd?an#r-mF|nExbwP!daPugS#7Z-m`m{Nmv4US(#p^hRf{`h_wD@6XHTe2X=<`a;UOnrd}Q-ZD@{6@)4Gk< zQfV$>iq=b+apfW%#1GK^m5-%fMK!!l97GcAQAymb5U%m9sdB@Z|>~gA0l{0WOX2xPh(KY;l;a8v`F4n#EXNB12<*c&9JXeu+lhom;eU7m-ZBc4n!hcyZ5tyF4+_8xgGn`-p2xTA{ni-X?X^T+4rMk<~GsI)p@0NEYo=ij9lFvkxZMLoM+}gmuyg%5KR%8qcV&Hjwr_f9)AEoI_8u`EO#(!7fiot zm(GqyVgsvv-g;xF6}=2r_VzH_L>tSGI6LrMpKtTlu#|aMnY}yB747dOmsYq~Pm>I4 zB0(O0UtFKeRSjM>zM4B3FEb=$I8&4Io(SZ2#cw{-AV8oIo_HMp08e_WEse6@CFU_C zLuiplCmH9S4Jgmvxl*Ncl0#)_qOnnJbLCt&?$Mle7#Zv84-}dold0NDY|L@PY<9^c zhiWPR0PFe+hf=(j3GIH(VfM(TPnD%@q&9GVn5_*xt!?g8NAshIE33;4Lo8hMFTl@H z)1?!4iJvo!tiKXgu(G;$RlJI8l|%A*EP?=N7AJ(0r`rd=l;2pQx50If?Lx`dDm zWw5tc$iovjIKlMe6{jMgf*;>50mt6S=riw%ZcZz!zpP*P-*%CEHl~w+Kknd91!7JbJt1`(Iv^$bGh*k}@Rq8!S>-bewWw|m*h)}Wl zhyhf9I2aX&Jlc2I==MvQa_M72%xTu_ZSH42TV+FipDKSpn0;$oPw?Z}c%IxP)QxN+ zGKp0-P}Ht<2%?3KeYKWi{pek}Nj6j8dw@kU%E`zgohy)mG}~?C{%wtPzk>z?l#T&(R z>FUSNY9zRdQdBaL#yM|eQeFvl7@~wo;Kp)_m{2+AjC45fTKaX|38qgay^XK-lp$HA zyZej_V~lj|=}_t#jQ5eoCtFKc_zxAa^D;C2?%zs=-srVcnoc~XWL_P#x}Ga2OfbnK zv#e_-?tKe%&l#&hHXV{Q(YE4>t&dUHHD(#DEpG+vbD!P5S0e^OItrn0s+lt z36AIAwQI_ZZc?3Rmp1Z^rXX1zMgbp6NMoK`a|`YBSmGvg#%o^T3<1Q-yN5;`s}B7C z0G_n_Mvgd5tJ+8;X*|UnKJw$C&uV${? zS~G&RcJjtp=JTXf2(oj6NdWX2{VN5dYgzE9T2sb7DXTD2I?~_;LcmDFa8h|R=>@&i zZ3W71PrBLeDx~)9Hm?%_@-7MdKmBwsgq`Zp-rUae{l*X>L5IWpe{!Fi*K7o;my~ zEq%&XO6YTL!3`;CNhb%Qj>G=|t=pOsX4t>Q_+qhQ)ux`_?&U#Ca2IPoA2B%N)~)H+ zPkl7e9DJ^DM;r>SMv*a+dKPYVICR(*gaJ&4AyWXKQfj^KxQS&1(QgJ~M%Li**R@%> zw1V*vPsZQ6M_SIju@cjg7k_f!?J>nQj`< zcF0s@IqWH`2$D(KWXi@u?OfoT3RfmljkPATnWf*z-24o8$4>s{uGmF1mq->SV!tqH zPB#0Btvg7ztQT)R2H)jdw>qpER+$vDpPZlwgOW%e%CnPuCArHfUFeLBYF{>B9z-)h zNE=6Qddsrcl^JtTD)b5$1nl5w%lg~fo_YZWHBJY!zNP;{Xbm@ew%t+HO&jPqau}t2^Tf8hpfe$^2?) zlH^#Jo@fz{er@^k!R^N!RJN@x#ktzc3bP!AQT`PBjY=g{w4TlfJZ)x8!ydTxp|`Ro z&iuA2eU>3+A`&c*_f@mkp4j&7NhYF^BBW7dC=1EJ6-}j-vE3g|b}-w4+lrDYS!5)F zqvqocSeFyV^NYj`v7!<9SPkUO(EOyTgDRVwMY@j50v}ReML5%JSj78D$&J z{&Bb+x%}7ktoX;;zRhxy2-kCmQhIhZrKHUqP~1;+Gb1j-2+8?)AJVE@4AYkH?04#I zinH8CtCryq)8@Rv+?{q0s!?j$gg{T z2VF#xyB29@xC|OY=AOI8?%$p&y{*{OUt`1esfW&yG1?U~+d1d!OEVK^=Gw~-n&Zq# z9kvdALGSHO)7DF?Xqx1yo$#zz8DI|pWBJu2lyw)yD=joMG`WqYm3ea8VS1BePEG1_{pL@A_4TjU`g;EQ`-5q~H%v^XW@%J%x&~OIvn~ zpW|$nKBc<;6p~iZ6kJ{GWZuOrw^O{f@Vq+!55JN>{bIEA+m%@3xJcSRD2<;!F^;1? zhu*8-i1lPD@(6kyL zX+(;|svKvYp1c~41X$WNk@g13DieBA!+DnU_s4$EqW4Pm|G+f?K z{grO_3KDl4itZ;M`uZAP&UzH4wN2Ui5y>X2Hl;nT)`}@%Ws*<`WF2_t)MBTwvTY?I z^CgmXB;b`T_$cpM3#d$O5sZ$^$_Z8jgXk+dT}`*_fj3W%aJkxRepd9gk(K#hH&aqc zC%KOzR>@zKF*yS{$%U`Xys(Em9dj)yzU30yhMn^=+I>xO!34dOg2snH&9Ji(x4tg(gL^wjvl0VF3E*QqVCU;il4&GBhj3NRnt+V zn$)3jtzAsfiC{q(A9XTw>74hX*1}l{xPAF2BYX6zC4xfe){#XT6U)eQ!{3^@b1;Gk z+D|dzEEjT|iP$epxk3PDV>`(?016o(bnU z=C83ZoN=B@1M5=z4*C=|TN9{h?uKk`A9uD!4r>}4T|-2^S!G2QEfVgPh|d5HJLfgg zM=Opnes2DN^{B7lx4E=%5R}?U0CG9cU&g6PtscQBtODSZ# zo>J^GP#<4f(9$)x`xuJi2?L-3To09is&}^Y-N>650D9HiOEtED5O=xfnsT#Rkt%8E z%=_(H@-1#RjT<=S5O9C}RlRB_xt3k5BUU?{)wQ|xveDmbu6XoK#LSG0SQ65LE6r7_lpj^ z)kKVd2_u!sKhBvuZ5tOIF_YV`_|v@2*1J^v*dz3!#nXo?Y`vz=uWPj!Ga)1JqTLo- z`+`|Rjj8K_jNpDG{#DH_uAyfPZv)}D+`iRn%KlhxrhTeVHq)M@k9uw!*veGh+eYow zLP%K&UCIeY9lh&6Pt>i}(R|OAKu-z@&py>1{-F0ZPYem=vw|J7o;!h#^>@K^0m_e?j!R~>Y;@b0E%#4638%gdt3G~l-+(j6xTT76%l1Z&8Iw2( z8U5!TglC$q;zKKHc7j{kVDqg(js`p0KDh_pxt!%T;%zAMIP#kJI)9O49prL{iNDrJ z8-dRQZ)}lV+gk3CFxh5GT>t<{FC_Aku27-Y?ZT4-Q63w%YdASk3uS~ealUwZrQ_1-#a;G?~V;=+M`3|`B8$h zNH;`O5ZLd}9qXUAx0ZOm(;KWV>;cL5>?@#x^2Xayiqh)mFvvEsB!CMHejPE9>s<9< zx*JA*?xk5Yt7)#}%mJqoF#|h#bmFI)HIf|2fq*&r{c5e<{5Equ7dTHXijBEpj>LM7 zoYkZWHL3ZElDSsjo_kcTWbY#V$uDem9+fS` zA~@PdRfH+aGjq6lj8&<0)JRG^k&L<8SmSmo8>nWrL@J2GAOOQEKEBjkEJtG#%^OIU zk=($&a=2yx0PEG1xQ6XKxvg0hPB)1LKDa$U3cD4gwkFz1qKGEqTgy-eNZ{h6)grUH z39&}v0XsOytxfL5eQYkrRE;dwHozN9V6mq%N@WWzyy3aydC5P3t9pEiYiAdgBBYDP z(gsd@{b^m)T?UZ+z+~j|DlW-$*48xs+i$5@D4ma*fJY=u2;6(0V}tEk7uMfmxbm2p zBV`R7j5sTfpHWrsrCDzoqGAHz1{vnATWWW*+j$b&!E*rUB;)d_QE}KxE>rlwW2@w!8*B{EXS2bFM z^2uj@ZBPbAl#so^IN(*uwxzHPRGrxa)xZ694eTzlJgB?w8;$)o=dbBaj?k=eE_39A zk3z?({6D2mgNkcHIkd#LyG>o@f858nVB>fH09v%;b&)}LY&qQ69V#hpR_58Fl1B2* zGC{{qYOR&xTr|L)u_NtLPu{@i^rgBPry>t5fVpM+qdi6s_*QnAelLe@U=a6+)iT=f2ClI$*P7G<_pMo423%raBu=zVJ| zO~1O*ZxJquk(icJ2^b@eq*m^iWpk>SwHa?#8&IIh2Hr`>uVGPKq!)=LyRqKGjIQqi34hNFEvle<~7D@a>vpY>qmkHWU?ou!Q&M#@iBJYukQ8)nn&mfkNgE_iZz z&!In!XxiM%E5b)T^d1jY?k81E!tf{k6uUzAP-uuVH&lv#|dD= z4z3O{j-QqdU(_WRT11HhDV4Zo1F+{g>DP+Dg6`u+jyYKgE;f;zf1b4CcCXZ#Ue-2} z?@$met;4IxM#f>zMt_}8aPQ`9m5vokKmV90T`yN^*+bo4hGi%MiZUJ9I^0Idrv8*6)q zXmWRY{{Sj-v8o?B6VG$fqgP8{WG*nEvky#jR$fGVcYI@GVCJY?+nKH8Mv)_p56s~1 z2iB7F2H!!p7n9vQl1Knt;YZf4T0)aufa(qx661c>N* zu6X0rQ7*x4EL+qgiJ(P9213JVQS%>KV3jRnWsc#)DuN-MNYAi1{{R{-Z&o{_Ex|5! z?F>dp=RGRHgkKRY!v5`#?$bEH^*E^8MsvwEq>SrZ)YUafEoVn%{jVH-zjY8%W3H}VET^PRoVZcSi5t*4UmwYAKGGq5uN zHj~FExzAx*(_Y7@+sB3SLqH>qxCD0u4_un5W2XI!Pj9tIx2$Ck61XP}c<=PhbHWL3 zXxrXBNOW7G1i>wyD8p`7k(l-E?f6xVOH!R}(O{ecSccqi2dC6h&py+G9|}5VvRtH> z8Jad_CxFCYR_ZCclHHY!f3#cq1=eM3C@fU)J#$nR&KX(dw8B1msTgXm@v_PiGqn#J z7m#Xymf8u75Mos$BRB`&ns%_S+)GQug!vb7H=nx$en($=*8b48*nq*fDwJ-V`c_KK zJ=1KNOrRk+L!ACSs=RUA%M&OPKt~~qa8IY!hLhZulib_3zm0szC4@U?wh0_#{F*8X&H_cJx{5o*!fv**3`o>2t%*l zXU|Wdsc&?YxVeVzQpklsWg|HwryY6Xp^DD>$4z}pHF9* zZF2rwZRv~}Y}fHyz@eRli~tHKy@J}#ObF1vdZ5}*v8?&7C;L#@YRqw!+MRxtymePO z=ju=8#cVv&3o|!4Jm)>CcGOYr3A94;r~#K83a297`FrknL_g=A&`v4^xq>Tq@)*GR z>5Tnx?^SD%X|zz>qECAsBt^#1eo^(R>Ia^D+koZ%;tn_+IPFeJC;J*V%*D05Tvq4j#Wc^r|{1`Dpj9d43^BXA&oDI6DvW& zg>HUP&V7ebRQ}OxAYvp{R6Ta_`Qw^(m58&qF+7hT$=$nwpQl=6x_R=X5diE@3fw6C zD%Fcg?q0LCj_l=MY-&1#l7BkVAlXKwe)2GKat7rd$Dyo^KH=g(r4u>9RDLSN`h!au zjbhplP+v6-Hp!Bcengi<+&fAkRbBu*oPJzWWwMshm(IeM8x&y`4bqx9nD;H1nyUYDK#0Vi%wzpm|!4L?~1E; zWix3JS={YgiPL&#u?LL)wAQw{7Pg&Blb{jtwd0XPDLjT1opE7pHNwaU^H;9N>M3Qo zeJ4(`ibM}0jhF|a>rh|Cb7?e}HgI{eoNbRiGuE0$idt%1WbXBIK&znr#Vj9d z{?)*ZR@qZ_Pu|^4O*NqnTT(ksE?qt{@(vjF$7;{gjMmzuu{l3BNe9$(n&>a}3z^v< zDyB6#+@l>-)Q?Z(yJ;iVgO(^J$>rel(TL#FYddHlb$Y z`{Td(RUJ}eZ5q56G90i_hC)VtY4Y8P#kNwkXM+4i1S+<(WuP=r5B=iZ;FTFa&BuO^=|`PYx;ZW4tdbHE(u{{Yuo zJc2&w8;YqiQ5jvhM-HMv6KTI-kJQnyQUgM;AAHe{{KD zDB$2{IIJrhl=Gv1V?=ijv;nTvPC8XLZ3Nz>8;eC*u_#yD(5qD|u-3B5 z%(;eP!j|d&O=!Bb=t4!xgSdmqKTm3*sLrM#xDr)!$?RxD=vKA3k|@D|721Am^rq?YSxr1iw|P6kronYR!9dosTS}zvX>*h5kZQTRxoJ(rtZiY0;AG&j z=LVMA_S@`J+{<@7vlayvl$?&|{3P*I4sBf^{rcbIP`5oOG3b@Uy?xGezg&T zi|)lLvDzzBp1QuBwye=cW5+_Bz5QxyO-5TZlG!4TIR5hp*KhKx_YvB(Qrw&g8UFxw zqp$=6$Gusw!oW~q*7`zwaUXRGPdZv(;}`{ADu}B@}z9m&^FQ2 zJmmX()fpWGve`cLLN_Bh;~v$qrueg!xCG~OVT^1T2K zL8k1E(jgj7IKTjo-RmY> za_G%&Q6!LXPBVae)sO7CSS7{9yk9m%yw zYT8|P8jAcmYlTEvkt{h5!ZL5rcGx!P>LH9ix*0t8f71yv`GhW7e_I5hT&w&cWsRf-g87f0aJQ z>F$|gSQ#RWaCd)R^fXeCx`?`2RqT-+SE}Q$r6{(&m2Kn>kYt7A3% zw&j>f9!6Mjan}Z;zui2FmKRaSI5`O4#67(#ZEcl@%%1w17SYRdJ4XV>+Z4aeDN?|8 z1K0lmtxRK;!B+r+*dUF$?0D-;ht5*b2>{8-a56`@^{D>f%URNeJ7lvGvT#g#@#@q!oLv2E>s#vPjl%bYHHjw^0B;z(2lmI(mhzeXeL_|-dz zVKQ&Xa)gn`BDRc-+P=f#l08QEVM4LQp{3)JPeEG}3wUkV?M06m#@uGO>wQ8?9bt?| zFb*f((4N1Tt=+d8q>Ci%sRnYPI#DR=+$*XO!x#gAJu{4An|coV+_VE*>4(W(s-H04r)rMl zKZRz270#RutWl|H7zc@XiOAreK!2TIv(qFNvPUw^6_Cae#uq2Q){9*;`#uA29$G-; zv2rt>fN|~o>RggqZd_x`%$KpawO=`<^Bq(j$~nzRV|*Zp++CL1uZ`K<$3NuOjCQLW zQ^=CJciilixm$m<2^fN*fZ96M%Pj~zrbL~Y2PA|zA74s!_1>SP$36*?Lf`~W8Jiu? z^`~`vB6(8Oir&uC%|PQaz`Iy(#zDydf!?fK+!!pH+5qUxcB3lfob@BWYKu+0xBF0t z+C8q(nPhgrE!=$Ex7MvB;`&xkGjl73RfHdudJKwA_td2BtcozC8hwS%>5P(=0iH(M zGJAem_pACYp3}%pp-rR~E?)`Tk8@OSrMGQCEaZT>+&^}DZt0Le8iv7Tw$(3VlPNJ6 zF^2n#*?*M{T@v3!YF}DI=AutDQnW?$=>T z^5}OB;r%J$D7L7od#UPuYni#z;?pnWhT(~gh7pM4Z$FhnFK=yt-tQYJ`A-B6eY?|c z?=0f1^=)rb?2ev9$rkd9A zB(-Hpx)4S~dm746y0y&ZO|_#mZuE$(?k>_s*&3YN;d-g(udaBdS<+Y$q?9WG%A5>T zce=E?fQl=)PCm);vJaok;bs(@|b zTNo8mEmt~%*_eSn3uAzB{Hs##%I8mgw=oq=;bl{mKaX$5sxzijDg;%G|{YW zZ5c}T>12?>0->BXcE~tCN{;GBb&XZmbB=kyKSr$)YY~xTCsrxjkjFc{&5S6H zxFsXpTkBFAT?X+4il*G2hBhnD)1_t2ZP2}=j_Ase{DTLn{VID9Jjok^>VPgs=lrU~ z`W%X&+D-PD{{Xs3Rv%8rp|`iXwVb@xlPVP|S(hMs9t|AHtpzHH#iTsJ0$p37+NvUM zU%BJ|0IyWREZfltiHTKXMeWpb-VzTA0Lk_5{(Y)Pie(6`6GsvNa(EeG$Ky|J!XD~R zxdq+JS=v0Q7?G5Lfz;2Z`W`Z{p z>(?ZE)m(i{)Hc_eft4JIVi{mZ4UXW}l<>Wjs(?oEJYm*F@1IJj_Tdl95Xo@mPtxl)rd21P&vT}OhbpExYd7@d#ELX4c+@SeECz0t{ znvIl^HM>OLb+~cc@$Xr)>a));l(MqIasKi8(aPlRnUh@BaWC9RW3gizhM%J30;|`W ze~0hx;n6N`Cujx4l6hWY{H0J2nB%u<$n%A~`3UzDH}_8htsO;V)2EIop!-M#ys|d~ z>(jm|N}9EjHf_DxJ*K3W^T!Nfq?3^ECp-DzliH(bbqM@Qe5nlfOs94>sww_wk=B`` zPX)EQPaFuMY$=XknL}rs6V5n4ojxr&V!0QPYIFUZF2MsK3>ft9g1PTmt42*0+i7S+ zb*IPXfEiHd=1s$s*CM62)b2FvX_Db13DO`GoxkVapB$D>^E`}p2MR>QY)*5=InS`C znPh@s^S!J#MEr=SdpPyyrEPwU%3QB-x78t&N?5Jpn3ch8%gH&;ee+Z`4NCIuk~^gq zv2P3)Vco~AXvusfa=Lb(DUMH-W#9LG_#e)w2`{5*BDk8|Zuwy>H$Lq3p;(?(x<#vd zXEvMdEs_c5Aa7+KDtghbNYPtFixiW>S3hW0#9)q~(nB5eg_dS$?9sfYm&+u|LFXR1 z_2?=cRpXY}!4H=6Tc{C0&BrG^^yY=}4ZEwN8@Z;{ZJ>_T5-T~IWUTlbfXCu%TF6;N zZxlA{N@12fDJqOVynT77W3_aSE1TDaW{Lm>xFJx#rhi;gT4_4dKbstDC7U@esy3Bw z2qX`0T2gAlj^pm^vK%t027{=#!nQJ>8*6K zqehBj8`pH8d8e?fSGc^jjT+(Lm7`!5(lu863!Vq&Xty=BFPTL&ixKKjOCYxtwYf33|IhXr6Awk1P%bd5O=dZn4NZ}KS)(d-h+vj9MlC6yWI6s{|u3{1_ zA(dK8Yz1?YexFKCYh*aVwRf-;xJYhMu4OAS?IsWqk9w%rT6UEhS{T51&M>LDw|cXB z)u}Gx`$F1@9b|UFaG-=A=klzZn53|{^6f3*a581PxcQWG+@1%nI@5~2JBnpz~+QbZCoAu>L9ZrD9Q{VDd-yq5O&GeNfX z*J5x(82A}spRXpPwuPbBt)A6kftEg|RBwKbzl}OlSQWPIv zYOuC1W3u8%MaD6h#uQbjrI}rH5~VU zGMxGj53kmvxYBMWn+D}1OkgSOJp~s+e8#EOBDt{<$f{h71JECTYOeDT1+;RmBp{r0 z&-AJXOEKF968!7P?V765%rN^~ugnMYq@ubC)-N^OODC2v&IihK!EZ{-f+uUl02Gj% zfCmPxjXFt#G-P3yBRm||&Ac(HMsDPZBmG)={ zbqqm*kckYRFeGtZE)vSdCHquC3`O09d5%uJ?;P+r=~*^6PpjF_J-iYuYDz}Z9Ouyc zRxp&CyV$unwPm?N4OL)VGyd>)9sR2HtbcA(CZvN^y27S1!<(yV=={@*Ygsm3vo&yYVn@mQ*DJsHla$!y5(?XGNGM{x-QkQ=CP z#-oq3D|YB>TEkq-scC$w%I&dcUVxsNUZ0g{UTAh<8_Y(FCyAoY7wReV+esJgTwSDc z(b=qbFLHxyj)i@VNp}pl7VNQ1VwdkLbp-kv*o(tAc?EAFX!{%Ly-Ud&`G z{KY?M8}47UgX&asy}}H2#VnB`@Nz*PF77}%q_vJa_?l~Zx8;u08OZw4D}1jT<9IkA zP!f`E$Rm;BL};Wc=LfGI^=Wlm^qOZpJ(H>sgExNL3iJ=Z5vBl#mg#8{3Lp ztD&2wt$l}FTU=V)Y`I)xp&xYpDU-R163G#fi2x}6b){#1(?j-a)!z{~4tOB@)k~XZ zx4Du6$BsjPcpj9~*37uc?8=PT1-KdKijAEjguIRwSF14q3XsCd+z@(WHBRc@JEUlx z%A*hu%eY{6#agkYn^z}UQbvz!60A2VR^Wp`1{s!U0NkH=9^(`$C~>zK83~b$4%F{B z;GszOk%l1T&`T(58jQ?fGHnVN3@F+$=~Q8`0^#Jk+@X#_4!Er;;fg>EtGS~&UU(d5 z`U+qy_Yq`{SaNx)PRl^$Nec#$#EYo%xj!P4kOgO6$7vnel`{;i3Y7j5dS}|YFE(e* zygLyFSjeM!12v7R+CvBpB<{~4A#$9Y4EoU*uBK{Q>R+{e)&)R_&M-2{2~Yti91sl_ z(R`1Yyv6}{JA8nF?bwRX(XM2eMA4?$pLmh*aD;F_J~*jPul8}|OCOVQGqhkiJm3S* z9W%veC^tO~)YM{8)bwX`l2~L#nnw(SA1)8KuOh8o>AIev_UI&zNYw%dI0P^sF~&2= zrwvMB0|_mn7YxJ648cRUPNu4~_d1=@!)-g+J-cAYydx3R1IK*gsyzF)XFg4m)S7>{ zT3_1570@iJvIHj^+xW7*Gn&;=WofBfE};hdh?mZmLy)R*lfdgpnp82%ZKz7yS{>Ur zI4vRRpU$y$XmKo8a(S~VxKxb~-fU#p;#CUU6;aLvhL0L}nB^s8Dnp#K2yj@-utvmdlh zNG|^Xc|hb3YQeRh>exkYkRfY=S(T&8sLv#4zB|_Jk_EIyk(MiP3Wbm5WBGLYQoOTuHMAt>=D;JjeAH6vZEtb*TSO7YRAba< z{{Yugv)~r(il&NT3qJ@qM+1S8PHw z07gE8tz*i@-ep;`?Vgm|yNvl#BNRJ|kPkm!)`d>n2R4>xLk65;jvcJ`Ge)O$hn8WE zexI#kYEW9-$Sv+}rGh0G2&Z-lIm>a@q_EVk(&4sD>K6@w*azCQuI+TWp-oL zS966O@CQ@H8pY~McUu*0VYf0tm`)-psLaJd1JHVZjU~;jNv6o{5xA9Ka6uo+#B}Gl zsq9kR-L$gZ3Co@8tfXxj;A8ToMxN4n9#=0Ya^7A}00V*kKUz(@=vqqJlwucl)_!)P z>?M~4VEd#mZaaM{3AG6>K!sE)?#>tH!TK7naP55{l48j_u>(AUD`m8)t*w?{DL`NW zVnHS3CADKt!h@p=2-4fE!*uV*hclO>sg@kf+<#Y z;j%JwT=cI5#XoXc1era3g+42ttu)al31A5gk?YUBCayTj#;xw8EVT%rXtygx=5JAq z9(s?$nP#hh18Oosm=lKia8#4q{A&D0DRv2>QV$1;t#N-1wVlF37FZ)v*iMYYvByf! z$pg{eg4c<-e-F#a`qZs|vAo&JK!^CGYM$=W>EvG_f`WQd zGRE>7X`l>HZ#$F_PfUs(fmso|tkI#iaUm`O6z3;3=`rr|)H%yzpRIBJ98WV^B^7+A zqvQe5o|V{^3Z-E_{f{%!mi-zJ{qRdq)2NC8csd zjlag1Rn?=BmMFKkAd!R5KAxO)s1s1PL_TfUfR!w)>5xzLKGY=aXB6yV>5)g_`&EGn zb&%(IJ4rsZepXqhX?E>aAmEx$EJ?$zK*8zhPh2ZwZgM&b)t$VlJCHO=Um#~R&z0Op zUtv6M>x#GXVM)$%3HJX0JXTu}+LPKT&p9|9sjW&)+Qy{zscjT)j9JOs&vQ|`OKmi6 zt{k$E2Hu91<1n;EoSYNE{t$otbnBTT-jTz|jY|{iO(bNwoRnUq3|tH=cKYV8>JFwW zJA;wBJ&Zn;Qr$*bn1D|LrdgHj#j~oiG;)PqG?Div<1nkMXj}DR_r7Qz#Mhw{QJ_BK4u5rZ(fJ|nvgI} zjVCeZEHT=lWmBKw3P2-1`KkxxGXcI`t+zNi9q~oFM>WiGHvpdW&ox<@da@Edc_8$u z7B*nKk{6Q6+pRVo39l|ChUhdwQP-@fqXc>y#MG~3mi{G{GW$42Bc^!Z4)u8I(Z>v{ z<(b2E+rtm8Ydl+BU&JOa_ISxS<1LSBYSGo1!Z&udrnZ5mNplFaOhp`025BGe_5OW$ z?@4=U1n(^GbsUph?ntgj-Ba`(aoo}y7namPy~Ii}85t|P7~?z>{uN#=G8-Euu~N?f z$af@wT=GV1pPN@JohlCYdlK4PTHjdy{lw_1-@ZY~Vd>a)`qo|SaK_Nzz_Q&Xu~bMy z{IO%J9^~iw)Ydvgvs~T8Wr=O2jI3$qZ2$wf zIq8}uY4o@)F<|7j3o3vDsU7_*OOb16Hrgf9Y?2+U%5(C&ax>0z*R5pCTJqr_;S)Rq z(=?Y_jJLB1SAgR<9M2Dipg+@j?;2dU>H@KTx zF2%gGJ8{^G=rsAm>E<+ix`t9cv;3)+mhsORaUKL|#yI)F$jwha_am9B+|0Ps+)B-K zj3g{fV00t@0IgA6XuG_&D$xMnb@|U6{{Sl8xtZ9$o%^3_J^GH^jXPuHNW zi#J<~ds!tb5{1E)&kNL6CZ!_Ds$5!>FCP7y`V;SiQ5M{_DcZ>irb5>-8DO`SyuX+< zi=5!|x0>nY)S41&fdBh789bC3|@HBVEqOI!GDqS~y)=V|CZpGqxIIApt)Mfr$X z01ZL%zOYZ3gMovN)luF?ZJUcUY*;=%Z2Qobp#<(|U1_ijHS*hT&`S*PN4;}!>AUW3 zPEVAaAwAgi#cj3i&CDKARPx)BM;Yh&R8RyG4eWtSXP$@rilo{_#WnF1|>~{zx zIBmq>V?Apw80D7cHFesXzHAH%?{4l`S;qoQtn+0;?cgp5CpE&}OSV&Q7=~<%W`8Y?sPV|RZRTKrE+`m!K(1-R<>6Y+(KkNXu(3<4s*v96^x9wFqS-=^ks=FKOmLMS7(7ZV|E^S0+^Ijo;PRiAR_udQrd`JoSzGJcgx&KtGVAu`0i zQn4p^03Hom+Tv>47Olh$1nIXPZZb!IYUynwj%7h^fm0X=4mk9pBv+4sn=$ux=Wy?e zr*Ud6<}$3Z#Tf*YjR_!*pjIvO7)nZBLoJ~U(lT?f{Jimu@toAGNF_JnzV6TIihN+( z+p^qQFdKsIV~nq3!8Lx~{@U*CWM~)7QIJAzAxC`r`%-VH`JVRB=kKGuo=CpR#|r-d zJ{uVN`qk-ymBK2Pk@!VlylZyu&eB_=$sDTeGR(M8ed=piW3^W>Mv>unNj#r10;SZ@ zUfCt09i(Z|=+=xKgHI{nAs@@=DxBK1w~|P<%mN_$#AFlfKdGvh*R693qA>|G3?VsZ z9;|zI;;Bsc7E-;sDip=Xh@X+sSu_j{vd8&^Z~$ZfiGF z)-3KV;`<|Rou&x58)B3l`Yl3b|_f03My{<)gr{J?n*0kG)uTqwP^gA~r@y!1Xn<_OB+CmJzb4W=-sWg-1E2 z(y~K$OG0G2j`rNNVYajF`MTq&q*&E*qxg?J)p?pdJ}9Axl8A5*ez`Qmd>2rP)UJ0Z zC9~Y-mAegSi%P{Gjt^QQD)%LL48*YdVx-s@fK(jwP`t6gX1iQ8kwkE#w@O6X&9RsF zsbM|5Z4)y{00%q){!LMm;UI0t0QShIM|`*XOqY|Qi7lH5N7IVU0N%$8vam?kEd2T& zKf<(%?lrNg<|v4h$O>|E>D&DJ)>N9Bu!y3CCArUfgM6T>WQLftW7rS>09_@daYYd< zKX`+9Q_y;6@v2E7-sS6=p%O@ga_z%z9e*01O4I~U&lJc{&UTP9k=nB*xE@O8Fz)=x z^ruG|F>h9385vdw7(F?mrE4)Y&7of6X%^&|lm`Uzz3SU)7cfU|ZY}c+oJiz--%tMl zRaLdNnro094pih3R;ISRztfW1YxRXzUHe#N0uIEEoy`()Ta9ycC%(M90>yOL148_n zo4D>rW$)9PZTxrASx#;5VzgMw{`yzg52(&M{VPrsvb2OmizJY8;BMMEILA($b?;dk z?ftq4+a3G+))9AUBx=>5-S;$XA%ft- zpy2R*DuuqIX=$q6TBJ7~RB;uNlwnk;#z*7LOQL9&GHMNbVR)cC8A8OXDvSa^$G5I) zehn8()mBODbsM&11j{6ms;ZoKBn)Pgnu~Tax=Y`Gfyr6PcYPD1JZ%iBP+SRBc4v;g zzZ#I+n?|>_k`SrS)b*(i&gJgs)1c?mo|S7?Vj_wkFtcsW;nN@K&syprvHNH^l~MK%s(RNuXER#9(E*iPlEaGLiDgok5y;UJyS%aoALr>> z&gOR0LAI+kt;5E^2|n&lHsYInPSv?{h8m%w!FN)lV3%zfhhiR6`Uhti)kR&e2@{Es|$lq>?aC z2L`l;+Zsh(Xlq-7`gOcK9r5~i$^QU9jb&Yk;qeS@I*psRzoUB}#(#gpH$sax?l+`?O%~ zYqCu^M=qQaKp3kMz~qt7Ks#1V;w7!rpf~Q0DnPEPKFpyI70Xj+R`mO113L$dIrKc!8Wh42>vn&M-*jyiGD znQ5mrysIOTybe#LPjPc^1;}hGP17J}1KyMJ1g)zOOTIa!n0axh2nS)(t|ZA7vYDg@ z0~z)7s2Uk2m7{DJBwj{vdUIJf+eE1xJKRJtLm#{-waUGTuFc!sfT8n}R~g1C6Pn@_Yh=aq&JPCY=yVctiWBHa?iqm9|lN4TxM2J#2f;)Ppust)BHdiqv1HI31g zWq92cwP=iJtFtlkLk^(Ub-b?(!dRaY433<0>svaslB-7;h#`?Ijb@i%xhM;dT1P5)fB(cUk!at=fGuz(k zHvUvzTE0V;TztTAGfS!1`2^g@{;wl)uZeBQhr8#&AG1-5PhE!dAFDojcT&DPR@66_QeR#~kQ9q{#K7gT*PqIl>`=*d70@iMIL>;EwR!Ax2NWd9AikV+!1_1M5`U z&MR1&J9XL*%t<{>L|QbnY|PP>_Z`Cvk$^|*TJpJuCS}Pu;11cS;$>OzSOx=;Q?m`= zvkY;cVNJWTt)+^0kxMjjU}c6kR9BWSEz}dnMo@)uy9bW{0O!)KUfo;UDVNL&qpo}X zDj4N=B1T0-VUTwRC)0pvSGC2ZwKSlf3ESk57mwv&4N{#U)9xj>xL~nJ3XsQxn#l9u z)2~$)5doj0soS{r$0xlzPCwd=8&k4&F}Vgca-@%~9Jbe?7e)B(p_bG^Z5oIh_k^=x zH@<5>P*1blFp4d}VM31lkMR`bx02G=Qx7ve++cZXPwvomRR@FhtM-#c4TZeda+7^3 z6XhTPU?04Fsq7s0RrOL6j! z{c6OqG?GQJju#$k^aL7pt^5yhaU4qm-XI%TrwYEg$31(}Q*5b1+ZA=&`zsi}#U}AQ z_WPBPa03noew4Zyxv|p)yu`#w^CM7l#AB;^dsd8!^Fsu8K26g@&zmG*+<@ZCA zxOL=b+*G(Vxn9vpK0&sL6{eQSZv=4L?g+Ao^Ckx*?maSaaY<`&YjCsqUTly^hB*-* zA-L{6bBtBntwc&LZS3t<8(G(9?=o19rvp56tU(Mq9lVlT!-D5KQ|9O2)BgakS?=^S zg7-CA7wq5|D56H2I9LhB^&_v$c^yB(r+Fh-!l+L;vEJj5KAaDK%Cf8`ypriByL5>i zM&@=Ukb}oxVO=${SX_y|*#s+S@)ujkn5oI+0h7*s1vx7{33GAPlyr>(-&D4})F){n zbj)bLj)R=@(>0f@+lIMRh(DBwyGU?*4D;TvYDpfe16#r_5eoTHZa+5M_2UD-Pin`! zw)+*LTOlfuuG5prt?4_x%q0mX=Po(N<5m9vwWPO*n56kjW48vi^+>MX(TuS$Tx1Y4T=Q5W zK*3w)13b5SxY?~nnsFt#)RDh>&hIACnvYF}E7xp3z!b4#$R3}msv_HPEs&z7)9;?s zP=+Y&-PYry^!Y|cN3SRJs(LL7PgI$wr$rWxrpFZ1tClU!4?g(o)|sW*rQN;U`kOLK zAIWan{{UH@aDPwEt7^+M+NoHhm6Rx0`s3dwwd~Xsjv&Kyc==CWxctxbtfcu>`m;v4 zpDNKKoxIbptfcam7gTPNfz*FWo_5p-R32mP;aD6JamnMF-nf!@p+@o^MJ<9$3=hoE z?iwa5hh-Ssk)LX>Y1~n#eaziD#iX41ldRW8f*ig|p13BrV9}x6(7QTb5y_E3j~>K( zRUI=_)AU<&BszA>uu8BJa`_$eT6TVZxY0$1hiU@<05>3dV;CIfrfyrkjN5B%5uZ9m zwc`rU1ZY^4Tqpo!;CuVleT}7yZIax4(hR95KZR~Gm=eox1>dB z4AGJrM{VJHD)t{!%~!Luv)r-VN+ZgRrbg+{e_BeWtkI>*%wg(x6I;g+jc^@^W&=BG zKJMWm8(}N_QAW~RqKDh&5cvR) zVOI&JWzEg8JC=hCQ~c{4?` z7qb1P)&{tVR6HXiIojCGV`)<)8ljEMWIhxyI2FT&S$3Lk2>nO)Z6BtFSqo&p^Q6o2TD2`NhIbMY4ADv)oUM8L1IA&PZ z888Wi7A^Jk9@Uu;)b%)3rnsNWiH25JILFegCYNt-cOZCjZ|Wm`jr@2 zE@H-;4aWP3?n5#^KR17+XsW|$Xk$g%gyf25oh#khtn*t-ZSxRTQ-;7jeujt~YMRlv z%rS*?lt#|g9W(A}H+!1hAx(|_!ows9h<9YT=Ofrt>}7^Kq&I3x0Ct=lAAUO4o~m9r zA-7p%-7D=N{{VCjNAjvyHc+LL6;(%%kbfGLE3HgDrT6MXJIm#;icni0E?WTBP&ZF; z`|G5b#CJ3?I9v}?xQzWO+nCxnfsB}t1tc~OJ-sTJ(5+QeD3UecvhK-a?^dm2cg$R} z)-7*gTRU63VqL(jsdV^7 zl031@+geEQS}#rqVOFjtgHV7Ot(H~YfKEdx$F*F73u|j}95D!hdq^nXaTb4+LERyD3B~)Zsxso}D8*$D!?@c@ILyuvre#;q6Rhs!2 zstI6|BnOmW{oZlM@~ZG^Yc-oiZtBh$E=kIRj^o#pSzZ?x_FAm(ZxnIcBJTbBe1nme zB$1A57`rEOs`p08Q#4E=RhKMD^4sSB0KRHTBM^VAcz$z&KK2RZf(A|jKdoQZV6|z) z*4Hu1=B7Z1j+yFl+N!|vTUfC` z5!iOBS|+OusJ7z*baV`rKd#|-JF+p(W0i^>ImBJj{Ma-*hDx^NG~F~x|(}| zzCg+cQoTk;r9-H^Q%*Fvt|X4)<$n6{w;e$_=xA_mUO~;uYQ-3iGZcG)iFX-X{(t0F zMygg)vlZAeouJm0y}#Kle#>%L1dA-Pk{UpxJqNBU5$+oP$z@=B%*?6H$JGZbWJC+Sb!fI^>RLr=HzLP+QxiTM)%=jshy zjr`4p!QQ0cXCQt({{Z^cly5GdPmUSpjx9z({;0M(o<5&ShR#w;X|mlv+SBKhj)RU@ zKhCjf$=&R3oK=$CcDDMAWWA7Q$0ukck_~O>8b8}CP5soS-s)^Dn@3;6@v0h~wZ+Y< zC8f!VX9tpdWA6Ty5?#7lEHm7yTq5j}SlEv+`eW0*Qf_gwGpQDt+!-`zrj1o1c1X@b z;{bQa^{#elU@vkl;o6(Tk-2Q(e}@&Frd(WELGxS%;FK}820P}eTliEZhTHcDjwcH=C_poe@Wv_~euPKGlG4_CaX_Hbv1R zBepAE&fXhqaSLM-mXI+$hyMUts!O?T#JrbYY?4aM9!BSrk%Lb>l4O-b0g$|${V+JJ zgx+N^2pLG^6g(fLTD7{4b&v@N^3KM}^*HPY<5J^d;`$+FxVDFK24GZWqbGhadkj=o z*H&7}nV~YLme|V;xiuYuv!4ETis@yRLR~@j>N8ZX^t&sKL(Q8|mvZfM0gw%u=sl^Z zpmR5-h|Mzjo7OC3?K#OE@%UAnYem1jiZcHI==fmIk{P%hXSH+jBWkyC+*{pV+eHWp z%JJc3`g8i$m5h;_soqJXK3fJwjehfbXSGXO5hZnamUZt6*hw618%m-+2I)5k9l*~c z9cwnvO@hwdRs;yg8_Qtv>-c?ZYgE*J(g}_SQRS37x$0?j=y5zMu*D#39l6M1{{Ysc zq||jYij-rilYeV!whMK498+7{{__Jl&#>>>vL+L16GN+7MI%}`!Bhi+KiTxJMAbCg zSuLIa0M)o5u)GEXu^l>AFWD`lm>34p*%yKS7@`#}t`MI#r#Bw4Z#*oEYvj!m;Df_4 z9+>s0?CnOn(nlSqlo@dM1B`RP&-v+Ff(Q}hk>HJ+aVsK^yX~4(y^hSeFXh76-ph>O zdS{`l=F{CqmUhsFJtJnCAck$LAIX;IanD@#r&)M<3oEp^-!zh*PH};rDhsW8#u-#B zjOake)3sfW>`bCXX%~Mv$?4jY=CE0_NLs}#Z7%#e9Bti#ao)40(5#bClT=BhhT+$e zr-FTP>rWClRwU)Jz@8eZUrJ@ZwXsgvJ^{ z5ZM`~kt6cTZ2(|x1GnQ)+oXk^q!YEpx~{^DfZnF4t)Gzuk-G+ISZ@CSKaFU<7F^(9oS#~j)GyncCmq)#uleGp z_7t6!%qi^{s>Z6|^&IWZI_}=;OM7sE_bx#jvQ2cisTx~s+@;xu7jNrTM$o?~>BwyH zO-z`}nHi>et*t`iHvvc97|Ht5Y4=lHql<{J!M6O}>UnLFSeFA8cgQ>Rm`9 zDo6z69Q`T}vz5AFC1QjOhCC8My;-faf+%$;IBj^nlzBEpiI}lvakL(pBpwDooh|*+MQUxqNi5-JX-b#c z%m-7?LB%pBYbeXW*Ir8;G;y&|csK-(yi|75t^SbVV-FL76CnGz{JA_*Ny}La-cHWU zqkI1V6a6+9vbh%FI5|62ln$H>iG2HXcKczB#J~=fQ z&eVBW>{%1052Y%GYh;n)F%!<-Fg>anbvP_gh$DhIgo+C~?>X(k>CIx4mEw)1qD-CQ zh{uMNC!Yd6!LyNq4gmwcIjrk@NSjQW8+3f5v1TKXed@){+z~+1%_JzSLXsD6C-A5E zH&3{0{{SlOXPr0iR#Cx0pS_;Bs!x(PGn&@z>|$F)%{D&h{ZG9^JM59d9!Vv#IW@g& zq-v{ky5`mhgRzn^BY?$oz}h+dhkC7dq%gRXa=O0axOL=Kv8@-mDxWvPWF+(XJ92W| zV?0#0HZIxPjKz)u*2RX7vPre3QNcMQ9Xkrzv$BoDrMzmp7bTeRKN_b|Es1NF;xnx5 zV}=q|NaVQPvtmCnpHeZ+MsDn8zBYeqGD!(6KvT+sI)713g>CGb^5)7%VoqDWsXgETU*Dn5Cpm9o0@OiuHh(MmQdWB9FKZLfX)P{qvkv*Y-IXVj8X?HjcJ%$ z5G0a7P8$P(P&h#m@|;FYD;|$9Uo2mEsHc) zHpWyOw+s$@4*1U%oplToYlbM5h0|h52UQ?;&0Dj-cYCC3X19XdA3K7h8OKxk)HnKq zmz3Mv8;eO-Z`xuxY~!IIcA_cWu}Vn44W+_rjddiyxGF&hAoS_Z@ZQ9(+adyREA`!z%{2nNh>NqBlc{XizOX`@M0OuY`I>kO?k z?H~d}F!bZEV_9ElbLM4^)6l5NBR#4kv&_S}X>o(dCytcZ-9@~P@w+5vmnQ?SG}KZu zNi9X5CGz&EEWjKV0R0*jqjW@U{E=L0;_$#~*Lic;!hl~xlH=XP_!{Hsa<1;mjQk1;yruQ(^a z6&|-^5QlPnjEBq{eq3?Je>!)&vm=?OV@2;`Sfgi!-zC-XY| zzc?MKT}ox8P|54*NYh0Wii}lRvOqr6jN=IJeF350G;xpOam8s#YK>}81`Uidp4@f( z>oNB*QVtv(gYR1i(NAh7-M5Z%dNB2=mGmaP?wN$#lw;9=H1v$!$kD=u&Uqt%zl~ed z^rLt;&K+&nAYrgun%23vvePV56pc}Oq)KvJ`sdT!(C4j@Qmt=wZeh)*O=LqzK*;YP zEzh=UxGyY=61GRzIH$`jlisz=^Azl9*;PC>xIlJ;heGEh+N(j+s_Fc6i=W zQ<7L@b@!|}$ckBRT6~rTVr3h)pIm!Zo&1wuTRdpX7Z?B>=kct_>~08H;}NkeH!&O~ zTy$EK6)QesZ6TT~C60ThyOrGSc2W*K&wgqv>+7gwo=D*Hqqkui{JG@ho;^RrQrfha zdW_a6M(EF$TOGm2_|UiT`S*yQE87wbhd4a)K%uR6Ej475v79tnE@TpmrY@k$sUshi z&ts0582qb8N4K6E0~C=qA!57Rqjl%$&MIpO&D>sOak{SUs;8@C9Grhz(1zHHjq^#o zfx`A5N{LCz@n(^VjinNy5m{Sqxu1DfB``7geJZuaziDVKg4(o#Mk|nUx1UUMd8_xf zk!qlP`&oLNaZ7Ke!Dkp;te80ju6gyQxmNp=DM`B~Pd&7Ec0xI~Dj%2()HXVNR%O{6 zEZt35XY&vMPC&=jmKbD`K^$XqwJYdLvek_i@W#(5Axm{Rs)EkO2R9x=Na}ZIk~8?$ zmHm{K_#Hs%Ln-I!oYfdzqHj!&opVBWR}^HKpQl{iY6no9e(mHc6WbkyUDG`GSytvS z7$3cimiptVq`bPfwGgYqPoBj|Uf%V7V10!ei0V?>XQiU7$A)4%hqt!d)a zqe(AYA_2G<#!p;-T5geZnPa$)I3p1`2ni(Po=-levug0T+LLWP$gNvi)ivvg;<1uh z{IcGBl6}?(gPxs7O1*J!dl7fJhji- zb`o4h*(dYq(x0hm*HhbK2A1AL2kj2)f!m?=1dv#?h97VJ!&mB&2Nx45l(}bh$hOri<;yppFBD4`i#51vf zyNoOG$6f-fl# z+0hJx=0Vu~MQIk&)XhU%B4(6aM%QK;454>oMFrUU4%H0I+LepDKw)VYK3ZjQ?T*ze z0)w36y*g={5Ue6nG05Z*_=C+#@*R{)k$FPeI1^~{kKI-2>rC5esIt!tyV~84%Yrfa z^%W$S5z5yPx-vV*^X=sA$5Bnut!yNYN4H}z1SkY!9mylst7URqQ6K$}aeV^nAjfSa zLt$a?3T)sL{+OAW*(AD(_#k6NW< zTw`USVe-lX0?foA^N;{scvIA2e8u2G(6l*@4bWR29H>FHD5 z1$|QTY%^wN>NyxV=klmnWMd_>*mGJ!Dk$iqO(lMyB*x`2uGaoOyJ;gR*I^mAL=PvX z>Io>NfE!0pPCr^;yjM-=Kx91Ovx>7uwRUE_x^26oZH*M|%CZa^t79x_r=L0D4oNTS z$33e)<59VLHv2WeXT~rW2C|yv?BJf(WOkM~_X)R-K{(@&%jro$wPs<+(zKbd(*2%E zZlQ`e*^kS(ed2nb^Xpju0Bb?0MFo|zP3AV+rwPIz<(~BIHfd&CM3zTKKuJbj+mo?5 z{5safm6g4kIEgkRF>|||`}6o!Jq?n&Lv&3sw7X^vFPhV_W_B1S{{YvDTYX7xZ=MBE z6@KaO$6A{G+}RTYZ6JaTR))bOxLd3RJ8kGgmrMM);Kaa!sYZf%=m#=u~v25CIerNksik>-;s zuCJ8?*EN-CIf^+~Cv2Ger;+%54ND6MY@@Y^m536>0X%1*{&hUP3|w99a-4SZ`J3c^ zTaro1HDFuE_mr}c+|hy4BoX{OyHg~SYT(Fm*OQvPS+r%)isstVONgE18OR8mx}Mcn zPLojkT)%7FA{hY~4!PqV)kf-DTdU=YSXta02M$3Wm228CFtkjY89h&Gr=ly7Jy9ji z%y!o8G}zdqkSj9oAMM>ZMW)IlnVZbYk_&;Kl=@bk%uq>c(Mp0;ZlIiG`&M1tcXn~b z4UX$|$jL2@gM-gXjV`UZO+H%^O=^#0Zw$>N&Z{6NhyVl zUSbB1b~?AEP}6R$wKF`+c<_Y=Cu13G6Z}ATO$J6o{f((5%o8_|=XTPG)b;9X zDYmp_N^nuPex`crI*gXF>8*0H+vGH^kYKRtLC0Fnn_L$c4`&^;c*#-aMS`Fa{6{CZ zJksAwb*A6R1orc&Vqr#?m40!%*mS5ReL*0SSfO`zP!WmwUHgu>z@nT}(T-7y_o8Xb z<$al=!>(5!DCm0rb&`hS4K{bQR9k~E2&O<}bjKqd>P<4=QCn7%QMe?t5C9v9J%$G! zr>_;XrrS@qj`8A+uARShBMt zt)V-wc!|Qp^(|6M{UpGV1o?r>_w~T47P@?wFA|t^WL^$w25Jl%~-A` zDlk*!UVglqNOiM+e>67HEPh($glmKVU~`{ZIjp_ZN{U(&X__Fikk2E%%!H037+-!* z;nJ*6xn+}N1~<;!Em`Hnv|7Cj{os%D9n~`s=8gTC74aepuudu6L8d;w4=ZZ~{x{%oJ!?kVC6f)bG5kf@1OVk8Zt`O|Z{u${V=4eQ3h>_SEeAo2NB82NWNC7kCu_vV>C;tR5$oB}wh zZk1I8A+qRpk9_2l`P2f6i_m1dPVAfwq~{nF9EGN_DBF=s1LgsZ#CFeWn*L~~iU5KB zPG5gvk;lJEnq9Itn8<=N(Lp%rMaY!ewb6vuF|1Nt8CXn15LDANmW{@49U5GO8;{J5 z{{Yt&4cFTmc1vKxPIqO$`#bc#J)4YYGl0z({f>lT)0uSR_J*-;LZ1YcLcjdb) zBnn@H8Il!fhs<^iF!vP<7P+$XXOvBu46#0XR<56{MLYn;=!G~xE*m|? zCv~bOa%pHRD2WaRA0T|ha((H>Kp2tRy+V?!88-dh*%;##D!j7@Z{q>SWx_stEAtM>0usl^#b z4I64~PiDHUquj&)03QJI$Z#ab_m`+RtjJ)VUz2DHA17!8{FIKWw*LUTa*Soi-bPRf{{RZbG1}T)ZZr8szHN*? z_v2kO#2_no7{DA&9(d!war~>1)UD#Y)Djt4iYWj{hmsHJT0>HyHun+;X1moHgt@oc za-0pPw@$UT%NpAVWZKiH00tSy*XhUSQ7T$lICU%>j)SdOx3m*XWOe|l+!s8Of2B=H zy)1ObF}36}!%j|-1+n(925I}Od$sKv=Sr4Y8YC`D9 zMhC1ohszn7@o|!c3tKYRqLLx;7Cu)vK1EDASQbea|-bT`R=cNH1 zbsw}z!98#R^sL+2Ueq8+eBI}kVZi(aXd=CA3weX!&9zyG!5KeVp2JFLpo%ebZye$brlUnfVXwP#UVzq9`0R*qM}^JHDij`;QbYc308Z+af)`bUtBzr0WmfA&#LT2k~h z_)xlbpLJ;*vfRwlNXh_3Y^wf0&~&S|xAylJ5|=9y$HE70PhVliam79hM7PhMGtAA7 z3WB)$XB61GK+mINLPrNj}x znH^2d7_1f(iIWe)o zKK_(m&d7w-?DsNkw236TdxelFb-`SMq5kGc$47tud>gANl0tg=qgYurl z8cWARcN?I=BL|M6hbGvRpFb-z78GFXC=P{B1a&oTTUkZGyKSu70`R!Pfx!H!j~3jJ zKIqToTOneS2{#kCugKisan^}QDoMu88F0vum0^vbqA>H1SL2*zr~g`w1E*chxx4(A`pd(wIJFgkNv6Zuv^K^`KA46`2R-`25rpa**nSdpGM%_Q%s z5uZC2>KM`mk}Ph|1Ti~-p`jBhA}e*|ZuxWc&q_q{OZkCIs+@eI993OGZlp+(*_Bkt zHftykhkMk|ncKJ`isGwipsUwN+HV9v^X+78;>wIEz%bgP<6i*>xZBPJlA z4C5K9tL4iQx;Q(IKmBy z0pNky(~EBD+JkPSYy!3!mm{$mtqTj#u&uPXRZ>cN5$T_5#=f0nR94)o`T582A9{32 zHvhpkB*Zz{QH z6-xK&KPqJQ?{hS9v!RMJmm{~fC`lq|)8r&X^JZB}ovn@!r==HnO=%hcibx!{YLZ_+ z?5FJG<^zF_Gf-PS%%PU%Tn6<{%$GGYs=qALTq65Jmu1K!>I~ zQ4{9rBiXpI+P_*Kb0?y>su|=|UUzUgt5(tf0Hkb)31tHyumofM+LVO1ghvy?SfLNF z=LD19uIX=WX=E-g9yBGe8M^1%qEgdCo^=~UR~Prlp(Ko=pOj8^&ctRju^ z^1B1b{Q6a8v%eaLnRzPtEJ!(!Ve`-d+N7|vywa}kCX(g_)B47Mfw%F;rxjf{GrMnM z{Hg=7i2}LgoxOgwg&@1sHTdoAq*&sC)>nP!EsjEuOyd=-w?ag~K_qf=lvr&=t}3yDDM zfu6sOW-X#z{ie@xiQ9k=O~a5urKQPwjK79UYqau_Ufh4JMElB5bM4Z#?Cl}9-z;UD z%<41!DwVSdFCt0FNtJdMBZur&t6tjI&bkpq+c+7>qEAgnrtDphpE?7Qy?N?Cr7YKy zOSDNIzz%;JvvHH{6O#h>IwXz990(u9)lHy19UOP`crbO3g& zc=fGFZTaJT58_|&sIE1QLg3)asJ@Y}^{H{2(Xw1B%SO(jVp-D{FvWw&kAgq?^vBb* z(520-x&S#LLjM3N;@U$a7c#qJzW$Vji{vz{PrI5umzdQFcRG`#*v9*Xx;Hrl@(BJ_ zUjFtwWsV4ueAXe906tHAdi52NWqCc5l^K!}erU^npwx2AWS&ch7-)|)f^hGuJL zERy`H8=q`*Rpf@`pSpum!dkt=Yy0TIV5hkBKb>R6X!q9k&n(hMF5xo|e@|ilHK{PX zmOai8g*n)YdJj@h=~{L$+s|Vhih;Ls$I^~&R4B zmv~ZlmvD@qF1wG^Vl9wxV}3?ER+WqhmXNHBBtSVno|RtQ zt5}0ERoXL_t8fJoF2j}Q2mC5z^s%I4cXA2t{I-$ACtw&=10<9so{n=(5kT|y0@h}D+=WIBIZX=CUjK;7zeFRNZ2|S z1Cl$MuNopo6+?n~&U5+G!iDHbKIW4WLwvwNEHDRpwPOHvlImz#qlJ-?gMq)El@+z0 z_MOPL5{TQbNY53udutp?X{TD}Y-chs=Vn~0-%HnI=gz|Hc5>yKZp z5nRbW=v;iwlB~HI_RVzicQA9aWu~{ax1E_47H3VWKb?9-f}G`?i7@potJj2xGa2Jds^2=#OgXBLJu*upIHz@~n1_A_ft1 z2`Dl5bLm2r^%Nz{QL?d`@-?_sL`Gx1vB^2d6{5DWhf%Z%qaQ0N$YJU#e-7TM6GI$K zh9@Y#XK|ms{OaZWVloIIksu=;{Y57y&0;y)m6=xGM~Yu2>+SQTjq@-JHuKPPQQX)p zS0&tSnJ_~S;uRz}4Fbs%Mjk9>H+HFKoX+UP94dw9^QCuklA4yg88?WL+@!?;W*H{6 z1ky;+y1v4IDiA*C{}rPS4nQkFgIJX`tu$Z`4}l#ySX zYez_Mj99T;brs~SkbZ=ouQAV7msn|?us)L?fcBxAEi{)5=PSy7mh_imoi`~k6KA;LX3VSX*Q({ zu}$S$N+or|WljP5`c*qAZ|x_TOLSH_2nvb@0**O8rlXDi(6Gl1#Cy|ekyGXv$MXLG z8nUp;W0GlFFroIOWc=S#MUv1X2(1gZT7pYt(5CI|Hj+jLDvV?R0qlOXn8OV89jMQK zx#onDG>jB>B}8eAtT@`aHD9Bt{vNdd05FJ|P~&jHr`X#)<r}Lsd5VBOTqyS@p9R&dw{hFtxsxqwfm35n_n}ISYUfIq6LiBP4r9#6<&Ye7t&7-Gqu62g*1Ab`&;3PpF_S z3jvm6*BzjOMLrq^;*OGQ@zpg4hfI>^q#&cSxFz#yT2e z+Sb;=o=MWzc;UzT&OJTr4&}|npCNG$PSD4xca z*xU=S?2qyQyc3?)Hiebb-L35!7(?UDcP+U7p4F{mdu?kVkzzuT=Wr*X>~oL8qP@R@ zswQhw)tKQm=mGTr9 zQ-Sh>#~(_{lHtVWLnvEy2XnXG{{T6rT`!p`OtO+QjlP)nrpgpRZJGp{+GgLii3dG8 zgT-86qp=k#N<%LgG?2{7t_B%ccOKPl(%R!LsUF-f%1Qg*O47E9&_xtzh^Pn3q~{0M zCZSKX$0NMJv2T@79R55}%s~51I^OC)FWHNz3!IUIR=lW-9J0G9DjcuON3LoZ)eM6S z!;hM!ir#th(6^Q|g#?VBQ&Qhhe(tS|XmT>l7uPM)q#Kb}=J}V9eJNwNYo}mJpp5M$ zMsk0pE5_FE zx_tLn9%68jFz2s5Yfk4(xwua$fB~_&m;V5-k7|cBovvdksI-$g%r@XrwlbbEN*ZQ$ zX&f9Jjyj)O*l!Ko1!EP;voB{&p#C{ECAWs;BsJ8-0DaWR{b=@)?pv!*a~C_@MyOr7 z;Xx<-^sCV5_tyZB5)uCZO^JRNH14n9)8bfThz7^Y8sG&UwT%L_rI9v+jIk${NUZN+nM_H~p>21C1AIX=sb~2H=84!#PlEidcPcZ4oa=O(sBk-c+b|H_ENc4GHs9y z@S_9qsVy(2wS-3^M=sfSsC@jbp1B=)q}}!#mY(4NEflR2u1X!O3C}~#QAq7l{$DCW z^RO&bFgWgim0tOL_m)h2&RMWA)aI&O#cyKMUBe4B#d%oC{{SMID*9?obri4UeV$mx zHnC;rBe?cFRJKU}0Bg+h@7x<^MlsJ%z*cjDV*BOJ$t) z{A#3ZS|H7l#xah(`cgzv>PHQncHqqvVOQ>vyE{$+z#Xbb!E699am`{rtvvecS)jy7 z+e)4Z_pP1zUPlMG8Cf%$Vf*ds8%hF)VFRvV$=hW9e3}4lVZQ8`)R$HO)EmJ1veXwRxSE z#eFedn&RS5y(8dp*njo&S`^shSeI%H3U_+VyStdP%AZTNd-yM%cS{KhSO#EJ4!-{Y z{d%8Jw=-QW)}bu3D3{BI-XJ#P83d8WFh(my!`a%|Lk!GAsf4&emO1?TVxpGZ*~o0z zfb6RK74!0(;GA^kx)i2SlN$0!?37KjE;(`Hyj)uFg@x9nV#ukkA#eVRT#K? zo8ugNimo@dKKJ8Jz+Okm&IUSXr!^ud1hC+9)2aH@Xm|q{&q3`{gSWR)%}XCGz>&Bf z`RIA5rz(-|-Hdm|IfqVh#WAo^m5+X%`e*$8C_`gXMUi(L22r~nm0xT+nvKhajl*L( z;;UH7(p+3SeZnw3&04qeY#;{WSmNGBOPpen8MK8H9J6bof*b1OoR=CiOHxZ}Sbm3^7wV1Tg*Kp>_I5&37eRlB%L zRcu4F58_<$^vR_ONxKy~aZ2oB#lG@enC59@RN^q)DDTJXOt_L%Ve{ok#@c{h#~fSj z)sdSdu}!3(Q&0gM+g?UyU^g}oKGnKMJADx8wmFtF9(H7@Q`5C5V8#F<+E`#@@J3HG z$c${w<;coFAZ{bS^P~?th=Z?~&e$CGrb#uhFtCpHbT?TWL41Bg(yiOYG;Ur=6KMw+ zZnTo{2RfkVZ zRPoH#*gi0^_+px%KZ-#VcuW)8-b-EOP^g z{pRHa{d4V0aW|bOo4br{QQVH_^r}orbwPuY6>>dD{{R}${>)9I=aX?CcpQF|os+pT zQH-w0#pJQuHMo(H3LFT|2q)gE`HX=tt+Ly>X%8iO9(`(A+H-J=ZmJ_B?j(bO_4XB3D&uah#i5ApEX+W_;}tZQ zM%@IGC^+Fp3C&Tyo>-+>U;Rpu6yRg<&pGQ%x|mCQ2_kZ(M#Kjrp605;Y`^xXS2N1n zH{jH!I=EKay@fdiET8FVP=I~oP}n|13ha-+DliOY)sNi9~n`R??YAs zn%cnnjL|!xkg(b0lhAY`q`i_$nG)a4c6jjSP5>Z#)%l~dg52C$!t+d9<`2nJ?0VG; ztBI~%ph%uK3Ou}EZYPpItqI&k<<<$?70F0s?U9mw@F;uhTS<`|vl$4$11ALcs8%~$ zX`qJahq@pTWCM_W>MQxKJnLy-K?*V1_cF(vXX#4F5=&yqy10UQbLA{48DS}ILC5&j zDBkGD1o4D-ciSOOc&HM6mlxB@g>p6t18xA5(;oGuWDTB{&dnJpcN9b_Tz($Z+OQ%x zUfWQM;1$QG;aB8HRI`#vrc1cwXyPhEAs;q#*EGwE ztII{Yx|Ku5p;|w1c>c95YBi>!LveR|ZIsMdatM{UBZ|^g+<=qv;~DA)YNqzl>UK~@ z%+Wqn;bI9CzlE|(y-xFbRMVDi<;deLAdV-ic2xT0CG)qx)y@a zJ6+ykq;6Gzx+>O@p(T+Q7bHn^M_$S7D`vUsRlbop>ejxI3X0|rv_-RVRtyt(y(-4aS}9f ns`+D*?Nsl)LoAQwSU1WzHw*_q?vGD%S~y->8hbY>Ss(w|u@bn% literal 0 HcmV?d00001 diff --git a/train.py b/train.py new file mode 100755 index 0000000..a17e396 --- /dev/null +++ b/train.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 + +import torch +import torch.multiprocessing as mp +import torch.distributed as dist +import torch.nn.functional as F +import torchvision +import albumentations +import albumentations.pytorch +from albumentations.pytorch import ToTensorV2 + +import numpy as np +import pandas as pd +import cv2 +import os +import wandb +import copy + +import unitopatho +import utils + +import re +import argparse + +import torchstain + +from tqdm import tqdm +from collections import defaultdict +from sklearn.model_selection import GroupShuffleSplit +from pathlib import Path +from functools import partial +from multiprocessing import Manager + +torch.multiprocessing.set_sharing_strategy('file_system') +manager = Manager() + + +def resnet18(n_classes=2): + model = torchvision.models.resnet18(pretrained='imagenet') + model.fc = torch.nn.Linear(in_features=model.fc.in_features, out_features=n_classes, bias=True) + return model + +def preprocess_df(df, label): + if label == 'norm': + df.loc[df.grade == 0, 'grade'] = -1 + df.loc[df.type == 'norm', 'grade'] = 0 + + df = df[df.grade >= 0].copy() + + if label != 'both' and label != 'norm': + df = df[df.type == label].copy() + return df + +def main(config): + checkpoint = None + if config.test is not None: + print('=> Loading saved checkpoint') + checkpoint = torch.hub.load_state_dict_from_url(f'https://api.wandb.ai/files/eidos/UnitoPath-v1/{config.test}/model.pt', + map_location='cpu', progress=True, check_hash=False) + test = config.test + device = config.device + p = config.path + config = checkpoint['config'] + config.test = test + config.device = device + config.path = p + + utils.set_seed(config.seed) + scaler = torch.cuda.amp.GradScaler() + + if config.test is None: + wandb.init(config=config, + project=f'unitopatho') + + path = os.path.join(config.path, str(config.size)) + train_df = pd.read_csv(os.path.join(path, 'train.csv')) + test_df = pd.read_csv(os.path.join(path, 'test.csv')) + + groupby = config.target + '' + print('=> Raw data (train)') + print(train_df.groupby(groupby).count()) + + print('\n=> Raw data (test)') + print(test_df.groupby(groupby).count()) + + if config.target == 'grade': + train_df = preprocess_df(train_df, config.label) + test_df = preprocess_df(test_df, config.label) + + # balance train_df (sample mean size) + groups = train_df.groupby('grade').count() + grade_min = int(groups.image_id.idxmin()) + mean_size = int(train_df.groupby('grade').count().mean()['image_id']) + + train_df = pd.concat(( + train_df[train_df.grade == 0].sample(mean_size, replace=(grade_min==0), random_state=config.seed).copy(), + train_df[train_df.grade == 1].sample(mean_size, replace=(grade_min==1), random_state=config.seed).copy() + )) + + else: + # balance train_df (sample 3rd min_size) + min_size = np.sort(train_df.groupby(groupby).count()['image_id'])[2] + train_df = train_df.groupby(groupby).apply(lambda group: group.sample(min_size, replace=len(group) < min_size, random_state=config.seed)).reset_index(drop=True) + + print('\n---- DATA SUMMARY ----') + print('---------------------------------- Train ----------------------------------') + print(train_df.groupby(groupby).count()) + print(len(train_df.wsi.unique()), 'WSIs') + + print('\n---------------------------------- Test ----------------------------------') + print(test_df.groupby(groupby).count()) + print(len(test_df.wsi.unique()), 'WSIs') + + im_mean, im_std = [0.485, 0.456, 0.406], [0.229, 0.224, 0.225] # ImageNet + norm = dict( + rgb=dict(mean=im_mean, + std=im_std), + he=dict(mean=im_mean, + std=im_std), + gray=dict(mean=[0.5], + std=[1.0]) + ) + + T_aug = albumentations.Compose([ + albumentations.HorizontalFlip(p=0.5), + albumentations.VerticalFlip(p=0.5), + albumentations.Rotate(90, p=0.5) + ]) + T_jitter = albumentations.ColorJitter() + + mean, std = norm[config.preprocess]['mean'], norm[config.preprocess]['std'] + print('=> mean, std:', mean, std) + T_tensor = ToTensorV2() + T_post = albumentations.Compose([ + albumentations.Normalize(mean, std), + T_tensor + ]) + + print('=> Preparing stain normalizer..') + he_target = cv2.cvtColor(cv2.imread('data/target.jpg'), cv2.COLOR_BGR2RGB) + normalizer = torchstain.MacenkoNormalizer(backend='torch') + normalizer.fit(T_tensor(image=he_target)['image']*255) + print('=> Done') + + def normalize_he(x): + if config.preprocess == 'he': + img = x + try: + img = T_tensor(image=img)['image']*255 + img, _, _ = normalizer.normalize(img, stains=False) + img = img.numpy().astype(np.uint8) + except Exception as e: + print('Could not normalize image:', e) + img = x + return img + return x + + def apply_transforms(train, img): + img = normalize_he(img) + if train: + img = T_aug(image=img)['image'] + if config.preprocess == 'rgb': + img = T_jitter(image=img)['image'] + x = img + return T_post(image=x)['image'] + + T_train = partial(apply_transforms, True) + T_test = partial(apply_transforms, False) + + datasets_kwargs = { + 'path': path, + 'subsample': config.subsample, + 'target': config.target, + 'gray': config.preprocess == 'gray', + 'mock': config.mock + } + + train_dataset = unitopatho.UTP(train_df, T=T_train, **datasets_kwargs) + test_dataset = unitopatho.UTP(test_df, T=T_test, **datasets_kwargs) + + # Final loaders + train_loader = torch.utils.data.DataLoader(train_dataset, shuffle=True, + batch_size=config.batch_size, + num_workers=config.n_workers, + pin_memory=True) + test_loader = torch.utils.data.DataLoader(test_dataset, shuffle=False, + batch_size=config.batch_size, + num_workers=config.n_workers, + pin_memory=True) + + n_classes = len(train_df[config.target].unique()) + print(f'=> Training for {n_classes} classes') + + n_channels = { + 'rgb': 3, + 'he': 3, + 'gray': 1 + } + + model = resnet18(n_classes=n_classes) + model.conv1 = torch.nn.Conv2d(n_channels[config.preprocess], 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False) + if checkpoint is not None: + model.load_state_dict(checkpoint['model']) + model = model.to(config.device) + + optimizer = torch.optim.Adam(model.parameters(), lr=config.lr, weight_decay=config.weight_decay) + scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.1) + criterion = F.cross_entropy + + for epoch in range(config.epochs): + if config.test is None: + train_metrics = utils.train(model, train_loader, criterion, + optimizer, config.device, metrics=utils.metrics, + accumulation_steps=config.accumulation_steps, scaler=scaler) + scheduler.step() + + test_metrics = utils.test(model, test_loader, criterion, config.device, metrics=utils.metrics) + + if config.test is None: + print(f'Epoch {epoch}: train: {train_metrics}') + wandb.log({'train': train_metrics, + 'test': test_metrics}) + torch.save({'model': model.state_dict(), 'optimizer': optimizer.state_dict(), 'config': config}, + os.path.join(wandb.run.dir, 'model.pt')) + + print(f'test: {test_metrics}') + if config.test is not None: + break + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + + # data config + parser.add_argument('--path', default=f'{os.path.expanduser("~")}/data/UNITOPATHO', type=str, help='UNITOPATHO dataset path') + parser.add_argument('--size', default=100, type=int, help='patch size in µm (default 100)') + parser.add_argument('--subsample', default=-1, type=int, help='subsample size for data (-1 to disable, default -1)') + + # optimizer & network config + parser.add_argument('--epochs', type=int, default=50) + parser.add_argument('--lr', default=0.0001, type=float, help='learning rate') + parser.add_argument('--momentum', default=0.99, type=float, help='momentum') + parser.add_argument('--weight_decay', default=1e-5, type=float, help='weight decay') + parser.add_argument('--batch_size', default=256, type=int, help='batch size') + parser.add_argument('--accumulation_steps', default=1, type=int, help='gradient accumulation steps') + parser.add_argument('--n_workers', default=8, type=int) + parser.add_argument('--architecture', default='resnet18', help='resnet18, resnet50, densenet121') + + # training config + parser.add_argument('--preprocess', default='rgb', help='preprocessing type, rgb, he or gray. Default: rgb') + parser.add_argument('--target', default='grade', help='target attribute: grade, type, top_label (default: grade)') + parser.add_argument('--label', default='both', type=str, help='only when target=grade; values: ta, tva, norm or both (default: both)') + parser.add_argument('--test', type=str, help='Run id to test', default=None) + + # misc config + parser.add_argument('--name', type=str, default=None) + parser.add_argument('--device', default='cuda', type=str) + parser.add_argument('--mock', action='store_true', dest='mock', help='mock dataset (random noise)') + parser.add_argument('--seed', type=int, default=42) + parser.set_defaults(mock=False) + + config = parser.parse_args() + config.device = torch.device(config.device) + + main(config) diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..71d4809 --- /dev/null +++ b/utils.py @@ -0,0 +1,164 @@ +import random +import os +import numpy as np +import torch +import wandb +import pandas as pd + +from tqdm import tqdm +from pathlib import Path +from sklearn.metrics import balanced_accuracy_score, roc_auc_score, confusion_matrix, recall_score + +def ensure_dir(dirname): + dirname = Path(dirname) + if not dirname.is_dir(): + dirname.mkdir(parents=True, exist_ok=False) + +def set_seed(seed): + random.seed(seed) + os.environ["PYTHONHASHSEED"] = str(seed) + np.random.seed(seed) + torch.cuda.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = True + torch.manual_seed(seed) + +def binary_accuracy(outputs, labels): + preds = (torch.sigmoid(outputs) > 0.5).long() + correct = preds.eq(labels.long()).sum() + return (correct.float() / float(len(outputs))).item() + +def binary_ba(outputs, labels): + preds = (torch.sigmoid(outputs) > 0.5).long() + return balanced_accuracy_score(labels.cpu().numpy(), preds.cpu().numpy()) + +def roc(outputs, labels, average='macro', multi_class='raise'): + if average is None: + outputs = torch.softmax(outputs, dim=1) + else: + outputs = torch.sigmoid(outputs) + return {c: r for c,r in enumerate(roc_auc_score(labels.cpu().numpy(), outputs.cpu().numpy(), average=average, multi_class=multi_class))} + +def binary_metrics(outputs, labels): + return dict( + accuracy=binary_accuracy(outputs, labels), + ba=binary_ba(outputs, labels), + roc=roc(outputs, labels) + ) + +def accuracy(outputs, labels): + _, preds = torch.max(outputs, dim=1) + return (preds.eq(labels.long()).sum().float() / labels.shape[0]).item() + +def ba(outputs, labels): + _, preds = torch.max(outputs, dim=1) + return balanced_accuracy_score(labels.cpu().numpy(), preds.cpu().numpy()) + +def class_ba(outputs, labels): + _, preds = torch.max(outputs, dim=1) + preds = preds.cpu().numpy() + targets = torch.unique(labels.long()).cpu().numpy() + labels = labels.long().cpu().numpy() + + class_ba = {} + for target in targets: + class_labels = (labels == target).astype(np.uint8) + class_preds = (preds == target).astype(np.uint8) + class_ba[int(target)] = balanced_accuracy_score(class_labels, class_preds) + + return class_ba + +def recall(outputs, labels, average='binary'): + _, preds = torch.max(outputs, dim=1) + return {c: r for c, r in enumerate(recall_score(labels.cpu().numpy(), preds.cpu().numpy(), average=average))} + +def cm(outputs, labels): + _, preds = torch.max(outputs, dim=1) + cm = confusion_matrix(labels.cpu().numpy(), preds.cpu().numpy()) + print(cm) + return cm + +def metrics(outputs, labels): + return dict( + accuracy=accuracy(outputs, labels), + ba=ba(outputs, labels), + class_ba=class_ba(outputs, labels), + recall=recall(outputs, labels, average=None), + #roc=roc(outputs, labels, average=None, multi_class='ovo'), + cm=wandb.Table(dataframe=pd.DataFrame(cm(outputs, labels))) + ) + +def train(model, dataloader, criterion, optimizer, device, metrics, accumulation_steps=1, scaler=None, verbose=True): + num_samples, tot_loss = 0., 0. + all_outputs, all_labels = [], [] + + model.train() + itr = tqdm(dataloader, leave=False) if verbose else dataloader + for step, (data, labels) in enumerate(itr): + data, labels = data.to(device), labels.to(device) + + outputs, loss = None, None + + if scaler is None: + with torch.enable_grad(): + outputs = model(data) + loss = criterion(outputs, labels) / accumulation_steps + else: + with torch.cuda.amp.autocast(): + outputs = model(data) + loss = criterion(outputs, labels) / accumulation_steps + + if scaler is None: + loss.backward() + else: + scaler.scale(loss).backward() + + if (step+1) % accumulation_steps == 0 or step == len(dataloader)-1: + if scaler is None: + optimizer.step() + else: + scaler.step(optimizer) + scaler.update() + + optimizer.zero_grad() + + all_outputs.append(outputs.detach()) + all_labels.append(labels.detach()) + + batch_size = data.shape[0] + num_samples += batch_size + tot_loss += loss.item() * accumulation_steps * batch_size + + + all_outputs = torch.cat(all_outputs, dim=0) + all_labels = torch.cat(all_labels, dim=0) + + tracked_metrics = metrics(all_outputs, all_labels) + tracked_metrics.update({'loss': tot_loss / num_samples}) + return tracked_metrics + +def test(model, dataloader, criterion, device, metrics): + num_samples, tot_loss = 0., 0. + all_outputs, all_labels = [], [] + + model.eval() + for data, labels in tqdm(dataloader, leave=False): + data, labels = data.to(device), labels.to(device) + + with torch.no_grad(): + outputs = model(data) + loss = criterion(outputs, labels) + + all_outputs.append(outputs.detach()) + all_labels.append(labels.detach()) + + batch_size = data.shape[0] + num_samples += batch_size + tot_loss += loss.item() * batch_size + + all_outputs = torch.cat(all_outputs, dim=0) + all_labels = torch.cat(all_labels, dim=0) + tracked_metrics = metrics(all_outputs, all_labels) + tracked_metrics.update({'loss': tot_loss / num_samples}) + return tracked_metrics \ No newline at end of file