From 0e8d2c3c4fba01f5d5531547644e91a7c4ae156a Mon Sep 17 00:00:00 2001 From: wangchuang <994001556@qq.com> Date: Tue, 17 Jan 2023 13:04:26 +0800 Subject: [PATCH] =?UTF-8?q?ai=E5=9B=BE=E5=83=8F=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SC-boot/build/output/ocr_result.png | Bin 0 -> 43291 bytes SC-boot/build/output/table.html | 1 + SC-boot/build/output/table.xls | Bin 0 -> 4096 bytes SC-boot/build/output/table_cells_content.png | Bin 0 -> 23249 bytes SC-boot/build/output/table_cells_detected.png | Bin 0 -> 28449 bytes SC-boot/build/output/table_texts_detected.png | Bin 0 -> 19331 bytes SC-boot/linkage-scm/pom.xml | 103 ++++ .../jnpf/ocr_sdk/OcrV3RecognitionExample.java | 83 +++ .../utils/cls/OcrDirectionDetection.java | 141 +++++ .../utils/cls/PpWordRotateTranslator.java | 69 +++ .../ocr_sdk/utils/common/DJLImageUtils.java | 99 +++ .../jnpf/ocr_sdk/utils/common/ImageInfo.java | 48 ++ .../jnpf/ocr_sdk/utils/common/ImageUtils.java | 262 ++++++++ .../jnpf/ocr_sdk/utils/common/RotatedBox.java | 29 + .../detection/OCRDetectionTranslator.java | 574 ++++++++++++++++++ .../utils/detection/OcrV3Detection.java | 34 ++ .../detection/PpWordDetectionTranslator.java | 120 ++++ .../ocr_sdk/utils/layout/LayoutDetection.java | 32 + .../layout/LayoutDetectionTranslator.java | 104 ++++ .../ocr_sdk/utils/opencv/NDArrayUtils.java | 64 ++ .../ocr_sdk/utils/opencv/OpenCVUtils.java | 72 +++ .../recognition/OcrV3AlignedRecognition.java | 129 ++++ .../OcrV3MultiThreadRecognition.java | 194 ++++++ .../utils/recognition/OcrV3Recognition.java | 140 +++++ .../PpWordRecognitionTranslator.java | 108 ++++ .../rotation/PpWordRotateTranslator.java | 69 +++ .../utils/table/ConvertHtml2Excel.java | 235 +++++++ .../utils/table/CrossRangeCellMeta.java | 42 ++ .../ocr_sdk/utils/table/TableDetection.java | 227 +++++++ .../jnpf/ocr_sdk/utils/table/TableResult.java | 31 + .../utils/table/TableStructTranslator.java | 246 ++++++++ 31 files changed, 3256 insertions(+) create mode 100644 SC-boot/build/output/ocr_result.png create mode 100644 SC-boot/build/output/table.html create mode 100644 SC-boot/build/output/table.xls create mode 100644 SC-boot/build/output/table_cells_content.png create mode 100644 SC-boot/build/output/table_cells_detected.png create mode 100644 SC-boot/build/output/table_texts_detected.png create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/OcrV3RecognitionExample.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/cls/OcrDirectionDetection.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/cls/PpWordRotateTranslator.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/common/DJLImageUtils.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/common/ImageInfo.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/common/ImageUtils.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/common/RotatedBox.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/detection/OCRDetectionTranslator.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/detection/OcrV3Detection.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/detection/PpWordDetectionTranslator.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/layout/LayoutDetection.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/layout/LayoutDetectionTranslator.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/opencv/NDArrayUtils.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/opencv/OpenCVUtils.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/recognition/OcrV3AlignedRecognition.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/recognition/OcrV3MultiThreadRecognition.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/recognition/OcrV3Recognition.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/recognition/PpWordRecognitionTranslator.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/rotation/PpWordRotateTranslator.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/ConvertHtml2Excel.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/CrossRangeCellMeta.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/TableDetection.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/TableResult.java create mode 100644 SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/TableStructTranslator.java diff --git a/SC-boot/build/output/ocr_result.png b/SC-boot/build/output/ocr_result.png new file mode 100644 index 0000000000000000000000000000000000000000..b7e340d9c21657fb56b5272361c4a27f8c804085 GIT binary patch literal 43291 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 aqo&|4F!>LFsb06i^a~en`^zi=Z!>2 zKYbobjpU%ZBQ^Z5U*$hEphYB%qJjga@AN%=-01NRY;-mp<`8dvUa4YU~T!d7#%jjR`xHNM> z(VvB={N!0}HdpG8N;fTCMAikaUAq?Hi`064LyREi2#vOPzwSUpXf6E1Z~OM`L-Q02 zfQP939pEC(I~AA;mNS02xKvU?o#xjz43P{u*mckSMuV*c0l4S8Z{H}&p*01dho3nv zLvSKcAc0i$8cL~z?jhMcgbX4Y-_M-!D!7j#yt%QlJY~_%nnbcS0;cckG7B7o%&$#n zAgV|(R6-jp-&zx}E!uOr1s5&^LB8N;5o17_8FoKO>t z2Vueczk|$3fvAEAJnQiOSnj{-Ud18jR93FcuoG-Z*7Z<21Wp9AyJ5!VWynA%>B;Mu z9)BII5PShTzGg?NkQ*gV3E>I-D><>&gb`Y>rb>|s3=a>Z_~_i~mt;r_?zUmV5=xJ( z9oP+Il35b@)}THjTPN_IL2LcsJ&IUoIG`xa-*JgaoPfq0hs=G;^7K*Ly7=nGU&Jb; zboB+y2DuzM3xW?M-37#Zwb(33E1)_+{i8A9L3Qxzkq!CkU+Qo&tcnVPFV+=>$cq3tkjMVL!-jBhLoFEGSD zv|U9E(ehX*a6Cy$U)6{?gchV(nUAv?MQ1=%_-DRy)&>-K`$aSZ*)vLFI8k!!3z_`F zKaett3VUg0N>c2mr%!1jQarFamH7;th1)Dfq2w0-lSEhBkbmxCdQrY;#iYJ3?At*E z7J7TkK!Y(z!p^OZRgu?HD#0SfMMrmCgfOs< zkL1CNAC({xiK%l!Vx~*Vv#S!qkC4=&JRTo_3fx|!2m?0`y3AB;b^bJjP?JfDWVINl z4~Gw}9e4MBn(5~2kBypT_iyUcwkeollpv6jhKS8o7i}|lYM_;oS<~*)!;ok(mBXv< zxp(8i6D&?4SWGRPVGU-Cl~tVP@OM9Zw&A_vKN1YPOOZ4%{%UJ1 v1fFE&3&)usWJ&%1l(?RFt^ap_YwA
版本
上海长江云息文档编号密级
CJYX202211240001<V1.0e 机密
」数字科技有限公司再生资源智慧管家平台之货客智慧管控平台一共51页
\ No newline at end of file diff --git a/SC-boot/build/output/table.xls b/SC-boot/build/output/table.xls new file mode 100644 index 0000000000000000000000000000000000000000..1c01445acf5f1ea6d52e7ce11fecdc6dc7b4862c GIT binary patch literal 4096 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

_M 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 literal 0 HcmV?d00001 diff --git a/SC-boot/build/output/table_cells_detected.png b/SC-boot/build/output/table_cells_detected.png new file mode 100644 index 0000000000000000000000000000000000000000..05c9552fb9da4256fcd32a9e7487c9c2bd81c7fc GIT binary patch literal 28449 zcmch=2{@JO7e2g2a*EI>B||DnMN!HuB9S2#N{}w zl1hbyN~TQT{X8!X=XC!6^ZTyv`nt|_?Dn?bVLfZz>t6R-&s&heaxI?Wg2P!X7SB>` z4I>td-2|`qaSg%$@25UG%3_JImTIUO?_~RQ+bMWV$H@hqEjQ=LH%5kNpPIR9*#{A&~lACqVus-SKog^cu{Uk zYeP^p*Aelh`0q(x-1Plo+RR^gqar2ti;%aEh%0&Sik8?zf9J@z&;S2_p=iOSJ~izp zd!v=+a*!+fzNQ`*-KrDs!*}G{dfS8CYUDC)%=O1;hOaj2-FxW5uw@<_=v~P+;?nQj zJn~UEU%u(EXg)rzPb7zln1`2vsapQ@9#ax==S4Gd8kfxnx!JVac*UMgo}kml8-1Yt z9-p}Fqk(r|geQBgT1rRn3~Z{(9^0xlcuMW-TK+hoz-6VmVK|q@iz$l_U067!TqV#1 zSMp@>r^>#Khb|@Y4n=KC<2RUR88%2l|QMyQZ0n7pd6ea>ESy$w~7Tfzg{CoAnvZg;5JKhoo1 zr0V$YG{?K1X&v={<&*c^qX#V@j&H&c!R6Ed>PJ2tgb6MLlqdH-ls@d>ktkG^!+rEc zTAkp9VUO@0Ulm?{ICo1+UnJSFdlicuJHV?dy;*73kkQY{MW&F?((qZ)XeGljmf1^x zSl8-G`#l4)in98!pNLX^Xp@D%6O6?98uuIiG)<{OL}?p&vl1_*d7E&|qc76E8eswH z!#IhRa+Sw74l8miilw79`9GX8?iaCYI(!a43hWq@940Hc^vH)J*4-y8uP`@Y4}-uo zc^AmJ8g@I|_s`8IYvaBdYZLx7yJ<}aj1}~WtnqW4jPHlFAMbOh3P0L+uz%B-Wai?n z;Y3*a@O%AVqjrfdJtBMJ+pJE<`l}8lsW>BBWm??}mMNpXURDPHZca<`|awBuO9=#qx@gatKVAFo7;@;Nu*UcWKB?oM z^b{6pa4g+raN4tbVQ>m#GE1X1>w5W~Oe!eS@Czt9y)zzo0`>@!eX`gJr%shUz zBa_EWukjvPix_O8-g$hril1x%K6>+J-n+=#CRo&=R}oK&9GrJKi^kc^KBg1C8v#mO zM#C>-)BTPPOS6A9Eqw9jchL(AgR!!_Q848qcptgz@I7kDyYc3er_#=sjPG!N(=ha# zZC^D_tVn5Db`EdZ_WyLpow(yel>?z8$E?_eOO_vuF8NI$Xlh=WrU#QP^rw#X+oJ#D z%4oc45I70q-p@l!9qWH`*{`mvcI1PA%``D!_S<(=C6=duzy_S>&_ zv(uBjS+|<`E_9!D-7)@xhaXv^Y8?NL#ra9f`?}`5%$P7Q?{}{fS?bJ94j5pvt6EgM zL$C4EZ0(LAc*kZLA4gqD)SPh#LTe@VTM2L$#2ZV7ELm(g=HWHzm#Q1#ga&# z@&5A+MTt9SPRX$U`7U+^##9LHajCifsCkde9Nd>>8i(4pzMd|to8V^(- zqCkHB<{5A=U7dxV!-cYU7T+;BnEn3jfo*j*;_>AlkIh`Y|E}1m&{~t@`;x=Sg~iv( zKRhv~LN;OT?2fj!w)XZ{H=OdWNwL)UPOJE2n@Cw*u}jU|aQ}$?`~xFHYYnTSBOkia^go-%VnxELEvbSm^9nh=#p8cHLfoJ`xpcmjYb;Hlq^pNjFR=RHsqFLq z!Ljk{cD_k2oa}7AAgW~i@O?TlPqwDddZ>QIEaBBn_cB?%lGZ!Y*QG9-C9KQX9)ktl zHYqMCDak)QSzb8p z9eFpqy|5Yce`+~jo0NvkL_YPEE5-MSeD<=yib*b#P9bmPlQ)Eu*Ulbxc3qOS7;$I4 ze7dHR1(r7ScB9P_(##SBH=DsnwKvQ?xg$%+U$o%C`|bXgXBVeDS9Jam94)~T z4B_Or6f3t&Ew^)cd4vD2LMN$*ca-kJ5tq&g&=>u3cgwT$jtj%D!MR*ME4CDbOuTD_ z>&f|iXGBCS+qOy5x$|_VrP!6n&s?1-;Ln!pbXuC5e~K6@vaY<*m0X~;P5#`I!L|`B zxMo{pMY{d-Bg^Atj@QaR?eob0Q;4T*&6N*Kp3PfTf7R-&-)s_ylU)G7;k)5~R(Y?s zYPT8yS%O{T=HHN3;dng|o~e(ThrD)QC*Xg4o@1ZanAvxKdK7Et$4ha_eVx_-u~qO2_tse3i2B#m ze}8zNIHGjdw^_qhjB9=wtda8E#e8*zt6tfI_h09gD6Y6%&U&|fVV<;N$wVO`hxDrR z=g;4@;!ob5(Q{|SoXruOxve*M-HN=wLAOk!$mw);cDDLCgM?LcEV6rgT1$MIwpirJ z3;XoQr1nov#xfPLeozzb8;LXhR4G5XGEsV@n3H=YCo$^C69G+Q*{AAmsI1~?Py;>j zn;?LX=MOrmdVoUe?R{OM`OQx`$A1WTYz_=(F`!j%F#AMR(|jupKPTdwQGqP^P;1k1 zEJTVx!O|F`IX6G=Y3UA+SUbC-s>-AHdmG@$>dLYcvzscD**jKX;quKkxv6E2CxN+U zkBGcapJ%d(#vOWJW(m{$3|6K-x6e#mrcs|;)a99NAYm*ymKAHeD{{f2MRpO>MwQ#O zeQ#@ox8<9hm~H+d*?QPdFbtZrE@iUU!>+e-BZ9PQwm&u}u|9lHZiiRCA76aD*oOfu z-8M=`q{4>A#+hS=2d=~^kcbF|leIY!$TF5Zw&eUdiQ&_(Z7z)#is~Qw)3Oz= z`}o3-gYz8PX1w3=ULlvPz7_y<6Y z^M$Ps4dAN_Kk@KXvxIWHOUCggQu9ygRk+HG;}_*8ccjfF`sj>8oWyY7(o(}R*_p?N zv&v-oFEgoGBP=hD??>j@PK;|y3h}>)#kC1{3wLENP}BYR_3AA+aS%yFcg+&U`MN9$ z88(s-jP*c{N@9IQ63Ri06N?WxadlNy|772druC2Q=a-ybm|~g7Ne&zzzlyB&?jSzH zjJ2EwQsdaw_0v#s-Kl*;LiK8i#arysR0)o$hdQ+Gd+1vdEu*MSAo(<`Kd znP~25(D%vQ{$3~9WP^>UUwk}J(Jo=t!@Aae>TJo3&GRLX>6|a+1HE`*m%2Pk`4+$x znYo*g=A%X37QZ5uNaZ3XAmZO_5P7mLrmjjN!ps#g2 zp-rXd%o2vwH#RzBt&#MmjiO%fH?rt-NgeVz!4m+XOrg^qX^rs|3`Cy&Y7^7HBeq@Y zuG#{ghu2_2N5>>ffg&kbp`7F-=Zw>q0<4JQ*v8DvM3weR>%>MYF^zZb>u#+Z{|{3K z{n69`Kk_t~Q)jlmM&MAj*WJqeqJR|vf5KiqxW7Uo2K!=2&~|c8!f>QauU#;ZST6-y z`!nUO1%+I1MvBxsuI8c~qe!J{Q|;@-H#$Sneo7k2eSIzct;04wI(>h`+|0zdGFui6 zP-DrquMg+oui}a8ZhhzN|IFLqA~@QKq~f^9U3RqvA@xWPTT)-F&{s-zs$CMi0_@YX z^;FH;Tf5|@xxTrxFJ{?^`1t)qu$Zn}4xNzvkt~M;J=J?RsD`u&Z zr;4q%T7&I#a|;aPpV+=*aYO*vD%*_}6Z0)nq*eTq`>I|aZQEr|Df}v$*M2T!W@2~t z!IA2L^EN#ibKvgwjGLBu{s+d-nQeNFBbOnmeG!8>&R^ql`S<$s2VKkyrT!jCv@)=X zF=_}gA7o*aby!DC%e+XTLj#Kd}dYCugcOacM5r?Xf1?I63 zDzBzTc}5eeuYKKYR-qH?ibakxOgf1;^ix;!GFk9-gg^V|&f0^BH!*y6Wz}WZukY?x zr8}y$JzcR+UG&UyYq4UR))P5BQ+>goHl>*AnqDs7K51Ak(qPJSse-@uyskZ{vM$>* zBi45O?6o&`G(~JQ)i%CdZkEvgbcO!d;roD(PT{76a7d@4l(*yrCk^Bor16jMGSQj2 z1$kH+ehf+B`ogev7c~v-u!rwKJvK$Ue<(URkCH&g%ByeQ-aWAFxD?wq zkZG&rz@>5o?g^;Ab(BQiXqAwXB~m?tu(w$Zf1q&K0UBAN*s5z`5!IMUYqaed+-FgU zh#k`z>T4U(dCCe-cLDi8`Vy1UZ)<=mmYyTb;J{LT{sP=Tpq!A*Hcw>>i7|>Lmn#}G z);^Nv?vZ=&W<}sWg$34}LskFOru`{#-HU{V zL_q`Pf|`aXapVbvCrRwzXo@nNrp^D`0*L5||K`z*ZbGGmkrp>?*mi>7=Z;S@Wr2aJ zjeql~IGp`|Bs4u1{cS3?5u8{D#qLij{XHf9JxIRxl~>hJ8=56NyMJ_gPH&TA(z;uM z=0xB)m)IB)>mKO_n!PE?YgL7-ixbd&OGTp6m;BK+U60(JG+eK9*H`Y^09XBKjSh;}dqc{Odh zygF$Ks$fLg&=KS8UtDjf$mwef6;gP7`^WmvgJhGmHiKwR|EmjJ=#Hh*7nLWe^hI&W z{C`+rmm7-q>?Se>>Yu(|2)Lg?C%|YK9g;B@*z@Slj$?PR`34 zPE(*r40wlSLFlA~+q>JFbf+O-F5&=V$&QvdzR~iu2$yHg>M?h(N$L8Xq(S05@iz|L zL&((=FS_(fcc4`MT>6~`e*af|Eyx6mNd}8)Ue_BH_0+Z6>9pRd`Iq89k}DmYc25H$}af4G_(}b_HQuS$=u7V@-B%sg`iUn)KPHilg_| z%3BJ2BxJY1z3z;ga`&fsFK_kr_2Cz%&_W|TjNYW!fYMe)s!etj5BWcIz3}6t(!T&r z{C~Z(*e#V+<_W8kEON2|=q!Ro+8Qj+n*QDvpB(GLP-57U_uE;C_yuT0;O@y}^$bV? zhU5V8!BQZ652z}WGk<<4Er(LT>9otD0m?svi*S_x1XpPOH1i^XXuItNcl8O`-Z|z& z4iN*mi5T#wlC{CiLxkP7uk+!sq}Oleq4G!DulI*B)FK3lwohVLdKTIcqHjn*|3!Kc zW!KtOIiGm8p5R?~d$A3Q+oeY?bd$4EVR%4z{u8;4-Pp97`$E@;Il?#h1l&iNhK^h;6tB^~;%P0DDc`Y+U5Lbn1?8QWR$Xo@i|R8be_;^^ z_nv)X9ydE{-ln+|%4C&(FasAFBvfTIp#l-*3og`O=(|5x;&Vl3wlIg9k5B){^2>)W zuDX0;j>WoUE6sBTs^j6FkB&@T=H1_2Q5(ao?XA9 zbVw~=nd7S=SHFhb4d3IOwBWHQ$^BzBlch%Z@XVXJ-^z5HuiGp4x|9;-#+DWfaD7%y zM^0OUBVvxQ(vl5xEd>&3{QP8~*8!e=eH}SI5UwAcURbs@eXOP_rIG@5clM2#GG)r& zEW^5PmQd5zp6+kC{xrTDxzKqUkxqv)9An9}qcVD0$n z7knF>1=#lH&b@ouWQoo*dG5J=`@O?cAbK%|VLh9CPo#bCK9zM>*WYG1F72kEX ztn^F2;*V)&))w9khi-iPkZ{aVsvzu)$F1EXe5paFv$GDnJV?aArR%6Vv$!cga? z9cyTLaTp5vW?9w+qf0HGbqX{wm)o6D_FBZfIDfKF&leaf+CW02y}ioO2e}Ig3+nsC z2@`IQ`PR`vXbr9P^EHev89O?*z`8_p!U4I^HqneGwMhucW3dGOw^m*ih0$sFf{<5U zw>=sk@0iouzS_b|Q-gZZj^;Mb59~I8Hb9N)CF4J8O2<=&&DCDJtM%D~1>@M^HnZ|q zWhV<%_pX0)&}l*Bu8)^c(hor%#!!ofUvfn82h^?~+ZI{}y}9#?^+BarX#S|M zc4W;WrDrm2qOj_b;mjyhQ8Fa;P$#a8;o-mD9+vul4GrcVlYu8@T?JMyPOwvK3|*=3 zgh4=5o%WV$8Dliv+n;YjnVAXke}G&xKk-0q3+bDwCv0u05M&ftx6g93>9|p=lh*Z~ z@aa0?=-qk>9>BQ`_m8sQ-!d>6F7>pgh7>)|i2pvi^(xmBJuePqkc^Q3nn6aLcy%)s zASlrIqDxnN{yL1~lEM#;z_i-`H6rS_4ZuTw%mT** zWZer3KfG+@DH=vE?o{m-fgmB_-u~}TuF~1VDFdanTJNbTBdUQ%D>hK1iYo6;hbzHT?KCk6W*Z@;jo&tFzH}t zI^w9hc7*{kKhC`n4Li_(ssu=fGJVtCONA`*0&n%OseL3;JTF^q^=grj_qV={e(6J+ z%70`iTfk5XGMLoiFWYe%>*Ky>aDA+e6qKce+#eNaWOqBi$(-%$smyzJ)nzss*dnVK zt)AeXu-7+!i1sVdF1ozxa;h1R>Hr~`a+o%8#0Z}9uOQr8oC+6^#er{9M+s%&e z^Q&`5YL2yy9$~XYm-4Nl1zAa|f)70!rlYDS&1j(?RPk^}jZgnkePy<*&UMRmFZ`Z$ zGLd!Gn<40Z#fd*~P{%B-4&_~@6`4|c*Ue}4+KQovO6o@J+e7d@mR_{LJa%f4+vMg) zs*)_jqBwbmn35y6NMPpC`{A*Foh2S1=VCyN$}}H?r@}gN#HKQN_rlJPaY8QaXqt5S=T zd*v0O{E#^5F3Dq(;|>HKBWW&yNH0`gLRJ-kjW9ppXUXLo_{-`-3q|nwG9t$?w8g0I z&MQJJ#m{jYs++y(IIALVRk|?LD?(^^Ff9~SF?wb+Y}nVMqs3U85;4jocxX?M_NUlr zCNrQB%n}2UJUDk$?*(O5E&Gw9_TFii+-p67GEamx~)D ztL$v$JZb4m0L$bPDk9p|M0{OE9f-Cgw0B0XPO1pv@~v}Uu=S?D1-n_OS#<`euuFeFAbc+GGq2Blys#Ae;T_Qf6%mb=ossD-4MqqX z+Ng#9fkdgFW4%s?l+;?H($d|xAg8OirnlJ*LeabQ?6!m?pWep)_j~(!h>JjE!7`wY zAU>%OrAEid|5HKvPrn#H;1_Pc`UQ*D!(`RMCorDEv{Ep@;f08x(NXY)gebd6G+j4} zbgCI^x29K7pbNfR#A4bj6d-98gpZq>n#@tXv~;s*zn8&eewlfIYKX+r{5G72r~}Y^ zA*X40N1B5ixtuoGbz(^8hOQ_sgbl8%N+Q;Gd&bMH>2$%0xEW_AZPC)rm|-%dJ&y$^ z9 z#U%9L{k3R`SX)w<(YcsFDx_1~XQ7$1CgB+*bIc(W0^I<=A~HRBQ9M0wwFI`n4<>fr zJt)nOwK_{?6m1NtMor~bAmJ?YOye?HR3Xs%FkI94?*35-X(&-K*FqUz=A0c6BJ@>y zzrFJrFVdc)+^+JH4!^k2)4fy}%$6eUecde;jcr8b4v+W^6fkRSO&QSw+-PRy7t4lw z&hs<7n@jdJX0pDLz6y(v=-#ZkCw0%4YSR)%h=EupryQn6Cd-In8vr>#A)F)u8&blP zHX(VBmZD52hejValkR+JMubDq6XnX&G{;7Sa@ijZGYnD;tfw92(^UFoatq52lS5+a zn5>ZIo_De+e&uDPp|ez6rpcLE);A>K>eCBPrY)MKtTk!&OXJGIPyQC(X|(Y|`$wuI z^-^gW%zmS$A&NZt_uDg)l1ikx#TmReOjzak>ZUD(IcACi9OaMz;GWa_%amZbfi(^f z?0BO8nCOow7uU{aH$ic|Ao~jskv7(3x?F+?P9rmj=^Q!wbXOrc>>!g$WL)R+s)PqN zDcLRQW(iZ59q*}7=^1Ucrq99wYYqk-vrJ*fr`(^&?@JOm!Gi;HvO90Jc=4_%`%S|g z9PT5SU!bwW(`3=t(;-W}I_l8FWHC}C?S=+2G$R>Vj0^$gksGhx%1*l%Ol`x*AbvPy zL{z?&W6@P%7C1kUi%LAge_LG>W_4+v&kvvA%lDSor*rQ>z|&6tIg(idY0Oi4CL)&q zQ#^fu%Oq|zFjGTAeiUsZQNDH<`Ea^^B*#@+_=Zf7|1FF2qqB}4DH#rt=00#&tU&U_ z!~M~i>vIBaLNCiwKwH{*_6al~$ZXhyV>4x*q0_qKQzcvO&W^F-{yv@aSnN5S{EN8z zKW`#0ro9Cnbs^)F_7JfaIKHw^ufn8WKdNsR^Zq7BpR8Yz8(l!)wGR{|pN_1Was^!$VE%~q78Fn zZOQzNfIlz*<6!le)dGV(L|1?jymBx&ung52-QkYG(6c~3v zlQkH!!7B&Ax)w}>X7)qtKY*^GbMY#}IgYXTP_oiAa?j=E&hhq}ee;C(o{=zT_*kZi z`yPx6BQWVy-)BKeRG7Ky$~~Kuffycv4sO;C2V@5d6T100Tz+4nO2YSks5~$A87$U| zD?^AnuJinIx5cZ)Fq7bBC74 zeW`$ecXYabXlST1nnZN0K-ZLyBg&_*3*HsAJg)RMsosdJ1?AdF^4J6DfUB>VAQNIr ziQFw%!cjg7{0CEik`I#x|b}B zzJ{%2dNvpcTe))VydBU6aF!e)QkHfUqZV-*z?7U1St{cP7f>AWl~w(S8C_)j%Ei3H zOPHyv2E-DQj>l1Y{g1rOqTlDFpkB|=JYUM}(L9I3{x`=9qPd8Th%bUxgu)&Sl1Rn? z-v1kZFzy#{ky!;*&;6^ouaLry^_jnw#-?LPcFas<9~l4qi7b-}F$9Cqp)<$Db*)5u z)&DUR6eQ4+y(j;3f8pdwcjvoK{eGu^idyWoP0KTfEp*m-+GKo!y*vEbi*Mo=#y0q8 zcxad`_~=?DKQ_Z*!Pp0RCPhLw%y_-7Uun6-yKIk#V3)GSMbqmRd`^q3MIYJSsFrkH zq4(t7p@hfJYd>hr3-XZIRGB9wv0YZ~aF5ru4fl^baK(h>1^eVwyi&VAq9uFx5&d-@ z!*a>1UHQu#&kwNyvKTAM6F5*<3q9Hg-E>6k+t#kf zsvaH+ADe9`78C`S|gXn&DF8uDK^a)z|NA&Zx7ou~}Qe!?`!s zR&xIy9#rh3M~`NypW_qdn1}*w)v8q}60SN_rJXw`YP>#}4Kqn)vOIF~b&+)qnOh^K zNpgvlBG z2L+Sn4*N^=`0?YB9?coe&0Aka=}w!sU_rW9ce}Ruo&Z0YB{MY3*qv>J7l*v@2|3ue zVu)Kxmk;Oa)vMVIbPPpL>Lw;6URA<_@Ujf6q6S8-E;n7+ih(xWo1pQd3Mr^duo7k>}I=Q z-1FUznJ0C3xVWI~N==n}c+FtSNnOq%yEn`37gP^CcI=pml>ZTGD6JVoMPd{OonSG~ z*G7x8pN>|%a(`33z@TaH#lf8o4p=oqoiDv_PY2{#nv(&J>K5+>va)`o9%XDay;!i& z=gZykV>Q`w7xD(=AF+8rEQy7L!Z|94BW6s8y3YNDj<&W7UFshf6$KNj=-t~^0n=lCs3OsP z#E20OA3nqw1;&KgL^(t`#>&?jJ?QK6Ub)in`27uEyA(w^rW!p!rnku2(FC5**w~1b zG+4Im;j@G_Y>Jpp!#Dk8IDL)w?;<{U6282M`k)*!J7%_?)wN!V-(LWv%FoaLP<(3E zL-^v@3t_1L=jx5#NS0I&UZ6x5Yv|tFv(}_Z&6#tG9OnT!qC-u#(x{-bk@oXFuxmn9 zu?_RtxlNclBcQ)9$*?ls6PX~TEWH;t?kdC)*9Vy2-Y#2aa{YK(} z;kVaI3x*8Qo_2K0mMs^@adVz8-7I;`%-md44mCRf;)8ssLlujDK~t_kz&9~*cAm5W zoTB2{GlN+I+)K_I8lyZ(b71Y=yBC7S>(d^Q0mIGB&HXJ|e7~H5{*LkIF3cEnk0r41 z-4h!g{D-|RQuaTxSa^jj4t_rDZTD0^6H`;uHESAaNXQ+zzd?jAaLIWIC!C{+o}8Lm zgWMkrmn|eD0e&U6ukY?iNUWLj}|3X zQugi(2=QY%y4WW@1%YnKPP8NHGw_e4S^eV{Evd6v-RiU*Mp7z_-*4A2JW=e!_ClhfZ z04xl|nO77R7CK@!q5ko?lP4!I?kTkKv~x)-fh(c53uO)O-@kwM>>2y8kr(5KL*&uZ z)#V;J5`&7}Q*ew0V}|pqe?Ih!7tUwAa6LP`(2sy*{M4iOQ&Uq7Bs93xmQr^b$~Sx; zm#73kE_;NC0QdJ^?+RB#YYKo!`glExxSj+Y^&&rgRrj)0(HP(kciH%7v<#McN^x+^ zSX5mO6B#I$ctiZ}2;-&G2t-e%9v)ppF%`+mDp??&qzWuKj?J6(;HyMw)s2m=6Jed?TdFw6yt53@I#H#M&7-J?2|?@2pv~Si|?Jdw6vB_kB>* zLE^5xZ5@2K)`?Y-WZ{E059{XlkC)e^Z@ug90PSwG$YE^*9jAjA!cOW=8x=&dVs)BB z74n@5MlcY%AxkD$GqDX$i_KoVBxQ`*h^-h74#n4`UKV47siLBS?OR)$ZG``DviuS; zXaG-F=?aRLz>A))R+>9x3Rp-tM;O5jnvuGo6&_M+J)y2fAdw{y@l^Gn@o#ytb(lTON_Oa*BpCdI>e^^Wy z0q07J!hx{Zc zR(VF3o__o&DOiP-`TBLPT|MCz%#93C0!~b9MN4#f_8}`^Lb8b0`!l_ej6OROkHhB8T)up$+n*xaBsN}@QBY7&QWBzJAc<%M)?mfoyefY5Xt}=rutDtB$~%}D1_*Z1#>eAi2Nq9ksvOvYZpsx+1U9_NSG#%R7djV)W4M~wx+ zn98^gD-Cl(H*W?jynt82^(E<&%RNn@gi9>urfcKz`1oKMR+!P#yoDW#B?OoZ)xeb! zZ6i?Au+s&yLoH9>QYQv*)ZS8)6fZfDe4V|$y@~n9P;Vg__80#GT{O}m7pAt4Nvjly zLQrfaCMDHwbumW|BC*k*Ak|09;B$UP`-knN1aPkl2;hn`A))W#(G8&j)?r3H4kXAW zJ-@zCknjN4`MhAs&2RIw#|IOG4af4inRo0Pzb?1H{3X#fQ9x@`rF|Xe;OkV#=X9Q%-;zj9Adg9w4y)xOS1arqD z158axNvZK{4Y3rEuncAcKuspuBGy(JEXAiYPZ%BY8DI_s3gq21!f!Ufh0RaJnR<`izZA;x!0f0k;i%_SJnW1x;L*kVk0p05I@{QOoI3p%nLIOLY zu%*go_39Ir0-b3j%&7q-wP%dgRGU!u@|u*Q0Ex{A{ONPHCL}O@chC3novVHnA+aHr z1kqjw=wWbzM|{*@Iy0L0g>B!y{YguQu;R{bHZ}!BD-`zllzi#ZC3s44NlE(k>n6vC z`pUqKNKIg0+1V-YyLT8bY*j#+j-aoB#5asrL%WN?R=OhW1mxu89HaS*jZQ6a7>Mqfl5Ts#HK&mcY zPKs`^v9bLxf4W-s*e_42F6M#@McY~v3OuR9as~Efu?K1FG1{*nm?0e$8=E7h#mjvb zHfqQszxmdY9kKp7;YZ@pn@Eea8@#6=qzh_`j|wxAoxk03l%AfCYv^PF)fFpNlzEXQ zDs`DdPA%)eZ7ntVV6E~`FD|XTw{NMeTyOcpUF%tT_|p+WL*y2r_odfmXaICyy<{R4 zy<0;}ZHA`xl=0IHYl~0to`09nQTVu0KYKQ~p}lXde44|sA#4w?ZPP;g5+bKG_g+Fv z;n1PAo?z=n53oMiq05VT^UTEaoFwzpDrxEL6Q{z%X$1%!<{KtanUDdy7WVwCwr0&K z<6JPT7mL1A-`-WTx4 z=CTLGo!h^Cb8&N9HlqZh5GCQ;w0`C;id11Fwr~PJK9O0Yj^^SZ>eUY>Xs+Y_@#!VN?VC52FVPjj3NS2- ztX9vL!dtg)p(H|UwlVW!fYJU@E~d%mXU zc{!FETpGib@(HZcySJ_@3 z;-l28Pq(U{la`h~3lBj)!no0nmRAk}oPs~TEnyw(@akq)WwJG;wi7AIJ!-Tr)8)Rb zo+pj1m*?+&>D_E+hvfV%=s@Re%A4(W(en1U*e+NyTQ~X>0w@O#5`U}f>znjf!i$wu z?%m(Ln@!Vj+X8~fLR=h}nJ*y401#qwfdHzzkA(A5@FMaJ3SRjBIshBT5Cym9PtDCs ztm!w&wW0q0N3K1u%Vb@aH^l7TT9v-qo}M0T$4!U*+*;P5R9?)LeEmA+g-H5+M+Al- ze2k$4y9obXeta`$&P0Wc@(C0hIh>I0U#Q6qt;4n5h6j}vIgWID8~Ul#JuE!$%w{RC zw_?Z>lTFEoN0goTYbaqpcq6%NQ>lwqfQ8RckiS1cQrMtjQWsb#_dM&izF0Ts#A;+V zk6pWD^%g6sbFuUlzvN^7*t~*=V+iJ}tx1_}2ntK_4RbeF?fS-*w?+h z$~T;93n07pS6Uy`)7P)L=|0-Gq^=5C2h+qgEtK#WJChoea0xb%7z`rzUqpldN5UNb z6D$as_EfGxCK@?O{%kz9ah-{XFte~2#>dSGRn``)_sGNdq_1C0Oa^i~{5ABqi&w8- zzn-1lOly>nhmDZp6h^^MsaNgPCgDS!>7wPqN9@u83Z=eEi+FP>qhT}f5Fjq zZO{yD(OUW=r-WN?WMTLC$c<1 zq{_6A;Q&SNoPKPa>FDnD>2ULL!)7|FqdCn%wOvxTV&zySswoUAivNHn;}osB;y24Z zTwcM3O6~PG7rqR#V}9&@B*3Y2Aq>afy>_-6wvh}lYqu$W5@UuR*5>HI zF}?e`>ZpV|S5yP=t08uA`kCeZp9556^+vu^aODr*!HwsaJX zJ#Qh9!H@4{6mUsmY1ECt7OD_yJ+Ev?+5EycFDYkFz;2mw2Me8qf0h87?cd!Im%KRu zO3zQt5$60%gU`pMw*rh6%Brf!<0nt7BBp)3&lH>-Lp<6)mCEu=%v*TEXtKf%ZeTUK z|5D=?hZswyW@ZWw--r2(pz`vSH4Su_!!2jVt-uHEe{9iF8wstQk_NL@R7fs714 z#mrl%gCPm7F*W_7$UBv?T`K$DRoXwP=HPI;?tkS@KC>V|VF0e8Bbc~~L|g&huUxtE z=|w6;fUp5^j$tDtC!yGjjD&1=AuMkB8FlO3s=3#sAmS{n!*-7{S0PwHYw0-yj8E;4 zhSF*x1sd7w?V}tu46Kw*d&WT_1^;XI^A+Ue$|=Y`xDbW{qN%9~tE30;h>YZ9vXCHE z(dpp-pNW|nMx)AQ zc{-+V_f*kAXuV}O^U+z@g``M{C?gD;WW>u9l!&)1_-FsI`_=aC{rFboX{AbL4!4X>rx{ioC1X|Qq!ZmF-N>VuBIcrqA4`=> zEfw3SAu8-}bF*eAIqqLC8YrHPoAw1cMHMab2Vb(A6e7eX4QEnVB&aZCy}0{uHHynk}AMAAEEw z@61IT=Ermp8I-_y1jx&0&*me5OH28KH0-PTu#%(|aM$VdL6yE6IXM>S^ZdAGJFttTcWCW286+}5yDO`AisGl#b@9ex$Qg=W@48j5OEWwa%*jfwv#wc&Q zW&A__qNM>Y45Xekgfvgq;>k+q(T*T!R8@7^R#3-}Z`rbCB}yG@-P{(;$8%+pa{69K zbHm+wUhn1o+C{kHj2Pr{{1&<%pkgC9_d>y_a1|;a(9*-UJzBG7&6LAjP+o~YK-C{N zZk!bCv(K5{q}E>yQjJvQV)NzOgQ^7(1p#~jkTfa$JX%NweREqnpf-HCT%URGj*t*|*UKrS4lCf~AcgSf+~EHUxHZ=glmQ=^>bqt*kqSJ3)1WNcAcvlPA`9DwK7YO%_Vs0kGX}3F z2nr4n@!M=`i{ZJkl%n#~De`e)k%6xY^X8S&j0RmFZ4D+t&`yQHEO3}_rCKVz4f;sb z`>2LoLFK0AgSF%-I!{+a>pyakkvmOrN!y->@7(M=rIc=m$ zOayd|Wju;ao^deF-~X!cy)1POTPN+O(JEvwX+htuW>j`kuiWm~{OOx%*e?}8fpX91 zJaoxfe{ZRVgsctMuy)M$>?A5qZYewaX1njJe%2m^|HhC=Qwt+53{k<*y(_rSxpU{> zU>FZp1Vg*?>C>kZ7T&fIu}~8&5qJwxmz`r8mEtFL(Gm*V89APd!(bE_hhoVH+8t)R z@f`$HOjv>ac6qKd3`1#ETIz(cIWQmw5ZLk2nKNh5PNpqK9xhV&p>Mln#>U`t$p#X5 zx`+rL-1<&uXD2j0EI3)9;b1jjmgZSINL7;NV1eDgcgI^8j~Ob5&0bHRK4qo{fQV@h zwV_CEFX>V~z(QwbWwDV$9;mlJdfOxP#h=3^sn{}qpfNnqNf}Vi<5SbpMsjn%%yix2 z-~d%OO#JB5oSK89N?M2PX6c|eTj$P*iKt)E&pfT@ZUk+60PByG)S**^ClUda@S3f& z*Bc+5OGzOuXi|12vr){gz$Wui{ zMbQ4RA6>Vs=>2w5h$}}46P}Fpg6(bS%zvuRTD@{*7&H!u^{fF74NrM6kdlL)Cv$NS z0tSu44gT$KDY1Wu?{AQjiZTXHq#?#{d~d^C7UEluVoUQqr_*=uYGTAx?7bsHL}jt)fMi_ zgM(wt0iZ^xY=F!&R6{gXCpJdgw|*!_TO5@la2tgL2*^%|rRW==MvbiFp>+Z$kvyT0 zR!ehh8nU)A9s<`R{PK9QO%6N(gE6S;Iep;-(+CH-E%ZCB!yNl)Zq9{um{`7U8$tDf zbLr_Fb9}m%Nedu~HWJ^*u6l=vX)Hpcz~9;x9ntcyF+;=gpvBgf4HNl50yNU`^gD0w zY}k(ny?;uEPzmE!M+Gs^@$`^Udq)SxWqj7HU5m;ACB(_7s9}6u9MCGDE~Gis+;Fm{ z9ki#IKR9#pWJ62K>XpY}URR=WF$_^pCj+&`5vG#Dvyb485wUN!Vtwx1xic_Zr5*?X z#RyD@)1*2){${JA;{rYB?b}DG>g$)_Sqk0=6Q*6mIk>F3xHD*}7-x~yt0k?&&H}_hM*ikorAV#VynFazJ9ahr%AMCY~7^u^T+9zo(7~Cp9j%*Fn=GJHH(C={Yal zyLtQexqMs*zc4C7VK9g&1`a!R?69$kr8FF__>@$q-QT}MFqE6|9*tm~J-5%o*`^=8 ze?2u75B?CrB1lOocD!^}>AcNrVD02oih2LPiIvwNRvMO+Rvte5ZpO>vfuE&sDlF6( zID++qd!j5JNraty`(IfefuZ1GJb92E6I}8!a?^lgF!q@k;W< zq4_pHn?S@#-&QT6eVKnUL&2-GP6=bGQbBE0rN~Pz=Hu1LqwNeNpfR4sh!aff^EPPu zWRFiWkRvg1W?tb;W|%l)!Uib$gaIB0sS=NMNWpVlsKPLW*ai;D%Uj#WRCly@%0nU? z52}%qv8kAt7}B~B zrl@O2g6raK(oKuR(;Ti{d)v_sp*S`!uGhPtw572@8l?&^56`(Ji_XmFMq3;Afu4)Q z^J`N_1-;Hf>v`4c)$F@jAA!Xo8e;!B`8}y?y=x^y`-ly|Um>dX&`1S3*r=5WXMiOJp%*@CS zh=hNMUX)Q*21q%`4q_|gF0?rdsTxJ!u1IDkL@eg?$&>EQ8FlEW51sKI4LL20TnUiU zENgW;2Yq@6+~@(RK(KYEMD=n{O#8h}`^7=%7vgk_PrWDVQo6xFk7g^A)7-*P zQZjlI?XhMhCFx@u5A_@4hm^#H84Z+AZ;3pFM>$~TbQGBtz&tEgo08GjlbMQtugo|w z`@GhhQ?x>%b?vgRT4!dqtNl4)(wIlXpx35Nb!4MDW&-UfrM0BTo6Cf;{D7V$c+@CT z!7ULT|8L(mNBE;XvNLiMX7*6oVwPV{j~$PDB`!g7S}!(aU`X)}rM`!a$a4T}Z5uG6 z39ek%in&X3uxBG3upbH%qLkt&jiU80r?nIx*IEBMcdgJ#MCqbp-bakw&mJ=z42kJF z%h7pDI=N$S!bsIry%3~cgB`ng@aAwbEWrLB5=Re@{7*`f8zAi6iL)Wac5G7Vp3r>v z_h8q!&(vkv?Tc@W{#p&DjP*tAqO))Z8Z-mYv(7BX15y+OLxzhYAY}!Qp|vzf zTeYY(V8{GovD4|Ut}dkG--E%*8n_N+2P+0)iFdN%MF|}mkNay{;DF1g9mVu42AC(( zVT7#=iF_mf2-$C=>E`uJ?oCK&LIgrx#pjWArpNbMLZ4;#u6i65} znI?#_=ONpgUdWZnBJ0!kpd54L5Au(CdOU$Qf>6}y{3k`0WErxU%&(AHQY(y4069Xa zf+063p~~Tj7L*KM{4Zj34%<)B(5LfF0E&i7(Gu96DK1#w0yg-M`HF*qBl^FV?93(o zBSt$>l{^L+P*!hXE!3d6w-Bo3>gjnCoD#7*Zve4#Gg8;wq2r)pRfd6j6;AB-1 zU%^3Qgom0hhmvtpH(NH2#&od~3gfM$()?qQ>d-L^1Pn_cSS+NAfhjD*&x#=|! zDJwP%xA&Iz;pkil&%k1$6d{2=1-%*<7l=T!PCUGX8PWk>HD6gd8^brG|5;ym&F%dhSQCkzJIUe@)>7^AI zbyoRadQhI}@0=loX<%pxYcdgH0?A(kF+~=fX|bGc#Up_sbK@x<# z3UV={pkaifKt8m!t-@lWKihgw&zE(rO|NcwSXqe)a4YwIDOY_^zM&=6FP-YQq*ER8Cn-TyS|FHmqrnvU(vZ#-H<2;%y)yozXwUd$*jH?% zjJ186G=j+d7CGL%c!m!9XJqEB)tO^EtbUT{fk`T@+xvXFHib>#|54Sb5sR7(rIOxbi0jgg~9+aE>I9VN{!i@Z>GkGJr2 z8mvHAj57Egy4SaFsF0zn(@v`(lSHv=&pRiI4?vuaSt2s{O7P3RDjRj-#_7lkbA&S&7oZ5>=yFzUut!pk(?6yHKA4C6! zwRpS}W{b232M_@HKKTCQT0@g3}7%7 z)@~{y4`a5~gkf%(QQKFCkY+a9+P^u&ucT;2na^BuO>|-@tmkHj9(q_YWeuREX=!Op z`HCXyQ-6|@{3_cGyYH}icmvhrg*Nbs`5B9?y1hgrOm+Cl2kNGL&+X4F@oQz}Ddw_S zSeie%GJjPv{{}(LeSzw_as2jM1vp1c8>=;+okf@HmX`&CSiA$_i)j6awt)F_2(wmK8)fm1;I?e>8eJ`1gAXzZ4f2 zfBa|{SH`x@(^FAJ#X|BJOT$oh{`~nE$sTW9-m2L1?#q{C6RGdn2eZYOVkXqHhWtBH z#Wt;7T@$>zJiRSJv~c9~D&&a7#n1L-A8pyp|qWYi>Su>DSMnS!b)7 zq8&GE*nrO8u3dtvN5s!8pB`E(zO=ce<-pyy&@>w}>$7g$kXKRh*|}5Vm=2z-)PEsf z8&C1VEqZINR4U2SpDZkle~F`|o1Ed@yZjC>JW2Mc{d_bZOHY*4jUP8|_`V^&JF=8y zWU$?8ox#crqxo+h5{KZGk)7T5t>I`(H`)>U`ugYu)Hm$Ng7RF~&|r)0on{GQ`{MU= zdB>G*#%+~yti-WN}fF|FQ-Wrjp+QII<-@%u;`E&_hNpw(nE&0hKULG$J8{w4ezef>g0-- z6cZ5?AYYi1@AT3x6};)oqR^9~ngae}L!w2`V_DMIJeS-XSZ~qfDEwyc#rQzq&FhWv z43?XFt!=dV{mXQkGE~jiZCeqy2L6O8g`V6^-n~{RGmnp- zpWm=u-iA{Qe;!OOEK()E^1L>D0{IOIk4f{`@f-Xb4_e!Z;5VW_IsLylNFS$Al;nr! Vi+SGQpT1)))m*NTsJ?#B{{!TD3fCu37_X+H`cC{W*fg? z1Ijg|J%}r^FuSymC@_jjO zWcl(;7sk1|$HnoM)sp8;T$jwqJPrC_Jo3bfk;h{#)lAmU8XDGqe&fcCRoaf4Q&Sv2 z>#xx^24kV*WVI}4PRjej?JSJx)b{jxS<_6_|5Ha0ePc0V+P zuLd}MOq?z+A6SuDc;nF#&H4AdNK7+%}a(Luek^a*7>Z)(F2t)Fd~EvJH@-$x5L z3FKfY^7@z6ha?i_C~JE}=~+u2;D{xdl10WP$f%eFE`SwG8EDkI-Fg##AXc_L%|P(p@>k(v+GwI1J3%)(L^o?H z3yLZ}|DXs=anP*Y+9S}<^U<)U1>sxDbMC?#g(>BkRu$K))FGY~gM*Xzr8AT_nVrpj5}C3> zm^r%MW7HOq*FCX9=0KT(ZcvMeq{#us{kd-nD|-WT6Nq9(MoFxCy<1)3;^3W>Q6QIj zIix8!waT`DI5s&wyam#^TCsogKUve3Xs)4F?{@rXlA+Ce=3v;d7x(DHK=Q%?Q((Jt zdo#E99kwPyRnPOXV#Gz0ky$n+3A5StJx`kJ_ieQ-XYQ9<chza?DS!K&Clk7>^1JLt`@*k63nB?nd6mn&8tc+Y$neqis^_B2>e=xMHFM#-9gA@5&Z;j7Nh?L{#IEM-tK zUgbiXxn>1xa)_PsQ~*p5CN8X>o)=$d@`EVXA;F$#(C*RiYL&gz#Q6AE z@)%w~W4e{sFRmJ?Qox(fy!Me$7<)n)qHBnxqSNmrn)h0rHVh*6ajVaGvw@ zwD=8kMh>1eG_Wi`Ki|6hnY9Il#4SyB(0#5L<@I|1;sndIT-gjOW~gnXM`J3(u_8VH znO94`;))C_Yjg7|lA@v_vYRiKVk=5@qi`$>5-(l4)G?eeSJ+rQq6;}!-Pq}&u}pui zRRGNgVP8HYPT2+epg}v|Gr!qv087*(EJUq93y6(-Wd|}8&2B)VJWS%fW0gB8Pt~{& zFu8|ef|LYtrP7_%yA=RL(qbS&rP#!7snT|}yu2P+X}Of@>guI#jS_7~xoh>h#%p{? zb7QtQl(il81}}g_lGE1FIaA%So!<2>klbC}+37Kl-S~my%WsmW78Pj}QmIsT-NlBi zx2D#g{bOQcpmkVm=U=xI4pDvONh7;s$gP1^Ud^lbc2%8KwNHk9Dt7+`dKfZ!$`p_D z#Mqis1Gc9)8mGE3E2^q~lw7)Wsa{8x6jVqpMS7;$kZt8&JA*3N0%?NJji+JC7nL+v z07P9t%+q{!QhN3QC6ft@@J7(IPfwhdBsY(2*m`xj4vb<=BWYuCzqx61RY$(n|& zn!<4C+`#3~p_b-6`5{Cf7Vn`Fl{)s`?o?qxhZj z+*k6(I!l&3_GHJ~g=sleL_|d~rqBKS;>C;CuP2y>gBb#M(f?wL^&E0Q`}Izt9461T zspfa{J7Uwo>65zx&ZAzLc}=*Z^3<4xa5@V{uH=mgLywO;k=IadboozSV@7++{fmdN z=XmC}AvQ332}gD8%v0{z|I!qqeuJ}@S43=WHigIbqJ4PGT^#tPh<-HE>G1#vy&b4$ zxr!Ely4Y&@8c+3%rq@gN8HEY$=$ulpqtt^^DIwsn7{eQ!5oAJ<|ALYOW#d)dLQbsh zz?vf9qXEbmxga9uCTn+FbKHh~rEi-`3-b0E#Ug)B17J3h03q^2zJj`M;#UAzgmuxL zGP~<7BM&07zr2qh%+}7qe%S})$W0t3Sh5Qt0Z4)qaabw^`KkVR?BO=yc3^AzI(V1wxOFiJbTSP#MU?sKCTs)LW!_eO*OiUJHBmz9O zy25sB_mv3eO~e+0Gk_g~7;y^-bPnEv6bISAuUgE$y|xQ2qdq#5$b#*k2tITl9{?o> z$hJ>$q!i9>W2Hng{0Sr2SK2`Bp~ydgT>?M)SgFuI=;PVB-&xK(e8!LQX~o5mExL+< z6M}kwkBEMHy-G)C+#H2TMft~-)NZ+N9IJn3-7}XKk>7`At~Zz$88pOrmq~NP^xs69 zLsuHQiqOfPY?tq-1w(n@DSUNUK__d`6u#ae(Hy#psKj%hh$p9B=%6Yevn=%gn^B17 znpRYYHzK2Z9T{CO+TGQ=r6JICeL~*-y2o2i%pA9>X`6RVdh=&m=|+ai3v!Jr8hJ|`|J%8z-vM?v`1FGP?D&F}j({$^)(R-T+gvhy@8j$pF;| z`GPb7*dTqR1cJmz(F9QoAc%s(%5V;U_ ztB}#EA`#4Ir>kiD<9Y*W;w8>}(J%*b0z^DEtSDdj3WD|m=A`Q5RihVTP5}=pL_gKf zaybMq5z5sV56eu*@bZT!s!zY@AU+OeUKl{oPr1Fb1@(jNM(Qb;I=sG}2LM79h{>QA zwEf<^y+<29?}mu3iUD#SJY|rTD}DCM+@)7GqQhNFFvNA%%{jNb_{c33A^Pad>7dQg zY>;&SAK#4H6cKSoNzJ$kkt$LtAYe#*UmgoL5&{CX{m#75T0W1Wi-P10^J)(-K}V+o zQ+09ot)K-ti{WIJlQx6GbD9lv1yVrz-go;z?U#W~LD&?VL(J1w)If`QAbM&_>6#SGi1ciPW5PZN!-ACB7ovmHgR{RwZ1@WD)j!XN z3l>qgwYnXYY^X z&1YS?a>d8T2Tpa@mPuko&N+|vP-V^flG?-g^qH_n{5T)x3c3z=7=cA>@}VJyY1xgL zd>zwc!quo0Q>o_;lx^6sLDwG+tlfq5?!v-wEe3<(VQ~H8m5qvs8amgDo8_x^rvA8# z4lll<7KFa)8#z%f!-ycZtu=)@r>k%+IV(m1MMuw6bbMSwX7f_`m8u;}XWpnESZ4+s zrtQMw&0}!%QnPf-LJpn&Tz1oDr+w$*iDn_@nE|UN$6j*M_}%B$=dy?(^ek5$wV8U) zYnsmjgHYn7lhbk_Hb48K#sc~p^Ch3sKoO*{@wGGh8b!kE*!RiUgN*OU{FCy==1D;dACHnblEsL z_SomLmlu8J?&w@h6D>lcL6JcaE&>6<3|uIp-EwU3G^@yLxC*_}duzIjIjMbg@{VNtBO4)olVl-^j1VjMCZB?)@b7tnU;UPpU@ z*4FzZp^Y`1g9c3v5y>a&3`W}(J_5_C@iOWHur(H7x?m~N=TsLfCCz9Y$EX%e*~ zFjf#sU`@oz6w&V_)a|GOB>dP^4v%3m2%r6`K7IN`u}hG6mi2tw-D_K?`}@D0%8(v0 zdv|i~&-2EvF_5{vNX~~EpoIb&TFW0%xvpAkw!BMmh)7CPi%!h6+#U8IV0>^}!&F+m zqyNo2PQ}``ZoBnuywh?u)27XyS68-38d{)>6cK~li<{Tp9hJU0mE-!!3(Kewwy6g~ zgbG*~^o5h$PiS!j;t44a$9Mxx5_#;(b=53ut)`TKqI>WHcxLqSw3ceG^&LGBqMnHedR z+(sL>-9te>H$hGyzbzn@Cta=qFB)@4ZllK=mwb>J1j3pJLfhe;w&owHoT~$k`kWzp z$jY^r)J2QpwWm-lw{7c);@6X}iNZ1hNr{Ru@din4DaPbIlo^Yw5tO6*3z9l(CKj6} zexAhgA)uIQJz1UCUEK3608||!6fhy|uZbD77adV)>zHKx z3J3*}gNXN*;yMVYr_d##h^yUm&!}?c;)I*Ub^NN!{0r;x+RtOHw8KvzR76+VI04)R zdgann(5t%AIEPSUl?7O?))>tqU?ilwMWhEj7$UeH*vDMLR3>#uTsCngSW(&YMPxIa z5XUp(rGpi^dC_6Vbae5W-kf#hjoA7HGWz*|sHl2mLl>Z>tF^VYn8Is=z-ri5 z9N_-8oJ1Ed%0!`ag(|ydmhiWjL`5V)$V1eI%oD*Efk${Eu2*xT^CRZ3K!?{vT~61L zc~}b0l&T$j?!4PXXzW=6P%gcS`}EX(^WCw8rusp6(oJvs%AJI=ZUQ_ZwM3!=Z5`tS zWOpyq5(y3Ia!)ZKu9TFNJXyijQI=^kg`y`H80FcR8m{GJ(g`7JUaJxR@Xno;L`H{FY8el(m^zWi|>E!(Bya0T;^!ZkeBDj@mr%vd^VX( zBJo;V4{J|XQC7YZ5z&n}!NTH`e^AiGj}S|@c>n6vD<-#s`PLo+>+#LW5WRPDcK$tY zEppw`4nruty1AcW+Z%?+VsC6OkEqNPVx8DHfH1Oym_tN=yQtw^1U~`Dn0S5%F%yg- zi5orZL&-WZm@$Y=7ud2Xn?+5U?6a(q3%5ELfQ~!?bN44^jt$Ptie;Tn{PUa!!i0tF zb=&>)sbIS&!uH34IvAVLaDDZPWwV==v%o(AYkBS~~A>Mq0K zaOgm}P8I1t)H9h^jXi50#WsPqR{ zC2PmM2M-=3CT9C^TjTV&4NhdvT_^9lJUw1BfWRK6b296gMkeP2lT^2z{(=?ZLSnd) z;!5^cqSqOqd0N;E_{OkYbFT42%PTUB+gn20uj=o`6lwN=$&GdVH9<8fKY<<^K-F z4*f_Wu%Mf{l#J5zs{xlj5PAy0g z(H^Lhn$N-(B__6n)4OXLKcvAf`%JO=!sd8HQd(Zjhf5qKzzO~Fk+VqMHyRm%hlz9$ z-M4g=QMhGr_)&8{dbBH!+!e>K)a&-8bQe&%Rr#MO63`diPIivqf0UOXGnw2Bpb3TR z7mtlQVF!KXRyPwDG`1o@ob$0F!xJvyQsOOP75Vw=rsq*uODU|io=%q^J^Tqe zKHy_dA>m&HV{*Y1L!4~jZ~(pv*j6yW1d<5DBJhUyzQAV(!tfBDAu=}LClj=;(N4mB9b9R-UY$I@BahPYOm(;bGZTVA51QT6F;t#v zMtQZYi)7F{scyVxJzgH%oe#kumbqH8!2)?L!%TJc)xL62g*<&6GBCguc)jN-!e=tT zext|N1vcyqH)bmygsn5;7r-#lQQ90_4|!@7qn+5mY?Q#2qK#hgg;iGN7jVu zY$Is*6{6j6M}a?%Kid14y}Y|Ar}E(QrxYpPqOjOymAnouihiXaN}|3KAJo=A!(;$X zH{qERZaBD(N)UkPbqu2+OaM$`43P$+8o>_`GLaqz)(TS!Fb5Ee*kO_|OM=6ri_rgm z1!ofmiEt!zf58he0Ogl}6EnCMaEf+bgEPw$FbSyKA>t@sAdcda`xP543dio76VKgYg7fNC@yYG;PJ^M%VFoaxo*5t`2o6t7TLYLpsPQ+mybiMr zT;BDih!@8X=Ct_@3cmE}V}zC3)z|7%4QTU+Z!uqU((dT9W9tj{9o(28v&l|&Ol<9> zA@h`DvNSVxt+-*M84_zcI%MB~**72f4`;OEK6iwP_vu3PxzMm*pHK7)_6?AYhSH8R z9gX^=iT4>HKF^-U(!C&rfX}aeQBRyJMi4KE3yj}!=UqrF27%mlZ5d;=tqw{enooWI zK}PL9947jlHBh|7039x%T=eV#r`Yl6mNt4E`c}dhS?O2sVF#y-&f68U;fCqxW0ziC zeLa7;a?DC$VnxBeO(5K>HkvnKg@39~-E#Dq1c-KC#xAjh1jr!RN=*XxW_^N;gjgT2 zF|rxEP>qSi>+dT<4PuaQ^-Ij{Br)r6OjOVZbV&Vzml8s}`mLy6MX0gz>jDs;+cyLt zHaF;SVWB*~=CTqI2EP55Fm4!)!oU;bg*wGDOi5E)g}yeEMmM_$cf#jNb!-kg?!m`o zorvM>@lL#agElV904Hv?p=mGNoFD^}(!hzU??lRp%E!$`%Kw?l1C0Fjo{7o^tUO95 z{DTVbUvcp8OG$A8>l6_q9mvbEcs~Vv6gc{-H~+6&Dxd6maGZU<#|-ywzP;Od=h(@m z`q#704mF;Y{W5!yvITF=&>K1faln*WL)-v|7#I?sLl^y~$DjnA!|{%5rP)vH%m)0vOo&hTNhvN_JwG)hNE zv{K}|%7b4xk>sDPNgS=tNp9S~mU`1!PG2Zq46ROfV;Z@Yod3xDl8--^L}xEEss~qI zk6UYG#-;I8^JQwrfBEBd`cApy@%GQB;e@|vLQpb5H7H5@W0q6M+N-yxww(4D`nV^d zE}9UO?CxI;O6vQJ{Pn?zIcYOKSbG^5OB>Z4)Nb`QYWA2u);E(mH}YlW!AF^yHL@1U z1*>vN6ecIKh?z#`Hm)Tf{*-juV-M}uF^tiy=YQOu3Uk}@mPp0ko=TOi`TbYZMSF-c zlrl736ru5Q_H7V`%-@!wn&I*G+O{EUw_3hEuZG*9)MCrG2dN7K?|?{P;eS(xYVLph zM`grLIkO|b{+x^3p;XCXNI6-MgQAuEswSuPSzv7pSj;RbQj-yff)@$PlFiW_3=`3M zIr~qXNc`)M(_*^`3BedEOc#9`!x{-gR4fdHgg^#jdnXXq-}c(OZ31C_SFavSUe#{D zXm@kpoNG)^zp7C#+SB6`5e4ZY!S}(IHdz4;xkY1dUpS8{3 zx4Wh5)zOrZz@4?)xO<)_dy4#K>1VVh)5m(#H%nWGfQ#tuU8o_{h$ylDhR+pe z%=N1>EfG_z=#FeaSzVn&WY*in1$*k*oJ?kC1UM@(^1LQrIKWUE`JI&J-v*E@v6Tn{ zLKP9>0k+8FpV_a0dL^{*d-qF#f(o?GymI(vc7t`-Ylno4+}U!v?oHKjBAZ6<$c1l9 zAAPCJ@zv{k6^Md?lz4|scP%X~)y$46IMjRWf-gj0>2>Gmr8nnJJ7US&Nv{@Jk&M-X1Be;c+=pq^{a_%KWjZscGpMP6 z5%~1F1)YXdky9bW3QcKeRJB{78QN8H=C^9&he(R zb?>7NAqM7M=`)4IfygTSb00ddrP%%H!a&Qnqgn6!tCj1o8ZUQal@UYw`(F!D6T|}p z?Cn3&NDXzLqw9*_#Oz}_lGFdj0&d)a@wRF`b zP`XP zBk#}SA$m^c#K6Bd6D&Te?HkK0SN}Reo#EEo>ZKv^IgvXZba(Y~y>R~tqX5Q4#LV_u z79o5DTs)9&55yP)^y>c9tu$ei^fQ-wJ~x4q4huyQKM6{^1Eswt+^J=l_E!nesIJVK zH+8i$$19F2x!PEIwegc)s`D^M%An*|##q{5s^k}~#9NfEL4KZak^P71Tsi0D1*2p+ z+)SotF&n>l^Ef^0Q34$&q84Z_*0ow;%*ZuMTM%bU-UrM*# zYNTM#0JLU4(`K$KIo0P8{?q62mBW8ASzM_=6du2AWA^+#lZ||a6!0&>!HzrR;zA=A zJs+jD?!{`Tm#PFC>Cy9Jick1Yb5tP*{rDVxFzEWHo*x#{ju$STg;`ygBKUF0_|ZC( z7RbRrR3x?5t+#?4^yA;#wR%OU-lwEK zbRuK%Q(;H(yZz_let4g}D7@NkFbJ~-S73zz{7O#sMMs27FNyk9UD81;&DsLKR2NfL z7D2KFvB4*V+We7PD-ec`A)raDTfgSfpbHn%5fgY26*0nkO07_-zep)`vj^cmf0Rf! zy>0(X25F{t&+^ElJ#cVPS%ReF@(K%XEf%oHU{q39ZC9GS$OuFPf?6$77mEvPYtMuQ zi)HlZS1&nmO62o?e@o`;$+OIQjKBr7;Q0dUgRH{WGC-72*316$k!-&!<2(B&?4QUC zzPEqzDFFf%nEeDr0FO2FT%29;yC=QKOnX=pjb%G!27)uze!=?1M=$F&_H+FRPq}Dw zYHDgN#nx-9Kx93j8Si-Kr@2B@;?+ZEVlY%kHhs=)1W*A4ch3=p?(A}~zv@CzV>JFv z$Fh~V>n~gw-;4b3>Ug9UnCl(|02TJf%82MrAje*%mB)lA0H@+XOal2!BatG1nqh=k z0MTL}#lovf8JGxJaPQt-5yg`QZ|Yf+hb`}X(UM4o#jqt2K(omX4(dS3;6`-b=_QE1 zL`>C*RM-mpUF{yW4d~e)xFgjt{*ukC-4r_8+sIhpmqxxQEG(Q44z?$`%N9I0>7tq| zoj)(H6SNXta!o!~yL@e=i9v3=n;uy13) zo#`co+NCVta?JDy^uSQ%W5J^KF>8VerS(QBS34Kr*q+C5)y!tY|1{LdIS-(G$R7hs z7^sbTXAhyPKtci%0J1_l3+@SGrapcEHf#g%xQ2UkfRhQ(f^HHxz=Ye literal 0 HcmV?d00001 diff --git a/SC-boot/linkage-scm/pom.xml b/SC-boot/linkage-scm/pom.xml index 35197e4f..49097941 100644 --- a/SC-boot/linkage-scm/pom.xml +++ b/SC-boot/linkage-scm/pom.xml @@ -11,6 +11,13 @@ linkage-scm + + UTF-8 + 1.8 + 1.8 + 0.17.0 + + com.jnpf @@ -62,6 +69,102 @@ 3.4.1-RELEASE compile + + + + + + + commons-cli + commons-cli + 1.4 + + + org.apache.logging.log4j + log4j-slf4j-impl + 2.17.2 + + + com.google.code.gson + gson + 2.8.5 + + + + ai.djl + api + ${djl.version} + + + ai.djl + basicdataset + ${djl.version} + + + ai.djl + model-zoo + ${djl.version} + + + + ai.djl.mxnet + mxnet-model-zoo + ${djl.version} + + + ai.djl.mxnet + mxnet-engine + ${djl.version} + + + + ai.djl.paddlepaddle + paddlepaddle-engine + ${djl.version} + + + ai.djl.paddlepaddle + paddlepaddle-model-zoo + ${djl.version} + + + + org.bytedeco + javacv-platform + 1.5.7 + + + ai.djl.opencv + opencv + ${djl.version} + + + + org.apache.commons + commons-lang3 + 3.12.0 + + + commons-collections + commons-collections + 3.2.2 + + + org.projectlombok + lombok + 1.18.18 + provided + + + org.apache.poi + poi + 4.0.0 + + + dom4j + dom4j + 1.6.1 + diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/OcrV3RecognitionExample.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/OcrV3RecognitionExample.java new file mode 100644 index 00000000..df6b287f --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/OcrV3RecognitionExample.java @@ -0,0 +1,83 @@ +package jnpf.ocr_sdk; + +import ai.djl.ModelException; +import ai.djl.inference.Predictor; +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.ImageFactory; +import ai.djl.ndarray.NDList; +import ai.djl.opencv.OpenCVImageFactory; +import ai.djl.repository.zoo.ModelZoo; +import ai.djl.repository.zoo.ZooModel; +import ai.djl.translate.TranslateException; +import jnpf.ocr_sdk.utils.common.ImageUtils; +import jnpf.ocr_sdk.utils.common.RotatedBox; +import jnpf.ocr_sdk.utils.detection.OcrV3Detection; +import jnpf.ocr_sdk.utils.opencv.OpenCVUtils; +import jnpf.ocr_sdk.utils.recognition.OcrV3Recognition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * OCR V3模型 文字识别. 支持文本有旋转角度 + * + * @author Calvin + * @date 2022-10-07 + * @email 179209347@qq.com + */ +public final class OcrV3RecognitionExample { + + private static final Logger logger = LoggerFactory.getLogger(OcrV3RecognitionExample.class); + + private OcrV3RecognitionExample() { + } + + public static void main(String[] args) throws IOException, ModelException, TranslateException { +// Path imageFile = Paths.get("src/test/resources/7.jpg"); + String relativelyPath=System.getProperty("user.dir"); +// Path imageFile = Paths.get("C:\\Users\\admin\\Desktop\\图像\\AISDK\\AIAS\\1_image_sdks\\text_recognition\\ocr_sdk\\src\\test\\resources\\7.jpg"); +// Path imageFile = Paths.get("src/test/resources/7.jpg"); + StringBuffer ocrStr = new StringBuffer("本次识别的内容:"); + Path imageFile = Paths.get("C:/Users/admin/Desktop/AAAA.png"); + Image image = OpenCVImageFactory.getInstance().fromFile(imageFile); + + + OcrV3Detection detection = new OcrV3Detection(); + OcrV3Recognition recognition = new OcrV3Recognition(); + try (ZooModel detectionModel = ModelZoo.loadModel(detection.detectCriteria()); + Predictor detector = detectionModel.newPredictor(); + ZooModel recognitionModel = ModelZoo.loadModel(recognition.recognizeCriteria()); + Predictor recognizer = recognitionModel.newPredictor()) { + + long timeInferStart = System.currentTimeMillis(); + List detections = recognition.predict(image, detector, recognizer); + +// for (int i = 0; i < 1000; i++) { +// detections = recognition.predict(image, detector, recognizer); +// System.out.println("time: " + i); +// } + + long timeInferEnd = System.currentTimeMillis(); + System.out.println("time: " + (timeInferEnd - timeInferStart)); + + for (RotatedBox result : detections) { + System.out.println(result.getText()); + ocrStr.append(result.getText()); + } + + BufferedImage bufferedImage = OpenCVUtils.mat2Image((org.opencv.core.Mat) image.getWrappedImage()); + for (RotatedBox result : detections) { + ImageUtils.drawImageRectWithText(bufferedImage, result.getBox(), result.getText()); + } + image = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(bufferedImage)); + ImageUtils.saveImage(image, "ocr_result.png", "build/output"); + logger.info("{}", detections); + logger.info(ocrStr.toString()); + } + } +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/cls/OcrDirectionDetection.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/cls/OcrDirectionDetection.java new file mode 100644 index 00000000..cb038469 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/cls/OcrDirectionDetection.java @@ -0,0 +1,141 @@ +package jnpf.ocr_sdk.utils.cls; + +import ai.djl.inference.Predictor; +import ai.djl.modality.Classifications; +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.ImageFactory; +import ai.djl.modality.cv.output.BoundingBox; +import ai.djl.modality.cv.output.DetectedObjects; +import ai.djl.modality.cv.output.Rectangle; +import ai.djl.modality.cv.util.NDImageUtils; +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDManager; +import ai.djl.repository.zoo.Criteria; +import ai.djl.training.util.ProgressBar; +import ai.djl.translate.TranslateException; +import jnpf.ocr_sdk.utils.detection.PpWordDetectionTranslator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +public final class OcrDirectionDetection { + + private static final Logger logger = LoggerFactory.getLogger(OcrDirectionDetection.class); + + public OcrDirectionDetection() {} + + public DetectedObjects predict( + Image image, + Predictor detector, + Predictor rotateClassifier) + throws TranslateException { + DetectedObjects detections = detector.predict(image); + + List boxes = detections.items(); + + List names = new ArrayList<>(); + List prob = new ArrayList<>(); + List rect = new ArrayList<>(); + + for (int i = 0; i < boxes.size(); i++) { + Image subImg = getSubImage(image, boxes.get(i).getBoundingBox()); + Classifications.Classification result = null; + if (subImg.getHeight() * 1.0 / subImg.getWidth() > 1.5) { + subImg = rotateImg(subImg); + result = rotateClassifier.predict(subImg).best(); + prob.add(result.getProbability()); + if (result.getClassName().equalsIgnoreCase("Rotate")) { + names.add("90"); + } else { + names.add("270"); + } + } else { + result = rotateClassifier.predict(subImg).best(); + prob.add(result.getProbability()); + if (result.getClassName().equalsIgnoreCase("No Rotate")) { + names.add("0"); + } else { + names.add("180"); + } + } + rect.add(boxes.get(i).getBoundingBox()); + } + DetectedObjects detectedObjects = new DetectedObjects(names, prob, rect); + + return detectedObjects; + } + + public Criteria detectCriteria() { + Criteria criteria = + Criteria.builder() + .optEngine("PaddlePaddle") + .setTypes(Image.class, DetectedObjects.class) + .optModelUrls( + "https://aias-home.oss-cn-beijing.aliyuncs.com/models/ocr_models/ch_PP-OCRv2_det_infer.zip") + // .optModelUrls( + // "/Users/calvin/Documents/build/paddle_models/ppocr/ch_PP-OCRv2_det_infer") + // .optDevice(Device.cpu()) + .optTranslator(new PpWordDetectionTranslator(new ConcurrentHashMap())) + .optProgress(new ProgressBar()) + .build(); + + return criteria; + } + + public Criteria clsCriteria() { + + Criteria criteria = + Criteria.builder() + .optEngine("PaddlePaddle") + .setTypes(Image.class, Classifications.class) + .optModelUrls( + "https://aias-home.oss-cn-beijing.aliyuncs.com/models/ocr_models/ch_ppocr_mobile_v2.0_cls_infer.zip") + // .optModelUrls( + // "/Users/calvin/Documents/build/paddle_models/ppocr/ch_ppocr_mobile_v2.0_cls_infer") + .optTranslator(new PpWordRotateTranslator()) + .optProgress(new ProgressBar()) + .build(); + return criteria; + } + + private Image getSubImage(Image img, BoundingBox box) { + Rectangle rect = box.getBounds(); + double[] extended = extendRect(rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight()); + int width = img.getWidth(); + int height = img.getHeight(); + int[] recovered = { + (int) (extended[0] * width), + (int) (extended[1] * height), + (int) (extended[2] * width), + (int) (extended[3] * height) + }; + return img.getSubImage(recovered[0], recovered[1], recovered[2], recovered[3]); + } + + private double[] extendRect(double xmin, double ymin, double width, double height) { + double centerx = xmin + width / 2; + double centery = ymin + height / 2; + if (width > height) { + width += height * 2.0; + height *= 3.0; + } else { + height += width * 2.0; + width *= 3.0; + } + double newX = centerx - width / 2 < 0 ? 0 : centerx - width / 2; + double newY = centery - height / 2 < 0 ? 0 : centery - height / 2; + double newWidth = newX + width > 1 ? 1 - newX : width; + double newHeight = newY + height > 1 ? 1 - newY : height; + return new double[] {newX, newY, newWidth, newHeight}; + } + + private Image rotateImg(Image image) { + try (NDManager manager = NDManager.newBaseManager()) { + NDArray rotated = NDImageUtils.rotate90(image.toNDArray(manager), 1); + return ImageFactory.getInstance().fromNDArray(rotated); + } + } +} \ No newline at end of file diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/cls/PpWordRotateTranslator.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/cls/PpWordRotateTranslator.java new file mode 100644 index 00000000..27921a85 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/cls/PpWordRotateTranslator.java @@ -0,0 +1,69 @@ +package jnpf.ocr_sdk.utils.cls; + +import ai.djl.modality.Classifications; +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.util.NDImageUtils; +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDList; +import ai.djl.ndarray.index.NDIndex; +import ai.djl.ndarray.types.Shape; +import ai.djl.translate.Batchifier; +import ai.djl.translate.Translator; +import ai.djl.translate.TranslatorContext; + +import java.util.Arrays; +import java.util.List; + +public class PpWordRotateTranslator implements Translator { + List classes = Arrays.asList("No Rotate", "Rotate"); + + public PpWordRotateTranslator() {} + + public Classifications processOutput(TranslatorContext ctx, NDList list) { + NDArray prob = list.singletonOrThrow(); + return new Classifications(this.classes, prob); + } + + public NDList processInput(TranslatorContext ctx, Image input) throws Exception { + NDArray img = input.toNDArray(ctx.getNDManager()); + img = NDImageUtils.resize(img, 192, 48); + img = NDImageUtils.toTensor(img).sub(0.5F).div(0.5F); + img = img.expandDims(0); + return new NDList(new NDArray[]{img}); + } + + public NDList processInputBak(TranslatorContext ctx, Image input) throws Exception { + NDArray img = input.toNDArray(ctx.getNDManager()); + int imgC = 3; + int imgH = 48; + int imgW = 192; + + NDArray array = ctx.getNDManager().zeros(new Shape(imgC, imgH, imgW)); + + int h = input.getHeight(); + int w = input.getWidth(); + int resized_w = 0; + + float ratio = (float) w / (float) h; + if (Math.ceil(imgH * ratio) > imgW) { + resized_w = imgW; + } else { + resized_w = (int) (Math.ceil(imgH * ratio)); + } + + img = NDImageUtils.resize(img, resized_w, imgH); + + img = NDImageUtils.toTensor(img).sub(0.5F).div(0.5F); + // img = img.transpose(2, 0, 1); + + array.set(new NDIndex(":,:,0:" + resized_w), img); + + array = array.expandDims(0); + + return new NDList(new NDArray[] {array}); + } + + public Batchifier getBatchifier() { + return null; + } +} \ No newline at end of file diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/common/DJLImageUtils.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/common/DJLImageUtils.java new file mode 100644 index 00000000..00bc1d64 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/common/DJLImageUtils.java @@ -0,0 +1,99 @@ +package jnpf.ocr_sdk.utils.common; + +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.ImageFactory; +import ai.djl.modality.cv.output.DetectedObjects; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class DJLImageUtils { + + public static Image bufferedImage2DJLImage(BufferedImage img) { + return ImageFactory.getInstance().fromImage(img); + } + + public static void saveImage(BufferedImage img, String name, String path) { + + Image djlImg = ImageFactory.getInstance().fromImage(img); // 支持多种图片格式,自动适配 + Path outputDir = Paths.get(path); + Path imagePath = outputDir.resolve(name); + // OpenJDK 不能保存 jpg 图片的 alpha channel + try { + djlImg.save(Files.newOutputStream(imagePath), "png"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void saveDJLImage(Image img, String name, String path) { + Path outputDir = Paths.get(path); + Path imagePath = outputDir.resolve(name); + // OpenJDK 不能保存 jpg 图片的 alpha channel + try { + img.save(Files.newOutputStream(imagePath), "png"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void saveBoundingBoxImage( + Image img, DetectedObjects detection, String name, String path) throws IOException { + // Make imageName copy with alpha channel because original imageName was jpg + img.drawBoundingBoxes(detection); + Path outputDir = Paths.get(path); + Files.createDirectories(outputDir); + Path imagePath = outputDir.resolve(name); + // OpenJDK can't save jpg with alpha channel + img.save(Files.newOutputStream(imagePath), "png"); + } + + public static void drawImageRect(BufferedImage image, int x, int y, int width, int height) { + // 将绘制图像转换为Graphics2D + Graphics2D g = (Graphics2D) image.getGraphics(); + try { + g.setColor(new Color(246, 96, 0)); + // 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角 + BasicStroke bStroke = new BasicStroke(4, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); + g.setStroke(bStroke); + g.drawRect(x, y, width, height); + + } finally { + g.dispose(); + } + } + + public static void drawImageRect( + BufferedImage image, int x, int y, int width, int height, Color c) { + // 将绘制图像转换为Graphics2D + Graphics2D g = (Graphics2D) image.getGraphics(); + try { + g.setColor(c); + // 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角 + BasicStroke bStroke = new BasicStroke(4, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); + g.setStroke(bStroke); + g.drawRect(x, y, width, height); + + } finally { + g.dispose(); + } + } + + public static void drawImageText(BufferedImage image, String text) { + Graphics graphics = image.getGraphics(); + int fontSize = 100; + Font font = new Font("楷体", Font.PLAIN, fontSize); + try { + graphics.setFont(font); + graphics.setColor(new Color(246, 96, 0)); + int strWidth = graphics.getFontMetrics().stringWidth(text); + graphics.drawString(text, fontSize - (strWidth / 2), fontSize + 30); + } finally { + graphics.dispose(); + } + } +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/common/ImageInfo.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/common/ImageInfo.java new file mode 100644 index 00000000..cc9d3da0 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/common/ImageInfo.java @@ -0,0 +1,48 @@ +package jnpf.ocr_sdk.utils.common; + +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.output.BoundingBox; + +public class ImageInfo { + private String name; + private Double prob; + private Image image; + private BoundingBox box; + + public ImageInfo(Image image, BoundingBox box) { + this.image = image; + this.box = box; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Double getProb() { + return prob; + } + + public void setProb(Double prob) { + this.prob = prob; + } + + public Image getImage() { + return image; + } + + public void setImage(Image image) { + this.image = image; + } + + public BoundingBox getBox() { + return box; + } + + public void setBox(BoundingBox box) { + this.box = box; + } +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/common/ImageUtils.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/common/ImageUtils.java new file mode 100644 index 00000000..23b82e7c --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/common/ImageUtils.java @@ -0,0 +1,262 @@ +package jnpf.ocr_sdk.utils.common; + +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.ImageFactory; +import ai.djl.modality.cv.output.BoundingBox; +import ai.djl.modality.cv.output.DetectedObjects; +import ai.djl.modality.cv.output.Rectangle; +import ai.djl.ndarray.NDArray; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class ImageUtils { + + /** + * BufferedImage图片格式转DJL图片格式 + * + * @author Calvin + */ + public static Image convert(BufferedImage img) { + return ImageFactory.getInstance().fromImage(img); + } + + /** + * 保存BufferedImage图片 + * + * @author Calvin + */ + public static void saveImage(BufferedImage img, String name, String path) { + Image djlImg = ImageFactory.getInstance().fromImage(img); // 支持多种图片格式,自动适配 + Path outputDir = Paths.get(path); + Path imagePath = outputDir.resolve(name); + // OpenJDK 不能保存 jpg 图片的 alpha channel + try { + djlImg.save(Files.newOutputStream(imagePath), "png"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 保存DJL图片 + * + * @author Calvin + */ + public static void saveImage(Image img, String name, String path) { + Path outputDir = Paths.get(path); + Path imagePath = outputDir.resolve(name); + // OpenJDK 不能保存 jpg 图片的 alpha channel + try { + img.save(Files.newOutputStream(imagePath), "png"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 保存图片,含检测框 + * + * @author Calvin + */ + public static void saveBoundingBoxImage( + Image img, DetectedObjects detection, String name, String path) throws IOException { + // Make image copy with alpha channel because original image was jpg + img.drawBoundingBoxes(detection); + Path outputDir = Paths.get(path); + Files.createDirectories(outputDir); + Path imagePath = outputDir.resolve(name); + // OpenJDK can't save jpg with alpha channel + img.save(Files.newOutputStream(imagePath), "png"); + } + + /** + * 绘制人脸关键点 + * + * @author Calvin + */ + public static void drawLandmark(Image img, BoundingBox box, float[] array) { + for (int i = 0; i < array.length / 2; i++) { + int x = getX(img, box, array[2 * i]); + int y = getY(img, box, array[2 * i + 1]); + Color c = new Color(0, 255, 0); + drawImageRect((BufferedImage) img.getWrappedImage(), x, y, 1, 1, c); + } + } + + /** + * 画检测框(有倾斜角) + * + * @author Calvin + */ + public static void drawImageRect(BufferedImage image, NDArray box) { + float[] points = box.toFloatArray(); + int[] xPoints = new int[5]; + int[] yPoints = new int[5]; + + for (int i = 0; i < 4; i++) { + xPoints[i] = (int) points[2 * i]; + yPoints[i] = (int) points[2 * i + 1]; + } + xPoints[4] = xPoints[0]; + yPoints[4] = yPoints[0]; + + // 将绘制图像转换为Graphics2D + Graphics2D g = (Graphics2D) image.getGraphics(); + try { + g.setColor(new Color(0, 255, 0)); + // 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角 + BasicStroke bStroke = new BasicStroke(4, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); + g.setStroke(bStroke); + g.drawPolyline(xPoints, yPoints, 5); // xPoints, yPoints, nPoints + } finally { + g.dispose(); + } + } + + /** + * 画检测框(有倾斜角)和文本 + * + * @author Calvin + */ + public static void drawImageRectWithText(BufferedImage image, NDArray box, String text) { + float[] points = box.toFloatArray(); + int[] xPoints = new int[5]; + int[] yPoints = new int[5]; + + for (int i = 0; i < 4; i++) { + xPoints[i] = (int) points[2 * i]; + yPoints[i] = (int) points[2 * i + 1]; + } + xPoints[4] = xPoints[0]; + yPoints[4] = yPoints[0]; + + // 将绘制图像转换为Graphics2D + Graphics2D g = (Graphics2D) image.getGraphics(); + try { + int fontSize = 32; + Font font = new Font("楷体", Font.PLAIN, fontSize); + g.setFont(font); + g.setColor(new Color(0, 0, 255)); + // 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角 + BasicStroke bStroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); + g.setStroke(bStroke); + g.drawPolyline(xPoints, yPoints, 5); // xPoints, yPoints, nPoints + g.drawString(text, xPoints[0], yPoints[0]); + } finally { + g.dispose(); + } + } + + /** + * 画检测框 + * + * @author Calvin + */ + public static void drawImageRect(BufferedImage image, int x, int y, int width, int height) { + // 将绘制图像转换为Graphics2D + Graphics2D g = (Graphics2D) image.getGraphics(); + try { + g.setColor(new Color(0, 255, 0)); + // 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角 + BasicStroke bStroke = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); + g.setStroke(bStroke); + g.drawRect(x, y, width, height); + } finally { + g.dispose(); + } + } + + /** + * 画检测框 + * + * @author Calvin + */ + public static void drawImageRect( + BufferedImage image, int x, int y, int width, int height, Color c) { + // 将绘制图像转换为Graphics2D + Graphics2D g = (Graphics2D) image.getGraphics(); + try { + g.setColor(c); + // 声明画笔属性 :粗 细(单位像素)末端无修饰 折线处呈尖角 + BasicStroke bStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); + g.setStroke(bStroke); + g.drawRect(x, y, width, height); + + } finally { + g.dispose(); + } + } + + /** + * 显示文字 + * + * @author Calvin + */ + public static void drawImageText(BufferedImage image, String text, int x, int y) { + Graphics graphics = image.getGraphics(); + int fontSize = 32; + Font font = new Font("楷体", Font.PLAIN, fontSize); + try { + graphics.setFont(font); + graphics.setColor(new Color(0, 0, 255)); + int strWidth = graphics.getFontMetrics().stringWidth(text); + graphics.drawString(text, x, y); + } finally { + graphics.dispose(); + } + } + + /** + * 返回外扩人脸 factor = 1, 100%, factor = 0.2, 20% + * + * @author Calvin + */ + public static Image getSubImage(Image img, BoundingBox box, float factor) { + Rectangle rect = box.getBounds(); + // 左上角坐标 + int x1 = (int) (rect.getX() * img.getWidth()); + int y1 = (int) (rect.getY() * img.getHeight()); + // 宽度,高度 + int w = (int) (rect.getWidth() * img.getWidth()); + int h = (int) (rect.getHeight() * img.getHeight()); + // 左上角坐标 + int x2 = x1 + w; + int y2 = y1 + h; + + // 外扩大100%,防止对齐后人脸出现黑边 + int new_x1 = Math.max((int) (x1 + x1 * factor / 2 - x2 * factor / 2), 0); + int new_x2 = Math.min((int) (x2 + x2 * factor / 2 - x1 * factor / 2), img.getWidth() - 1); + int new_y1 = Math.max((int) (y1 + y1 * factor / 2 - y2 * factor / 2), 0); + int new_y2 = Math.min((int) (y2 + y2 * factor / 2 - y1 * factor / 2), img.getHeight() - 1); + int new_w = new_x2 - new_x1; + int new_h = new_y2 - new_y1; + + return img.getSubImage(new_x1, new_y1, new_w, new_h); + } + + private static int getX(Image img, BoundingBox box, float x) { + Rectangle rect = box.getBounds(); + // 左上角坐标 + int x1 = (int) (rect.getX() * img.getWidth()); + // 宽度 + int w = (int) (rect.getWidth() * img.getWidth()); + + return (int) (x * w + x1); + } + + private static int getY(Image img, BoundingBox box, float y) { + Rectangle rect = box.getBounds(); + // 左上角坐标 + int y1 = (int) (rect.getY() * img.getHeight()); + // 高度 + int h = (int) (rect.getHeight() * img.getHeight()); + + return (int) (y * h + y1); + } + +} \ No newline at end of file diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/common/RotatedBox.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/common/RotatedBox.java new file mode 100644 index 00000000..628b5133 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/common/RotatedBox.java @@ -0,0 +1,29 @@ +package jnpf.ocr_sdk.utils.common; + +import ai.djl.ndarray.NDArray; + +public class RotatedBox { + private NDArray box; + private String text; + + public RotatedBox(NDArray box, String text) { + this.box = box; + this.text = text; + } + + public NDArray getBox() { + return box; + } + + public void setBox(NDArray box) { + this.box = box; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/detection/OCRDetectionTranslator.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/detection/OCRDetectionTranslator.java new file mode 100644 index 00000000..731845b6 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/detection/OCRDetectionTranslator.java @@ -0,0 +1,574 @@ +package jnpf.ocr_sdk.utils.detection; + +import ai.djl.modality.cv.BufferedImageFactory; +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.util.NDImageUtils; +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDArrays; +import ai.djl.ndarray.NDList; +import ai.djl.ndarray.NDManager; +import ai.djl.ndarray.index.NDIndex; +import ai.djl.ndarray.types.DataType; +import ai.djl.ndarray.types.Shape; +import ai.djl.translate.Batchifier; +import ai.djl.translate.Translator; +import ai.djl.translate.TranslatorContext; +import org.bytedeco.javacpp.indexer.FloatRawIndexer; +import org.bytedeco.javacpp.indexer.IntRawIndexer; +import org.bytedeco.javacpp.indexer.UByteRawIndexer; +import org.bytedeco.opencv.global.opencv_imgproc; +import org.bytedeco.opencv.opencv_core.*; +import org.opencv.core.CvType; + +import java.util.Map; + +import static org.bytedeco.opencv.global.opencv_imgproc.*; + +public class OCRDetectionTranslator implements Translator { + + private Image image; + private final int max_side_len; + private final int max_candidates; + private final int min_size; + private final float box_thresh; + private final float unclip_ratio; + private float ratio_h; + private float ratio_w; + private int img_height; + private int img_width; + + public OCRDetectionTranslator(Map arguments) { + max_side_len = + arguments.containsKey("max_side_len") + ? Integer.parseInt(arguments.get("max_side_len").toString()) + : 960; + max_candidates = + arguments.containsKey("max_candidates") + ? Integer.parseInt(arguments.get("max_candidates").toString()) + : 1000; + min_size = + arguments.containsKey("min_size") + ? Integer.parseInt(arguments.get("min_size").toString()) + : 3; + box_thresh = + arguments.containsKey("box_thresh") + ? Float.parseFloat(arguments.get("box_thresh").toString()) + : 0.5f; + unclip_ratio = + arguments.containsKey("unclip_ratio") + ? Float.parseFloat(arguments.get("unclip_ratio").toString()) + : 1.6f; + } + + @Override + public NDList processOutput(TranslatorContext ctx, NDList list) { + NDManager manager = ctx.getNDManager(); + NDArray pred = list.singletonOrThrow(); + pred = pred.squeeze(); + NDArray segmentation = pred.toType(DataType.UINT8, true).gt(0.3); // thresh=0.3 .mul(255f) + + segmentation = segmentation.toType(DataType.UINT8, true); + + //convert from NDArray to Mat + byte[] byteArray = segmentation.toByteArray(); + Shape shape = segmentation.getShape(); + int rows = (int) shape.get(0); + int cols = (int) shape.get(1); + + Mat srcMat = new Mat(rows, cols, CvType.CV_8U); + + UByteRawIndexer ldIdx = srcMat.createIndexer(); + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + ldIdx.put(row, col, byteArray[row * cols + col]); + } + } + ldIdx.release(); + + Mat mask = new Mat(); + // size 越小,腐蚀的单位越小,图片越接近原图 + Mat structImage = + opencv_imgproc.getStructuringElement(opencv_imgproc.MORPH_RECT, new Size(2, 2)); + + /** + * 膨胀 膨胀说明: 图像的一部分区域与指定的核进行卷积, 求核的最`大`值并赋值给指定区域。 膨胀可以理解为图像中`高亮区域`的'领域扩大'。 + * 意思是高亮部分会侵蚀不是高亮的部分,使高亮部分越来越多。 + */ + opencv_imgproc.dilate(srcMat, mask, structImage); + + ldIdx = mask.createIndexer(); + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + ldIdx.put(row, col, ldIdx.get(row, col) * 255); + } + } + ldIdx.release(); + + NDArray boxes = boxes_from_bitmap(manager, pred, mask, box_thresh); + + //boxes[:, :, 0] = boxes[:, :, 0] / ratio_w + NDArray boxes1 = boxes.get(":, :, 0").div(ratio_w); + boxes.set(new NDIndex(":, :, 0"), boxes1); + //boxes[:, :, 1] = boxes[:, :, 1] / ratio_h + NDArray boxes2 = boxes.get(":, :, 1").div(ratio_h); + boxes.set(new NDIndex(":, :, 1"), boxes2); + + NDList dt_boxes = this.filter_tag_det_res(boxes); + + dt_boxes.detach(); + + // release Mat + srcMat.release(); + mask.release(); + structImage.release(); + + return dt_boxes; + } + + + private NDList filter_tag_det_res(NDArray dt_boxes) { + NDList boxesList = new NDList(); + + int num = (int) dt_boxes.getShape().get(0); + for (int i = 0; i < num; i++) { + NDArray box = dt_boxes.get(i); + box = order_points_clockwise(box); + box = clip_det_res(box); + float[] box0 = box.get(0).toFloatArray(); + float[] box1 = box.get(1).toFloatArray(); + float[] box3 = box.get(3).toFloatArray(); + int rect_width = (int) Math.sqrt(Math.pow(box1[0] - box0[0], 2) + Math.pow(box1[1] - box0[1], 2)); + int rect_height = (int) Math.sqrt(Math.pow(box3[0] - box0[0], 2) + Math.pow(box3[1] - box0[1], 2)); + if (rect_width <= 3 || rect_height <= 3) + continue; + boxesList.add(box); + } + + return boxesList; + } + + private NDArray clip_det_res(NDArray points) { + for (int i = 0; i < points.getShape().get(0); i++) { + int value = Math.max((int) points.get(i, 0).toFloatArray()[0], 0); + value = Math.min(value, img_width - 1); + points.set(new NDIndex(i + ",0"), value); + value = Math.max((int) points.get(i, 1).toFloatArray()[0], 0); + value = Math.min(value, img_height - 1); + points.set(new NDIndex(i + ",1"), value); + } + + return points; + } + + /** + * sort the points based on their x-coordinates + * 顺时针 + * + * @param pts + * @return + */ + + private NDArray order_points_clockwise(NDArray pts) { + NDList list = new NDList(); + long[] indexes = pts.get(":, 0").argSort().toLongArray(); + + // grab the left-most and right-most points from the sorted + // x-roodinate points + Shape s1 = pts.getShape(); + NDArray leftMost1 = pts.get(indexes[0] + ",:"); + NDArray leftMost2 = pts.get(indexes[1] + ",:"); + NDArray leftMost = leftMost1.concat(leftMost2).reshape(2, 2); + NDArray rightMost1 = pts.get(indexes[2] + ",:"); + NDArray rightMost2 = pts.get(indexes[3] + ",:"); + NDArray rightMost = rightMost1.concat(rightMost2).reshape(2, 2); + + // now, sort the left-most coordinates according to their + // y-coordinates so we can grab the top-left and bottom-left + // points, respectively + indexes = leftMost.get(":, 1").argSort().toLongArray(); + NDArray lt = leftMost.get(indexes[0] + ",:"); + NDArray lb = leftMost.get(indexes[1] + ",:"); + indexes = rightMost.get(":, 1").argSort().toLongArray(); + NDArray rt = rightMost.get(indexes[0] + ",:"); + NDArray rb = rightMost.get(indexes[1] + ",:"); + + list.add(lt); + list.add(rt); + list.add(rb); + list.add(lb); + + NDArray rect = NDArrays.concat(list).reshape(4, 2); + return rect; + } + + /** + * Get boxes from the binarized image predicted by DB + * + * @param manager + * @param pred the binarized image predicted by DB. + * @param mask new 'pred' after threshold filtering. + */ + private NDArray boxes_from_bitmap(NDManager manager, NDArray pred, Mat mask, float box_thresh) { + int dest_height = (int) pred.getShape().get(0); + int dest_width = (int) pred.getShape().get(1); + int height = mask.rows(); + int width = mask.cols(); + + MatVector contours = new MatVector(); + Mat hierarchy = new Mat(); + // 寻找轮廓 + findContours( + mask, + contours, + hierarchy, + opencv_imgproc.RETR_LIST, + opencv_imgproc.CHAIN_APPROX_SIMPLE, + new Point(0, 0)); + + int num_contours = Math.min((int) contours.size(), max_candidates); + NDList boxList = new NDList(); +// NDArray boxes = manager.zeros(new Shape(num_contours, 4, 2), DataType.FLOAT32); + float[] scores = new float[num_contours]; + + int count = 0; + for (int index = 0; index < num_contours; index++) { + Mat contour = contours.get(index); + float[][] pointsArr = new float[4][2]; + int sside = get_mini_boxes(contour, pointsArr); + if (sside < this.min_size) + continue; + NDArray points = manager.create(pointsArr); + float score = box_score_fast(manager, pred, points); + if (score < this.box_thresh) + continue; + + NDArray box = unclip(manager, points); // TODO get_mini_boxes(box) + + + // box[:, 0] = np.clip(np.round(box[:, 0] / width * dest_width), 0, dest_width) + NDArray boxes1 = box.get(":,0").div(width).mul(dest_width).round().clip(0, dest_width); + box.set(new NDIndex(":, 0"), boxes1); + // box[:, 1] = np.clip(np.round(box[:, 1] / height * dest_height), 0, dest_height) + NDArray boxes2 = box.get(":,1").div(height).mul(dest_height).round().clip(0, dest_height); + box.set(new NDIndex(":, 1"), boxes2); + + if (score > box_thresh) { + boxList.add(box); +// boxes.set(new NDIndex(count + ",:,:"), box); + scores[index] = score; + count++; + } + + // release memory + contour.release(); + } +// if (count < num_contours) { +// NDArray newBoxes = manager.zeros(new Shape(count, 4, 2), DataType.FLOAT32); +// newBoxes.set(new NDIndex("0,0,0"), boxes.get(":" + count + ",:,:")); +// boxes = newBoxes; +// } + NDArray boxes = NDArrays.stack(boxList); + + // release + hierarchy.release(); + contours.releaseReference(); + + return boxes; + } + + /** + * Shrink or expand the boxaccording to 'unclip_ratio' + * + * @param points The predicted box. + * @return uncliped box + */ + private NDArray unclip(NDManager manager, NDArray points) { + points = order_points_clockwise(points); + float[] pointsArr = points.toFloatArray(); + float[] lt = java.util.Arrays.copyOfRange(pointsArr, 0, 2); + float[] lb = java.util.Arrays.copyOfRange(pointsArr, 6, 8); + + float[] rt = java.util.Arrays.copyOfRange(pointsArr, 2, 4); + float[] rb = java.util.Arrays.copyOfRange(pointsArr, 4, 6); + + float width = distance(lt, rt); + float height = distance(lt, lb); + + if (width > height) { + float k = (lt[1] - rt[1]) / (lt[0] - rt[0]); // y = k * x + b + + float delta_dis = height; + float delta_x = (float) Math.sqrt((delta_dis * delta_dis) / (k * k + 1)); + float delta_y = Math.abs(k * delta_x); + + if (k > 0) { + pointsArr[0] = lt[0] - delta_x + delta_y; + pointsArr[1] = lt[1] - delta_y - delta_x; + pointsArr[2] = rt[0] + delta_x + delta_y; + pointsArr[3] = rt[1] + delta_y - delta_x; + + pointsArr[4] = rb[0] + delta_x - delta_y; + pointsArr[5] = rb[1] + delta_y + delta_x; + pointsArr[6] = lb[0] - delta_x - delta_y; + pointsArr[7] = lb[1] - delta_y + delta_x; + } else { + pointsArr[0] = lt[0] - delta_x - delta_y; + pointsArr[1] = lt[1] + delta_y - delta_x; + pointsArr[2] = rt[0] + delta_x - delta_y; + pointsArr[3] = rt[1] - delta_y - delta_x; + + pointsArr[4] = rb[0] + delta_x + delta_y; + pointsArr[5] = rb[1] - delta_y + delta_x; + pointsArr[6] = lb[0] - delta_x + delta_y; + pointsArr[7] = lb[1] + delta_y + delta_x; + } + } else { + float k = (lt[1] - rt[1]) / (lt[0] - rt[0]); // y = k * x + b + + float delta_dis = width; + float delta_y = (float) Math.sqrt((delta_dis * delta_dis) / (k * k + 1)); + float delta_x = Math.abs(k * delta_y); + + if (k > 0) { + pointsArr[0] = lt[0] + delta_x - delta_y; + pointsArr[1] = lt[1] - delta_y - delta_x; + pointsArr[2] = rt[0] + delta_x + delta_y; + pointsArr[3] = rt[1] - delta_y + delta_x; + + pointsArr[4] = rb[0] - delta_x + delta_y; + pointsArr[5] = rb[1] + delta_y + delta_x; + pointsArr[6] = lb[0] - delta_x - delta_y; + pointsArr[7] = lb[1] + delta_y - delta_x; + } else { + pointsArr[0] = lt[0] - delta_x - delta_y; + pointsArr[1] = lt[1] - delta_y + delta_x; + pointsArr[2] = rt[0] - delta_x + delta_y; + pointsArr[3] = rt[1] - delta_y - delta_x; + + pointsArr[4] = rb[0] + delta_x + delta_y; + pointsArr[5] = rb[1] + delta_y - delta_x; + pointsArr[6] = lb[0] + delta_x - delta_y; + pointsArr[7] = lb[1] + delta_y + delta_x; + } + } + points = manager.create(pointsArr).reshape(4, 2); + + return points; + } + + private float distance(float[] point1, float[] point2) { + float disX = point1[0] - point2[0]; + float disY = point1[1] - point2[1]; + float dis = (float) Math.sqrt(disX * disX + disY * disY); + return dis; + } + + /** + * Get boxes from the contour or box. + * + * @param contour The predicted contour. + * @param pointsArr The predicted box. + * @return smaller side of box + */ + private int get_mini_boxes(Mat contour, float[][] pointsArr) { + // https://blog.csdn.net/qq_37385726/article/details/82313558 + // bounding_box[1] - rect 返回矩形的长和宽 + RotatedRect rect = minAreaRect(contour); + Mat points = new Mat(); + boxPoints(rect, points); + + FloatRawIndexer ldIdx = points.createIndexer(); + float[][] fourPoints = new float[4][2]; + for (int row = 0; row < 4; row++) { + fourPoints[row][0] = ldIdx.get(row, 0); + fourPoints[row][1] = ldIdx.get(row, 1); + } + ldIdx.release(); + + float[] tmpPoint = new float[2]; + for (int i = 0; i < 4; i++) { + for (int j = i + 1; j < 4; j++) { + if (fourPoints[j][0] < fourPoints[i][0]) { + tmpPoint[0] = fourPoints[i][0]; + tmpPoint[1] = fourPoints[i][1]; + fourPoints[i][0] = fourPoints[j][0]; + fourPoints[i][1] = fourPoints[j][1]; + fourPoints[j][0] = tmpPoint[0]; + fourPoints[j][1] = tmpPoint[1]; + } + } + } + + int index_1 = 0; + int index_2 = 1; + int index_3 = 2; + int index_4 = 3; + + if (fourPoints[1][1] > fourPoints[0][1]) { + index_1 = 0; + index_4 = 1; + } else { + index_1 = 1; + index_4 = 0; + } + + if (fourPoints[3][1] > fourPoints[2][1]) { + index_2 = 2; + index_3 = 3; + } else { + index_2 = 3; + index_3 = 2; + } + + pointsArr[0] = fourPoints[index_1]; + pointsArr[1] = fourPoints[index_2]; + pointsArr[2] = fourPoints[index_3]; + pointsArr[3] = fourPoints[index_4]; + + int height = rect.boundingRect().height(); + int width = rect.boundingRect().width(); + int sside = Math.min(height, width); + + + // release + points.release(); + rect.releaseReference(); + + return sside; + } + + /** + * Calculate the score of box. + * + * @param bitmap The binarized image predicted by DB. + * @param points The predicted box + * @return + */ + private float box_score_fast(NDManager manager, NDArray bitmap, NDArray points) { + NDArray box = points.get(":"); + long h = bitmap.getShape().get(0); + long w = bitmap.getShape().get(1); + // xmin = np.clip(np.floor(box[:, 0].min()).astype(np.int), 0, w - 1) + int xmin = box.get(":, 0").min().floor().clip(0, w - 1).toType(DataType.INT32, true).toIntArray()[0]; + int xmax = box.get(":, 0").max().ceil().clip(0, w - 1).toType(DataType.INT32, true).toIntArray()[0]; + int ymin = box.get(":, 1").min().floor().clip(0, h - 1).toType(DataType.INT32, true).toIntArray()[0]; + int ymax = box.get(":, 1").max().ceil().clip(0, h - 1).toType(DataType.INT32, true).toIntArray()[0]; + + NDArray mask = manager.zeros(new Shape(ymax - ymin + 1, xmax - xmin + 1), DataType.UINT8); + + box.set(new NDIndex(":, 0"), box.get(":, 0").sub(xmin)); + box.set(new NDIndex(":, 1"), box.get(":, 1").sub(ymin)); + + //mask - convert from NDArray to Mat + byte[] maskArray = mask.toByteArray(); + int rows = (int) mask.getShape().get(0); + int cols = (int) mask.getShape().get(1); + Mat maskMat = new Mat(rows, cols, CvType.CV_8U); + UByteRawIndexer ldIdx = maskMat.createIndexer(); + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + ldIdx.put(row, col, maskArray[row * cols + col]); + } + } + ldIdx.release(); + + //mask - convert from NDArray to Mat + float[] boxArray = box.toFloatArray(); + Mat boxMat = new Mat(4, 2, CvType.CV_32S); + IntRawIndexer intRawIndexer = boxMat.createIndexer(); + for (int row = 0; row < 4; row++) { + intRawIndexer.put(row, 0, (int) boxArray[row * 2]); + intRawIndexer.put(row, 1, (int) boxArray[row * 2 + 1]); + } + intRawIndexer.release(); + +// boxMat.reshape(1, new int[]{1, 4, 2}); + MatVector matVector = new MatVector(); + matVector.put(boxMat); + fillPoly(maskMat, matVector, new Scalar(1)); + + + NDArray subBitMap = bitmap.get(ymin + ":" + (ymax + 1) + "," + xmin + ":" + (xmax + 1)); + float[] subBitMapArr = subBitMap.toFloatArray(); + rows = (int) subBitMap.getShape().get(0); + cols = (int) subBitMap.getShape().get(1); + Mat bitMapMat = new Mat(rows, cols, CvType.CV_32F); + FloatRawIndexer floatRawIndexer = bitMapMat.createIndexer(); + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + floatRawIndexer.put(row, col, subBitMapArr[row * cols + col]); + } + } + floatRawIndexer.release(); + + Scalar score = org.bytedeco.opencv.global.opencv_core.mean(bitMapMat, maskMat); + float scoreValue = (float) score.get(); + // release + maskMat.release(); + boxMat.release(); + bitMapMat.release(); + matVector.releaseReference(); + score.releaseReference(); + + return scoreValue; + } + + @Override + public NDList processInput(TranslatorContext ctx, Image input) { + NDArray img = input.toNDArray(ctx.getNDManager()); + image = BufferedImageFactory.getInstance().fromNDArray(img); + int h = input.getHeight(); + int w = input.getWidth(); + img_height = h; + img_width = w; + int resize_w = w; + int resize_h = h; + + // limit the max side + float ratio = 1.0f; + if (Math.max(resize_h, resize_w) > max_side_len) { + if (resize_h > resize_w) { + ratio = (float) max_side_len / (float) resize_h; + } else { + ratio = (float) max_side_len / (float) resize_w; + } + } + + resize_h = (int) (resize_h * ratio); + resize_w = (int) (resize_w * ratio); + + if (resize_h % 32 == 0) { + resize_h = resize_h; + } else if (Math.floor((float) resize_h / 32f) <= 1) { + resize_h = 32; + } else { + resize_h = (int) Math.floor((float) resize_h / 32f) * 32; + } + + if (resize_w % 32 == 0) { + resize_w = resize_w; + } else if (Math.floor((float) resize_w / 32f) <= 1) { + resize_w = 32; + } else { + resize_w = (int) Math.floor((float) resize_w / 32f) * 32; + } + + ratio_h = resize_h / (float) h; + ratio_w = resize_w / (float) w; + + img = NDImageUtils.resize(img, resize_w, resize_h); + img = NDImageUtils.toTensor(img); + img = + NDImageUtils.normalize( + img, + new float[]{0.485f, 0.456f, 0.406f}, + new float[]{0.229f, 0.224f, 0.225f}); + img = img.expandDims(0); + return new NDList(img); + } + + @Override + public Batchifier getBatchifier() { + return null; + } +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/detection/OcrV3Detection.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/detection/OcrV3Detection.java new file mode 100644 index 00000000..4573c345 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/detection/OcrV3Detection.java @@ -0,0 +1,34 @@ +package jnpf.ocr_sdk.utils.detection; + +import ai.djl.modality.cv.Image; +import ai.djl.ndarray.NDList; +import ai.djl.repository.zoo.Criteria; +import ai.djl.training.util.ProgressBar; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ConcurrentHashMap; + +public final class OcrV3Detection { + + private static final Logger logger = LoggerFactory.getLogger(OcrV3Detection.class); + + public OcrV3Detection() { + } + + public Criteria detectCriteria() { + Criteria criteria = + Criteria.builder() + .optEngine("PaddlePaddle") + .setTypes(Image.class, NDList.class) + .optModelUrls( + "https://aias-home.oss-cn-beijing.aliyuncs.com/models/ocr_models/ch_PP-OCRv3_det_infer.zip") + // .optModelUrls( + // "/Users/calvin/Documents/build/paddle_models/ppocr/ch_PP-OCRv2_det_infer") + .optTranslator(new OCRDetectionTranslator(new ConcurrentHashMap())) + .optProgress(new ProgressBar()) + .build(); + + return criteria; + } +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/detection/PpWordDetectionTranslator.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/detection/PpWordDetectionTranslator.java new file mode 100644 index 00000000..c3f5e2b2 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/detection/PpWordDetectionTranslator.java @@ -0,0 +1,120 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package jnpf.ocr_sdk.utils.detection; + +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.output.BoundingBox; +import ai.djl.modality.cv.output.DetectedObjects; +import ai.djl.modality.cv.util.NDImageUtils; +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDList; +import ai.djl.ndarray.types.DataType; +import ai.djl.ndarray.types.Shape; +import ai.djl.paddlepaddle.zoo.cv.objectdetection.BoundFinder; +import ai.djl.translate.Batchifier; +import ai.djl.translate.Translator; +import ai.djl.translate.TranslatorContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +public class PpWordDetectionTranslator implements Translator { + + private final int max_side_len; + + public PpWordDetectionTranslator(Map arguments) { + max_side_len = + arguments.containsKey("maxLength") + ? Integer.parseInt(arguments.get("maxLength").toString()) + : 960; + } + + @Override + public DetectedObjects processOutput(TranslatorContext ctx, NDList list) { + NDArray result = list.singletonOrThrow(); + result = result.squeeze().mul(255f).toType(DataType.UINT8, true).gt(0.3); // thresh=0.3 + boolean[] flattened = result.toBooleanArray(); + Shape shape = result.getShape(); + int w = (int) shape.get(0); + int h = (int) shape.get(1); + boolean[][] grid = new boolean[w][h]; + IntStream.range(0, flattened.length) + .parallel() + .forEach(i -> grid[i / h][i % h] = flattened[i]); + List boxes = new BoundFinder(grid).getBoxes(); + List names = new ArrayList<>(); + List probs = new ArrayList<>(); + int boxSize = boxes.size(); + for (int i = 0; i < boxSize; i++) { + names.add("word"); + probs.add(1.0); + } + return new DetectedObjects(names, probs, boxes); + } + + @Override + public NDList processInput(TranslatorContext ctx, Image input) { + NDArray img = input.toNDArray(ctx.getNDManager()); + int h = input.getHeight(); + int w = input.getWidth(); + int resize_w = w; + int resize_h = h; + + // limit the max side + float ratio = 1.0f; + if (Math.max(resize_h, resize_w) > max_side_len) { + if (resize_h > resize_w) { + ratio = (float) max_side_len / (float) resize_h; + } else { + ratio = (float) max_side_len / (float) resize_w; + } + } + + resize_h = (int) (resize_h * ratio); + resize_w = (int) (resize_w * ratio); + + if (resize_h % 32 == 0) { + resize_h = resize_h; + } else if (Math.floor((float) resize_h / 32f) <= 1) { + resize_h = 32; + } else { + resize_h = (int) Math.floor((float) resize_h / 32f) * 32; + } + + if (resize_w % 32 == 0) { + resize_w = resize_w; + } else if (Math.floor((float) resize_w / 32f) <= 1) { + resize_w = 32; + } else { + resize_w = (int) Math.floor((float) resize_w / 32f) * 32; + } + + img = NDImageUtils.resize(img, resize_w, resize_h); + img = NDImageUtils.toTensor(img); + img = + NDImageUtils.normalize( + img, + new float[]{0.485f, 0.456f, 0.406f}, + new float[]{0.229f, 0.224f, 0.225f}); + img = img.expandDims(0); + return new NDList(img); + } + + @Override + public Batchifier getBatchifier() { + return null; + } + +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/layout/LayoutDetection.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/layout/LayoutDetection.java new file mode 100644 index 00000000..b7ff2131 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/layout/LayoutDetection.java @@ -0,0 +1,32 @@ +package jnpf.ocr_sdk.utils.layout; + +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.output.DetectedObjects; +import ai.djl.repository.zoo.Criteria; +import ai.djl.training.util.ProgressBar; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class LayoutDetection { + + private static final Logger logger = LoggerFactory.getLogger(LayoutDetection.class); + + public LayoutDetection() {} + + public Criteria criteria() { + + Criteria criteria = + Criteria.builder() + .optEngine("PaddlePaddle") + .setTypes(Image.class, DetectedObjects.class) + .optModelUrls( + "https://aias-home.oss-cn-beijing.aliyuncs.com/models/ocr_models/ppyolov2_r50vd_dcn_365e_publaynet_infer.zip") + // .optModelUrls( + // "/Users/calvin/.paddledet/inference_model/ppyolov2_r50vd_dcn_365e_publaynet/ppyolov2_r50vd_dcn_365e_publaynet_infer") + .optTranslator(new LayoutDetectionTranslator()) + .optProgress(new ProgressBar()) + .build(); + + return criteria; + } +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/layout/LayoutDetectionTranslator.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/layout/LayoutDetectionTranslator.java new file mode 100644 index 00000000..80efcaa8 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/layout/LayoutDetectionTranslator.java @@ -0,0 +1,104 @@ +package jnpf.ocr_sdk.utils.layout; + +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.output.BoundingBox; +import ai.djl.modality.cv.output.DetectedObjects; +import ai.djl.modality.cv.output.Rectangle; +import ai.djl.modality.cv.util.NDImageUtils; +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDList; +import ai.djl.ndarray.types.DataType; +import ai.djl.translate.Batchifier; +import ai.djl.translate.Translator; +import ai.djl.translate.TranslatorContext; + +import java.util.ArrayList; +import java.util.List; + +public class LayoutDetectionTranslator implements Translator { + + private int width; + private int height; + + public LayoutDetectionTranslator() {} + + @Override + public DetectedObjects processOutput(TranslatorContext ctx, NDList list) { + NDArray result = list.get(0); // np_boxes + long rows = result.size(0); + + List boxes = new ArrayList<>(); + List names = new ArrayList<>(); + List probs = new ArrayList<>(); + + for (long i = 0; i < rows; i++) { + NDArray row = result.get(i); + float[] array = row.toFloatArray(); + if (array[1] <= 0.5 || array[0] <= -1) continue; + int clsid = (int) array[0]; + double score = array[1]; + String name = ""; + switch (clsid) { + case 0: + name = "Text"; + break; + case 1: + name = "Title"; + break; + case 2: + name = "List"; + break; + case 3: + name = "Table"; + break; + case 4: + name = "Figure"; + break; + default: + name = "Unknown"; + } + + float x = array[2] / width; + float y = array[3] / height; + float w = (array[4] - array[2]) / width; + float h = (array[5] - array[3]) / height; + + Rectangle rect = new Rectangle(x, y, w, h); + boxes.add(rect); + names.add(name); + probs.add(score); + } + + return new DetectedObjects(names, probs, boxes); + } + + @Override + public NDList processInput(TranslatorContext ctx, Image input) { + NDArray img = input.toNDArray(ctx.getNDManager()); + width = input.getWidth(); + height = input.getHeight(); + + img = NDImageUtils.resize(img, 640, 640); + img = img.transpose(2, 0, 1).div(255); + img = + NDImageUtils.normalize( + img, new float[] {0.485f, 0.456f, 0.406f}, new float[] {0.229f, 0.224f, 0.225f}); + img = img.expandDims(0); + + NDArray scale_factor = ctx.getNDManager().create(new float[] {640f / height, 640f / width}); + scale_factor = scale_factor.toType(DataType.FLOAT32, false); + scale_factor = scale_factor.expandDims(0); + + NDArray im_shape = ctx.getNDManager().create(new float[] {640f, 640f}); + im_shape = im_shape.toType(DataType.FLOAT32, false); + im_shape = im_shape.expandDims(0); + + // im_shape, image, scale_factor + return new NDList(im_shape, img, scale_factor); + } + + @Override + public Batchifier getBatchifier() { + return null; + } +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/opencv/NDArrayUtils.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/opencv/NDArrayUtils.java new file mode 100644 index 00000000..1da0badb --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/opencv/NDArrayUtils.java @@ -0,0 +1,64 @@ +package jnpf.ocr_sdk.utils.opencv; + +import ai.djl.ndarray.NDArray; +import org.bytedeco.javacpp.indexer.DoubleRawIndexer; +import org.bytedeco.opencv.global.opencv_core; +import org.bytedeco.opencv.opencv_core.Mat; +import org.bytedeco.opencv.opencv_core.Point2f; + +import java.util.List; + +public class NDArrayUtils { + // NDArray 转 opencv_core.Mat + public static Mat toOpenCVMat(NDArray points, int rows, int cols) { + double[] doubleArray = points.toDoubleArray(); + // CV_32F = FloatRawIndexer + // CV_64F = DoubleRawIndexer + Mat mat = new Mat(rows, cols, opencv_core.CV_64F); + + DoubleRawIndexer ldIdx = mat.createIndexer(); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + ldIdx.put(i, j, doubleArray[i * cols + j]); + } + } + ldIdx.release(); + + return mat; + } + + // NDArray 转 opencv_core.Point2f + public static Point2f toOpenCVPoint2f(NDArray points, int rows) { + double[] doubleArray = points.toDoubleArray(); + Point2f points2f = new Point2f(rows); + + for (int i = 0; i < rows; i++) { + points2f.position(i).x((float) doubleArray[i * 2]).y((float) doubleArray[i * 2 + 1]); + } + + return points2f; + } + + // Double array 转 opencv_core.Point2f + public static Point2f toOpenCVPoint2f(double[] doubleArray, int rows) { + Point2f points2f = new Point2f(rows); + + for (int i = 0; i < rows; i++) { + points2f.position(i).x((float) doubleArray[i * 2]).y((float) doubleArray[i * 2 + 1]); + } + + return points2f; + } + + // list 转 opencv_core.Point2f + public static Point2f toOpenCVPoint2f(List points, int rows) { + Point2f points2f = new Point2f(points.size()); + + for (int i = 0; i < rows; i++) { + ai.djl.modality.cv.output.Point point = points.get(i); + points2f.position(i).x((float) point.getX()).y((float) point.getY()); + } + + return points2f; + } +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/opencv/OpenCVUtils.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/opencv/OpenCVUtils.java new file mode 100644 index 00000000..92a7731c --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/opencv/OpenCVUtils.java @@ -0,0 +1,72 @@ +package jnpf.ocr_sdk.utils.opencv; + +import org.bytedeco.opencv.global.opencv_imgproc; +import org.bytedeco.opencv.opencv_core.Mat; +import org.bytedeco.opencv.opencv_core.Point2f; +import org.opencv.core.CvType; +import org.opencv.imgproc.Imgproc; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.awt.image.WritableRaster; + + +public class OpenCVUtils { + + public static Mat perspectiveTransform( + Mat src, Point2f srcPoints, Point2f dstPoints) { + Mat dst = src.clone(); + Mat warp_mat = opencv_imgproc.getPerspectiveTransform(srcPoints.position(0), dstPoints.position(0)); + opencv_imgproc.warpPerspective(src, dst, warp_mat, dst.size()); + warp_mat.release(); + + return dst; + } + + /** + * Mat to BufferedImage + * + * @param mat + * @return + */ + public static BufferedImage mat2Image(org.opencv.core.Mat mat) { + int width = mat.width(); + int height = mat.height(); + byte[] data = new byte[width * height * (int) mat.elemSize()]; + Imgproc.cvtColor(mat, mat, 4); + mat.get(0, 0, data); + BufferedImage ret = new BufferedImage(width, height, 5); + ret.getRaster().setDataElements(0, 0, width, height, data); + return ret; + } + + public static BufferedImage matToBufferedImage(org.opencv.core.Mat frame) { + int type = 0; + if (frame.channels() == 1) { + type = BufferedImage.TYPE_BYTE_GRAY; + } else if (frame.channels() == 3) { + type = BufferedImage.TYPE_3BYTE_BGR; + } + BufferedImage image = new BufferedImage(frame.width(), frame.height(), type); + WritableRaster raster = image.getRaster(); + DataBufferByte dataBuffer = (DataBufferByte) raster.getDataBuffer(); + byte[] data = dataBuffer.getData(); + frame.get(0, 0, data); + return image; + } + + /** + * BufferedImage to Mat + * + * @param img + * @return + */ + public static org.opencv.core.Mat image2Mat(BufferedImage img) { + int width = img.getWidth(); + int height = img.getHeight(); + byte[] data = ((DataBufferByte) img.getRaster().getDataBuffer()).getData(); + org.opencv.core.Mat mat = new org.opencv.core.Mat(height, width, CvType.CV_8UC3); + mat.put(0, 0, data); + return mat; + } +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/recognition/OcrV3AlignedRecognition.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/recognition/OcrV3AlignedRecognition.java new file mode 100644 index 00000000..0ac9fc9f --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/recognition/OcrV3AlignedRecognition.java @@ -0,0 +1,129 @@ +package jnpf.ocr_sdk.utils.recognition; + +import ai.djl.inference.Predictor; +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.ImageFactory; +import ai.djl.modality.cv.output.BoundingBox; +import ai.djl.modality.cv.output.DetectedObjects; +import ai.djl.modality.cv.output.Rectangle; +import ai.djl.modality.cv.util.NDImageUtils; +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDManager; +import ai.djl.paddlepaddle.zoo.cv.objectdetection.PpWordDetectionTranslator; +import ai.djl.repository.zoo.Criteria; +import ai.djl.training.util.ProgressBar; +import ai.djl.translate.TranslateException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +public final class OcrV3AlignedRecognition { + + private static final Logger logger = LoggerFactory.getLogger(OcrV3AlignedRecognition.class); + + public OcrV3AlignedRecognition() { + } + + public DetectedObjects predict( + Image image, Predictor detector, Predictor recognizer) + throws TranslateException { + DetectedObjects detections = detector.predict(image); + + List boxes = detections.items(); + + List names = new ArrayList<>(); + List prob = new ArrayList<>(); + List rect = new ArrayList<>(); + + long timeInferStart = System.currentTimeMillis(); + for (int i = 0; i < boxes.size(); i++) { + Image subImg = getSubImage(image, boxes.get(i).getBoundingBox()); + if (subImg.getHeight() * 1.0 / subImg.getWidth() > 1.5) { + subImg = rotateImg(subImg); + } +// ImageUtils.saveImage(subImg, i + ".png", "build/output"); + String name = recognizer.predict(subImg); + names.add(name); + prob.add(-1.0); + rect.add(boxes.get(i).getBoundingBox()); + } + long timeInferEnd = System.currentTimeMillis(); + System.out.println("time: " + (timeInferEnd - timeInferStart)); + + DetectedObjects detectedObjects = new DetectedObjects(names, prob, rect); + + return detectedObjects; + } + + public Criteria detectCriteria() { + Criteria criteria = + Criteria.builder() + .optEngine("PaddlePaddle") + .setTypes(Image.class, DetectedObjects.class) + .optModelUrls( + "https://aias-home.oss-cn-beijing.aliyuncs.com/models/ocr_models/ch_PP-OCRv3_det_infer.zip") + // .optModelUrls( + // "/Users/calvin/Documents/build/paddle_models/ppocr/ch_PP-OCRv2_det_infer") + .optTranslator(new PpWordDetectionTranslator(new ConcurrentHashMap())) + .optProgress(new ProgressBar()) + .build(); + + return criteria; + } + + public Criteria recognizeCriteria() { + Criteria criteria = + Criteria.builder() + .optEngine("PaddlePaddle") + .setTypes(Image.class, String.class) + .optModelUrls( + "https://aias-home.oss-cn-beijing.aliyuncs.com/models/ocr_models/ch_PP-OCRv3_rec_infer.zip") + .optProgress(new ProgressBar()) + .optTranslator(new PpWordRecognitionTranslator((new ConcurrentHashMap()))) + .build(); + + return criteria; + } + + private Image getSubImage(Image img, BoundingBox box) { + Rectangle rect = box.getBounds(); + double[] extended = extendRect(rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight()); + int width = img.getWidth(); + int height = img.getHeight(); + int[] recovered = { + (int) (extended[0] * width), + (int) (extended[1] * height), + (int) (extended[2] * width), + (int) (extended[3] * height) + }; + + return img.getSubImage(recovered[0], recovered[1], recovered[2], recovered[3]); + } + + private double[] extendRect(double xmin, double ymin, double width, double height) { + double centerx = xmin + width / 2; + double centery = ymin + height / 2; + if (width > height) { + width += height * 2.0; + height *= 3.0; + } else { + height += width * 2.0; + width *= 3.0; + } + double newX = centerx - width / 2 < 0 ? 0 : centerx - width / 2; + double newY = centery - height / 2 < 0 ? 0 : centery - height / 2; + double newWidth = newX + width > 1 ? 1 - newX : width; + double newHeight = newY + height > 1 ? 1 - newY : height; + return new double[]{newX, newY, newWidth, newHeight}; + } + + private Image rotateImg(Image image) { + try (NDManager manager = NDManager.newBaseManager()) { + NDArray rotated = NDImageUtils.rotate90(image.toNDArray(manager), 1); + return ImageFactory.getInstance().fromNDArray(rotated); + } + } +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/recognition/OcrV3MultiThreadRecognition.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/recognition/OcrV3MultiThreadRecognition.java new file mode 100644 index 00000000..b2e33798 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/recognition/OcrV3MultiThreadRecognition.java @@ -0,0 +1,194 @@ +package jnpf.ocr_sdk.utils.recognition; + +import ai.djl.inference.Predictor; +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.ImageFactory; +import ai.djl.modality.cv.output.BoundingBox; +import ai.djl.modality.cv.output.DetectedObjects; +import ai.djl.modality.cv.output.Rectangle; +import ai.djl.modality.cv.util.NDImageUtils; +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDManager; +import ai.djl.paddlepaddle.zoo.cv.objectdetection.PpWordDetectionTranslator; +import ai.djl.repository.zoo.Criteria; +import ai.djl.repository.zoo.ZooModel; +import ai.djl.training.util.ProgressBar; +import ai.djl.translate.TranslateException; +import jnpf.ocr_sdk.utils.common.ImageInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; + +public final class OcrV3MultiThreadRecognition { + + private static final Logger logger = LoggerFactory.getLogger(OcrV3MultiThreadRecognition.class); + + public OcrV3MultiThreadRecognition() { + } + + public DetectedObjects predict( + Image image, List recModels, Predictor detector, int threadNum) + throws TranslateException { + DetectedObjects detections = detector.predict(image); + + List boxes = detections.items(); + + ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); + for (int i = 0; i < boxes.size(); i++) { + BoundingBox box = boxes.get(i).getBoundingBox(); + Image subImg = getSubImage(image, box); + if (subImg.getHeight() * 1.0 / subImg.getWidth() > 1.5) { + subImg = rotateImg(subImg); + } + ImageInfo imageInfo = new ImageInfo(subImg, box); + queue.add(imageInfo); + } + + List callables = new ArrayList<>(threadNum); + for (int i = 0; i < threadNum; i++) { + callables.add(new InferCallable(recModels.get(i), queue)); + } + + ExecutorService es = Executors.newFixedThreadPool(threadNum); + List resultList = new ArrayList<>(); + try { + List>> futures = new ArrayList<>(); + long timeInferStart = System.currentTimeMillis(); + for (InferCallable callable : callables) { + futures.add(es.submit(callable)); + } + + for (Future> future : futures) { + List subList = future.get(); + if (subList != null) { + resultList.addAll(subList); + } + } + + long timeInferEnd = System.currentTimeMillis(); + System.out.println("time: " + (timeInferEnd - timeInferStart)); + + for (InferCallable callable : callables) { + callable.close(); + } + } catch (InterruptedException | ExecutionException e) { + logger.error("", e); + } finally { + es.shutdown(); + } + + List names = new ArrayList<>(); + List prob = new ArrayList<>(); + List rect = new ArrayList<>(); + for (ImageInfo imageInfo : resultList) { + names.add(imageInfo.getName()); + prob.add(imageInfo.getProb()); + rect.add(imageInfo.getBox()); + } + DetectedObjects detectedObjects = new DetectedObjects(names, prob, rect); + + return detectedObjects; + } + + public Criteria detectCriteria() { + Criteria criteria = + Criteria.builder() + .optEngine("PaddlePaddle") + .setTypes(Image.class, DetectedObjects.class) + .optModelUrls( + "https://aias-home.oss-cn-beijing.aliyuncs.com/models/ocr_models/ch_PP-OCRv3_det_infer.zip") + // .optModelUrls( + // "/Users/calvin/Documents/build/paddle_models/ppocr/ch_PP-OCRv2_det_infer") + .optTranslator(new PpWordDetectionTranslator(new ConcurrentHashMap())) + .optProgress(new ProgressBar()) + .build(); + + return criteria; + } + + public Criteria recognizeCriteria() { + Criteria criteria = + Criteria.builder() + .optEngine("PaddlePaddle") + .setTypes(Image.class, String.class) + .optModelUrls( + "https://aias-home.oss-cn-beijing.aliyuncs.com/models/ocr_models/ch_PP-OCRv3_rec_infer.zip") + .optProgress(new ProgressBar()) + .optTranslator(new PpWordRecognitionTranslator((new ConcurrentHashMap()))) + .build(); + + return criteria; + } + + private static class InferCallable implements Callable> { + private Predictor recognizer; + private ConcurrentLinkedQueue queue; + private List resultList = new ArrayList<>(); + + public InferCallable(ZooModel recognitionModel, ConcurrentLinkedQueue queue){ + recognizer = recognitionModel.newPredictor(); + this.queue = queue; + } + + public List call() { + try { + ImageInfo imageInfo = queue.poll(); + while (imageInfo != null) { + String name = recognizer.predict(imageInfo.getImage()); + imageInfo.setName(name); + imageInfo.setProb(-1.0); + resultList.add(imageInfo); + imageInfo = queue.poll(); + } + } catch (Exception e) { + e.printStackTrace(); + } + return resultList; + } + + public void close() { + recognizer.close(); + } + } + + private Image getSubImage(Image img, BoundingBox box) { + Rectangle rect = box.getBounds(); + double[] extended = extendRect(rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight()); + int width = img.getWidth(); + int height = img.getHeight(); + int[] recovered = { + (int) (extended[0] * width), + (int) (extended[1] * height), + (int) (extended[2] * width), + (int) (extended[3] * height) + }; + return img.getSubImage(recovered[0], recovered[1], recovered[2], recovered[3]); + } + + private double[] extendRect(double xmin, double ymin, double width, double height) { + double centerx = xmin + width / 2; + double centery = ymin + height / 2; + if (width > height) { + width += height * 2.0; + height *= 3.0; + } else { + height += width * 2.0; + width *= 3.0; + } + double newX = centerx - width / 2 < 0 ? 0 : centerx - width / 2; + double newY = centery - height / 2 < 0 ? 0 : centery - height / 2; + double newWidth = newX + width > 1 ? 1 - newX : width; + double newHeight = newY + height > 1 ? 1 - newY : height; + return new double[]{newX, newY, newWidth, newHeight}; + } + + private Image rotateImg(Image image) { + try (NDManager manager = NDManager.newBaseManager()) { + NDArray rotated = NDImageUtils.rotate90(image.toNDArray(manager), 1); + return ImageFactory.getInstance().fromNDArray(rotated); + } + } +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/recognition/OcrV3Recognition.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/recognition/OcrV3Recognition.java new file mode 100644 index 00000000..89687b9b --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/recognition/OcrV3Recognition.java @@ -0,0 +1,140 @@ +package jnpf.ocr_sdk.utils.recognition; + +import ai.djl.inference.Predictor; +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.ImageFactory; +import ai.djl.modality.cv.output.Point; +import ai.djl.modality.cv.util.NDImageUtils; +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDList; +import ai.djl.ndarray.NDManager; +import ai.djl.opencv.OpenCVImageFactory; +import ai.djl.repository.zoo.Criteria; +import ai.djl.training.util.ProgressBar; +import ai.djl.translate.TranslateException; +import jnpf.ocr_sdk.utils.common.RotatedBox; +import jnpf.ocr_sdk.utils.opencv.NDArrayUtils; +import jnpf.ocr_sdk.utils.opencv.OpenCVUtils; +import org.bytedeco.javacv.Java2DFrameConverter; +import org.bytedeco.javacv.OpenCVFrameConverter; +import org.bytedeco.opencv.opencv_core.Point2f; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +public final class OcrV3Recognition { + + private static final Logger logger = LoggerFactory.getLogger(OcrV3Recognition.class); + + public OcrV3Recognition() { + } + + public Criteria recognizeCriteria() { + Criteria criteria = + Criteria.builder() + .optEngine("PaddlePaddle") + .setTypes(Image.class, String.class) + .optModelUrls( + "https://aias-home.oss-cn-beijing.aliyuncs.com/models/ocr_models/ch_PP-OCRv3_rec_infer.zip") + .optProgress(new ProgressBar()) + .optTranslator(new PpWordRecognitionTranslator((new ConcurrentHashMap()))) + .build(); + return criteria; + } + + public List predict( + Image image, Predictor detector, Predictor recognizer) + throws TranslateException { + NDList boxes = detector.predict(image); + + List result = new ArrayList<>(); + long timeInferStart = System.currentTimeMillis(); + + OpenCVFrameConverter.ToMat cv = new OpenCVFrameConverter.ToMat(); + OpenCVFrameConverter.ToMat converter1 = new OpenCVFrameConverter.ToMat(); + OpenCVFrameConverter.ToOrgOpenCvCoreMat converter2 = new OpenCVFrameConverter.ToOrgOpenCvCoreMat(); + + for (int i = 0; i < boxes.size(); i++) { + NDArray box = boxes.get(i); +// BufferedImage bufferedImage = get_rotate_crop_image(image, box); + + float[] pointsArr = box.toFloatArray(); + float[] lt = java.util.Arrays.copyOfRange(pointsArr, 0, 2); + float[] rt = java.util.Arrays.copyOfRange(pointsArr, 2, 4); + float[] rb = java.util.Arrays.copyOfRange(pointsArr, 4, 6); + float[] lb = java.util.Arrays.copyOfRange(pointsArr, 6, 8); + int img_crop_width = (int) Math.max(distance(lt, rt), distance(rb, lb)); + int img_crop_height = (int) Math.max(distance(lt, lb), distance(rt, rb)); + List srcPoints = new ArrayList<>(); + srcPoints.add(new Point(lt[0], lt[1])); + srcPoints.add(new Point(rt[0], rt[1])); + srcPoints.add(new Point(rb[0], rb[1])); + srcPoints.add(new Point(lb[0], lb[1])); + List dstPoints = new ArrayList<>(); + dstPoints.add(new Point(0, 0)); + dstPoints.add(new Point(img_crop_width, 0)); + dstPoints.add(new Point(img_crop_width, img_crop_height)); + dstPoints.add(new Point(0, img_crop_height)); + + Point2f srcPoint2f = NDArrayUtils.toOpenCVPoint2f(srcPoints, 4); + Point2f dstPoint2f = NDArrayUtils.toOpenCVPoint2f(dstPoints, 4); + + BufferedImage bufferedImage = OpenCVUtils.matToBufferedImage((org.opencv.core.Mat) image.getWrappedImage()); + // try { + // File outputfile = new File("build/output/srcImage.jpg"); + // ImageIO.write(bufferedImage, "jpg", outputfile); + // } catch (IOException e) { + // e.printStackTrace(); + // } + org.bytedeco.opencv.opencv_core.Mat mat = cv.convertToMat(new Java2DFrameConverter().convert(bufferedImage)); + org.bytedeco.opencv.opencv_core.Mat dstMat = OpenCVUtils.perspectiveTransform(mat, srcPoint2f, dstPoint2f); + org.opencv.core.Mat cvMat = converter2.convert(converter1.convert(dstMat)); + Image subImg = OpenCVImageFactory.getInstance().fromImage(cvMat); +// ImageUtils.saveImage(subImg, i + ".png", "build/output"); + + subImg = subImg.getSubImage(0,0,img_crop_width,img_crop_height); + if (subImg.getHeight() * 1.0 / subImg.getWidth() > 1.5) { + subImg = rotateImg(subImg); + } + + String name = recognizer.predict(subImg); + RotatedBox rotatedBox = new RotatedBox(box, name); + result.add(rotatedBox); + + mat.release(); + dstMat.release(); + cvMat.release(); + srcPoint2f.releaseReference(); + dstPoint2f.releaseReference(); + } + cv.close(); + converter1.close(); + converter2.close(); + long timeInferEnd = System.currentTimeMillis(); + System.out.println("time: " + (timeInferEnd - timeInferStart)); + + return result; + } + + private BufferedImage get_rotate_crop_image(Image image, NDArray box) { + return null; + } + + private float distance(float[] point1, float[] point2) { + float disX = point1[0] - point2[0]; + float disY = point1[1] - point2[1]; + float dis = (float) Math.sqrt(disX * disX + disY * disY); + return dis; + } + + private Image rotateImg(Image image) { + try (NDManager manager = NDManager.newBaseManager()) { + NDArray rotated = NDImageUtils.rotate90(image.toNDArray(manager), 1); + return ImageFactory.getInstance().fromNDArray(rotated); + } + } +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/recognition/PpWordRecognitionTranslator.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/recognition/PpWordRecognitionTranslator.java new file mode 100644 index 00000000..5d886ac6 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/recognition/PpWordRecognitionTranslator.java @@ -0,0 +1,108 @@ +package jnpf.ocr_sdk.utils.recognition; + +import ai.djl.Model; +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.util.NDImageUtils; +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDList; +import ai.djl.ndarray.index.NDIndex; +import ai.djl.ndarray.types.DataType; +import ai.djl.ndarray.types.Shape; +import ai.djl.translate.Batchifier; +import ai.djl.translate.Translator; +import ai.djl.translate.TranslatorContext; +import ai.djl.util.Utils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class PpWordRecognitionTranslator implements Translator { + private List table; + private final boolean use_space_char; + + public PpWordRecognitionTranslator(Map arguments) { + use_space_char = + arguments.containsKey("use_space_char") + ? Boolean.parseBoolean(arguments.get("use_space_char").toString()) + : false; + } + + @Override + public void prepare(TranslatorContext ctx) throws IOException { + Model model = ctx.getModel(); + try (InputStream is = model.getArtifact("ppocr_keys_v1.txt").openStream()) { + table = Utils.readLines(is, true); + table.add(0, "blank"); + if(use_space_char) + table.add(" "); + else + table.add(""); + } + } + + @Override + public String processOutput(TranslatorContext ctx, NDList list) throws IOException { + StringBuilder sb = new StringBuilder(); + NDArray tokens = list.singletonOrThrow(); + + long[] indices = tokens.get(0).argMax(1).toLongArray(); + boolean[] selection = new boolean[indices.length]; + Arrays.fill(selection, true); + for (int i = 1; i < indices.length; i++) { + if (indices[i] == indices[i - 1]) { + selection[i] = false; + } + } + + // 字符置信度 +// float[] probs = new float[indices.length]; +// for (int row = 0; row < indices.length; row++) { +// NDArray value = tokens.get(0).get(new NDIndex(""+ row +":" + (row + 1) +"," + indices[row] +":" + ( indices[row] + 1))); +// probs[row] = value.toFloatArray()[0]; +// } + + int lastIdx = 0; + for (int i = 0; i < indices.length; i++) { + if (selection[i] == true && indices[i] > 0 && !(i > 0 && indices[i] == lastIdx)) { + sb.append(table.get((int) indices[i])); + } + } + return sb.toString(); + } + + @Override + public NDList processInput(TranslatorContext ctx, Image input) { + NDArray img = input.toNDArray(ctx.getNDManager(), Image.Flag.COLOR); + int imgC = 3; + int imgH = 48; + int imgW = 320;//192 320 + + int h = input.getHeight(); + int w = input.getWidth(); + float ratio = (float) w / (float) h; + imgW = (int)(imgH * ratio); + + int resized_w; + if (Math.ceil(imgH * ratio) > imgW) { + resized_w = imgW; + } else { + resized_w = (int) (Math.ceil(imgH * ratio)); + } + img = NDImageUtils.resize(img, resized_w, imgH); + img = img.transpose(2, 0, 1).div(255).sub(0.5f).div(0.5f); + NDArray padding_im = ctx.getNDManager().zeros(new Shape(imgC, imgH, imgW), DataType.FLOAT32); + padding_im.set(new NDIndex(":,:,0:" + resized_w), img); + + padding_im = padding_im.expandDims(0); + return new NDList(padding_im); + } + + @Override + public Batchifier getBatchifier() { + return null; + } + +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/rotation/PpWordRotateTranslator.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/rotation/PpWordRotateTranslator.java new file mode 100644 index 00000000..3eaffe02 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/rotation/PpWordRotateTranslator.java @@ -0,0 +1,69 @@ +package jnpf.ocr_sdk.utils.rotation; + +import ai.djl.modality.Classifications; +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.util.NDImageUtils; +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDList; +import ai.djl.ndarray.index.NDIndex; +import ai.djl.ndarray.types.Shape; +import ai.djl.translate.Batchifier; +import ai.djl.translate.Translator; +import ai.djl.translate.TranslatorContext; + +import java.util.Arrays; +import java.util.List; + +public class PpWordRotateTranslator implements Translator { + List classes = Arrays.asList("No Rotate", "Rotate"); + + public PpWordRotateTranslator() {} + + public Classifications processOutput(TranslatorContext ctx, NDList list) { + NDArray prob = list.singletonOrThrow(); + return new Classifications(this.classes, prob); + } + + public NDList processInput(TranslatorContext ctx, Image input) throws Exception { + NDArray img = input.toNDArray(ctx.getNDManager()); + img = NDImageUtils.resize(img, 192, 48); + img = NDImageUtils.toTensor(img).sub(0.5F).div(0.5F); + img = img.expandDims(0); + return new NDList(new NDArray[]{img}); + } + + public NDList processInputBak(TranslatorContext ctx, Image input) throws Exception { + NDArray img = input.toNDArray(ctx.getNDManager()); + int imgC = 3; + int imgH = 48; + int imgW = 192; + + NDArray array = ctx.getNDManager().zeros(new Shape(imgC, imgH, imgW)); + + int h = input.getHeight(); + int w = input.getWidth(); + int resized_w = 0; + + float ratio = (float) w / (float) h; + if (Math.ceil(imgH * ratio) > imgW) { + resized_w = imgW; + } else { + resized_w = (int) (Math.ceil(imgH * ratio)); + } + + img = NDImageUtils.resize(img, resized_w, imgH); + + img = NDImageUtils.toTensor(img).sub(0.5F).div(0.5F); + // img = img.transpose(2, 0, 1); + + array.set(new NDIndex(":,:,0:" + resized_w), img); + + array = array.expandDims(0); + + return new NDList(new NDArray[] {array}); + } + + public Batchifier getBatchifier() { + return null; + } +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/ConvertHtml2Excel.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/ConvertHtml2Excel.java new file mode 100644 index 00000000..f2fe29e4 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/ConvertHtml2Excel.java @@ -0,0 +1,235 @@ +package jnpf.ocr_sdk.utils.table; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.poi.hssf.usermodel.*; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.util.CellRangeAddress; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.DocumentHelper; +import org.dom4j.Element; + +import java.util.ArrayList; +import java.util.List; + +/** + * @Auther: xiaoqiang + * @Date: 2020/12/9 9:16 + * @Description: + */ +public class ConvertHtml2Excel { + /** + * html表格转excel + * + * @param tableHtml 如 + * + * .. + *
+ * @return + */ + public static HSSFWorkbook table2Excel(String tableHtml) { + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet(); + List crossRowEleMetaLs = new ArrayList<>(); + int rowIndex = 0; + try { + Document data = DocumentHelper.parseText(tableHtml); + // 生成表头 + Element thead = data.getRootElement().element("thead"); + HSSFCellStyle titleStyle = getTitleStyle(wb); + int ls=0;//列数 + if (thead != null) { + List trLs = thead.elements("tr"); + for (Element trEle : trLs) { + HSSFRow row = sheet.createRow(rowIndex); + List thLs = trEle.elements("td"); + ls=thLs.size(); + makeRowCell(thLs, rowIndex, row, 0, titleStyle, crossRowEleMetaLs); + rowIndex++; + } + } + // 生成表体 + Element tbody = data.getRootElement().element("tbody"); + HSSFCellStyle contentStyle = getContentStyle(wb); + if (tbody != null) { + List trLs = tbody.elements("tr"); + for (Element trEle : trLs) { + HSSFRow row = sheet.createRow(rowIndex); + List thLs = trEle.elements("th"); + int cellIndex = makeRowCell(thLs, rowIndex, row, 0, titleStyle, crossRowEleMetaLs); + List tdLs = trEle.elements("td"); + makeRowCell(tdLs, rowIndex, row, cellIndex, contentStyle, crossRowEleMetaLs); + rowIndex++; + } + } + // 合并表头 + for (CrossRangeCellMeta crcm : crossRowEleMetaLs) { + sheet.addMergedRegion(new CellRangeAddress(crcm.getFirstRow(), crcm.getLastRow(), crcm.getFirstCol(), crcm.getLastCol())); + setRegionStyle(sheet, new CellRangeAddress(crcm.getFirstRow(), crcm.getLastRow(), crcm.getFirstCol(), crcm.getLastCol()),titleStyle); + } + for(int i=0;i tdLs, int rowIndex, HSSFRow row, int startCellIndex, HSSFCellStyle cellStyle, + List crossRowEleMetaLs) { + int i = startCellIndex; + for (int eleIndex = 0; eleIndex < tdLs.size(); i++, eleIndex++) { + int captureCellSize = getCaptureCellSize(rowIndex, i, crossRowEleMetaLs); + while (captureCellSize > 0) { + for (int j = 0; j < captureCellSize; j++) {// 当前行跨列处理(补单元格) + row.createCell(i); + i++; + } + captureCellSize = getCaptureCellSize(rowIndex, i, crossRowEleMetaLs); + } + Element thEle = tdLs.get(eleIndex); + String val = thEle.getTextTrim(); + if (StringUtils.isBlank(val)) { + Element e = thEle.element("a"); + if (e != null) { + val = e.getTextTrim(); + } + } + HSSFCell c = row.createCell(i); + if (NumberUtils.isNumber(val)) { + c.setCellValue(Double.parseDouble(val)); + c.setCellType(CellType.NUMERIC); + } else { + c.setCellValue(val); + } + int rowSpan = NumberUtils.toInt(thEle.attributeValue("rowspan"), 1); + int colSpan = NumberUtils.toInt(thEle.attributeValue("colspan"), 1); + c.setCellStyle(cellStyle); + if (rowSpan > 1 || colSpan > 1) { // 存在跨行或跨列 + crossRowEleMetaLs.add(new CrossRangeCellMeta(rowIndex, i, rowSpan, colSpan)); + } + if (colSpan > 1) {// 当前行跨列处理(补单元格) + for (int j = 1; j < colSpan; j++) { + i++; + row.createCell(i); + } + } + } + return i; + } + + /** + * 设置合并单元格的边框样式 + * + * @param sheet + * @param region + * @param cs + */ + public static void setRegionStyle(HSSFSheet sheet, CellRangeAddress region, HSSFCellStyle cs) { + for (int i = region.getFirstRow(); i <= region.getLastRow(); i++) { + HSSFRow row = sheet.getRow(i); + for (int j = region.getFirstColumn(); j <= region.getLastColumn(); j++) { + HSSFCell cell = row.getCell(j); + cell.setCellStyle(cs); + } + } + } + + /** + * 获得因rowSpan占据的单元格 + * + * @param rowIndex 行号 + * @param colIndex 列号 + * @param crossRowEleMetaLs 跨行列元数据 + * @return 当前行在某列需要占据单元格 + */ + private static int getCaptureCellSize(int rowIndex, int colIndex, List crossRowEleMetaLs) { + int captureCellSize = 0; + for (CrossRangeCellMeta crossRangeCellMeta : crossRowEleMetaLs) { + if (crossRangeCellMeta.getFirstRow() < rowIndex && crossRangeCellMeta.getLastRow() >= rowIndex) { + if (crossRangeCellMeta.getFirstCol() <= colIndex && crossRangeCellMeta.getLastCol() >= colIndex) { + captureCellSize = crossRangeCellMeta.getLastCol() - colIndex + 1; + } + } + } + return captureCellSize; + } + + /** + * 获得标题样式 + * + * @param workbook + * @return + */ + private static HSSFCellStyle getTitleStyle(HSSFWorkbook workbook) { + //short titlebackgroundcolor = IndexedColors.GREY_25_PERCENT.index; + short fontSize = 12; + String fontName = "宋体"; + HSSFCellStyle style = workbook.createCellStyle(); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setAlignment(HorizontalAlignment.CENTER); + style.setBorderBottom(BorderStyle.THIN); //下边框 + style.setBorderLeft(BorderStyle.THIN);//左边框 + style.setBorderTop(BorderStyle.THIN);//上边框 + style.setBorderRight(BorderStyle.THIN);//右边框 + //style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + //style.setFillForegroundColor(titlebackgroundcolor);// 背景色 + + HSSFFont font = workbook.createFont(); + font.setFontName(fontName); + font.setFontHeightInPoints(fontSize); + font.setBold(true); + style.setFont(font); + return style; + } + + /** + * 获得内容样式 + * + * @param wb + * @return + */ + private static HSSFCellStyle getContentStyle(HSSFWorkbook wb) { + short fontSize = 12; + String fontName = "宋体"; + HSSFCellStyle style = wb.createCellStyle(); + style.setBorderBottom(BorderStyle.THIN); //下边框 + style.setBorderLeft(BorderStyle.THIN);//左边框 + style.setBorderTop(BorderStyle.THIN);//上边框 + style.setBorderRight(BorderStyle.THIN);//右边框 + HSSFFont font = wb.createFont(); + font.setFontName(fontName); + font.setFontHeightInPoints(fontSize); + style.setFont(font); + style.setAlignment(HorizontalAlignment.CENTER);//水平居中 + style.setVerticalAlignment(VerticalAlignment.CENTER);//垂直居中 + style.setWrapText(true); + return style; + } +} + diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/CrossRangeCellMeta.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/CrossRangeCellMeta.java new file mode 100644 index 00000000..9d780046 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/CrossRangeCellMeta.java @@ -0,0 +1,42 @@ +package jnpf.ocr_sdk.utils.table; + +/** + * @Auther: xiaoqiang + * @Date: 2020/12/9 9:17 + * @Description: + */ +public class CrossRangeCellMeta { + public CrossRangeCellMeta(int firstRowIndex, int firstColIndex, int rowSpan, int colSpan) { + super(); + this.firstRowIndex = firstRowIndex; + this.firstColIndex = firstColIndex; + this.rowSpan = rowSpan; + this.colSpan = colSpan; + } + + private int firstRowIndex; + private int firstColIndex; + private int rowSpan;// 跨越行数 + private int colSpan;// 跨越列数 + + public int getFirstRow() { + return firstRowIndex; + } + + public int getLastRow() { + return firstRowIndex + rowSpan - 1; + } + + public int getFirstCol() { + return firstColIndex; + } + + public int getLastCol() { + return firstColIndex + colSpan - 1; + } + + public int getColSpan(){ + return colSpan; + } +} + diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/TableDetection.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/TableDetection.java new file mode 100644 index 00000000..a4270ed4 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/TableDetection.java @@ -0,0 +1,227 @@ +package jnpf.ocr_sdk.utils.table; + +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.output.BoundingBox; +import ai.djl.modality.cv.output.DetectedObjects; +import ai.djl.modality.cv.output.Rectangle; +import ai.djl.repository.zoo.Criteria; +import ai.djl.training.util.ProgressBar; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public final class TableDetection { + + private static final Logger logger = LoggerFactory.getLogger(TableDetection.class); + + public TableDetection() {} + + public Criteria criteria() { + + Criteria criteria = + Criteria.builder() + .optEngine("PaddlePaddle") + .setTypes(Image.class, TableResult.class) + .optModelUrls( + "https://aias-home.oss-cn-beijing.aliyuncs.com/models/ocr_models/en_table.zip") + // .optModelUrls( + // "/Users/calvin/Documents/build/paddle_models/ppocr/en_ppocr_mobile_v2.0_table_structure_infer") + .optOption("removePass", "repeated_fc_relu_fuse_pass") + .optTranslator(new TableStructTranslator(new ConcurrentHashMap())) + .optProgress(new ProgressBar()) + .build(); + + return criteria; + } + + public List cellContents( + DetectedObjects textDetections, List cells, int width, int height) { + List dt_boxes = textDetections.items(); + + // 获取 Cell 与 文本检测框 的对应关系(1:N)。 + Map> matched = new ConcurrentHashMap<>(); + + for (int i = 0; i < dt_boxes.size(); i++) { + DetectedObjects.DetectedObject item = dt_boxes.get(i); + Rectangle textBounds = item.getBoundingBox().getBounds(); + int[] box_1 = rectXYXY(textBounds, width, height); + // 获取两两cell之间的L1距离和 1- IOU + List> distances = new ArrayList<>(); + for (BoundingBox cell : cells) { + Rectangle cellBounds = cell.getBounds(); + int[] box_2 = rectXYXY(cellBounds, width, height); + float distance = distance(box_1, box_2); + float iou = 1 - compute_iou(box_1, box_2); + distances.add(Pair.of(distance, iou)); + } + // 根据距离和IOU挑选最"近"的cell + Pair nearest = sorted(distances); + + // 获取最小距离对应的下标id,也等价于cell的下标id (distances列表是根据遍历cells生成的) + int id = 0; + for (int idx = 0; idx < distances.size(); idx++) { + Pair current = distances.get(idx); + if (current.getLeft().floatValue() == nearest.getLeft().floatValue() + && current.getRight().floatValue() == nearest.getRight().floatValue()) { + id = idx; + break; + } + } + if (!matched.containsKey(id)) { + List textIds = new ArrayList<>(); + textIds.add(i); + // cell id, text id list (dt_boxes index list) + matched.put(id, textIds); + } else { + matched.get(id).add(i); + } + } + + List cell_contents = new ArrayList<>(); + List probs = new ArrayList<>(); + for (int i = 0; i < cells.size(); i++) { + List textIds = matched.get(i); + List contents = new ArrayList<>(); + String content = ""; + if (textIds != null) { + for (Integer id : textIds) { + DetectedObjects.DetectedObject item = dt_boxes.get(id); + contents.add(item.getClassName()); + } + content = StringUtils.join(contents, " "); + } + + cell_contents.add(content); + probs.add(-1.0); + } + return cell_contents; + } + + /** + * Calculate L1 distance + * + * @param box_1 + * @param box_2 + * @return + */ + private int distance(int[] box_1, int[] box_2) { + int x1 = box_1[0]; + int y1 = box_1[1]; + int x2 = box_1[2]; + int y2 = box_1[3]; + int x3 = box_2[0]; + int y3 = box_2[1]; + int x4 = box_2[2]; + int y4 = box_2[3]; + int dis = Math.abs(x3 - x1) + Math.abs(y3 - y1) + Math.abs(x4 - x2) + Math.abs(y4 - y2); + int dis_2 = Math.abs(x3 - x1) + Math.abs(y3 - y1); + int dis_3 = Math.abs(x4 - x2) + Math.abs(y4 - y2); + return dis + Math.min(dis_2, dis_3); + } + + /** + * Get absolute coordinations + * + * @param rect + * @param width + * @param height + * @return + */ + private int[] rectXYXY(Rectangle rect, int width, int height) { + int left = Math.max((int) (width * rect.getX()), 0); + int top = Math.max((int) (height * rect.getY()), 0); + int right = Math.min((int) (width * (rect.getX() + rect.getWidth())), width - 1); + int bottom = Math.min((int) (height * (rect.getY() + rect.getHeight())), height - 1); + return new int[] {left, top, right, bottom}; + } + + /** + * computing IoU + * + * @param rec1: (y0, x0, y1, x1), which reflects (top, left, bottom, right) + * @param rec2: (y0, x0, y1, x1) + * @return scala value of IoU + */ + private float compute_iou(int[] rec1, int[] rec2) { + // computing area of each rectangles + int S_rec1 = (rec1[2] - rec1[0]) * (rec1[3] - rec1[1]); + int S_rec2 = (rec2[2] - rec2[0]) * (rec2[3] - rec2[1]); + + // computing the sum_area + int sum_area = S_rec1 + S_rec2; + + // find the each edge of intersect rectangle + int left_line = Math.max(rec1[1], rec2[1]); + int right_line = Math.min(rec1[3], rec2[3]); + int top_line = Math.max(rec1[0], rec2[0]); + int bottom_line = Math.min(rec1[2], rec2[2]); + + // judge if there is an intersect + if (left_line >= right_line || top_line >= bottom_line) { + return 0.0f; + } else { + float intersect = (right_line - left_line) * (bottom_line - top_line); + return (intersect / (sum_area - intersect)) * 1.0f; + } + } + + /** + * Distance sorted + * + * @param distances + * @return + */ + private Pair sorted(List> distances) { + Comparator> comparator = + new Comparator>() { + @Override + public int compare(Pair a1, Pair a2) { + // 首先根据IoU排序 + if (a1.getRight().floatValue() > a2.getRight().floatValue()) { + return 1; + } else if (a1.getRight().floatValue() == a2.getRight().floatValue()) { + // 然后根据L1距离排序 + if (a1.getLeft().floatValue() > a2.getLeft().floatValue()) { + return 1; + } + return -1; + } + return -1; + } + }; + + // 距离排序 + List> newDistances = new ArrayList<>(); + CollectionUtils.addAll(newDistances, new Object[distances.size()]); + Collections.copy(newDistances, distances); + Collections.sort(newDistances, comparator); + return newDistances.get(0); + } + + /** + * Generate table html + * + * @param pred_structures + * @param cell_contents + * @return + */ + public String get_pred_html(List pred_structures, List cell_contents) { + StringBuffer html = new StringBuffer(); + int td_index = 0; + for (String tag : pred_structures) { + if (tag.contains("")) { + String content = cell_contents.get(td_index); + html.append(content); + td_index++; + } + html.append(tag); + } + + return html.toString(); + } +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/TableResult.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/TableResult.java new file mode 100644 index 00000000..f6e84c2f --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/TableResult.java @@ -0,0 +1,31 @@ +package jnpf.ocr_sdk.utils.table; + +import ai.djl.modality.cv.output.BoundingBox; + +import java.util.List; + +public class TableResult { + private List structure_str_list; + private List boxes; + + public TableResult(List structure_str_list, List boxes) { + this.structure_str_list = structure_str_list; + this.boxes = boxes; + } + + public List getStructure_str_list() { + return structure_str_list; + } + + public void setStructure_str_list(List structure_str_list) { + this.structure_str_list = structure_str_list; + } + + public List getBoxes() { + return boxes; + } + + public void setBoxes(List boxes) { + this.boxes = boxes; + } +} diff --git a/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/TableStructTranslator.java b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/TableStructTranslator.java new file mode 100644 index 00000000..160c6642 --- /dev/null +++ b/SC-boot/linkage-scm/src/main/java/jnpf/ocr_sdk/utils/table/TableStructTranslator.java @@ -0,0 +1,246 @@ +package jnpf.ocr_sdk.utils.table; + +import ai.djl.Model; +import ai.djl.modality.cv.Image; +import ai.djl.modality.cv.ImageFactory; +import ai.djl.modality.cv.output.BoundingBox; +import ai.djl.modality.cv.output.Rectangle; +import ai.djl.modality.cv.util.NDImageUtils; +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDArrays; +import ai.djl.ndarray.NDList; +import ai.djl.ndarray.index.NDIndex; +import ai.djl.ndarray.types.DataType; +import ai.djl.ndarray.types.Shape; +import ai.djl.translate.Batchifier; +import ai.djl.translate.Translator; +import ai.djl.translate.TranslatorContext; +import ai.djl.util.Utils; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class TableStructTranslator implements Translator { + + private final int maxLength; + private int height; + private int width; + private float xScale = 1.0f; + private float yScale = 1.0f; + + public TableStructTranslator(Map arguments) { + maxLength = + arguments.containsKey("maxLength") + ? Integer.parseInt(arguments.get("maxLength").toString()) + : 488; + } + + private Map dict_idx_character = new ConcurrentHashMap<>(); + private Map dict_character_idx = new ConcurrentHashMap<>(); + private Map dict_idx_elem = new ConcurrentHashMap<>(); + private Map dict_elem_idx = new ConcurrentHashMap<>(); + private String beg_str = "sos"; + private String end_str = "eos"; + + @Override + public void prepare(TranslatorContext ctx) throws IOException { + Model model = ctx.getModel(); + // ppocr_keys_v1.txt + try (InputStream is = model.getArtifact("table_structure_dict.txt").openStream()) { + List lines = Utils.readLines(is, false); + String[] substr = lines.get(0).trim().split("\\t"); + int characterNum = Integer.parseInt(substr[0]); + int elemNum = Integer.parseInt(substr[1]); + + List listCharacter = new ArrayList<>(); + List listElem = new ArrayList<>(); + for (int i = 1; i < 1 + characterNum; i++) { + listCharacter.add(lines.get(i).trim()); + } + for (int i = 1 + characterNum; i < 1 + characterNum + elemNum; i++) { + listElem.add(lines.get(i).trim()); + } + listCharacter.add(0, beg_str); + listCharacter.add(end_str); + listElem.add(0, beg_str); + listElem.add(end_str); + + for (int i = 0; i < listCharacter.size(); i++) { + dict_idx_character.put("" + i, listCharacter.get(i)); + dict_character_idx.put(listCharacter.get(i), "" + i); + } + for (int i = 0; i < listElem.size(); i++) { + dict_idx_elem.put("" + i, listElem.get(i)); + dict_elem_idx.put(listElem.get(i), "" + i); + } + } + } + + @Override + public NDList processInput(TranslatorContext ctx, Image input) { + NDArray img = input.toNDArray(ctx.getNDManager(), Image.Flag.COLOR); + height = input.getHeight(); + width = input.getWidth(); + + // img = ResizeTableImage(img, height, width, maxLength); + // img = PaddingTableImage(ctx, img, maxLength); + + img = NDImageUtils.resize(img, 488, 488); + + // img = NDImageUtils.toTensor(img); + img = img.transpose(2, 0, 1).div(255).flip(0); + img = + NDImageUtils.normalize( + img, new float[] {0.485f, 0.456f, 0.406f}, new float[] {0.229f, 0.224f, 0.225f}); + + img = img.expandDims(0); + return new NDList(img); + } + + @Override + public TableResult processOutput(TranslatorContext ctx, NDList list) { + NDArray locPreds = list.get(0); + NDArray structureProbs = list.get(1); + NDArray structure_idx = structureProbs.argMax(2); + NDArray structure_probs = structureProbs.max(new int[] {2}); + + List> result_list = new ArrayList<>(); + List> result_pos_list = new ArrayList<>(); + List> result_score_list = new ArrayList<>(); + List> result_elem_idx_list = new ArrayList<>(); + List res_html_code_list = new ArrayList<>(); + List res_loc_list = new ArrayList<>(); + + // get ignored tokens + int beg_idx = Integer.parseInt(dict_elem_idx.get(beg_str)); + int end_idx = Integer.parseInt(dict_elem_idx.get(end_str)); + + long batch_size = structure_idx.size(0); // len(text_index) + for (int batch_idx = 0; batch_idx < batch_size; batch_idx++) { + List char_list = new ArrayList<>(); + List elem_pos_list = new ArrayList<>(); + List elem_idx_list = new ArrayList<>(); + List score_list = new ArrayList<>(); + + long len = structure_idx.get(batch_idx).size(); + for (int idx = 0; idx < len; idx++) { + int tmp_elem_idx = (int) structure_idx.get(batch_idx).get(idx).toLongArray()[0]; + if (idx > 0 && tmp_elem_idx == end_idx) { + break; + } + if (tmp_elem_idx == beg_idx || tmp_elem_idx == end_idx) { + continue; + } + + char_list.add(dict_idx_elem.get("" + tmp_elem_idx)); + elem_pos_list.add("" + idx); + score_list.add("" + structure_probs.get(batch_idx, idx).toFloatArray()[0]); + elem_idx_list.add("" + tmp_elem_idx); + } + + result_list.add(char_list); // structure_str + result_pos_list.add(elem_pos_list); + result_score_list.add(score_list); + result_elem_idx_list.add(elem_idx_list); + } + + int batch_num = result_list.size(); + for (int bno = 0; bno < batch_num; bno++) { + NDList res_loc = new NDList(); + int len = result_list.get(bno).size(); + for (int sno = 0; sno < len; sno++) { + String text = result_list.get(bno).get(sno); + if (text.equals("") || text.equals(" boxes = new ArrayList<>(); + + long rows = res_loc_list.get(0).size(0); + for (int rno = 0; rno < rows; rno++) { + float[] arr = res_loc_list.get(0).get(rno).toFloatArray(); + Rectangle rect = new Rectangle(arr[0], arr[1], (arr[2] - arr[0]), (arr[3] - arr[1])); + boxes.add(rect); + } + + List structure_str_list = result_list.get(0); + structure_str_list.add(0, ""); + structure_str_list.add(0, ""); + structure_str_list.add(0, ""); + structure_str_list.add("
"); + structure_str_list.add(""); + structure_str_list.add(""); + + TableResult result = new TableResult(structure_str_list, boxes); + + return result; + } + + @Override + public Batchifier getBatchifier() { + return null; + } + + private NDArray ResizeTableImage(NDArray img, int height, int width, int maxLen) { + int localMax = Math.max(height, width); + float ratio = maxLen * 1.0f / localMax; + int resize_h = (int) (height * ratio); + int resize_w = (int) (width * ratio); + if(width > height){ + xScale = 1.0f; + yScale = ratio; + } else{ + xScale = ratio; + yScale = 1.0f; + } + + img = NDImageUtils.resize(img, resize_w, resize_h); + return img; + } + + private NDArray PaddingTableImage(TranslatorContext ctx, NDArray img, int maxLen) { + + Image srcImg = ImageFactory.getInstance().fromNDArray(img.duplicate()); + saveImage(srcImg, "img.png", "build/output"); + + NDArray paddingImg = ctx.getNDManager().zeros(new Shape(maxLen, maxLen, 3), DataType.UINT8); + // NDManager manager = NDManager.newBaseManager(); + // NDArray paddingImg = manager.zeros(new Shape(maxLen, maxLen, 3), DataType.UINT8); + paddingImg.set( + new NDIndex("0:" + img.getShape().get(0) + ",0:" + img.getShape().get(1) + ",:"), img); + Image image = ImageFactory.getInstance().fromNDArray(paddingImg); + + saveImage(image, "paddingImg.png", "build/output"); + + return paddingImg; + } + + public void saveImage(Image img, String name, String path) { + Path outputDir = Paths.get(path); + Path imagePath = outputDir.resolve(name); + // OpenJDK 不能保存 jpg 图片的 alpha channel + try { + img.save(Files.newOutputStream(imagePath), "png"); + } catch (IOException e) { + e.printStackTrace(); + } + } +}