From ddc9a3ecb324f1d11daec202b26cfc69d74f5fce Mon Sep 17 00:00:00 2001 From: Amr Hossam Date: Sun, 23 Jun 2024 19:58:13 +0300 Subject: [PATCH] Added pose detection and correction --- app/build.gradle.kts | 18 + app/src/main/AndroidManifest.xml | 21 +- .../main/assets/pose/fitness_pose_samples.csv | 812 ++++++++++++++++++ app/src/main/ic_launcher-playstore.png | Bin 0 -> 61504 bytes .../modarb/android/network/RetrofitService.kt | 6 +- .../android/posedetection/CameraActivity.kt | 170 ++++ .../CameraPreferenceFragment.java | 109 +++ .../posedetection/CameraSettingsActivity.java | 55 ++ .../android/posedetection/FrameMetadata.java | 68 ++ .../android/posedetection/GraphicOverlay.java | 244 ++++++ .../RequestPermissionsActivity.kt | 97 +++ .../posedetection/Utils/BitmapUtils.java | 241 ++++++ .../Utils/CameraImageGraphic.java | 37 + .../posedetection/Utils/CameraSource.java | 550 ++++++++++++ .../Utils/CameraSourcePreview.java | 181 ++++ .../Utils/PermissionResultCallback.kt | 5 + .../posedetection/Utils/PreferenceUtils.java | 134 +++ .../posedetection/Utils/ScopedExecutor.java | 53 ++ .../posedetection/VisionImageProcessor.java | 38 + .../posedetection/VisionProcessorBase.kt | 348 ++++++++ .../classification/ClassificationResult.java | 57 ++ .../classification/EMASmoothing.java | 87 ++ .../classification/PoseClassifier.java | 134 +++ .../PoseClassifierProcessor.java | 110 +++ .../classification/PoseEmbedding.java | 131 +++ .../classification/PoseSample.java | 80 ++ .../classification/RepetitionCounter.java | 68 ++ .../posedetection/classification/Utils.java | 91 ++ .../posedetector/PoseDetectorProcessor.kt | 361 ++++++++ .../posedetection/posedetector/PoseGraphic.kt | 217 +++++ .../modarb/android/ui/helpers/ViewUtils.kt | 8 +- .../android/ui/home/ui/home/HomeFragment.kt | 4 + .../activities/WelcomeScreenActivity.kt | 27 + .../res/drawable/baseline_cameraswitch_24.xml | 20 + .../res/drawable/ic_launcher_background.xml | 5 +- .../drawable/rounded_gradient_rectangle6.xml | 9 + .../res/layout/activity_app_appearance.xml | 4 +- .../res/layout/activity_camera_setting.xml | 8 + .../main/res/layout/activity_camera_view.xml | 64 ++ .../layout/activity_injury_today_workout.xml | 4 +- app/src/main/res/layout/activity_reminder.xml | 4 +- .../layout/activity_request_permissions.xml | 7 + .../res/layout/activity_welcome_screen.xml | 4 +- app/src/main/res/layout/setting_style.xml | 8 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 3 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 3 +- app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 1404 -> 1374 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 3058 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 2898 -> 2464 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 982 -> 804 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 1582 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 1772 -> 1484 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 1900 -> 2076 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 4894 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 3918 -> 3614 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 2884 -> 3872 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 8794 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 5914 -> 6660 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 3844 -> 6084 bytes .../ic_launcher_foreground.webp | Bin 0 -> 12062 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 7778 -> 10046 bytes app/src/main/res/values/arrays.xml | 13 + app/src/main/res/values/refs.xml | 2 + app/src/main/res/values/strings.xml | 53 ++ app/src/main/res/xml/pref_camera_view.xml | 79 ++ 65 files changed, 4828 insertions(+), 24 deletions(-) create mode 100644 app/src/main/assets/pose/fitness_pose_samples.csv create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/java/com/modarb/android/posedetection/CameraActivity.kt create mode 100644 app/src/main/java/com/modarb/android/posedetection/CameraPreferenceFragment.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/CameraSettingsActivity.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/FrameMetadata.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/GraphicOverlay.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/RequestPermissionsActivity.kt create mode 100644 app/src/main/java/com/modarb/android/posedetection/Utils/BitmapUtils.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/Utils/CameraImageGraphic.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/Utils/CameraSource.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/Utils/CameraSourcePreview.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/Utils/PermissionResultCallback.kt create mode 100644 app/src/main/java/com/modarb/android/posedetection/Utils/PreferenceUtils.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/Utils/ScopedExecutor.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/VisionImageProcessor.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/VisionProcessorBase.kt create mode 100644 app/src/main/java/com/modarb/android/posedetection/classification/ClassificationResult.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/classification/EMASmoothing.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/classification/PoseClassifier.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/classification/PoseClassifierProcessor.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/classification/PoseEmbedding.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/classification/PoseSample.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/classification/RepetitionCounter.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/classification/Utils.java create mode 100644 app/src/main/java/com/modarb/android/posedetection/posedetector/PoseDetectorProcessor.kt create mode 100644 app/src/main/java/com/modarb/android/posedetection/posedetector/PoseGraphic.kt create mode 100644 app/src/main/res/drawable/baseline_cameraswitch_24.xml create mode 100644 app/src/main/res/drawable/rounded_gradient_rectangle6.xml create mode 100644 app/src/main/res/layout/activity_camera_setting.xml create mode 100644 app/src/main/res/layout/activity_camera_view.xml create mode 100644 app/src/main/res/layout/activity_request_permissions.xml create mode 100644 app/src/main/res/layout/setting_style.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/values/refs.xml create mode 100644 app/src/main/res/xml/pref_camera_view.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e4f1fc8..ba65b64 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -71,6 +71,18 @@ dependencies { //Circle progress bar implementation("com.mikhaellopez:circularprogressbar:3.1.0") implementation("androidx.webkit:webkit:1.11.0") + // Pose correction + implementation("com.google.mlkit:pose-detection:18.0.0-beta4") + implementation("com.google.mlkit:pose-detection-accurate:18.0.0-beta4") + // CameraX + implementation("androidx.camera:camera-camera2:1.0.0-SNAPSHOT") + implementation("androidx.camera:camera-lifecycle:1.0.0-SNAPSHOT") + implementation("androidx.camera:camera-view:1.0.0-SNAPSHOT") + // On Device Machine Learnings + implementation("com.google.android.odml:image:1.0.0-beta1") + implementation("com.google.guava:guava:32.1.2-jre") + implementation("androidx.camera:camera-core:1.3.4") + implementation("com.google.android.gms:play-services-vision-common:19.1.3") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") @@ -79,3 +91,9 @@ dependencies { } + +//configurations { +// // Resolves dependency conflict caused by some dependencies use +// // com.google.guava:guava and com.google.guava:listenablefuture together. +// all*.exclude group: 'com.google.guava', module: 'listenablefuture' +//} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b012191..cd6e355 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,14 @@ + + + + + - + + + + + d&(j;Gn?v{f>@Zk+_7!XtN~_=)3{=IO_rBx-%ID-Hqm8zSAD z0ZQRK^shqRwNIL6NfC<$geSYCuAF%`xlIoj9u?@yOt0Ne4o0t>?p$P-7gS+Qak_SKVSaOEBHTE@c-{dm>}lm<&FLJt>EHAJzH0}XV=2? z=yvg|Cr>KKxoDnuGA{eJp|RoN;fh8^MqzRF1A~Ki`1w`K%FCI~vBjTlr*LrnsYwQg z{*t=`(O;$>FO=bGOthe&>Zcg@r+HTs6BA>TlKAZ>Dm0Iq>eacNX6u({ z%yCCgo*Wd0Jh*+Qx}#<)b4bVEIgF85ac~M(T#Fk;%P}yV@o~{y=@PsxIDh?4Jh~WP zV-U$@&k2leWOdEbQaAvPP$(|u^JnfK#?^Wb>5iiwX`k^EMt^*L5nbquoS8}Uh22jf zJjKtH9*621yx%?*s;0=WW!()e>hcqq`b|Tk76DP@RO|&p<>w@9#iFqCRff=)QA*mR zBeJ>ER%Y1k>u+#qm)hPo)9o2^m!l)_fxm-K)XsRLI(mF+@L(Xc#p86vx%*CGRf$guD|c9sH*U6?{`Cew2yL7_0{%kTj0ad zzkMd<&>Y#0R@0usPo{9$ww_W0^;MFjQ)qitICv2L&y-r6sN7|@?Z5q-F>l}+(}A>{ z9^FR{X2Y2c@J9x@_LEfu#ilZWEs;tP_Yjd_^6oJd`U>Dc;TT_wW5SA0^v;?#Jv_2s znD)?~nyfkiKLjKA(%G~>Zf34rXU9o^Z}(w!-ft=v2mgvRRwDl!bL1(~Zn3Fyh!-Vh z=%KmhfTeJ;p6FXjL3tTU!GCI3QVVx*w}RQOeuR7^W^}^sdK{X#2P2GeC-smPs}d@; zX{2f`uCf}Z^5g(2Fc9tdk178m0)Sy3)mJ;g;R=$QKa;loRtK`f4z{O_Pmi_=)urZ1 zZpi!8n7H;U+x-e*a9+!fFHjY#eG)0V*KwT`g`RJ$UUrid@96)?MFwzX&Q@H$6tGw@ zjSG#{)WDM`&$+?BGsV>glwxbJg2VuW=D>fS5w^uOq+@{*#j`XT_w|iTZEY!{CS5nj zG^3V0wxxk#*XNU6dSc#{p!H_FH-2Eg@M^#5jJ;F#eq8i$+$ODabG+PYq{z_j z?DSB})Kt*T&8;9W@7dno-kZPQ#LHaQv}zAWjFkeY|0=ecI#E@yMo*27YHj2D2Z*+c zvCgCSxH5gYz=zYg%>rZwra@34A>S?!o#!ew(BIMvC_Z_j_TQuSZG9NknHSlZRA#6a zYkOb@lu}&{O=iF8NUIR)cX57Zg+W0?ZTmQHh}(UsJ6$ha+OEgO?Jl&FTX!d91R~o) znJ6t_%^yfbk>@^@P7A-E0S5K%KY~z8UGZJMxM}8O(XlKQy0yLxrSBc}o0!(izx?AL zSO|>lj1Dela%An|x?>tTrHwNEk+pK6b-suqI62u%%-|1WNjVRw2oH8S2|gmXKM8L# zP4zo{D>z0*>1}9e2$1@=2GRI`_&@|gNzA~2maeYuox{V!fxf@$g!*{I+d%BwyiHh?xK_MYWM&EDEr$0*%!M+7b!8M|D;(2e$T@%V#jpBiSC|N)p_KVQe$b4OvWJF+xuyL1 z^Fmi!``Q&CNfJIX$tf}?(##5P$T0JqCw+K9*yk?!$?MSUQM++5F_n4w@(-FW4~GR{ z0YXCZ+Q8-}CncIb%1O^c)ho9OIr8uw-S=@J%uaE1y_|rk6Kg3E&PY= z>H4mp?Hq;s`>j^-rgx*{PxEB)o8{}h$n$dA9^+c)?8h0uIU4*SzC1f*p~wH)!mRo; zl_{&+D3I4EN@+h3c@uFeD41iJiOKGJ?x1lqpAlHJzQt5?es1c`8#^nF%-e{OFUvD@ ztZ}u~a9*7J0*%2`ynjGQh|c$0w`xhJm#*Ky9c}xL2U3(Dr1+h#O$^gYPP=|4ti3|U zddzsKsHI5jQc_U`5Jd^$1&ymnUNT5(Y&xw&pPm!<5GguILWnlfnSo{q2#nFFe^GNN z?JM`k=zoXb;J%mQs#L z>*?1QE##@lg|Qk13~M@8v|B z10C6yGsLUE+d1VJ9g+uv>0yy)mX1)`D=)u2;N3Zzgm>-tDnQvDD~22&FGRWd0g6(J zKUMmJKV7@GzI)DW?xP9!n6$6%^jb;Db)=kYCWo?B+in+xOuBHOYKu|6z7T|LAS=%l zKB~I=TwtoVb7@AuVRRrC&&`!Nef8}#PoAspk37f;^B3;79>p}*o^4H3YU8)6)3$Y! z#t9BJ^JMH(IPkafgtxR~WXppi<^1pse&L%coB$yrGt!HQ+N5u#W+8I0oE&zr+Akfl zQ#op;&7!>nwj`Mt`D}o>fx;3Fo9A?UC>WOyOgwHnmioCl8 zgJsn!D6(2hkh~QAIJsHCxd8V7a&82}M&QM^lf7>G{w*RKwYW!5o(NtCpE+l>Hj6+* zm=)sU(cvNFtg?H~`Ae5^1f0#CUHGfutI>Sn1CCzj9Q!Vl?Ab4@K7ThlFK)N=;R6CM!=ewJ<&&RBYAe* zo2yHnP~0^DM(o#jcdBKse7*vC>Oz`y*%TL_K*+ZQD4Lir;hs?itjTd@qD>;kD0W&6 z9Va$+?NcI3p=B&`uk#*L-TZw6_~H66Wx!td8#$ErQJ?i)T=OnMOp(4Ff8P|_uL5Pq z$P%pvsf@~{8$*Lhi);RcVn95(fpKZ^mU`*W^=_F@Wb?y&%Enr_%d>^jR$8iLujRy> z($umX#fC5w@3m~v*S{#-`jwg2@}m-PfTQ#xg9=*)mU*r+p_Lx>Jl5Hn0FL9a7tfnr z_G{Bp{(c0Ejc&qVcjLCseHj=uKVKq^GBF~@A;u%@O;P%H(l!Q12IyvD64kLRVW-u* zP2MF{{^;NRgkGUzmepVg5(Z*}KZ=FL)|*b_r4yOImjK4B46PEe@`20lsN+7+Z(;+K zvTY>Fy>8&B$7KF(U_NG@Ct|)w#JF1CBhBw=4_g+(ND&_|etz|)C;6eM$F@lzn!*f1a4UZRY zJbrB9E>FFqRILE(o$dY?EQ0#r{f9S~r<>7pH6!?Gs*Cm%8dlxKT9TU&pd1ivzX4eZ zYrZG_5+czI!J^$u!Ts+n%)=>4+8ZPeJUQuoj^~5_BsZg?Nu=&vCBE@@KD;MI#>+oD zVsUvnZ{$aLr6bm%`G{CCffbmdpPM0PVAzTStH=U?0i0Z!*TcZsrvRkCV+)4(Y%7W) zeA$$s4{S9YZ_aqF&V~EMd$D^+|XKb69h9WG1H_h+#6RjO# zE&w;oUqX1~(6Ac>Gy8B$sVBCTPCYrvx(miyp3;lMTR;Wl)E@xppL zDkQ|6cOW`?)3WXS{)z~~l=`%O_xF8g+;o$6uN*EMY+qw4sUqX}6($8nKa7G;ZJA>hP` zuICuv#!-As9JTTm6X~O)eYi+pdw$w4>b~hIZqcF=8(*zn^?v+%Ug5jwiHTE8B)d$d z%|e%O(-6{Qb1XxSqttCf4|^C#Uc%&`*U&J8P~g*f)NE~73QK*ND*GILIFhz>+CSP+ zx*t>^)+_y&>h6!T$7oh9bgwYCn67BK&)SXoOlV1*GpDP zd9sc~2WOIxIYf%CoF<-Exk+wP9}#b!&HCZlMO~KPi`(?bVo&t`&?yiEcTQH!|5Vw_ z5b+Sqk!Nlg63AGsOD;2uT>yyE+Dm~S$guM6d8emFhD{vIl16zsYDrw+$Se$J@yd{f zA8WiFl#n#4@PI&oKn4EE_?R{HOvS4AEvFeVE-pJg5bEv&BH%344103(YOJrx7#;Dv z(7qYko0L|8AmVj?VpF-(`ti!b2L~jNpx_7>oEDPM3V_5(+WO3@E3Sh|ST;BKD#+QM z-w<0qO%IdjAOW-qx5UKQfbO4ut$yt1ySrm7KsQzC#W}vKFJq-3OqkxUnob`1( zSGHe&zSgkqISV^>$L6Mbn>f^;S&MsjSC(Bsh`+1!Zu?gp3`OSia~ z?(YcjQ>}F29cnCG=VlPtIOQ<;&*Ja&@<9^msa$4+dk<0mySb!fX!!g#$=E!gj2%!_ z@od9Au3gQfTKuA|z8CsHNQ4oZ`YK7{2eAX77}MLK7=sH`>Cau2C0cI4pPg>si#PYn ze#xR>sFt$#smKuB%)dTM(s*)8esV6XKz4JzT2?jRCRL65kJKi{ ze&C?t2i{oaK6j7or2~F0Iklb8g2z}6R;aKUP;;`(N=zr~m?mIaXE0K1nzgqt`Bwj^ z%HDV=l?27WEfhy3{A=k$@8{3z(zmn@?8nCw5$r&BpbyyaX(nDvJc+Vz>rOpS1q`d* z%YAlFs{P|gfaBM6AlY*I*}@y>sgR?dQuI6GLc^iaDm=XIbWc$Vcys@fR5dZYeP-ZZ z{V-JE=>%)_jJL~NMleSR3BY&;uF`qSM8NrPXJSOd7+z%;D({n!?D=muCKF2rxSnC9 z0T|!HF$4&P^lia=>an9GO(t%`!=hGu_tpnrS$CbfojlsT*bBpQLDCy(A?fbIM2hyD zWs>rC5;SnMa>kWT@q zrn5m{kWroZ*Ax)*-v&}qeaQvH7BXa}x>IJGkjPrXUY8Zf@*rYeDgJ0Z;%hT7+6%QL z0@dl9fyCX5<{3d9J-wj2?S_p$wc+eCdcs#p5=?vLMrVWmIlOxD#u_I!b*L87oGL$t zi)Yb4t0E_7swID_1ZVTMjL~uzDag;*#*^UFqYd+ei^=NqI~vAl?E=0k z(yAdO(-cRQ!^xlhY`eS*lRq!sedlb_v7gZq*cfq$9WETnZxCmFGx+;4-0%~hz)p*z zQzDKdzA%DSa;=iwQCXR|fl{w#(3 zLN5;fHMEROS5HegNiyzFT?0|2`(JCr+Se%p=5?b;Q+o4dC=;g95 zq0eiBxm}scVMIqSJ;AYNTB;8}+LW}PC_)rQrb$X-y!m>+~Me}&Kj`JS54dpSZm`r^oHJf94 zve)m2>n&75Ai?(2Ubkzc)Y135^VQ0ntbo5U?%N+YQKhbV@E1mhuOHOh_>h)Aip&fb znQ&@$UOUnEM}Bz7lOKJk@623DUx9k0odDt&9I3G4j#)p`3NbBAU? z6kUZ~Ag*;}2yxAtJlFIvot~lJ;S)&m$N*3@;FZn9kJ zS4jhvv7V#H?N`=ATFIZs_e`Tj`u{3Dc&Lpz>dOrJ^m!DA?{^iP{6M~?C&*bw_{h2$W@U?7Y_i?JpT88}ZZ#CO-+P?6yQ?LR_)Te8 z1N(7Ajrpb$t>iZnPVa!Fh+& zM(4PM*&ir*C@8L)oii6s(EzO~mK4*I$j5s-bG#5y%Vq z8-XQ5xotT5CwX~iupjTU!n;6~r6iaLw%E}n4jgRu>k})ksP7Uf8i!p08a*Kx;nQXL z7D&2r^^E9h$ih&5F10;1aG@bnb$(S6ScH#p7*yBk?dj=}TKm=+<-9UnXF9!;+9tXk zFuvDZpT_qQmH%0uiWH@628aYxj1=+h9T7IZ*E#1F@Jxb6V!5?Y>qn1&{D8aY3S$_i1e$?d^;?&TQaKj>=!*b_-rWZJCVCiBTR4N)h6zIVwJTx z5TpMmy1eJ>(Uvp_6>FU1hW11}$43dM@Nl-y?kDt$IL~5zlJeWiRInmEG_a4cm3FKC z`GsF_L34n5tP0dy(~hrETi0sVlN1@OKc>0(&gyUIO~b~T$8kP-SC8L2GXl|h*8auUXg`ouutY7H`+=uYTmBic$)6e_Lzjrf0F{9<6 zYV}55L6NrjFpW1U`iHSQ-^XBu#=Sbi{F44sHnVw5{dR*sf+XC*6hYd?E;?|AHes6$ zI&h^v2qQ%uZ~_L1X5K{k62;}J?c1g@*j!o`&+mk7&@aerGF^g?tuvk38A_G#h~Nil z_Bw~e{oj7p^HfF}!U*L1i>)3u%Up#KC>aFJR5>`Z?IoLw#fnyy@hT#oU|>9LWhspQJYIo=)!d1w?iF`I3{> zd2Fy#0%KW~VQfdotq=?qMUR;yJ<#xaTsk+)b$d*OX4Ru+%4sso=-6Vgj?A#KNb7nx zug1R3N>_K(sFyO*3bsd5q8Me)#)6+=JDE!{gpi<~Uz69?z9)EeL>MKvxl&|g#+5WO z>KTX(N0(izuxWyDqH@8lZ1!nj`@_Giqu_Nma1nBw?E49AgdL#laWS1-1 zR^T(G47%vtxhEQ!Lmxhi+W zKBB~%?_X4Uv7+5Vi>PM4cvTk;;QU5Y$r7T<7WS1B6^(jSNWD-rmjfZl7ACCs_3M^18liDdWo zPP_IvN?1;A^n(Vy(CN>gbW$`|iJ_MJx96~9KEQc0(2pepm6xFZUKGGc$w(%a_!75` zwrVEaF>@-WeLs1s3@%*i`9*o3L-&jV!^V87VENstm5Pb30+L-4hkJ9}wO8 zmnP-z1lvSVEuK}cko`t0C@=!nFU{|`SQqKK*c5;M+V#R}Yn`vsL9LSU{lwMon<0iI z{4YFS=$hBni0TRnE74s^LF89-ZcQC0)6~~1%*-^g0V9m+dSQ5l#M^?3T~lLsFrXCKLfNK z`W)JRL{pElti3{vngsXxTd1YGY@7n=Kl12DS$8bDo56jw%wfwqa3#2v=&m9NFEV=v z+QJTTH|2UtF?Tt66mA$dZ5HK~kt;)>Q`P;41=pe&!U$0~}YZ4jU%YV#dCG)*Z zQDw4r;N+U%jtL^MpdmU4rMNqKUu_lbl_GNvhPrbgK$Dk8&>;A|l$JB?-Tm z=BeIu!@DurZMjz3U}uh){CU!dsc!E!zDqpI0T+6Fipe}NdvsAGvNJxVCJG-p#Y`D) zG|2gGZsAMYqb$$73JK`*e;>BYEZS$}e72bgDa|~{Jf8iyc>8xB%YiCS5vXE7cUrwk zP^zy#v({-|Pl`9GjX2A^-06dobek8}IdpAuf`abJvx3`eYj0yef4);&`;d>HYJuWE zkW~ah_5nL!=Diy=K?O@snsp=wLWR5`o0;ZXCN~rTMa>yqV63!Jk;=<=`Uq8v^(=Kz zI*SBGs)-?~(8qymzMe;k@rw->%$?&(J%jX)9nAqskhuvBV2m@2`TY@YnN={k+Hkpd zhCU$}5P4@08{R9^h*zjP-2yb$NKk+Fa)>aZH=IXKjE*xbgE^WW1DK`Q8<&gZCH{kt zmWtfA^1@C#`Gax^yh1h}E zDm0>f9rH^*Npi${{q}5t<(YM~xz))^L?CqV>{J;V*jQ9y_EpIcJ-Th&VEffX&aML2 zpsu09=J$p9kMp5HlLd58W%p_GD^*-}j#rwE-3v~p0VA(2pS3kJSjS6T%j2NOoR@aLmIR_!RZ2atAf9^a{^H9o=qar;Aqct zlO7dw;mJV(L0Nsi=PLkJOU}797ZaQo3l?a8aScKGq_qof1W7Nx5BTKXv}^hr+ag#d zU&4jrit_haPLL;3H0&G!LL09D;<_@GJMmwKNME1<4WkdOTqsVI5;u#a=6qlVlQR~L z>k47u9tN$H_$vF!|E%025a6E(0INEf5mFb&{)97f4z6G}SsoOTozH0}o8XQ$Jx6HW z!|C2|1`K2Ka)m6xJ_>^2l>10Rtb7vs^w7 z=i{&PQl3RX`8@I_)>E|f7Yg$ywn^^_OBD&~BuOx;e!`h0;K-DbhXs{oj`Od@y#^RT z0iEoIv}>O9R>k0VH;0WrC*SH0Nm2-)tRP3+rl8yo`HvM|yM%3WK!GG-e9_Sj4U&7+ zlGHOImM1o{`v+kYl@65B#Y}UlOmpTU9Zit{rMx$7H^)P7`qD^4=*H`)>6a`YrW&81t@t}ngy4!;b)PN^ow-l5-Pw2~b158GkSHA@3lB+dU_kvx0T zppiCxxdy0_poRq2CbDx6R%av~+qZBe9A3xI*^8{xj%=6@Nu*fG$6L^Ec~m*P`e$}tPQi5pPDhH0{l|wDk0&oE zta@R}e)qPhqbEa$mlq})5>hp|Cb!f0cq%%8}RITXS75u`*-^!bx3!*jfwNFmFQ9HCy+qp+BN%_ zNaYPoUia{5^-DK>}^C2NCvV4U%)?rDq0XDc`EIb4JaSN{>%U7JV zAN#)3!w1e_I2vz6QzS`T5Bkmae&*wx)>vN0yVk|034dxdA-~0r+t`uu@vmixKSa=F zx3@u{0v{v=!;nM!n-#=WvH(GpZLh5O=$pigWlG4dq_^i5%Xvda~9Px z_)4y1#tTne5^`PDRM2|AHSdB`;@+rn^D*kF#&bb0WSE4`8<`>|3@C17!d`GuO=56}Wu z6MrzIYM6*Kdq0zB-J8nmG2@n$?6l0g{_$E=j2XKAMg7sk0{i++-Ev4%b%uGQyJmCJaRnHZAN9R#~lDK6ZlZ;bU;w;VbE5rX;I>^!9n6 z{z1Wq`w)5T#ghC-62=9Cn6}ybKNaWs;j&c@$B(!h*rN3HLd6(gi>O1|I5zb4hsg@q zmX@UIGahK@>18M@_<*?!o{Fxra-dm% z+8u1uMX~pGZSGRTywVbi7+>gjhCnDQ=$DlZ>RQL)45e1i@hgXTcS*pJ8f544%x|A5 z*5rjNaHlhDv!e{Kir(H;2b<%@rw8M&QhfH_?rn+*uV(-l3`YFj2-);p_MNeX_94tU zhXSRyQ%tprnL*Ku7cGdfN^zPptF9_VU%6sh7UN3vkpq?EY8g@YP)9X%bdTj@qrl5;g^g_-m+&A?n)4I`r*9OTZO zY%}9T4^~F+BK^CnaRRZt+IPv#{7!J-g7S*wX?Tr65cNat9_bD0hx^@PiyhoiUzBJc zK4Etog57wyceQpYE@=pi4hbp+)5cgv$r)K>rd?(xGWZ1NIQBipi2=<%r8Mr)#yd<9 zPMT3*Xa2%A%J2NilcC2kLEq=dP|3+t0N7NPbHc5|>maC7l%%tl{0^_x%z2e2^lr3R zH!zUpiaGP9MOGdjl3vR2Sn)Z4I0O@ckWH)jxJ(T^Jt(6h~d zB&esCQXc;0_glOVpg^DYTuB(<2xd>b+O`bDy$=$e0eL^Bt05T1P0-?;-l~TJJ8a)5 z0WoVIu9Bv6ySWi#cpQI8g9#WI5_RjiMgF|ROzyi)@Ag2Jie%@XKh!fx-Q6#5NI5Ek zOVXMzIqhc2#^ZRQ6f{nZpK#vJ3ub#k%kv@lE-@gZ0|jL@(3QE(=sIXm;{ZHUQ=|Hx zJ#QdRMtoXUR+jCHrya=GzUOv+!rU>GuYE}HP-K>gtjDkPW^rawWWn&tEXc`#O_Ot& z%te*Pu%}N`PR`SDByrqizyV=_z_T1QyG6%yzA^ISYqlzLVXu6WFw|!!Rd)B6PZ1{hAHQw``9r@Wmeu)VaN*0hR3?KkRyW z-!9-JUn1h6l^Zvff(Ns$Qeac$*8+3JVJ%jnEN^UV$b)Fv`9P5AN556fP>!i4pBbN$ zb4QlQfnfubeBO^AUr)WM`QB?BPqJ6S$-#h6Xrrcy2^$GO&1F(<9dX?+LHu11VVex^8;^G_9va%hs@8tAgyDl^%0s$th@AR(^EDr)7 z=_4CBOYec?Z&tBiC8^&85HY=29=rJSl^={)8QPs5&JnhHJ4bMFC*Ef|!T` zaHqpGf@y%>X7lQNF0|zI#ouX31_tq4pg&6Z;6NGQWRsMd%J=r|{q0i&x`5Gt4^I|9 zSFoNfMCBHG?Js*OyVn+()<3-r(~DqwlHZw|n(|r~`{d;}o19Bk&nkM_zWBr}A`*`( z$;^C-Ew8J4Vc}(A;m^m@G5R6ug*cz4lKSG@gPIDMJ}%gtRlRtWXY!AaH;2HQB4=9Q9Wx6MO3gExe}XaAFk zXlT+lviexu1$*^}vRU-U% zbz;}rV@-xogdc#508n^R64ZwefB6)Up9RbsH157Se+HT^GR_MTmft<#$?dbo-iPaV zrt3Z8L6|EnHm$d7R#^M~nW&v3teicPw&LBW(9pZsxFEnPv`OFgMvlT!g6d0!d!h1mhraWzkeqr2HZn@8Ga)~Wg?|CxMI$5wwl#6-QF6Z{ zdyyS)7Ok089D8;&DO?N^!i`&~Yp-PQI}T~8bb^V*TftDj8oR+9wJ>5uZ=;+SlMz7l zx3UeQsEgd3H>6rx->3kCcB2c)8^C5OgW%E(^Z&Za;Yhe*xoLU+HX>l0pt*gXfEqJ7 zghkO|PARTf(@&uwXNw=Gg2=hooP^H6+|dD}j0ki4eFQ;j8B5nE*1p@?e#X!DN6C`J z(FAHwM#l1bk3*~sdeP2yZXG?JsZ4eFSPlf2!=vNb&tN!WaPqT)W{T&(D`v2V(mcxv z?H8BV4Pv-e3K+Pbc7X2c;>T<16r3!sYXf&s&Ay=W70MIp_ms?Av+My$4FqUE5*sN0 zYCfm=F^LlR`N2N4!T%yRcZp;`t2~8%js*4Py=i+<+P6@5*($l~&4LBRs~)z!wYUfz zudGm})fC_~UM}9y zIk;FSy$_g8-P`LD?g|rbV8d_j%A&L~(JVR9fwf!MF;NnWxbO(Y7dYzgW!>XtW=L5cH z9_KDN`M#C<1tX@@}kCP-1llOAPsfU>e2YJ+kt{-ABi$8wMm z=Y5OI9Bgo5a5QOy|1EV2f%V<1pPd%G%xfpRjnJ2bc<9P`9}F#=v<9W*f{Ki}Q2nO! z4VP9Hx9P*t&gk;sj&yEvQjzSE)WNjG40@oi68=5g=9#oQWM~k`vIGb~bk@M&v=Sgi z(?<*N9IUR-eifi$xIuA^gZ)7s&PIu1hZK_TN1;?qpsfT;{NN-ilQ-a6R+fY;WCqJsTj^E8cT@v{1QNi+yq6Hs>pb zP2neue_VO)BwyWO-+J)#adp(iDewGc%I_>oA&6_9W*{i*xrv8;xvDgVg{Y1v!t z#+Ek!(o`rVA9%IJ3fz!g6TwhR-BR4-VqR9{A>|n?km@?vf}QIJgT2w3`sUmqycBsa z_O~w=3~9T@DlM0GK=dtc!~jC=Y) zu;*IF$`)B{Dt2|uv|?K5_Py+!?BlKvSi00ScAXs)XWh_y;*~#e@~#x$vnz2#S}$Kv zj{N;ugC-jBV2XMtxAH~D?pDUO2F*MY%VUwD-gf_GHY!`Srh5RD zX^6yo{=EWk-H}lpoy~mF2JiH1YE7f@(+9&gv58AcShHiv>)7(pey2x?hRs{|B|B!2 zqpOl&Ger|xpm_h9uA~o#PBegPF7Z(`{$MSOdv!jPzql_EqW{Av+tHu1d&DMZyKNZ4 z&nHqU{JYT`N_oDLh6G*Xf?Y6<^UQ2UT2})VMq@Fe& zlCDZ&5&Bsb;Rn%-a7!y;I{3$*_fQd0==uAs(Z9<{978#KIBmZx-x``;rJz*d_NXOg z6=gi{E;0!eiH?2EtRYG5iI__W=Ru8*^4-2lCh*p5x3^n8zK9LlqF5u6@hqu*FAVvj z&^=-Tq1^QOjkPPyo9`Z1o@Cz|6O(Cij|>}#H?E=*2rWiPMEly68e>d( z#w6;fatvGDD6YJx#`i96kHriQE4*)_+~RX8~-PL7v-53*@}} zBQG#l3~#j;;SsubPsMGu?-nCaT({1bDDUg-_>zP8eDmbY>BOx5I4fb29ss3JhPO5m#$#&6_hJQ#v>jKRjKBxPcQzurXG{z zP^UZXy}`Z*?r@!HX1=C!I6|0n|DAZ-`QRSY`e#Y*_GqY&9;oVOv_d!}0o#5RoHNE% z;k8m!ragSFpUK`$?RD#SOX}{QFOVsw{f;qqIX5C_ntC3gwCP#tAm7(+0if-7ffMJj zX*VJD2PHc27;)KFHLP-s(k?HdQD7YIZ9cmase&AhG&uq2dkSYJ0m9#EjO8uJy%iw9 z6g&h!=AW|ZdW%66;ZeQ!?z&Q&A!X)wIg1@LFg5|15$XS$n$B$;pgs4i-!`O>-Gp)| z5WBcPu#*QkAI!PSUx+AfN;)tjn^-L0zbcwbD)z)XqUr@H;My(V?se||2v;q>ZF%$D zvObu)+qq~%-ppl_osIBYM6dnhdj(+b>>YLg$O=*ZTR(`TXn8UKqiqx4quXfn^Y9Gs zE@GphMYTJe)?c6W`&h*tG1V*Kj9>ocg|v+V;Z@61iGG_VWR!cZI4Ly}Do#daz1rYk(_9N_Uc^f@}CJM}@xD2bZH(<=M@@N`d-2U0s$hRQbN=mFiGZ`S9y6d*Ln; zyHn-Nr(JMo&O!PIQ8GtQ+z3whywo-CrFhm&i7a3D0>!9dsJWNgHX8S+b|+JHsC2(4 z{!u{G7upo*f+KdBRWqcK^wCY9ZMxBOi&F*l4VAEnJ7ntC);xqQ=E&b}KWOUag~OPb z*j4E%StP1?JXsBs=c7}8h>EB{VRt)hZS0XQaLKA2IZ zUCbCF&;`2T*B@_`yGVZNJ2om6l2@uOBwpon@ql( zEiipK>SE_anV0QoGc^}Rt$1xysvB9g zrrIajf39qQ6sPIp`Qr}PKQ6zYVc6dDCcT>PI>btYs_Vq(>y>%T*RPW!HjgK__dst%vPsj-><;P49shrs&b<6TCHE{jn#bNptNdFN?pgs+vU)-du=#q)g3DkscuNr^6}r3 zA$Zn3DytXzi;=koIHT!D<)h<*(CcGC)U3S~)Qs-X$BJ(jz{`yH^KZj-(Nd(Crgi0n4+6G{KzDddFO$?sow+Mf@6LE&MN50t=?q9 z+Q)(&21Gi<*zM&R8Sr0J2@w*+8)LyXJJrS^Ef(E0L4pLc?7;ftT_n!q{M~(0j_Fgf zM&oVoCJx!SGxSkd&6!tCnwoUa#M_TbpN_B;7x0ux`RK$?v$4~Y3z&CqRo8@Y9vt#R zt0cyfA?+s(N?cg-t&C?%S}^dk4Gg@tb)5|58w_e{oX^3dBC0#E-*#s7mf-~H`{#`@ z(4HHx(lT#SZleOWZ=rTjm_O=P8;cy3+0_InbPwXk1SQhlG_ef6z)2H{Z@Njg?C3k>JN71k!u_1FDu2a8kmD;NMR zwjb`#>$d}-&fnbW$ftixQAbBcs2OGHZ0)&Vxyhz0qzb)C8YIyI8M{DF5Rc%XFd^}wyazQPQGDaZ zmX5c#4EABhBs3RyLf`_exw#8l$z&xU-!Fq^6|n-H9knF510ucRd#T2oz?`qTrvd{J zwOB|uh0p?JIQ6gA+mCAPrSnH>Q(V1xj??KU5Pck*!>q?mL8zx| zhBIa^8$*iTceitOxqYGMXi(^DCk3}B1`0{u}E(A&#-<)m|*)ab)Lsn8n-CP0VEshY+@64 z-JkRpdD|`di({m~Vitv@zSNf0wmPc*Z^$(K{k`T!dr#jBZ||{L{TzahCwX0Nxgh?^cL(-AR3jq{(Gg?!x#Ee_U!!HB8 z0}X*K{QgS`0Zh+Ww&*ATmSH&KBJ=a-PRalpyiUzTn(Di({BWb&WwFNcl@>X>X95|( zMux&;5l{4fnB3bKT~Y-TN0&C5bMG`=p0PutlDhQTgP+=U53SlU7h8knt_vIPYlfxr zPYFb!rRQU;@$>Cb>HSrvz4D$i`Qpuv?wsZ9=V9%m(4Zp@uj*pEJ!cHJy+ zo^)lDyO!?M$EHs8e)9if>aC-q>Z1SQI}8jhUD8M@-5}Bp3IYm9cSuP}58eGBpfm~+ z(%m2kNQZ!Qcjr(u@AdosYRy`A&6+iT&78B(-k&<-9y+%BAJLmKg`c^z#a~qN_wyk`E z!LhDuaA=K#W;vqXtz1^3!%}Z4%%hFl_&g0rGfuK?1OfT%1Pp2)U!L)>7XvOp=aQYO za;syA5&V!A;(+TSUTMYyZ7CuQw4gSh7Mdp#;cQHBr0}>#9-crvMO}^wAi%NBfuM9p zCFZd|=wAilv!5YIL%qKHLZE|M1ekq}aV5+sx$R4xfbIVnwm^qoyZU^^P3rCit-J~m z=#AMX{|Jh8;j@ZGWKj6aV>rKD=xQR3_YPJ9A&*+`#l}eA;~OtBN*3ef30FfD6JbWHT`&0SjzIwI2i1 zKNb&ubjW}dq1?ZkIwho=@W~=UOvb#(`UcXOS+O4z2I+U49C@bKGF|SkWVf7`BAYZi zrX4;M&LDSO@N)i26$*Fzo#Kc2i>yvll$*Zz`$&-HG_iPzmD zqmba`JPVQCrGkXL>9z!?p>FD|w@Dx}^;aF}uX+Rt{RMo*dgV1ZeE8LclHqwv%5{K{ z>TxXr8vJr$_#AZa*UDS|JS4|_27Fy}A(Ul&^la0oBTIPc{Z%`rG1j>Umfbx0asfBa z;%~?K)x?(vzLC5$Q;q*^L8TFdaE@f($H0PPxIykAV2abIPtg$P)!6CpaDDYMiXQM2 z6||%;IVY&l!)QBAZx_=k971J{VxLf59@LrS`eP2gCc5LkjX;%k24!JCW03E;k$6H0 z1mJ>cDG;g0zU!tuKM4lzSBAlEu4)Z)5ybCzm)csIH^S_X6V%9Vq)N52^0XnMyK_6}jH~zykQI&h_Oy7BkN{q{UxuIq1Wa=P zwZ*?_y%wGv&U<0kI>mL_Aag}&DQ6mI%FH_N#zA#(Zl?R0T~Mi_(G!tnzfxlhz^Pem z>(>{jlxG#q?-Y%Fnz~!j+FJ2tD{Ye2oZV$#pf{fmXwIHX8#su?k49YM9pYp7Fju?H z?a^T}lw&WA zs+n)eXJ$}r`;nWl_zxxv;N9P{wF*erD!E~6-z-Ud*9X7M(Peg>gLRpY-Nx=uyeein zu5Kj`ww<$6T1L3DD@>$6KSh-50iC^RbF~V*2PKc5-?LNkovDJz&kqoEJAG(cyu&-P z4ejtp7_YiLUmwcbyAT-p^aMuBVlB%NsKs$z*M}2o!kjq@b$m(c*RUenn%~^AaOc@H z&ydy<;MU#H>y|(pam_XxT@@qcejUuWw0$0K@>MamRW^_w(YE8fHT_&JXEdGKDd~MV zEs5w6BBiIpSbF`Kbe^P?Vmyn7X-6MVG{*|puCrz&pi4cCN`Dt8a6dgs^qB7-`pYpZ z3}~)-Hwn zD1CUQN4fjim0?ZWi>HmqW@@XoP>!zR09+}9LI^<4Wyme^k=nrbXKc5HSKX~Z<&Bi# zfe^j>i5WRtj9txf%v{_(V$7cv`eTfd{B?2Yk`2 z6SD(9GCl^>kD;>C+TbU|qKEyll8Tg-If1YC>n93qO#W6ZBlo5zM<(!RpCm|9&4duV zcnVV737C*o?k3Bg$dg|zAWMQ1AYbD<|1rC2v|AmMW{cck1kh{n63IM`_9cOIawR8i z(>z6T;Q>qFOvvNnc`#)}>pv9r4sqOqBJw~%6MUBG{SxS$Hv)53keGDiot{XHwC}6i zCS_*ahVq)C2Qy3Vz=_Fd`KrjG4&V}VQ1PP}Z5i}yo8~gs>jpTdn;@vgatw}_9xNMm z5j-t^|1}3(ygb{n&={~tkos}Ita{)TxcoqXnfLhB^9Rlcur#c&s(_YXvY5(Sh)GWk^iBpW$+6QI`_iTBP9FhnNE!vp^|(2xWm1k^?0XVt2p9aDRym@Ov-`S2+5Xwj$CB3@Fa5AsiA zvbVk~u-W2_dWyQ;MzIe)io7}xH)7t-b_=5M8zV|iS`tr8jXFP_FD`Sy`z;VM;If)L z&F?T9c7`Ash2h`~Zrsra{MUgzw7L9gV$qITWh?$M|MZ*H8~e?YI%B>z-B;o_cpe)o z>tEfr>AD_T6uLu!yZd98AUx+X-e{W9zY?e+&Y^IM=QcO}U41@`n=0P>hXhk)e{sLI z<}218BeeW4FHJW3asrZa0&j|LuUd}3HP-4xt43S7$+XAPO1+({cc!&nWG%fxv*()0 z0BR~BXJ$W1m-NgmB7lvb7(rUvAMnxWSz)Rg$ApzO9HAE%hO~M%Usv=RUr^#evokx1 z*T_G%n65SCQpDNOXxd?jK!XPRfB$L>o81m^8~E@xjOt>h-iTuN&wKdAzE@@Q<+ShD z++0~C@;XrDHtqhQFA#>jx;EM1Sw3m^8|x=>m9b2?p^zWQo0m4p7Rg+~N~Cx|0`T&};Yc_-_Q5-Z33HmUJMsC0O7 zU8djfjI$%(b<1#2Mq2u|A}uEX*>HcBfO>R)KiKg{LkgwVj@mk5A+qqcNXAGgoyjc z{a*L&a69iN#QpzE4wB1pv@YZk`0tw?uffxQ19`Hz$%U%wC3KbL)iY8r10cgsVcO8>{e0*+6_MqYCvknMU>`o)@(D z`>>rl=){K`bM-%acW=<7KL_rm)5$qWB+N^Xix85_QQzokyo3D2hfOMWek%b>S z2cjab{clwm(Wj=IQVIHA>97LnH_U*xx(Edzdp=kc7I=PoP=W|Cs9fY-9j$YA`w2Uu zIOf4|nY6%J!vNBJznmN{@&SlL{f;LOW#%b@j6mx(@+1CGrOz;ohaENe41xMa2-8HY z_T?apK^??*&Xb;$QPpkk7r~Qm1rP>_N4b9ixFbKb)A=<>8K%>xam*t4z_lXc-2xU- zym~MoVF2V~pDNLM#5Qkz-nTs4D6Tf`k_}S{2Gx&$fJI2~7ZHlz7j^<@BFpYA*~dA% zjykJngNg$Y^y-NklG)#;w;cc5E4eitTY+9nF}3+zrM#kTDXn)D-!+!9v|gE8M0_w$ z02Js`l1-~~;&DjU@uX11-iXyAGz<)@cL&4SbE8te)L}U&wv&bqlmHgGiQi2Yr+)Q2 z)>oP3*@3GG@BXvUB|G0jK%M14RLy;)=~rsrgYo;c`@0S#+sV3y)ZiyagKuX0qN-F4yx^EpXl zOL@7`Bk$>u*SzF_`La?6+a4(opq?%YZoPNq(?3Sjj*(|xhpPhSFW^#kiRHzM#2>f* zQh2yBoIk(zJv->D>CkO~x$7_3V>`z%s-ebtT58k%-ut1)hu-~JUkoz6}QV;hr4)aw;^-jyIb(bFe zR&!H3zkYJ%4r9Q_ehtmdK1EMkBml+*(7Pj)nIOP&?^d?&I)6X#ehPZxTl!lRT8jbGqo3A1gTszbK!sB(1bU?l#wMkK(G?0y{@4COj&prl zIy%I$5NS$YH#F1FD<1pzxClycrwlpYd1jG4!XC;x*LQJ*g96Py@Hd_fiE{DiFZ(W8Du zKvw3Dk>%7kENW*{vsKz4_s^5cy>uS&h_(f{8KJF3h^|h_8v(iB4%z(smlk?8)qsU% zXE*ziseePY!O7pM|L@PuHMMe0@H9~3k#|fDM4qFxQ3%Xv37p}MKcmKaH+@n_HRP$r z@-h;c;;`H_StGaCAlP5KJIUfVB-y%pT9i}wX*Zms{?#+C+^cYc(Z5oLt3TiW`ljt) z%yHNn=jE%0dFbQlBg<~yKQAK_s94o}4i%7jb| zo!2RO!A%1sJ8+da!9VB@k95_+M6oa{Y&_ozBu4(RyC zi_)5j>yX*@qQq}U92roeU8RCD2EM2{x|8*C3!a#URt`i}gX6rhxcko6F=RCFQ$KOa7;>4fS5GHy&T7^8G;+%T^C*_-S@UBfB8;l*rONzS zX>>B&r+PBdB|0+FpJxq{FXxZN4~D6JrSG>jEibzr3k3Snt@4g!W{BBz9MAb4*73?| z=6|GmYH6G)GsQD=&*GXG&JKnv5EXu7*3J+8#>=^A*BzP>t(I1B9M+iXZ*hUfs~SRps~e-7Zmi?F&CSllp81}m@a>Oqro9|SylL01u{;t|k`#F!vrPp>vT5= zb5s;%4{z?b$n%%^{og5B1v!H=uMXMTa!nbApR{XW6Q{?bCzIS&ubhV#%S_V!7zxZ? zP#|k^Bcsq`KilF^xEcQa6o;nfZy?Z=#K3~F6 z8JVv+6m2^-{WdVOsj+B5Dx82<7sKW_*lrm-^vTLXrb8FYEY_~7Y4v%onBk#TVsY8kAUikRMpL-`j`cD__%#$$! zPyiuohK&WQ{6uWn+rfNH@5C}3K5Yhcb)8zSsf;7JJQdT1{ElZ17<9QE8*lAY z(|@z%G2=b*7fpDJ6?)(+Fw=WPMl>#kpT5re8XaE(T^C@xMv$D$@6C`F;_dEEyx4(CY;xa)ftf@(b9MW_ z{g5{lmUj9XI&XdHh^#O$=WO`L1P##KS2gR#DJf`^7VL?x*o!Rt^vicNlN5TE+N9tPssSQcq%jXfpE3p zr-?92PgQcXpZDwWan;xTt!QK%4h8In--B~o^Q9qXlxJz0I|kIK8W;n8@+vbYa*kXo zAkgs5xE_M#`vIE_T44rNdnps1wWaH0ApOO&(vqQEnO`tqIrZMjlworMU)@?+SH5#Jl}bf#nQ`n6}~=#F&Q;NF`&(4yhU<%=-z z(l#NJUXEPn2Ra?>bonO!k;Nr@I)dF!&ca2-dA&N9G~=jd26!IL72$PSIh#PCi2{tg zM>5Gt5Oy7`Yx}F4eSE+(Zl?wRGex&kP&bYaM5uL?*DdiUK-p$O8)yggoLJb z07ZGE}XVO#qNgYvD%c+#(KOOm5 z*l{FRnXn%1w`UhGl%F18UFpHreREBZw%?SKFa&t9G}YIlvyQfJW1mvoH z&=A0mU(aN|4VA%)tMhniR2${uaC)>=|2^|HT_$>%--jut^s`QTKPx&9M z*Li-9pq4i}mtVUw7bB3>n~yAqAcqA{75vAFUh(0_{|Im$rk~%q2;m-^lY77NVBY+8 z8M9q$^K*1`s(qweW!}WM9~s0fWum79Vl=>vRtA5nyBbi?d%e!qLyLyWd$ko$U_P_WiW4rE?d_gC`g%N`ru4qYA#EJbKxOpn@_cRqyO|= zzYXCqDwN>Ce@qE15(9W}J%C19+@jH>1GYx9WdeVAKgp}d~V3+h~Ym&&5bjP)vUpt+FTs|^N z|K$j9@NO@&Ob~APNn+E5M9P#qn~?)ez?rqv-)bRR@eOru?!G2&;4y5m{jiULptE-q zJ&scH-FU^Z^xoX2!EMR;4BpcR#Pt3XCK zT$pbSpC~nX4#!vN38U?i7q-7*6{4T%9#y8ZF$C=l2V6>^x3zR*7j-)6dQA7qv(`}Ra@9KHhjJmTB$t8oR)d(fNxZpalB zB`r^U=9QS=CJM z{pc@o;3|2$=~nXk2i0V)ipbt9l5KCn3^@0gVVv4;c|COf;@6mlKkN7jV~tSwNZ#v? zp`s5Kju|F>oL{>_3bmG(EXcUNczwIQGk&aB`@}$Dijj3Ky>Q zfp4o*Rfm2*wlxDP-@gCUK^S5mxEV?~hqanmi_9;`w;H^f%9YVB(XGJNE>MIrBFK7$ zjI%l3{0@JDI@>H2kc&V*#dBi#q6FVZUrrK?;RT9|^-xOYy8<}aQF&Yj*SueZ55MwR zuv)dz<1^Q-(u@N*E14fS=gU?<{AC69N61-JwN+yHb*e4(tE!m2CnkE~-^DY&>x!h;p# zKEF;GH7Hr+|5GxvB>~*%|E2+SMKpfh{q_jSl9HaD=yak_59!S1?L82~#lw@XGG{4n z{U}A~uUQcwXU{6BA-Ihe_eHSa7XR_tY4fFILCGKcK~+EjM~*$wh&$zkFidVpFi63i3a_(FKZb0k{3ZqqU;AEp z&PQD%yS0FVC(;{vPe9ktcn`cb^0|HXd&qBa<&^Sbae;t|=s{U#-kiX<-?xv|f+`iP`0h{Um|F{e?0xLB0U;+-aqGm3~#qXgoqHz9sK;kV7DCB2m{#5U@ zIA|&|Fk2N417<#sj3n~%mQcQQ5~I$Kk~cfm<4BH-X57(x2PM4W>9EyUZ1%J^}U|g zRG93U09n~Ht>TI971B^_uCH7K8~!E}>k;_yfZ`ahN3Awx!l&buw1|p6OF$jg`F|fh zCg$!dfx!eU4>AXCTj(42D4&z-%sA1HIq%505)iz2*Flr^R(7d(h6OVGYymJTg*w2u~{%;_{?zwLh!0@}p5%jZIR+SIVK9Do%rl zy{X_8FVlAoI|^Cicwf1*U;_Su>GDs)IrB}z=O+5iV(b=ntOPwK9$VjTcnAhwy>->A z&e5;8q~|hdPN1Q!uJj^SG<^={F{tqQY#2V4uuX1!B>1lWKoOk2!+2AFc&p^v5BR9_ z6oQ2FwoS+AUxHWR;ZrcwbGs;W|7^P zK)yeQCzSr&h7mlH!vhfQIcR7GE#y5-k|E|s?_Q$&=jU2KnnkxHJ45S@NnHzF8p&$r zGl@ChoAd?nk=~*q@92L<#1)z-#+rrW({iY*cnPy4KTWL?%ndrLQhUMcGY0t zK3BPW-o45-AJ9@N(Z1F$3Esq!`BG22TZELF99{E=W0MrQW_PyU)Z_VJs%P_uu)fTG zMZ)&7z(g^gD1pBJ;%G7f)L?LCS3tDSQ^dP9-<6zBvWxLj+)7Xee=; z*fY2x5T%ZVt};Xd!;V)-0(J?qOuUpS~wb9rd;v?{P=Xd5kX@K zR86q~w443vUuu)Kh8GmhZ4Rvu$Y~h*zA0sFUUAn3)KMQU?DZbZx@OlqxD?^Pe`R0x zLSjedF!Cju3p>JRM12UBtVxnUdaKbOT0Btyu853>oBlGFdai+t2k6g zk##nAECGI6rP+kZ+*5T1Ue?uww{g@BQKOD*Y{Shsd2$#pUA8?L7Ms1(oD8omoR&`5 z0y}(3{doQvuD=N!ZQC7ZheX?H+u)j!|FZ2@E|k6b?e}WIibvyZM;s@#b0=0;jOfSp zu~hn}=jGPZ<$vX0`4WutQRr^DZ%-EgW9tZt>Pt|iUK=+f)T(buOElD8M*jTrQ zSfFl>%69tVcKCRxbf)7pHY4HKvG>=@`QijD9iOYcpPbco+ka~(kG3g?*mIv1N;gCz}%K6TCUpGEkeUA8&|0kLPbU_)+W3vh-)5HeUDA!zxu zv{_8(5^+KOg+(2&Hw1xWXadCRC^V(*!3Q*Wnxt2B9Lat9^heSbh$iPM`u@6rkyc_y zH9LDIy;S%3tm%bcWEK`XPs}cIdFRHBQ-hF=)Bf+)JG3W-8FA{;Z_%Um8r42KnN5$ebb+;=fm9=9iLDbi6*;Zvt3m6 zvVPbTsCAsgnCs|^_}g&z^l)xV;mb^pwJGfCuw7c|k&!ly)Set$yj9J|aqG{*y+MVq z>M2-Vsv=?3F>c($8v{nFehq(f*m%q~6bn9ak|bII8?`@DQ-wqp^xLeTgdF&yTDlCA zG?*HK(Xixw#D{WOS3Z0w?nYl=BuFS7T59XF?F!x$w40Ey-k*jtl_b2Gk+pwXBGg@G zr-sH0J!|Q_CM9&qnJ!=aU1r>tfe&wrtY&!^RC;i}P2M05J<@KRX4t8hndA zer?JZ)+jqgZzEyDKauk~I?|nFjh-)xpx0N;SCuEb!zx?STY<6bdUq|XG&=R!^l4dF z?5-P?`(jTybc9~-$o7agUv~Dk zD4TRSK3W-yalpCvOlU}d6t@+hSBxW0MvI{y7dhojs9oVCM+cuGmgdI0BJ|JYg6yOm zUBEhrhJwKqZr{%@e2dYT?p7e^f+<0{a<(WuyaPMC^a=lA?KTya(f_2E+o%U7f9io2 zJVo{Vk~h>R`@w!Us+5@o-`|u*<-hz=F|fG!#m^6oN~!}(m?bJIU3mHvV5&+d70~zf zn;AARPntx*F!Q-)yayH*%d1?NTyswK`!a#}ag(a%(!#faR%rzV@6zz8$}W+qjhf?h zdSCUzf%WN$Ar6CdZvn5#{3gtP&m|e_-TZ)ub{hxUwOjSu>EspLZ)eaj_6B9 z+0gua5>n-o|4iqLGJjzzleYOkSN9h|WP2XDZ?RCjoPDJTi;&VOB%BF`yz|OqczAiq|P`rWwOteQsF&iTC$^1)p=KzRh_^THRhz_wSFy9MYsiG2_R` zoG*qNRoW#RIG7~1H1L5Q0WprOi7_czct==-!nsxudVZVlOu$^78-2(vwO+t`%?`dl z6*J0ySKnqWt*%x3h0$kIq|CZ?TqXT7gZR3{23Rm717i`b^gS$m{%-lOeiRgua zSw_ufcYT;b@Mod-GV=|`mZ0-TCWeCN4He4tzHy+|ai5!S_P}Bc!DcLKp8o~fzx7NG!Oo)Z zck`k2UC!I2qyaov5*JAjH0@>z{M4bQjaJ)=?wF--@SZ#=aKVE0DSy}_T8Srk+EfbJ zhHHMVsoH{02wJ5ZH=85Vt%U=Vri7#M>m*>>i>Ah z^~UMjbV_$eaQ3#4kt{Ew@0c3VH^2aAZym);LPIfXz(rkg zAtR$B3bIZhgpnXGAwQ! zm)H-LFH}9xv+PbK;>gq%>>i?HYRxvStRJG)U=GVGYImNue0N?q<9TgfVJDdU+!4Y3 zhaW|=q(}-@iR9AS=*H>>v2iMDh}^H#BfA&>BrLpIvNqIM$geAXRO}e49g6*3yQogo zSx`q-R|c2pFi$IwMZ0FJNYK1T?_HBUZH<3~u;Vk#8$Cw*Inv7MGR*$n0Qh&4x0g_a z#HqjZ2Be)nUGiqpq7b$mmHDIVI&~80-^c3v*-dP_{%a8&#KYC}za!JKre<$rt^{Qk zp4C|Mm>R}st-Y=LeuA|Y%Bv!SPIym11*Bte)JJ}2MDXXs5P=y9ChUonJEk4UpSbm3 z)yEu&37FKa{z}dq0d&d@Gw?ytC`L}69SSJgpE_&>h5K*CnehXxP}BeOIFX7Nhma`x%>udXi4LUbPc&ORjjHSFEXcXual(#v z?ZxR$aaX#aN{yR5jUJOZzM#KdqR4dem*WB&Wv`laT}j=?(JDc}+jZLzt=||mg98~j zYnc-UubAnu$V=@6cCN21N_5M=Kp)QFtd)e&NX|kZq1TZyY8JeX^n`>eBH#sDeF?r3 zUds*2F?MGH0gZr0{aE?Pi&l+Igbuh0G9R*J@kP#sO(3&0l)?@;+0*oQfww zwAqIv$?5hhdn?bciQe<1=o*`&r0reltylx#aBfwGdWPPD9GB$8JEeS0fU_^1B6OgH zLA^-v;da<|?xLv{1s4brLgo4rf%P?*{+9V`qQiM++*=a}gWbd^Pb|nxnVtCG-olb9 z5@5uVa&l|E*Qxy{uETOK5mx5kmogz4deQ58{Ik`)Q9;<0FMRlS@!=VSZ)+PeD;uDe64s4dl0`xC7o zzcyTB-LOa-YPixsV?$CPeD}WS-VN=sJ~jzMRJ31iWV~uW|Dz1`wV#5`^hAB5*xf(P z(LXGdP%~iWS`WjReH~gyfek~nL~}Z>A81LS#D_{oS2PSR}3vIkA@_O7{CG+ebGjEmO-|`^p^Pl zZ3U#K(cx6=+P{)XAY{weL>F&zpR6?1Q0z|hmZ~eeperG(3CEi2zKEVF)$zlhTHV@0 zRp20Br_qw>fq*qs0XA%=H-~Nm=UFl5f#_}uK>w^ZOurP>!GOW1bzspV=VkW@# z?Ignl$LtZqMwhJ7XTB9;`ESz!E|P~l!mp7hO6D%=g}yS_8@1PhWc|8P9JX&Wp6Ug> zG7^v1iKTA)1p1Bh@)7u@+$j&TUe#Z=f0gNbhSmQ#FQBD9;{T$%obM4YIdr?(UTdR9 zm?cWnMofT!n{gHXhaakP)`(wdLN2*FgMG>|Z75y=~ z8L=s;V7X!RYntaiAwHLsa?_jCR;N^N*iCf(3WYN2*L;_1t}CMvM1Rb(^n^FO>1AYI zcSf=usqHB)@G%pZOU3lMV-;d zI1C9;YVR*MipV&eCpfA5E&`B~ztt)Agq9c(Ty9WLEO^&*idReepA`uD+#S(AAp7Wt zVkn$@9?pVQh>j->A{UzL45<37-rFH6tERr2;TGyo^6Y-8=-9R}z4#s_yFf6RHGfTN zme*~%lt6)&*(o92j?TZ0fp};+YAeosV^!!tMa|vKGTNr~?X#fAZyIs$?hvuSqi9~W z@opUn>*UbCK7q9mfQoRuU{ZhZMk=NBLt(nAGVm>)*3Mn=eP4E zM_(7Px@mLiKSBE=Hm;-g7#>66kF8L1fl+JOrvXQoxe+0xBGYJ}IUWIUY-2U?QT5@3sWpv+2hGxHQkKKBb(j6 zB`GSwVp)8w5Y<>QPi%0^TMqwMx9{s9iJiqvRF%3F^rXwzx8t3F={`MP=cd{Wr90Y? z9iX|0_e~vAPxbup-81* zXGv_KY7c9>I~2JMiS)i%`w;|bWR`cTiwq5Vn0OvFeWKnwHe+m9a+4gL|FBR z&b93`EVh$SC%J!V)&hd6dlj9 zE!($H^rVzsEB7N=m#o*5#zTA*UUMziSckJ85~;*oCzjs>OzZlH&3b7EXeOT<-OJ2S z)xgP{mJfRM(KKllW|}&`#{dbCke@&Y(xIGC!>cPZ35mAvUOC8;Nd|D%M+w9}O6Of$ z#|NH5Redh+R}1g~Yv3yc;bj9zP6N`WR)-J$$_FC9-d48YJ?(i&h3M7U3&c?gq{{R) zzN>qTO>!QE_PT?)2m%m#dONY>`kifv@y*g&jbyd_`n8cG0yrtcR=#jskdgG2Q^2G} zpq2a)5SK&&AkYb5xL-kZ>A5aZzj`(QkO6H$+v!<#5kE%4n^uT48vOa=@ZihC?pctJ zh$7g^MwP*ZMa2i5fi1Vf{5W37^kr|SNsQg{>U@GQ!0Scq9+fGzoK*PAs9!zDT1~)) zlSt8&qsHvPxcTF%@Zx2HIa4>IIS|xHY1O`Lw{}SqRgC<{294K z1$RG{WXPr9L5xLj?o+k&4%&@%62V}(Zbi;Fl%1TNBeZwBTX6sex~>#7aT4%b-e)`Y z*0tfqH8m!zyGD6K;Z$$}D!7ft^3#)chI?cHTv_GEp zylMG2i0#t-C~3MY9RN7rsmROd-~;Vm_nGPG<#5QD2?Y_+2g5^+S2qRWJ+-@Tw)dgM zLzzu6wgVHJmdBOJrzfqQGc@nqECztr$h#gBXP~tL!Osi$-#*G^Qs5%}eg`&XC^kx3 zUb7w(YM==nfNEA-+Hqqs-|Uq-T^L;-lb!brpEE)$%^t`{$P&XsDtocS0K&ixAeUS^ zTf(+jKk*!Lap-9E63~CYykPjgzCZ*Zbd79SIOf`~8?+Rce^^~T#mm1q-}dieSnK-e zW4!v^5V_o!U~*2}IhfWVaC*v7>w9fhU1!ualE&M{^1Hs=yI0Bb|c@jw2za8sSS36_EOeAqrb@B``4($d3bPX=4eM~7RVG| z&7;|E7k(K!;%eBb)MjX+jWJ!&kWU`{Mv8OhTa49u{vXlGL^TP)J-F_lnvM>m#Uy^S}-6r67oNhNR>NTRYy0#|s zGd!-S-9Liw;WjQ8`JgloM;2sIO&;1F(;a;1=s_6b=7R=`Kzl&&urfX&E~|^6tY>fzs~9!Ht^F>6y?uqN>qY^PU6y z?m=5Y$L}+1Y|e;jbjMwTl-8nn9A!HE6W8498SWR9>f1bSdU7@kJnXW;1wD6NpQM@b zRkf%huf8XdAjG8C6}kRavO_hiSYKvHe2K@c!ib#aD@gnte2Fab%l=5$^c9{1vi%aq z#FT_1qi#Jnq&1_vSU*e};@$h$c|xIv=5@V#PF8+`hd;JZT1BbXQ_brT`EYtf@+A`kpNsnzvYq4*rgL&v2~3waoWFmFg!9=uXH2s?8Fcyvl^DsSAQ~it!5Cf~V!s&h>&h)}3ZRf4e(ZsV1EJDop1fUv z>R@nc<~IJuF&+jVLeV`;DaDG>7D~3rwpc%Z*bSkYRrfCHr_DPRfg4y-EOYT)#0B2l9JWQntoM zr4p>)mM;u#v(+Fkw;;^e<5BUdBt(XvDV0-fm)j@T+&IAG!4QAca`HLyc{JBPH*TJk z0h?mX=A{Pd`iPIP9LXj>FxGGEvZn}k&epqa+V%Mf0Qjg}4v ze~70AIFff|7{_>&Jm@r{xT`8Y@`$}(_^bq3DVceTjA%ifge z{II7jRNZ{1Q&=kmD)nMz*oMKtfr$hC7*h7e^R3I{iz6;R0+IM-9b znrd=;GR-7cZi52Pn)gJ~R1jY0fq&|+V>OQXP4(}|gYkDwyDjiu)3ri1-q22x?I%l^ zlB4PA%#3Gt7e};w&dW%~UvQg^WBL-kc0XDnr-|S+OZc0nXu}_Tfk8}ow@-HZ7zM4m zwK_l7eX)HA|KWagZ}sMhG%N7y*9*%^H9#e}I+`IF_L@f7lN{aZ_kU0P-}Gl%l=HF5 zo$#3dgChUm+4^5Wcl)1$-r)pD$33pY!vp40k*N^rK52cCTUoz$nK(#^t5~zgD zK^^=p9U1{oskNTJVtCSsWow|myhA-^+nO4Vf$D=Xh^~++gg<@^Y@pp2L-hPJtw(1& z)w6e$r3f;WU-;sgv%X3P&0_dg039xfiXPMR1Gl$??CbZsuw4I^Q^ zQurMZ3QM|7wKc zCu%t4oITz^u1+!kTKEt3;m`G2_u`&RS)xaZM@3a4H=Gz4;n5IAZ7O)1@1wC3FeKMG z2ZHAV+4+(TN-^T_r6ICLK-oixd2d-LWQUvS=>@?I=n=UUe_RWcVc1wRnadNU>1JMF z62Z#Ljb-6BuV>2J(pP`>O;%ahH2&f4JkNy%+CL?Z(QIVX#v8&WB_{rfxQj?inS7mE zhMp}ISo-m7LntT2;V6VF0`n0~5tqRgzuKoV;-p>qz2!jB8owJ@z^il^Mw(n0(U>){ zxp^gYU@3 zk8sO*pB*gyOgDCO^mESyR8F!_Ip~K};qSFv=l;;8F-}wK!Vz zSCm0&u#fSifL*Z#KGZjqG;o%A7kh zCLGRhyr-vjPY1NX`&(=k66l~kLfW74dZk)YU3Vwm$P3N|*+4p5o8mW)DLBv%X;VJk zhHKHatz|&~>|h@Oc(6vx|Hs!`MpgAiZ=?IbkuK@(QW^;nX(?$HknZkoIFumW-5{XS z-6`E6-HmkD0q*|2_y4|Q+%fLGd^%qld#$OzkF9EnEpO&6{@8apLu%954b8x}bnCWL-!d<28>BkTfa4f2KKPk~; zpK2h3+*S|AG=q3d!&_VL*Y;_k>KTjGOm?Y7shmLhblHm(*w&xEjp_kq3<_>3kmZTv zu*$E7R2YWi$1*es7CX$(zY=&sE^N_8&kz^RbHEF^Uz%j4T5frfWLHQX=@&NRm#7N< z%7`0(f3Kz_bF4a0!m{;3yGCsV{=y3==f9Nneod&E*5upe7~Xn`v_=K)J9~(Z8ig({ zNt`^I*SNUa_T|u-7`mb6yf0$X5k6B}L34ZoGiW^8AH8OB%Z}stk@+F zR6ttO>)nH)(g|Aw6*p8MQkzXhQZm1jW1pM%CEwFVvb_zlakjn$>b`kTp@@)xw3agT z>SZkJT9|(hNO@ceEr{`OKvL~DR-yh5vXjga<5M+M41hL*jS5kRtA93>P-EV=7-ifK zMDv{>Z09(M^OiL)%XZll_08w3yDuMTopu*u(-%FEqETNW7PU2C#6?~%-{IKo{T|z= z3U8;Oja7L~n@A%e^*Nbc2+V*^xL$Vi33_{Fa(Jj_A4Dq&8Ia$zVMk8eGEPAS^Mla- zz7_ORu7fZuv86rBtmu9Z+YITpnEH26V-Sp8Z9O5W`Lk*k7U>UtOPwEe0Z>j z-+U@ebfJYiRHObdX%v8=li$gy&38lHxewGb^1C14dx!hXJ(*D4?B%3X1Q^uA=?cbSFfon@mRtZ4D8XTaaea%Vc3ETG`_$6A{sJMwEXF z(IC>RHVO94kh92p|7?l#tuw8`VfXI$QV@3hm9R18 zuSq{km`w~6JRCO#`=7jpKa^>k7bN_p`2@3p?KQ3)9`@?rV3iD)Ukdvm>b^ zqHpZ)@br{fxxg!&Y5<4~XNOUc%kXTH|6Qa%yPX*Q)d#JO&CR|p^xRR@4x8V3>#v<%T@|sY z+j0rI9u|RUcw+wy2QIm!vI1C#{n>kFarzHfKC7R>694Jbw_k?t z&%U%E+nik(k*s&Ag}c^H5C*q3eyvCqo_EDkHbIJPXSxpu_d?4S#|cxzd8y*YyPoln zEiA*Yo3&~jJgC!w3KJJV9Sy2AvsC!^I(fi0>vxy$#4AVbyvoUS9wtiNcT`2E&8cFY zkTHCf^)BGU<7fwv!*l8p4Yqb(#Eq0hs{=3fdM(B@G^^b46f!uYEA^VPS~yle?Vx3s z`;O@SzRO8PK*fEaPcr$f($>Q8=PE+2yC(=BAetq$T+!m(*-`t_^o6&o3LZqlW1rFB zdV)sNtND!XnZT;c^vLgkDFNq2jEQ}U67v-;<^$W+YoN|wr1aTj3~R6h7fBab~DhOOv1&IpYSG2;PXHsp+XvGdepmY z(^EJny~V~w()*8|zhIp(i%t)@Fv3^AnLjmPfZeQk>}pAaSlfrJ_j@<~V@F`J^*tN8 zfMZ%`6e<14zjiN+62K7=BvRM7j?D9tjU&@!YtSU(DVJ)p*{)0%+A#f_zOy=B2Em8^ zUU74tpe>PB$Xty_vp=L*yYU?XGj-PDaK=FVEUOlLf(RtGKC%Drv57WX0}z_`8>3_2 z68|zfC^ez-))*gA)7ok*!$Cy)3_I@aOC0Uf^PNoKphp4Z{9myVuquDS=zl?c2pw;> zYdEuZi=r39KwF4R7x7bx$|GXaEbPFp4HS zG%)yltNPW24~wN^8lSIT`NcwlWZnva(YRmi@ZWsexbo08+h11_81Vb9#?d5%E5j>XKM&lq{nCg|SgcXjcx$KfVeM^OcsUGl?Oj-jb_OG*c}(nC{Ddcl$rL8veMXkCfA4W=*qG&Y0oo;=H^RZ#;&N2UL)NK)4)MK z4@l;Xtl5#B503w+`Q=jrv#o^b-A6P${m!*rgu9_ot8&)Q4ay#8#5|vW4Z^4GcywB* zhmE_O7zJd#pDl16R|z;Fj*Qei^!{KAG6F{8#ru-@n~Kg0VlC7?rsbQ9n4 zsMwQtay!vA_@(YwKU34)Rd`Mw}z7y#EN?PkjIDh_CVkYDqI; zSV!#}uY41rNOP!Q#hxP#Wk3*+7_XJPiRJ`SvOpc*s`mxr{KC7})c=9MNC0f#ofszQ z7C<6@(GB$Pva6M_{{1~0^q&?G^z%+CI{E<-HAb44C4<%5`4x#gQ3rz>r>l&NsL;@{ z@2T7-QK3@5V=_8B#y|_FH=F^paI>~S_?zS3HF!xY! z>ZqiTxMuE^RhjA1+Yh{Dza-f33)HCoVh0>gW{ns)A9f)2df4IHXzxo@Gg29wDhbG3 zm{At=Twrru*GbO-)YW?DG>AB4FA#Aa#>ikjR_+J%7&xJke=%UbL#B#|`YL&OdG1^X zSqkJ<{SB!1W#dkC+((rw7Rh;iOA0D+Hak%YOYM6c|tX_xY?wx)hcWbygPYvnx} zK`IdF?IZq&H~jJ0?kzIFC>pQ|^0z?AFf$b72TRiVrP7v}XwQ<4q}A@`eP$+qv9!$b zazSM%K$R1c(wpQ%tsF?py+MOVN-o#zMp@VyD8+(=Ni2)_@SI0H?iU0RxgukfA6D)e z3zV7|?=&KKyvG11IruRA_L2JEy($MNJJTvu4UftwN!B7J0uVV}&fmXt7>*aiLU05= z#bt9jF6#wAMN1yK@ntTNfDNop4r|@C&ass1vE+K@i#BqA=Q*ww05uH#y!?t8Z-=y- zoa1L7x6O*KCdMt@04Su&ok%oCxzUlWs-~nLpG`9@dCPe0WyA|&Eq$nHwW;3tS|^>w z?P(>C=OuKZepO@XnHiw~0>&iz4N^mJ9wEj~)F}WltnCr$^aP-5`lL)-#A2-o0^a`NrIfqzE6 zMXXs*j9CCNF^KvIj%K`A2)BO4le}&#hCQW=5F~zjTV?!zGV%x(tQqbY_AVD&1(d0W z`MIiyh`g3Fo9qUo(u$n6MPr}b-6J|jqCZe+K*7JD*UH@ZFA>u^U`t&PVW} z#NRSDi!OzfLSAp=j?~9n*X(tYE6oy;VrgW|C>a^T+oey9;61wyAWtk5E@HqsWR67r z{x-7f>ML}9R6zEku-RN5PejQ@H4#W2Ze*tQ(;Iy;$)b_`c7sPxs1&D79Go5{-#?OMc4F9yYEfYI-b%B5nbv< z7EO&?JyWTUPl|?Vdpx63%@3NuzBkooYz#`#zki)#qp{>JtPUj?H=y91BL66A2c3mk zdUE`Y2#Sx@o4yb@L~3&FykPSnq_)e7!@vc|rk?}S_#eVjg>6vpmcM_R5SAB(2NvNI zr4P{m5!u`l(V%9gf1AZ;Yj4Pj&S|;z#o>rh)%#k51;n}t71HZqV&|G4J|LjCw%I4r zzvliPEV8AG!F0%`xh7L<$!Bb-_MBk-dK$mC9KlEhp_+&}%_vcUi{-W2@F)KdZKQum znNJ0?BtF$SGdNJpbA}G0rzCQ60#k*f5Xa$<-&tizUG&?+RfQGYSqFar13Vsa;p3Yr(z zR25afh8cQQj4{j`rCy*6_~-aMl<|1gv9W#CR|1)kMe#-N9r2$gv zR?P{g6a0@wR2wMQWfNEF)7jYdQ*!9-2E!Pi!$|ood#8&vlFz!o zRIb}m36ns-Wo%sk!*_gg{1Iv^s?QfQ+g6<0rN11`Gcxj!?+Y4lASxVY8mj)K?v7}b z_^-ZvqjL}-$mUR*5a9*mG62LJA|GqS1TBO!!8k+_V*uw2Bp2f9pm0=uin24=ucV%-mv=n$A*D~DtkEtdUyaD!LRQRRYBY!qivi6RX^2!T^w1n z%9sJn2b6*$>|)4^^b)p0Iu4gXeFj$w=3Irt?}m22FCgvUndJX__6A@3aXLx8XpJ_6 zu#btqK0RD#g2cVD;cr*zue3u8c>tAIapEA9Rdh6?7!Kuv&=;wwXS_opzJLs__=X;# zM=YZ9I6#j0VkEo@rj*I2wZ7>UKD%sOjaAZZVfAk={~;jwuBx8HY3{B8XHD_nojyCy z8NboT#5zOBv&Sil>B8l_^VuY??E)=Xeb6__^BJLmtX5=#Vu)_ODMeLHjp@gvngaL> zWh-qjA*=M)OXLOMT0Nq>D$$vrItA}pRP)^*5rq^WvOroEpU@#&yqhiv6@`YWsN1lE zeGH$?TB20*GRH{TmgV}WNiluKeX3VAIaO7TBd%L~`dMV`Eo;MvAwpdmX&xwUiT0t~ z@8m&KQGScoO@)P)do@CSi-QHS8lONrLg5;tDbEADEqbpFh@|CDy$cM(8*E$}H3 zD*Z--R=nhC#7N}r_222Vl1mbe26LA&*-qgu9`Tq?Bbd&`kW5$i>2uv)>w`&OV+XCw z=LH0wADJ#aV5ophp2SPyN1pqYR`xI#D>s!sETNy07o+kQJDJ5A)lt(WKc%861I>4a ze{8K)e0OjVkgKe;xI84`viMp4iV-O?i>!^Xm(dl3Vqu${p0DW=YPrO$eyA-Ojq zRqFmv?Q7M46ZNat>nqp#2Wm`c=w8&LcrWsm7dt9UdnBg!q(#sSTn+Q$_WIX9^Bm7w z4DZFp@AaJ^E6dRs=179~rJ!==OUTQ-nGy>!ngj}hRhJWqRzG;^{pAet8%u7Y8?ne| zhpv<_rP}GkLHz;VbKx8x`1xm6UL?Nu6^~~erA?-@jo3|RVKZ&aAK%u53YwA5QP<*{y7JrzEP;}KPPvNQ@uw`}9ri)88>CbCO}x$0dM{tE>2 z!QeI$?mIO-Y@in$Dq0cfkB^DWpWr`utY&Bn-A=?dJk8}W$?Ejecpu_4Yyag4H#a&F zT_hP`N6Tg4AMO$jcLqWIVe%+JsG`a^1dlL(jqLX(NPmCn=&95c2NPRW^^w$4aH7+z zr-NLb|Fxp<&Vcl55ThW8iqPp>XhaD=x1=`YYI3uvm*iHA%YX}*FlGT7VGS;iN$08x zX1N_&Zlo7Dk-HK>Fk^E3Z5U_%CpBLaq~Y^?8EKlb!oRsjM5H1HVOJ96eH81T}C1 zdN;oyG-ih~)^Fy{XP_|F$BUu0r$;lh-u~Rs6(xOfU&IOVjoyn>C(abY>u-3g_d|9w z`O}TvH*TG_e;h$g=)C6Rar^U5)B=Ld|1e-lV&zKr;%cp|2~hYW9X=7qQcf&gk@E8s zT`C0J{df2qnU7OPdR&n2-=1C2rhQmv^h3jnG6|$HBd%zF|N7rc$si_fPGqGlxS~=v zoCbQr-{dFqz0OFfRey+Z^CN7}a<;}o%W@(QvD$H)(Eu<^T%*&{(L==%?+x}>zT(^_ zxm!jpq#G?td3=b93w*RB<#Wb(= z08T_tIuxfL&(@WAD1xx@7*u^7zYf^?Wj8h=F+DtzQb;AGVPM!Fu`j4Ie5g$94A=~3 zduBW&1lUf;d-=aR(B}MaEcZ{l9dPN# zp*0gm%4^xh2(aq4g^33%e?*Ll|M}VU{20<0q+9_YK}P_f6$0E14-14LO#-6k49TrN z4KJ89odbwCR>v3J{KlJCLnAvzw4S!s_j?ZKy+q$W@+D+OTA3rD;j@aXZCe*x)+(jA z9tlnv>>9Dcso&BENb0Q|>hF|zgvHYJeR}B-b$%Cn7oM({C4vFS_b5I3nUW`W1JF=0 zhG36`I74HAHa~w38WaRJ5zjfqIkBRu5$8LVIqzyUV_TPndkS5$oQ>OYW&=Bi%SPPH zmmR4zH|-NO+K7?XXxTE0Ac!$+Pisw_7_%vnEag+V%el@Ibcu~?vEsXmeeoey#)LQf zmjgJCLX^b~PIMX{9+eUxC{5{uUD(GfvW4Nh5(860FagIsv`8`|Ih}Xd`tNEUV$*o` zMP2^!n1F6+(ji!xg9e~QSi3&Xk%0v@<4!EX=fk*zSwsu??fmfOGvPL} zYtI_ZydW@L7sg}AvvrQ~z6v8w@h>hmj=J>c29!Wsad;#xT`*+M7v>MSy;7GzR4$Zf zc&Sr|djiUrGpQsbo|k|eZt5WX4k&ar+_)2|D9HRja)~d6AZR)D!4hO*S|fCb(wO2r z{`|bde3)F8$NQU~WqfjWwlBKu;XeLmn`}eZMnOPwVH}Xp4-`l7KX~PQ7ePJ$?rC(!k?dax%;BC*SkI!a}Ls54t)nt(B&04!qBFb z-7Oi9zq@^)x<7AVm>4r`8w03gxXIoA&)oL^a2Eivs$H-r-e_Gzc-ZYvWU0s5_73yD z3sNRzE@oEAqu0<{jEPi`G@U6CaK7lZ2@4H1q(pO2R2nPjBT-5PVX28^y_$T}5zR2M z;znZYUye2q**0%t)~e1r30biVT5vaFmd5E2wlPSc=MX52%g-3+^r=TQY+qpdurKZ2 zsA-c(bY*2WrF)e9ay$7*M$2jHdM%IN?RaU4Ui-s8LCzm# z_gldilXWh$78jOOrj4aU^P}%2(2KH7)fRMql>R=MtmB1fEd408f~Qg(AohDG(Pz3T z4D5hLHjPZCXECFVJuyX`2yyA^@%zU|7-;=n&uU?7vD6wVf%Gdk?LOfw)sqfXSGTTj z|Neet`25=x-GOa7AM{w`1`&rJJ>e7skCy^FpvV)S;QKwU625s7ebEmwZR;vpR@QIY z+DrRuop}Fo`6ZYdY4G13>?_;^(5h}02Hk$4Aym@vhJ0Y>W7POdBZ`H9cHQFs)Qr>W z_MW%JWr)&rsxXtsbeKNwic)lf_D6A^0_HF6eLT~-3NA3L67tS*3*YHMEyrEK;Qo&n zuxF5+b$i08qx=l!U=;tkyfGb_(We;XIi@e4XT$_rLBOUaI^{_d+=^j8uv8;`~Y(UfbA zI?KOV8P}BSCm2f&zUnPI=mvju!!9};$3LI>&~r~9OhaOJkv0tD_ePJXn_S+&1P(mD zi*R^H0VgjRL7~VJ?y7<{tEJL=L;K7#D-+szoJA}Za@Nf6;RH7Uvjhtj1}xQv+nSbB zb#%D{snAA+`yWzJBu`LgV%NO~``+%q)X0UGV_<43)e}ynj{oXY(D29zLB4c!E;|W`{ciM-4gQ!AO{-@*pQQrn7DXeg9S|nYFAGmx<6(?I=|f;CdH;ti>?&Y7X$B+y7Q#x zgQM-S2wu-A*IM#EC4ezREc)NJSW!C~5A2Q|w_hs@p522Xhae(RSpD6Co7MMmUVl!}dwqx}mmos=DT@;h7Me5;Y%9$)Q zwZD-{-(=6IyYBT$BL#-vL!n0lpXIf^qn|8v>FalWGBy2IU^3kz%m3O&CG(X=3mO57 zp=9tdTgtz`|0YA%jm-ZJx(t_PD$=515*VhA=JeyIDVPXLZI#4EKucLU7eOnuDxArJ zn{!UcMtY|qNWqp3y`(JU!d8+0i1Bo`At37b>0PV$S(I*T5UM*%5F-fsFN|7$ZCBO4 z*<=YD}SVU_0zrb)4wGa^R!?& z`t`RO$j{8`&S(csyt9OSMpnBo%r zOyWTX_m2>G*6G}I0@Htj4U9HFVoDF4pb*I^|3~Q{NJht6+GC9v9_WoH$bYP{tjM&y z3pl360nh*K<8H}rV<61h4V(nb%A#DB)8^};9C6vJI!H)s^VLLyz*|A8A7EhWR18h` z98`aP?Suz#7eWyUjr0K_0Yx-K0P<7;>2D*MK&_M~k*lgJyDh?JQ4?1wKq=&SdGy=} zF<-2ElqQC~1*d@b{T2XPj3la+<#JxL;r&*ka}P@rorC=XL&UW$Xp$IJm(>N3^K$}T ze8I%uMds>xm*#*5Q!>B~WJP(<`z1i;8g`i3GMZyk->+n)@!fw!@l);1`^xb86Usm? zLyD!U)zml*okioOB@*V3iXWHRyg(Z2trInXv%vGx1Mx%M+pJs8M1L_D4m=eiT03No zSkcoD2Z@C;UM4U8|7Kd0eMgx6!FP4iP#; z?lW8cwpX9Sz_Zm!+vY*>Xp4XTO87RTRu$`fZ_-k;cBMmdy0$|-L&{}#9nXEEAE+Nk!kQUyLc z^NzhS8_f_?e%kdDf8zs)X&p#$SAraA5@G8aUi%K^i}pi$z=aX=mM#`}e-}ta#Rn1P zju(d5Bj`@jIoxJTglp)g;{wnpIt2h{?Pc*q7(@n`7z_}_qIwiCcTu^0pu4p~fSvR` zb9GX|p}_YZ9k&$@ExwbQ(@cu@<{ump*09}1p=gB`8n| zX{%!dzCPzkhL;8+#{w<5i~yDQ8WUiP-XX{caTkrlgISV@nCkA;2qh0;XSvJyo_C9U zUG*G?-lS(GFJcK<>3E#oY;{dhC0<;MSnIf09Iyet2!JiZY4?c~9?Z-amK1U$F(&dT zy@@?jt@Nl^c6a{h*eNjk)yqwnHvijL0-Dn{HpNG;kCpMjXeLMiIb)@j^t$Wo;ocvA`BR z6Tl>&VTkYtYrgsccD05WJxEsGZd1Ml5z~nGzgc~*#;n@M1-xgV035p3js47(ay<(m zJU_T=&ve|+WH;S~;=$;wAh($|sme#0z>>p*k3n`Q=!^Tx2H`Z_ZKZY_%}-tX?ip_8 zPJZXIXdRss)p3IC2Y%99JT~B&`T6YRdA7?97_tmF4bN^js5_#ZZ!ky?J~_E#f(tA^&WSup~X>)^?$D z*G0t6uilQlofj_51fD@|f9}Qgwoqo~q2UiPz2Tu~1wSq7GJLKDa=|#!PHh1{;+f|a zFUGem;asz;)dxL7*JhwOrsr0yv<-qkHHGLDoua4=Qr`G(Zc9s`^YK!xB+)}-Ny+=I z>yZ8-iLSUgU3iqs-o2&Of${aBKo$6c9h?tMAOmAObp9Tz^uWnLwlLgiuk|LjzlE*+ z?nvu=r(7@D>!f+s8t*unP)3mtLEh}=fdH%XWS))VluPsg1;5{F)9$s|)`6avEM>!m z$w88pAMLF7ne5$QC)L-Gw#$-NUK+Nwy0L;-kJ`s=`@9EP>3Ye?$L?}%4ksrib}Kht z@z53)zneK^wiSNVj^^`jrYB<0^Rv}KY)ZdA_eIZ3|LnoQoA-3sOBXduezTt)7gjh1 zUs+*)Wnx=-4Z^BPUGR^;soSf!=A}ErO4u`h8&4Meta9ujwQ5GKa9j3>_I`d4sqnqs z`pnn}Z@ts@6$#6wWU4LNdO=Klj5l#+%Q1oYObA zNU85V(uPx3j5A#qQXRW7`P0`NlCS=3`mu1dUjC9r@nbJ6V66AK!37 zuTEC0pKkkZM%o@#pN>{JIVb(n{@jbm$K3bRx5@Y-p0r$GpIo-5cxa8Ju37OaS0~iI*jVreN{g| zhb?0IxEC_E&{3v-yB7_6qAAmBzgrwv(sP$=yEr;YH{L--&wkm0H?S`tRW`bo;QpJla$dzc%k0A`1#NdeUQq=lci5k<)sLG{C+C?$yo6h z$H0`5h>byOwdqLJm8cjb;8mG8?m_oc;{et@Z%#I1PA%g06JH%tAJW12)z!hQ5@^8B zO2%hh@n1-6wGT%c-u+1_wy^u`VRy*65?C(|l&IQR>{{;RJRT$U`p5jv!j?Q{DO{JJ ziOE{D^QqgpFC~fwvM7=V_aDk?uza@D&qt8`7*`*A$!6$oo<1EnKbkq7*pzG5Q{8(t zuK(H<9Dl5DeWlm_I7=Ak58f0l5BP-0S0(RF>^kg@=;!OlakuunCiqBq z`M0vRbDGA(3%CBxv1RsTZTH`-mMiq}jI1@J(j#%8aa!tTkO~ciZjmAF1i3xCVJvONK3Bg(yr?0*mZsLxyr#1^h86&n+NXt@G^r=N`VRJon_ zf}i5JqEz+yjOy9qaj0Ft1p-~eE){u1E#>j6JbmEsyGsM(bzElts_R+d!j2RTQh0j;ZbypnCVoI+=K}K2MCFlWBcN=R%X>btX!QZ2f>&F|v zXA$tdJ9u7DEZ=+1bJp`akKZkkrg|(~@_R;fT=mgQ22XL;R%}Wuj$I5-LJR5w;iL4Q4P~QsA>DKhVs+Yv>>$X9?&|0pll8?)X>b9$qlR*}a zXYLjT*rE^$+z$-1slKd()nVoIteKU!nbLTOA`d%_wv8{epU6LaoBDNJOZZMT312;^ zlmv#ABpJM&%eu%!J%P{#%~}5Mso;d@BXmGP+#L_(V)Ql0Kqy{7#nhXifQf*BfC0kz z__%%`D*~^!-{TdZO#9tV`b@MR+{h{K;oEyKR(L1KxgGR8dWUhEbD)fp_YSkO;pb1; z(RT+WT=vSFhDRAA}oWefXQJJy(vkRGgc%8gu$yee+ihB} z>IBwXZcDD>&9dXjY~LGuCI4Q^FvPnZ5|=k0bG~3S1uXho!JEvn@sY-GDp_Le1ubT)uDsMW3Ppr_-O^ zqu_R>g2xS0-hQK%6rUjes9`Gq>YtlEwu^cXtrd^mdcwhp_U-eb@)~m#A=g*umd`Sb<)<=0jvgP^Ol_B72aNdFj1Y*#>)^63`< z52hpvmT(lvIA%YJp|AMbXf!O;!4@UCqkTo5Tt2*{1B5$9wU=5n+HGHkJUN%;YYS1I?xadhG zap#iHk^T#$zIXU8pWd>xP_<6$^uo8^`wksjW7cg;YE>xu=S4VmZamjb%{dRlZBURWB)oCbAKgJpB{u8;GzP@WXTB$=_k-OA{}ex zG;W47l%5l#hnbMK~VV)tGJmF%jeOw%t{>CgzKFv9=*7azB@24m% z2t0Zd-XU$4<;M^l2ZYuJdG_>z>oEYYp0N%U7<=z+$Kx z3AxC2yT~8R+l4zd6O%$E!1Jao*~N;#9~9q~RF5@@f>*-odO{hxnQqtAEgXhAtLqe~ z?a`yYRq|#G-Dagmf{^{KY}zhIrE*`V97p{{rvm?qZ9IbZe0S^3duq2;f*QUhmtCK_ z{9PjUhd7tBU5@xtE!Tlz5~q?^OrsUiDuyk7zYDtRbPXN|t}$<(&Wur@553SeJe*fK zeB$Gdg10pGcNYb6D4Zf?uC?=C*N5}hA0NRsvL)E`Hv93=Z0FZba-GRqoE&?l7zq*v zvA&{=P#24-whtYw=|D0-Swn-Qi!DSYUn&?!Ux%4Y$g?JRD^~(JVw!0AB!8OV(?-VI?L^KC@9w^# zZg32h&I;KqpZ}#A%H?>@^uAlG$7A=BFT3xm7uS1tTcPc+KP%B(ufupSo5@yGCNxAU z=oy7koX!0Bpb+z|ZSzM-gOnq;pyqrk0iT6zL{^ByYP?c}7;+_0MthbZ>ERI^TmUG{ zl8Qn)6$~Q2Zx5+|!x`t?ftI;KMwWj3*j~qDa$hhS&!I8UNg;xG&Q-i5v=yU-MbLM>}yAG*Is{f;1n6c#u38baOayc2Xqr`12^^X31%69x58hZ%Z;gGqccvEh7qI3Ylk@ zNsyee&hcD{ZY?a7^E72(P%47hfJUQZ?#0uSH@T;mIK}Z?4u_bAM(W>cvq)5)ZWH*e z!_1rmqlyK&B}S!G6!)TtcMPxdoT!s{x!f;~w+>9-zDOb{r|27D!1~)Xm5AE?H3^-M7ym~Km0Hv% z0>TyZ6m|$o7N!yulO8~kf475htU-5csf+-XtTi5xy;g&y%|OJQ`(q3~>8-AccSwon zq5=qZ65TM)H6vYmD4Ge%JyQm3_E@nBQlZ!WO<7eH`z+A=oo_afYzr!JKa0yiW1t;N zjeC04)GZVNj}ig_72*gJztyuVuwTScK%d~=mp+{=?tzQ=g9<=QhQ0>WQz5{9NHm^_ z9Q%@D+!&PtwMahIl&VtIcl-tqxcO?F_5nK@y$KceP^_(?A^r1&Ul|FL+|=N*iz^Z@ zj(RT;Y5&{3SnjW5p}_j6I-X8CnvPp!83g_N8b*x<{hyfE6*=?tpPh&_Wv~=8-Y+v! zz@Sh%2HI1^6|(?nhffJd_e#@gpttyB3*Q9TA1czmn3h7(^rZs)n!LtV+e4yEmI$HR zf0eR@mosv9zV2qaWgWq9z(}$AHVBcPcV%OMhUt2iFhavM1aN!&%Kz_=YK=IHsEbRj zjQ{{Y78h4hS2r_w7l&IfOmy9|$}bZ7Fx0U4Yw>^)@wh7GTfxU@%(&Qp;2abwq;(my z@b03qtod(sUHTVQ^^Z5N_g-hdQ%`wk4~JypJB`SKd-CjAF~@4bU{7a$f2(l}5?<>% zUYki$i>y{1!qH_D8y!F8QNc~Z#lV#DGOMkx)Gqx*=EP zG_>tMqL&bPz@AXcGK!&)StH&XkzqAlY55CDb~nnWApY-S2D6~7nv_L2u2Y_T%PGU= zTMUA9Vs<_jbRTxyKyiz;PbfJJpL!pywiyv&`ajh78%U35N?*4O_EwA(OB_hU{`@X= zfiX>ojG;kRC=f{5P>xjv<8W{2kCFd&U!k5FtX7Pm$%Y??BW*ZWJj!t5N2PixtsGSp zXA;)dzUV__V_lF29-@qPsj!9AWC|-HeI8~)mb{*XpTFk(Kz;n zillLMg{Z(i&7VOur255K)|0ev5PR)O1eaK3ulaQJWdx>wJF4$JDp*|d2|*-r4Ov!X zt?^=`+SL;gYTJZytw}UDgUOyTXr|-r5EL3!1rXglRmrNumYusi`!L9;tE3RQ&_IL#E_JgTo(K z#MO$asq!JNs00MAoCj0V|H9~*e&^$Fxh8kDSpGKBXmvi}{_?8{t6mBtm8H6{EO~y4 zUlV=zIDsHs$;v@gQL{mvaBS6tEvJzenvrst?kKJ?YaS}4x_E%htbTLmY+sCDDROyj9iJ{rZtF8=mPzy-<$B%@bKGXu z!j}SV3SQ8a-A5$k`W^TV&8f)Y0guW_r%|~Wp0F*^t=L~ZU1@zN>%q#W+Xh@jqTIiw z9V@yhD-Yvbd(coqhe-59iS=QbLA#XXh6ZxzmJv4WS zCmI|Vo03S>A+=uPVEa{BRXx7ir2z1NkampwG&DLpy9}m68E-;Cl9?>>yqda3))Z0(~+RwmzO@l7dKJm87KNhn%MWeDtw9f#&4o z{8`ca&+GA)2&HM_VXw35c3D__VNw=*Aq}{7dAKV22rLC54jioG0t(hN_w|Md_6f8v zF1ctz?=lgVjRCQ#>|-P`=%;!mK&o3JCi|VsuOra90XF(}6T@nEG?Uxo+*qp!$!Pi~ zL@gW3-ay#R3l1D1JBu(EeMEA8yj@lT;6ZCwk+5G1*pYl7me|WE7m+%~P-oGvx9^N_{818s`;y)Y# zPl~5geDcR|a{Lf&-#_^0JGo!|7K?6cbO~e<^WIj}dH2Qiw(i-1Y75!v^4%k>Ol+cb zG4mMNo4;}V;lwtxNx^s0p}?8!^9$?euXl@R$MDh@?l)hXAuCE!YxV~h2iG?}*c~Vv z4R+;A!jLsa=-ylrL?h5~(%G5S+Ji~#HOk)cy^5ZSN~-MpKtdF#`va>V`X!yc-cF!P z(?ORrSZ%7R9mw(e*gado)B8gP;v9>8?1wG4%F}Z&hE02&Ns}BG@xJ&Ef$$ubE*p~toYLBkN3d=)^~_|X=9`bYHKtD7)#@X-UB6l{M7P$@sU)C7B#z_$?N+WAZQ zPY@CC>jqZ&ZwSSCLTg8#wKi_nnq$(CG%!0*YCOF}?OuM+M9E;WXC>RO*%ifI)7>T_ zO#0XtRyO(5=tpp&4?P5ShwX;Gahlghukn!SBy9BGDdvKh`{*r%{Z|>>d>H|G;FDFm z)2Nwz0Zf>Y5hy*8)8YcrqP5Yn|Jp^%Be&7Ji0}ByWcxmr$%_M6*~@peRIu}t%7b%H zD&3QwgGZnD>E(!!eSM+(CEt^k)}!5CDGa_zW~1JSN*}1{aLUWBA%8Gml)}>1R?$ti}1@)E_B&^qnp9wv?FhI zeaKji>15)2;cc~^_oB*h+fifQ@Xis#c8c86R&hla18Fm=m(Pn&c+8D8MxdJL+s=pv z?Z^eEl&Ahv%a2?Sr{ok5!XeNKM7-L}KtG{}X1Hi*VSAT@%Y<1ST|uL(>qWT9MAr|t zq&bLnS~^kCWzSn_G`bZ7W%6yTpf_N!iy*eHC*oJ4m8K9#0AP}0)*GRb6lGi#cm(`& zv(SN!ebv3AyE34xXMeWKzuFg-{3=zCS#f0hI444J+efh`llxRyC_qEOEK}d7L#Ui> zi=Om%6Y-Y9MLwG{;zRM0QLv5U?Cw-kLObU1>dWk8b{(1_MNJbnP3O4zLQ{)0-mx@Z zC)+rI>hzJD-2&wcZ*L{MDK5v==~r(gn7iK-uSp}{iBbIe`RZ%5cqU6yc`WEBzK~6f zAtO!xM_+zwj*+ZBX+35%4Se_a zc-cz=^kwE*%^X=r_o={GK?_Yj+e7!@j4nfWIhwEL2m-&ifkwNpb)aHj-M@5X$keir zL?*Zlp>#Zu7D^FuI7GOAZN&Qf;PaebCf2s*Xg-r$qtpGDSanU3dD8qGV?I7v$0?KcZe{B{eJGbf9!R_z)8S9@Rn4s{>>{h7rW zTV#Y3GnSAft!l&=DO8NCMPyH65Rz?XYMzLmO7 z>E0`O>WIX*bJ3A9SuA#3dsx}CB=zPSt+c0em37LmdFb+5Gk2c#y|28WCH97@Cc)|e zmxs&3Xu;mb+U1bM3x&j%DVw3U9?Jn>%mc%zlMiE7+Al4%drs7O@2&e7SasP`~2-p;t}BydWFq zb}kn&;^$TNe;*&Q-0k4u4QlbIz+_@N)=ti5A9K4%KQ&7w5po!BCqm9I;#^{~j`h7L z2g_a!=wj!!@&#!RlpL<&%1T3i6n&<;xSt`V3=*YVPyQ4M+(VXb677>SDunz5b;c#% zA*VHX_#y*AdKzir>0`9!9bCM`bdRzSm{wDTK1?KqnVJo9QDWenN)7O#w|roQZu$QZ z``dtW&_M-8f4Fdn7%((P=rW&YUDbEaMz0jD75TF7`gzNTKfSERs#1E9JBj58FN??L z{rGqx#XpS72{*6xA8&UmN;e05$&~K|l?kmi5`>{l-Is$>JJV-e3Tu3w@HC^y-rnM` zGgb9LUTYcon)(1k%G|W0!_ITo9c=EJHX7{eY#C^n$e0#E#t{DS z`*bc1>s8Q~1r?MbRJ}9dQ_n<0!^#FrHkND^mz1BI`*;a|_~R=8jJr&|at7*?wN3dM zA8rrlGs@%!M1njZ^qCt;^KR`^MD)iGob`Ssr&~sN9bU<0j?C|@e7%I*T$w3edp5b$ zRy^Ri7=D^mvpm1l&II(=T2WUGO|iQ_5Cwd=FKuFx_kO;*{zA2nPB&au5oWgVqvq( z`N`7Yu#kB{`A%0*q33chR@ko7v+!>TvOkz zlUJm|j8y*$rj4R3+#cx85dbNJ)k|F>dy|gyXl@2c*v#Ph-3|y@SNK+Y_*8GE@Jfo} z>zUno71gPaGuv)h<(;-~tUfY(+ZEJLr&o4ho1671ncRKBDyiaKQ-f7pmT7vd>`;dC zgJO5qHoXPC<=tQ@)qcl{LG&3t-Dg!|X3swpC_@XOHUmu8@K8l_J{rk`>FDV%c0?&A z_&ufVfm+MqgJT+o@8O~WV0tBXsVjDx4O+-j5i~T~)xzH`ppf>ludtxC_@a(IZ-j=k zGxTPnnv3N!L8LCSkV`7kA6jZ7_bu_b>+-J#B>H4S!?_v-#rHIMqhE@ACN^W^P7fjy z*3bUnY=0d`X{y_qby|2`70fSXsg%wt<+z$mWd4{@=1pS@I2O@wqr-SNizh6TlJ<6g z?e}fA?58(;QLQz?V*FN4fZz#XZiRal zE|;YALzezPt@fTTKx#FJFFW8GF!nHTB6cT=;r(|#AW;r=JYn>st_d|=hh zD70G{E>$+>Ob>+Bm()~d6S~snsePRDO$+;{cZ*xEM2<|vZq=96g(EsT3s~D)S*&qe z%lnt+b8oGgRzAcXT`8=IoqkK7(b`a2ja83RWKe6{KsRib;>36P_Mweg`Z1sJ1>b;h zPbM&%EiMchbCqRwp780$SFZ+0I8q>B%NxpK^#Eb|(1j1xp}X+30^aKnN8X83>2_mgeegu~7!O z0jJHjQ3Nc7e#W9gX)rPIhwqd7F5CD99Au}Wf?7izR9f1WvQ3&tT;BhYBqu%OdO))x zcZFz6J=s;MYyl!1xXGfv0LPy`MDz>&6>?-h<2=1BCzQDSf!z0F;}Ted;*Cx0SD%J5 z_6o{#T^3eY!JQP0AVPRiYf7b-P9$u1cv_omfmfH+RiVmD0(C3rAR6Y-h_}BFGWMDO zuwUOwp3%iLk?IXrxH7J`7}5^bobX|1eCH=$&aicUinq6z0ramEgFzzy8TH=2d=&17 zGk~@lLqXxE{gok$j(kGUx$?<&}2pCqb$w6 zzSISAX9+fnNg?8-62Fj!Zbk9o+J0r8squ}jJ|S|PTmAG?X>$BA=K)&}rQh?r(ymd8 z8+6qShPw%ksF1!GRZ+FH(V-<9y<@8AGw`3XL)il9K=*p8xVDAw96Pf>AY3G1kr`9Q zf`^P0ei`55(d`%}tQ7VAMKIgzN6u>>@(bZCmHl+t$CF@a8w@|w4RP~7^*ZwJZ@=W< zx_6r^8Uh4n;QrpzUaENK8f-2T495;9Wq(B_|MfW~rJ%?=WP)-Ny#4)eP5rBWZOX9M zHN3e|TpWGlRk=uQPC&AGg6r%plf2ui5lKN-l!e34yW8+`ZcY;I388Y{piJ8}fJ3{N z)a*f6PN(+;Z1)2p<$vF8-eH(QK25`D_@*(0AbDUuuNb$tytjvbq|@r_lqE+V92k3p zYkXj~A=aH|#xPdr@T8;ef3ae51rMn^zq1HEot!>^I zZ!+m4FR#VLrRu&NJo|=fLiCq3yTpsUar80shrb!~St$X%w&J~|Zo(NkEQ3=xb3PD` z@gy(yn38$=Q8~WBCPMRl(6vU#;KOZ;MQc$~+MnkQ5)t<|0%r>jZQ{A91DU!FBMAPi zD(*x%4{sicwJWYFN%P>9=ZFB|>h=lA^Cae8!ErfcGRb3~If^kgue{JO^1!{MPNjG( zJaDIQo9^zry|u6Ye}BOFe-#Vm)V-d+`{{rZljjZM5tI z8&o(aBG7xEk_**!Uwy3gRQqE(Dxeas^Wcg99qAyJ6F1VMql13G@=6WQ^!lpHcZJtU9vkO#)axD3H zj<%LuKU_Rozi(lsYxvAU(}H6E-nXa6aS8v$ke0KY`(VmUQ3-yxkB`FfQYuS->5#N1 zoQ;7M{=nsx`neImBt`c^c8>blx`GZ=sK9O;8!vG6chJXK;GMHG!TP$buT5%_YEMsH z_*vUH8r3l}1~A4(@bKO3xrf|4QE*7Qw#NJtXkJ1Qtt>tsCW}4XD)U#tc_KpHy&Ro! zU+Mv+<-fGVzt;Yru?*#hvZ*Kr|7R(KUeJgn z1Gy7H2Je%1iT)(uOJZVRYpa}YN10(prq{hAM@+SNc|!U`ILrN`&c>I)p7z5gw8+o& z%(aQy(h5!R7=NEk8jn?lz)elI>a*NKWh|3vpV*ggrO?x^A;VAdzfFNhB#v4S7u*0e zRfyW@qhm4Oq%X3mq-VjFm;H11*Cf&*1CF*HFzYADC@lbE0Bsh+sSM{Q_)K2?qVF!f zpAx>@UJBHYc2vAhnHC(x%vTHuqVbhyL~gnQ634w!CQFJ7&VucLt0QhXh3fSVO;V zuL)zmDeY)9#hZbwceL+C`|H=GGU?s$-rX31^KsOlKd-mGzgA>$rv+d9bWTjk-V5$E zRJQaC?H%0#jtI;rx#_l>?I0zQ`p=%@X?c$c>1X@&+dfbS3jBA&3ZlfVA4cDNq-iwO zp8LGxeQ5&UcVq?q3bzSgT2_j%H>O5_ScT7bQ3?dU(ZL^Jl__^=?ke7B6F=IO~n5RzZhKZ437VL+fm|yLpgZe1o^wn&QdZfK zX{%6M0%3t5Y>23@Alkk%WAOF++;49Cw_$73Iwf}*CB)BFT0A+W3CB0tfIqrjU~M)2 zw$ntT*3EY_SfSej+upgJ7o2cRXTY&)q1#q#7MSrs@t_MHpzr-4S?n-rSv8tb=neh= z4iJ@X3c~7;9~17&BX$4t7UyNFe``&ZRdPUD8)W4+5W@6D52@+ zbzn1gic7ooPQKpt(9to&yVqH42a7kpX>zo8w%@w*tkoVk)GbE00h_T_t&1A z*iEaOtL6M8_=s4BDR3UY-2Hn2alcIKeH|+=qF+~MbCyVz)cS>sdHbah4kpFk4m#A| z-1Auyv*?08?p^hF0A-(nZ@2x|+@3%q9{Edt331J9w)hRts=SFB-MOW;22Ax>(JrhW zJCScIdkCC}O}N3CASiUjG8r6PbLU1l`-QM~(je{61O6+7^XS$42yP);_qkrBQtb6l z<;BHj=WKrIKb~%X)Y^W*1XRlK*&Vvj#NCk8=CC7Zm+f2ISP{h$>=$f%JK;>Gp+m&Q z%u78~*iLv1G1~X@JEiUVw=H_sA zX5Mr1flci0H0_`vW!r|!M-`uz2F+z2mA@VqU31ak)*W3_s_A zB1%cH-yp}mk^%{sEwGorfrhjPkG#+MUJ>y8pPb{oY(ZGQb(WgS0g}LU_4=cMO%wB@ z0|Lj5;LmW2AotWK6fhWNV%5#AGADL$udaZ!6-!crshsDPBZiuH6i~Z63Fg74cKyyh z8@i0rIDA{7?iaWh#A3xhPM+vxcY7pQvX&rUZDLiEh(SpuN*{>)Lxlt27N51?v;&>$ z6u*%(5-PMPAa`F@D(UNe!)&u-#`)XUAi$>ocd?0V$=QqdLmGL%A*?edOJz-Qq60 zM24VoE%EZhaFa{E+YLNWy~h_Y_B_{Bz-7g-kH^}<7w513(g~N87?MP9es+iXll0n- z$ALPli=NATN0q#8)4zMJ#M^-2p*1)Qo_~CyHMQ03-iXw?J2=In6I>i^Qx_Rk>8oz( zSDqz|eEk_5HKr=5g*_WM<`U9QcR@wc`}`eimgDaiY4V$6r!O!4Nav3fP_m^sRz#Pf zH$j!*=JykWzs;d4Xp`k&U*4A^`BVO zuqJufE&>m$u8lV^`P^|zp+1@Av3393^!L>*QB^}|2_VX0i0W3Ue+vZ|*!Vj*b71HT z_aETO;d>`Klds)T4U)&Y!7zqL zkKKgDBfhz@QU0i^VBie_A6*>(sJoUW0JJ!?A8MwW0JkV4;Y3v;+)#kTsczc=lEgi5 zCv1j8cTSxul;XW)w^u`GC;`{vK5qMX`VyqsJj^qf8@Wp{;lIm3*m1RLfJDtbCv{+b zV30%=sjsfiUGIK;?qgku+$%>sHBc>zw9NFC1XgkX<%F6|SMC%@K<>q{7-~O#&38m! zf_Y+Bz=Gd<5Jv*Dp^KMT%-xFkQyXAmTb=mxq7x^DGWznt*ukO-Cga^QwpK z97r!amS$zxX6fTDk&@G!MNh`27uieI!{1$+oGF0%D;9Wwn$DDfdPM2m37Q2@) zLzqzbeYiUO#_o@T$4{Jpot$xm(kg^^p3ikzvjlVeYR;hL5cEFY=CIfq7zztSFhu}& z+8h%3#amd|b>!4J&^3KyGpSflVt+gS*By6cC6HWe*D7Nn9jj^Q@lJ7xF4gxs z!3`&l@=s6z{O5tFZTFP9W3P5JfUz6SxDUHv%mZ?2M; zq22^ll9@`P}*)Yr-lCp*X*FYL?D2(!`$FKE(hD@j{Z|N z-H7$NYg(e;7e*$cDbpEPy$N8d%Yz1{oUizf#N4ugRI^Dd;L8e=4oxmTcmFU5F(2qA z&B1iqKE%qJeZ9c>mQD!!?TR#{>~tcpB3FO;&I>EGxj31rE46#Cv6pSXhTEz7mA}v? z6Kk+;d#=N4ZeO#cwLxlBQ2biPZXvMG9DSdeFt`1FmqV*`Sm&n^2^AX~ zk7Mm^?$95~-;{ZH0&w46X+T;&A^$tR7h$?!=mpma{;StlQ$MMzvU)O7RFMCocz%$u z*hLrKpT3>LRXjP3nqHF{sED0j5{%tyMLa?*M}}*@xx@%}%zCL7-QFSM1XYAGb#_o} z*QtNFM7i22gNMr%-Dmk_WMl|l0xE^Sg!CRhm@?>Uk~uYJ?eCdBDxN0rrZ)vP1HZj6 zDBm4+0=TOnqS?@NDo_3fMC3!8_o(0tB;J5puMVgUxlzkJ0{HyjeZOfWs&le`r@GA5 zlpn%f#hgC%?6v$*{voaq_u^~72T~Ttiuj9ShycW-h6`}WRP>Q2d7u!SdmlEcmr(BZ zd6eQDLbpgP@mM6c1H)UEC&e9WzP~1nz;;Je7Uwi#Z|5B1`dridd`;A)dF18ri`Vp& zTfnX|XlRq5bQZJSUi<6Uo6=o}x-+%8PgWW<&yCRek48p^N&rqjIQaO?cij`O!nQoO zbj|+Yv2JNOzkd104)GmfS$@LNh~=?{7F`J%KB6!>#-E^KW;Zs@*E3{l+}cv9LEiE_ z5nQ-$^FH4Q-~EVt@rvRxqg=m++wh=79Ye#vS6twb`&{D_0{7PEJnsc#bnogo1SBF( zz$Ly?=uPn@M$vfFx`nhZg~3$+wV7AM{@9sGxAxE{zMhB7A_Au53Y7<>Qk^E+D6g;= zHdFnv7o`76DVOa0;|aDKS%_?~DEG$G`n5fVvT;Yq!X#fgg`oi4K=kBh4V=8Q|s*+rnYBos%YWJO00=79T7mitw1k#$4M4~TGi31oOx?cqsJJODRu49~d z;fi?9mY<|)^U|l1965NNKIHCI7@b|f4`w!PDWe}7b>3oa;HhrOadCto}kBC5Jp-25400>uw#)9|6_)x|L zmv9lk!7@wrqhF>Wb*dH?>Ap^YzT2c_r;)>Wh0p24Q$ErsPZn-eoRY!zQ8iR~cj7Gy z*|PZkN>fe|%h&B^g8sCv!wO22H{KPI2k&QJ)nlRQ<2hEWJV6Mm*eCw0w4;G@*Tk6- z5HLTjE==?fC~i9)?%8kULT7HwRP65gl4BXSivjA*<z9UYyBcG;waS4Q;h#?^FDHfgYG_<(@E6H|c>v5Fdj!s|KFuuP9# zHCmVB3JU1Ya8?`}_E4xzWxq+{c>>EpSQK8zn5wJ?M1H9^{WFLY825-0kR54?3PWR< zbJv)}b8loI&Y*$cHQ!+Zb7w|~uOwB{?M&y-JqNvdo9P(`-W~_$Qk;TZ&9VSXQ`$w) z)6XBA`c$M?b0H#}h5yKrz-HEREnukqw<&+3l}LS4}=pBjD&h;*GgGh-JW zZTMA!C%A^k_xkmpy2i#tv^pH_C?_L3O4#oNp-yTc&ABn_J0_~gCMq2`D>iFtXNE14ay+;LS#1J1i5 z1+93k!jw$2p13!uBj`5$iWqxy6^DdAPe|PC8z(*hAeG$gbt^kFu2D(mo<3##R;xUPy?7U+R$xra|;k`1>Sb3hbXU2T;i3BK`Y3(wTpTH`s)%mxA1 z!-wzef>!e4h4dcyEoy3f handleRequest( response: Response, onSuccess: (T) -> Unit, onError: (T?) -> Unit ) { diff --git a/app/src/main/java/com/modarb/android/posedetection/CameraActivity.kt b/app/src/main/java/com/modarb/android/posedetection/CameraActivity.kt new file mode 100644 index 0000000..c06ba5b --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/CameraActivity.kt @@ -0,0 +1,170 @@ +package com.modarb.android.posedetection + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.widget.CompoundButton +import android.widget.ImageView +import android.widget.Toast +import android.widget.ToggleButton +import androidx.appcompat.app.AppCompatActivity +import com.google.android.gms.common.annotation.KeepName +import com.modarb.android.R +import com.modarb.android.posedetection.Utils.CameraSource +import com.modarb.android.posedetection.Utils.CameraSourcePreview +import com.modarb.android.posedetection.Utils.PreferenceUtils +import com.modarb.android.posedetection.posedetector.PoseDetectorProcessor +import java.io.IOException + +@KeepName +class CameraActivity : AppCompatActivity(), + CompoundButton.OnCheckedChangeListener { + + private var cameraSource: CameraSource? = null + private var preview: CameraSourcePreview? = null + private var graphicOverlay: GraphicOverlay? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Log.d(TAG, "onCreate") + setContentView(R.layout.activity_camera_view) + + preview = findViewById(R.id.preview_view) + if (preview == null) { + Log.d(TAG, "Preview is null") + } + + graphicOverlay = findViewById(R.id.graphic_overlay) + if (graphicOverlay == null) { + Log.d(TAG, "graphicOverlay is null") + } + initSetting() + createCameraSource(POSE_DETECTION) + handleCameraSwitch() + } + + private fun initSetting() { + val settingsButton = findViewById(R.id.settings_button) + settingsButton.setOnClickListener { + val intent = Intent(applicationContext, CameraSettingsActivity::class.java) + intent.putExtra( + CameraSettingsActivity.EXTRA_LAUNCH_SOURCE, + CameraSettingsActivity.LaunchSource.LIVE_PREVIEW + ) + startActivity(intent) + } + } + + private fun handleCameraSwitch() { + val facingSwitch = findViewById(R.id.facing_switch) + facingSwitch.setOnCheckedChangeListener(this) + } + + + override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { + Log.d(TAG, "Set facing") + if (cameraSource != null) { + if (isChecked) { + cameraSource?.setFacing(CameraSource.CAMERA_FACING_FRONT) + } else { + cameraSource?.setFacing(CameraSource.CAMERA_FACING_BACK) + } + } + preview?.stop() + startCameraSource() + } + + private fun createCameraSource(model: String) { + if (cameraSource == null) { + cameraSource = CameraSource(this, graphicOverlay) + } + try { + when (model) { + + POSE_DETECTION -> { + val poseDetectorOptions = + PreferenceUtils.getPoseDetectorOptionsForLivePreview(this) + Log.i(TAG, "Using Pose Detector with options $poseDetectorOptions") + val shouldShowInFrameLikelihood = + PreferenceUtils.shouldShowPoseDetectionInFrameLikelihoodLivePreview(this) + val visualizeZ = PreferenceUtils.shouldPoseDetectionVisualizeZ(this) + val rescaleZ = PreferenceUtils.shouldPoseDetectionRescaleZForVisualization(this) + val runClassification = + true /*PreferenceUtils.shouldPoseDetectionRunClassification(this)*/ + cameraSource!!.setMachineLearningFrameProcessor( + PoseDetectorProcessor( + this, + poseDetectorOptions, + shouldShowInFrameLikelihood, + visualizeZ, + rescaleZ, + runClassification, + true + ) + ) + } + + else -> Log.e(TAG, "Unknown model: $model") + } + } catch (e: Exception) { + Log.e(TAG, "Can not create image processor: $model", e) + Toast.makeText( + applicationContext, + "Can not create image processor: " + e.message, + Toast.LENGTH_LONG + ).show() + } + } + + private fun startCameraSource() { + if (cameraSource != null) { + try { + if (preview == null) { + Log.d(TAG, "resume: Preview is null") + } + if (graphicOverlay == null) { + Log.d(TAG, "resume: graphOverlay is null") + } + preview!!.start(cameraSource, graphicOverlay) + } catch (e: IOException) { + Log.e(TAG, "Unable to start camera source.", e) + cameraSource!!.release() + cameraSource = null + } + } + } + + public override fun onResume() { + super.onResume() + Log.d(TAG, "onResume") + createCameraSource(POSE_DETECTION) + startCameraSource() + } + + override fun onPause() { + super.onPause() + preview?.stop() + // TODO check that + cameraSource?.release() + } + + override fun onStop() { + super.onStop() + cameraSource?.release() + preview?.stop() + + } + + public override fun onDestroy() { + super.onDestroy() + if (cameraSource != null) { + cameraSource?.release() + } + } + + + companion object { + private const val POSE_DETECTION = "Pose Detection" + private const val TAG = "LivePreviewActivity" + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/CameraPreferenceFragment.java b/app/src/main/java/com/modarb/android/posedetection/CameraPreferenceFragment.java new file mode 100644 index 0000000..3453e3f --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/CameraPreferenceFragment.java @@ -0,0 +1,109 @@ + + +package com.modarb.android.posedetection; + +import android.hardware.Camera; +import android.os.Bundle; +import android.preference.ListPreference; +import android.preference.PreferenceCategory; +import android.preference.PreferenceFragment; + +import androidx.annotation.StringRes; + +import com.modarb.android.R; +import com.modarb.android.posedetection.Utils.CameraSource; +import com.modarb.android.posedetection.Utils.PreferenceUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CameraPreferenceFragment extends PreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.pref_camera_view); + setUpCameraPreferences(); + } + + void setUpCameraPreferences() { + PreferenceCategory cameraPreference = + (PreferenceCategory) findPreference(getString(R.string.pref_category_key_camera)); + cameraPreference.removePreference( + findPreference(getString(R.string.pref_key_camerax_rear_camera_target_resolution))); + cameraPreference.removePreference( + findPreference(getString(R.string.pref_key_camerax_front_camera_target_resolution))); + setUpCameraPreviewSizePreference( + R.string.pref_key_rear_camera_preview_size, + R.string.pref_key_rear_camera_picture_size, + CameraSource.CAMERA_FACING_BACK); + setUpCameraPreviewSizePreference( + R.string.pref_key_front_camera_preview_size, + R.string.pref_key_front_camera_picture_size, + CameraSource.CAMERA_FACING_FRONT); + } + + private void setUpCameraPreviewSizePreference( + @StringRes int previewSizePrefKeyId, @StringRes int pictureSizePrefKeyId, int cameraId) { + ListPreference previewSizePreference = + (ListPreference) findPreference(getString(previewSizePrefKeyId)); + + Camera camera = null; + try { + camera = Camera.open(cameraId); + + List previewSizeList = CameraSource.generateValidPreviewSizeList(camera); + String[] previewSizeStringValues = new String[previewSizeList.size()]; + Map previewToPictureSizeStringMap = new HashMap<>(); + for (int i = 0; i < previewSizeList.size(); i++) { + CameraSource.SizePair sizePair = previewSizeList.get(i); + previewSizeStringValues[i] = sizePair.preview.toString(); + if (sizePair.picture != null) { + previewToPictureSizeStringMap.put( + sizePair.preview.toString(), sizePair.picture.toString()); + } + } + previewSizePreference.setEntries(previewSizeStringValues); + previewSizePreference.setEntryValues(previewSizeStringValues); + + if (previewSizePreference.getEntry() == null) { + CameraSource.SizePair sizePair = + CameraSource.selectSizePair( + camera, + CameraSource.DEFAULT_REQUESTED_CAMERA_PREVIEW_WIDTH, + CameraSource.DEFAULT_REQUESTED_CAMERA_PREVIEW_HEIGHT); + String previewSizeString = sizePair.preview.toString(); + previewSizePreference.setValue(previewSizeString); + previewSizePreference.setSummary(previewSizeString); + PreferenceUtils.saveString( + getActivity(), + pictureSizePrefKeyId, + sizePair.picture != null ? sizePair.picture.toString() : null); + } else { + previewSizePreference.setSummary(previewSizePreference.getEntry()); + } + + previewSizePreference.setOnPreferenceChangeListener( + (preference, newValue) -> { + String newPreviewSizeStringValue = (String) newValue; + previewSizePreference.setSummary(newPreviewSizeStringValue); + PreferenceUtils.saveString( + getActivity(), + pictureSizePrefKeyId, + previewToPictureSizeStringMap.get(newPreviewSizeStringValue)); + return true; + }); + } catch (RuntimeException e) { + ((PreferenceCategory) findPreference(getString(R.string.pref_category_key_camera))) + .removePreference(previewSizePreference); + } finally { + if (camera != null) { + camera.release(); + } + } + } + + +} diff --git a/app/src/main/java/com/modarb/android/posedetection/CameraSettingsActivity.java b/app/src/main/java/com/modarb/android/posedetection/CameraSettingsActivity.java new file mode 100644 index 0000000..5dcef3c --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/CameraSettingsActivity.java @@ -0,0 +1,55 @@ + +package com.modarb.android.posedetection; + + +import android.os.Bundle; +import android.preference.PreferenceFragment; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; + +import com.modarb.android.R; + + +public class CameraSettingsActivity extends AppCompatActivity { + + public static final String EXTRA_LAUNCH_SOURCE = "extra_launch_source"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_camera_setting); + + LaunchSource launchSource = + (LaunchSource) getIntent().getSerializableExtra(EXTRA_LAUNCH_SOURCE); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(launchSource.titleResId); + } + + try { + assert launchSource != null; + getFragmentManager() + .beginTransaction() + .replace( + R.id.settings_container, + launchSource.prefFragmentClass.getDeclaredConstructor().newInstance()) + .commit(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public enum LaunchSource { + LIVE_PREVIEW(R.string.pref_screen_title_live_preview, CameraPreferenceFragment.class); + + private final int titleResId; + private final Class prefFragmentClass; + + LaunchSource(int titleResId, Class prefFragmentClass) { + this.titleResId = titleResId; + this.prefFragmentClass = prefFragmentClass; + } + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/FrameMetadata.java b/app/src/main/java/com/modarb/android/posedetection/FrameMetadata.java new file mode 100644 index 0000000..b02a031 --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/FrameMetadata.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.modarb.android.posedetection; + +public class FrameMetadata { + + private final int width; + private final int height; + private final int rotation; + + private FrameMetadata(int width, int height, int rotation) { + this.width = width; + this.height = height; + this.rotation = rotation; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getRotation() { + return rotation; + } + + public static class Builder { + + private int width; + private int height; + private int rotation; + + public Builder setWidth(int width) { + this.width = width; + return this; + } + + public Builder setHeight(int height) { + this.height = height; + return this; + } + + public Builder setRotation(int rotation) { + this.rotation = rotation; + return this; + } + + public FrameMetadata build() { + return new FrameMetadata(width, height, rotation); + } + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/GraphicOverlay.java b/app/src/main/java/com/modarb/android/posedetection/GraphicOverlay.java new file mode 100644 index 0000000..0ba6a95 --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/GraphicOverlay.java @@ -0,0 +1,244 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.modarb.android.posedetection; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; + +import com.google.common.base.Preconditions; +import com.google.common.primitives.Ints; + +import java.util.ArrayList; +import java.util.List; + + +public class GraphicOverlay extends View { + private final Object lock = new Object(); + private final List graphics = new ArrayList<>(); + private final Matrix transformationMatrix = new Matrix(); + + private int imageWidth; + private int imageHeight; + + private float scaleFactor = 1.0f; + + private float postScaleWidthOffset; + + private float postScaleHeightOffset; + private boolean isImageFlipped; + private boolean needUpdateTransformation = true; + + + public GraphicOverlay(Context context, AttributeSet attrs) { + super(context, attrs); + addOnLayoutChangeListener( + (view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> + needUpdateTransformation = true); + } + + /** + * Removes all graphics from the overlay. + */ + public void clear() { + synchronized (lock) { + graphics.clear(); + } + postInvalidate(); + } + + /** + * Adds a graphic to the overlay. + */ + public void add(Graphic graphic) { + synchronized (lock) { + graphics.add(graphic); + } + } + + /** + * Removes a graphic from the overlay. + */ + public void remove(Graphic graphic) { + synchronized (lock) { + graphics.remove(graphic); + } + postInvalidate(); + } + + public void setImageSourceInfo(int imageWidth, int imageHeight, boolean isFlipped) { + Preconditions.checkState(imageWidth > 0, "image width must be positive"); + Preconditions.checkState(imageHeight > 0, "image height must be positive"); + synchronized (lock) { + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + this.isImageFlipped = isFlipped; + needUpdateTransformation = true; + } + postInvalidate(); + } + + public int getImageWidth() { + return imageWidth; + } + + public int getImageHeight() { + return imageHeight; + } + + private void updateTransformationIfNeeded() { + if (!needUpdateTransformation || imageWidth <= 0 || imageHeight <= 0) { + return; + } + float viewAspectRatio = (float) getWidth() / getHeight(); + float imageAspectRatio = (float) imageWidth / imageHeight; + postScaleWidthOffset = 0; + postScaleHeightOffset = 0; + if (viewAspectRatio > imageAspectRatio) { + scaleFactor = (float) getWidth() / imageWidth; + postScaleHeightOffset = ((float) getWidth() / imageAspectRatio - getHeight()) / 2; + } else { + scaleFactor = (float) getHeight() / imageHeight; + postScaleWidthOffset = ((float) getHeight() * imageAspectRatio - getWidth()) / 2; + } + + transformationMatrix.reset(); + transformationMatrix.setScale(scaleFactor, scaleFactor); + transformationMatrix.postTranslate(-postScaleWidthOffset, -postScaleHeightOffset); + + if (isImageFlipped) { + transformationMatrix.postScale(-1f, 1f, getWidth() / 2f, getHeight() / 2f); + } + + needUpdateTransformation = false; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + synchronized (lock) { + updateTransformationIfNeeded(); + + for (Graphic graphic : graphics) { + graphic.draw(canvas); + } + } + } + + public abstract static class Graphic { + private GraphicOverlay overlay; + + public Graphic(GraphicOverlay overlay) { + this.overlay = overlay; + } + + + public abstract void draw(Canvas canvas); + + protected void drawRect( + Canvas canvas, float left, float top, float right, float bottom, Paint paint) { + canvas.drawRect(left, top, right, bottom, paint); + } + + protected void drawText(Canvas canvas, String text, float x, float y, Paint paint) { + canvas.drawText(text, x, y, paint); + } + + public float scale(float imagePixel) { + return imagePixel * overlay.scaleFactor; + } + + + public Context getApplicationContext() { + return overlay.getContext().getApplicationContext(); + } + + public boolean isImageFlipped() { + return overlay.isImageFlipped; + } + + + public float translateX(float x) { + if (overlay.isImageFlipped) { + return overlay.getWidth() - (scale(x) - overlay.postScaleWidthOffset); + } else { + return scale(x) - overlay.postScaleWidthOffset; + } + } + + + public float translateY(float y) { + return scale(y) - overlay.postScaleHeightOffset; + } + + + public Matrix getTransformationMatrix() { + return overlay.transformationMatrix; + } + + public void postInvalidate() { + overlay.postInvalidate(); + } + + + public void updatePaintColorByZValue( + Paint paint, + Canvas canvas, + boolean visualizeZ, + boolean rescaleZForVisualization, + float zInImagePixel, + float zMin, + float zMax) { + if (!visualizeZ) { + return; + } + + float zLowerBoundInScreenPixel; + float zUpperBoundInScreenPixel; + + if (rescaleZForVisualization) { + zLowerBoundInScreenPixel = min(-0.001f, scale(zMin)); + zUpperBoundInScreenPixel = max(0.001f, scale(zMax)); + } else { + float defaultRangeFactor = 1f; + zLowerBoundInScreenPixel = -defaultRangeFactor * canvas.getWidth(); + zUpperBoundInScreenPixel = defaultRangeFactor * canvas.getWidth(); + } + + float zInScreenPixel = scale(zInImagePixel); + + if (zInScreenPixel < 0) { + + int v = (int) (zInScreenPixel / zLowerBoundInScreenPixel * 255); + v = Ints.constrainToRange(v, 0, 255); + paint.setARGB(255, 255, 255 - v, 255 - v); + } else { + + int v = (int) (zInScreenPixel / zUpperBoundInScreenPixel * 255); + v = Ints.constrainToRange(v, 0, 255); + paint.setARGB(255, 255 - v, 255 - v, 255); + } + } + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/RequestPermissionsActivity.kt b/app/src/main/java/com/modarb/android/posedetection/RequestPermissionsActivity.kt new file mode 100644 index 0000000..80fd161 --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/RequestPermissionsActivity.kt @@ -0,0 +1,97 @@ +package com.modarb.android.posedetection + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import com.modarb.android.R +import com.modarb.android.posedetection.Utils.PermissionResultCallback + +class RequestPermissionsActivity : AppCompatActivity(), + ActivityCompat.OnRequestPermissionsResultCallback, PermissionResultCallback { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_request_permissions) + + if (!allRuntimePermissionsGranted()) { + getRuntimePermissions() + } else { + onPermissionGranted() + } + } + + override fun onPermissionGranted() { + Log.i("Great", "All permissions granted") + startActivity(Intent(this, CameraActivity::class.java)) + } + + private fun allRuntimePermissionsGranted(): Boolean { + for (permission in REQUIRED_RUNTIME_PERMISSIONS) { + permission.let { + if (!isPermissionGranted(this, it)) { + return false + } + } + } + return true + } + + private fun getRuntimePermissions() { + val permissionsToRequest = ArrayList() + for (permission in REQUIRED_RUNTIME_PERMISSIONS) { + permission.let { + if (!isPermissionGranted(this, it)) { + permissionsToRequest.add(permission) + } + } + } + + if (permissionsToRequest.isNotEmpty()) { + ActivityCompat.requestPermissions( + this, permissionsToRequest.toTypedArray(), PERMISSION_REQUESTS + ) + } else { + onPermissionGranted() + } + } + + private fun isPermissionGranted(context: Context, permission: String): Boolean { + if (ContextCompat.checkSelfPermission( + context, permission + ) == PackageManager.PERMISSION_GRANTED + ) { + return true + } + Log.i("PERM", "Permission NOT granted: $permission") + return false + } + + override fun onRequestPermissionsResult( + requestCode: Int, permissions: Array, grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == PERMISSION_REQUESTS) { + if (allRuntimePermissionsGranted()) { + onPermissionGranted() + } + } + } + + companion object { + private const val TAG = "RequestPermissionsActivity" + private const val PERMISSION_REQUESTS = 1 + + private val REQUIRED_RUNTIME_PERMISSIONS = arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ) + } +} + diff --git a/app/src/main/java/com/modarb/android/posedetection/Utils/BitmapUtils.java b/app/src/main/java/com/modarb/android/posedetection/Utils/BitmapUtils.java new file mode 100644 index 0000000..3b13c0f --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/Utils/BitmapUtils.java @@ -0,0 +1,241 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.modarb.android.posedetection.Utils; + +import android.content.ContentResolver; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.ImageFormat; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.YuvImage; +import android.media.Image.Plane; +import android.net.Uri; +import android.os.Build.VERSION_CODES; +import android.provider.MediaStore; +import android.util.Log; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.camera.core.ExperimentalGetImage; +import androidx.camera.core.ImageProxy; +import androidx.exifinterface.media.ExifInterface; + +import com.modarb.android.posedetection.FrameMetadata; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +public class BitmapUtils { + private static final String TAG = "BitmapUtils"; + + @Nullable + public static Bitmap getBitmap(ByteBuffer data, FrameMetadata metadata) { + data.rewind(); + byte[] imageInBuffer = new byte[data.limit()]; + data.get(imageInBuffer, 0, imageInBuffer.length); + try { + YuvImage image = + new YuvImage( + imageInBuffer, ImageFormat.NV21, metadata.getWidth(), metadata.getHeight(), null); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + image.compressToJpeg(new Rect(0, 0, metadata.getWidth(), metadata.getHeight()), 80, stream); + + Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size()); + + stream.close(); + return rotateBitmap(bmp, metadata.getRotation(), false, false); + } catch (Exception e) { + Log.e("VisionProcessorBase", "Error: " + e.getMessage()); + } + return null; + } + + @RequiresApi(VERSION_CODES.LOLLIPOP) + @Nullable + @ExperimentalGetImage + public static Bitmap getBitmap(ImageProxy image) { + FrameMetadata frameMetadata = + new FrameMetadata.Builder() + .setWidth(image.getWidth()) + .setHeight(image.getHeight()) + .setRotation(image.getImageInfo().getRotationDegrees()) + .build(); + + ByteBuffer nv21Buffer = + yuv420ThreePlanesToNV21(image.getImage().getPlanes(), image.getWidth(), image.getHeight()); + return getBitmap(nv21Buffer, frameMetadata); + } + + private static Bitmap rotateBitmap( + Bitmap bitmap, int rotationDegrees, boolean flipX, boolean flipY) { + Matrix matrix = new Matrix(); + + matrix.postRotate(rotationDegrees); + + matrix.postScale(flipX ? -1.0f : 1.0f, flipY ? -1.0f : 1.0f); + Bitmap rotatedBitmap = + Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + + if (rotatedBitmap != bitmap) { + bitmap.recycle(); + } + return rotatedBitmap; + } + + @Nullable + public static Bitmap getBitmapFromContentUri(ContentResolver contentResolver, Uri imageUri) + throws IOException { + Bitmap decodedBitmap = MediaStore.Images.Media.getBitmap(contentResolver, imageUri); + if (decodedBitmap == null) { + return null; + } + int orientation = getExifOrientationTag(contentResolver, imageUri); + + int rotationDegrees = 0; + boolean flipX = false; + boolean flipY = false; + + switch (orientation) { + case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: + flipX = true; + break; + case ExifInterface.ORIENTATION_ROTATE_90: + rotationDegrees = 90; + break; + case ExifInterface.ORIENTATION_TRANSPOSE: + rotationDegrees = 90; + flipX = true; + break; + case ExifInterface.ORIENTATION_ROTATE_180: + rotationDegrees = 180; + break; + case ExifInterface.ORIENTATION_FLIP_VERTICAL: + flipY = true; + break; + case ExifInterface.ORIENTATION_ROTATE_270: + rotationDegrees = -90; + break; + case ExifInterface.ORIENTATION_TRANSVERSE: + rotationDegrees = -90; + flipX = true; + break; + case ExifInterface.ORIENTATION_UNDEFINED: + case ExifInterface.ORIENTATION_NORMAL: + default: + } + + return rotateBitmap(decodedBitmap, rotationDegrees, flipX, flipY); + } + + private static int getExifOrientationTag(ContentResolver resolver, Uri imageUri) { + + if (!ContentResolver.SCHEME_CONTENT.equals(imageUri.getScheme()) + && !ContentResolver.SCHEME_FILE.equals(imageUri.getScheme())) { + return 0; + } + + ExifInterface exif; + try (InputStream inputStream = resolver.openInputStream(imageUri)) { + if (inputStream == null) { + return 0; + } + + exif = new ExifInterface(inputStream); + } catch (IOException e) { + Log.e(TAG, "failed to open file to read rotation meta data: " + imageUri, e); + return 0; + } + + return exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + } + + + private static ByteBuffer yuv420ThreePlanesToNV21( + Plane[] yuv420888planes, int width, int height) { + int imageSize = width * height; + byte[] out = new byte[imageSize + 2 * (imageSize / 4)]; + + if (areUVPlanesNV21(yuv420888planes, width, height)) { + // Copy the Y values. + yuv420888planes[0].getBuffer().get(out, 0, imageSize); + + ByteBuffer uBuffer = yuv420888planes[1].getBuffer(); + ByteBuffer vBuffer = yuv420888planes[2].getBuffer(); + vBuffer.get(out, imageSize, 1); + uBuffer.get(out, imageSize + 1, 2 * imageSize / 4 - 1); + } else { + + unpackPlane(yuv420888planes[0], width, height, out, 0, 1); + unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2); + unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2); + } + + return ByteBuffer.wrap(out); + } + + + private static boolean areUVPlanesNV21(Plane[] planes, int width, int height) { + int imageSize = width * height; + + ByteBuffer uBuffer = planes[1].getBuffer(); + ByteBuffer vBuffer = planes[2].getBuffer(); + + int vBufferPosition = vBuffer.position(); + int uBufferLimit = uBuffer.limit(); + + vBuffer.position(vBufferPosition + 1); + uBuffer.limit(uBufferLimit - 1); + + boolean areNV21 = + (vBuffer.remaining() == (2 * imageSize / 4 - 2)) && (vBuffer.compareTo(uBuffer) == 0); + + vBuffer.position(vBufferPosition); + uBuffer.limit(uBufferLimit); + + return areNV21; + } + + private static void unpackPlane( + Plane plane, int width, int height, byte[] out, int offset, int pixelStride) { + ByteBuffer buffer = plane.getBuffer(); + buffer.rewind(); + + + int numRow = (buffer.limit() + plane.getRowStride() - 1) / plane.getRowStride(); + if (numRow == 0) { + return; + } + int scaleFactor = height / numRow; + int numCol = width / scaleFactor; + + + int outputPos = offset; + int rowStart = 0; + for (int row = 0; row < numRow; row++) { + int inputPos = rowStart; + for (int col = 0; col < numCol; col++) { + out[outputPos] = buffer.get(inputPos); + outputPos += pixelStride; + inputPos += plane.getPixelStride(); + } + rowStart += plane.getRowStride(); + } + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/Utils/CameraImageGraphic.java b/app/src/main/java/com/modarb/android/posedetection/Utils/CameraImageGraphic.java new file mode 100644 index 0000000..fdc5148 --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/Utils/CameraImageGraphic.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.modarb.android.posedetection.Utils; + +import android.graphics.Bitmap; +import android.graphics.Canvas; + +import com.modarb.android.posedetection.GraphicOverlay; + +public class CameraImageGraphic extends GraphicOverlay.Graphic { + + private final Bitmap bitmap; + + public CameraImageGraphic(GraphicOverlay overlay, Bitmap bitmap) { + super(overlay); + this.bitmap = bitmap; + } + + @Override + public void draw(Canvas canvas) { + canvas.drawBitmap(bitmap, getTransformationMatrix(), null); + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/Utils/CameraSource.java b/app/src/main/java/com/modarb/android/posedetection/Utils/CameraSource.java new file mode 100644 index 0000000..83a9887 --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/Utils/CameraSource.java @@ -0,0 +1,550 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.modarb.android.posedetection.Utils; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.graphics.ImageFormat; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.Parameters; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.WindowManager; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresPermission; + +import com.google.android.gms.common.images.Size; +import com.modarb.android.posedetection.FrameMetadata; +import com.modarb.android.posedetection.GraphicOverlay; +import com.modarb.android.posedetection.VisionImageProcessor; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; + +/** + * Manages the camera and allows UI updates on top of it (e.g. overlaying extra Graphics or + * displaying extra information). This receives preview frames from the camera at a specified rate, + * sending those frames to child classes' detectors / classifiers as fast as it is able to process. + */ +public class CameraSource { + @SuppressLint("InlinedApi") + public static final int CAMERA_FACING_BACK = CameraInfo.CAMERA_FACING_BACK; + + @SuppressLint("InlinedApi") + public static final int CAMERA_FACING_FRONT = CameraInfo.CAMERA_FACING_FRONT; + + public static final int IMAGE_FORMAT = ImageFormat.NV21; + public static final int DEFAULT_REQUESTED_CAMERA_PREVIEW_WIDTH = 480; + public static final int DEFAULT_REQUESTED_CAMERA_PREVIEW_HEIGHT = 360; + + private static final String TAG = "MIDemoApp:CameraSource"; + + /** + * The dummy surface texture must be assigned a chosen name. Since we never use an OpenGL context, + * we can choose any ID we want here. The dummy surface texture is not a crazy hack - it is + * actually how the camera team recommends using the camera without a preview. + */ + private static final int DUMMY_TEXTURE_NAME = 100; + + /** + * If the absolute difference between a preview size aspect ratio and a picture size aspect ratio + * is less than this tolerance, they are considered to be the same aspect ratio. + */ + private static final float ASPECT_RATIO_TOLERANCE = 0.01f; + private static final float REQUESTED_FPS = 30.0f; + private static final boolean REQUESTED_AUTO_FOCUS = true; + private final GraphicOverlay graphicOverlay; + private final FrameProcessingRunnable processingRunnable; + private final Object processorLock = new Object(); + private final IdentityHashMap bytesToByteBuffer = new IdentityHashMap<>(); + protected Activity activity; + private Camera camera; + private int facing = CAMERA_FACING_BACK; + /** + * Rotation of the device, and thus the associated preview images captured from the device. + */ + private int rotationDegrees; + private Size previewSize; + private SurfaceTexture dummySurfaceTexture; + /** + * Dedicated thread and associated runnable for calling into the detector with frames, as the + * frames become available from the camera. + */ + private Thread processingThread; + private VisionImageProcessor frameProcessor; + + public CameraSource(Activity activity, GraphicOverlay overlay) { + this.activity = activity; + graphicOverlay = overlay; + graphicOverlay.clear(); + processingRunnable = new FrameProcessingRunnable(); + } + + private static int getZoomValue(Parameters params, float zoomRatio) { + int zoom = (int) (Math.max(zoomRatio, 1) * 100); + List zoomRatios = params.getZoomRatios(); + int maxZoom = params.getMaxZoom(); + for (int i = 0; i < maxZoom; ++i) { + if (zoomRatios.get(i + 1) > zoom) { + return i; + } + } + return maxZoom; + } + + private static int getIdForRequestedCamera(int facing) { + CameraInfo cameraInfo = new CameraInfo(); + for (int i = 0; i < Camera.getNumberOfCameras(); ++i) { + Camera.getCameraInfo(i, cameraInfo); + if (cameraInfo.facing == facing) { + return i; + } + } + return -1; + } + + public static SizePair selectSizePair(Camera camera, int desiredWidth, int desiredHeight) { + List validPreviewSizes = generateValidPreviewSizeList(camera); + + SizePair selectedPair = null; + int minDiff = Integer.MAX_VALUE; + for (SizePair sizePair : validPreviewSizes) { + Size size = sizePair.preview; + int diff = + Math.abs(size.getWidth() - desiredWidth) + Math.abs(size.getHeight() - desiredHeight); + if (diff < minDiff) { + selectedPair = sizePair; + minDiff = diff; + } + } + + return selectedPair; + } + + public static List generateValidPreviewSizeList(Camera camera) { + Parameters parameters = camera.getParameters(); + List supportedPreviewSizes = parameters.getSupportedPreviewSizes(); + List supportedPictureSizes = parameters.getSupportedPictureSizes(); + List validPreviewSizes = new ArrayList<>(); + for (Camera.Size previewSize : supportedPreviewSizes) { + float previewAspectRatio = (float) previewSize.width / (float) previewSize.height; + + // By looping through the picture sizes in order, we favor the higher resolutions. + // We choose the highest resolution in order to support taking the full resolution + // picture later. + for (Camera.Size pictureSize : supportedPictureSizes) { + float pictureAspectRatio = (float) pictureSize.width / (float) pictureSize.height; + if (Math.abs(previewAspectRatio - pictureAspectRatio) < ASPECT_RATIO_TOLERANCE) { + validPreviewSizes.add(new SizePair(previewSize, pictureSize)); + break; + } + } + } + + if (validPreviewSizes.size() == 0) { + Log.w(TAG, "No preview sizes have a corresponding same-aspect-ratio picture size"); + for (Camera.Size previewSize : supportedPreviewSizes) { + // The null picture size will let us know that we shouldn't set a picture size. + validPreviewSizes.add(new SizePair(previewSize, null)); + } + } + + return validPreviewSizes; + } + + @SuppressLint("InlinedApi") + private static int[] selectPreviewFpsRange(Camera camera, float desiredPreviewFps) { + + int desiredPreviewFpsScaled = (int) (desiredPreviewFps * 1000.0f); + + + int[] selectedFpsRange = null; + int minUpperBoundDiff = Integer.MAX_VALUE; + int minLowerBound = Integer.MAX_VALUE; + List previewFpsRangeList = camera.getParameters().getSupportedPreviewFpsRange(); + for (int[] range : previewFpsRangeList) { + int upperBoundDiff = + Math.abs(desiredPreviewFpsScaled - range[Parameters.PREVIEW_FPS_MAX_INDEX]); + int lowerBound = range[Parameters.PREVIEW_FPS_MIN_INDEX]; + if (upperBoundDiff <= minUpperBoundDiff && lowerBound <= minLowerBound) { + selectedFpsRange = range; + minUpperBoundDiff = upperBoundDiff; + minLowerBound = lowerBound; + } + } + return selectedFpsRange; + } + + public void release() { + synchronized (processorLock) { + stop(); + cleanScreen(); + + if (frameProcessor != null) { + frameProcessor.stop(); + } + } + } + + @RequiresPermission(Manifest.permission.CAMERA) + public synchronized CameraSource start() throws IOException { + if (camera != null) { + return this; + } + + camera = createCamera(); + dummySurfaceTexture = new SurfaceTexture(DUMMY_TEXTURE_NAME); + camera.setPreviewTexture(dummySurfaceTexture); + camera.startPreview(); + + processingThread = new Thread(processingRunnable); + processingRunnable.setActive(true); + processingThread.start(); + return this; + } + + @RequiresPermission(Manifest.permission.CAMERA) + public synchronized CameraSource start(SurfaceHolder surfaceHolder) throws IOException { + if (camera != null) { + return this; + } + + camera = createCamera(); + camera.setPreviewDisplay(surfaceHolder); + camera.startPreview(); + + processingThread = new Thread(processingRunnable); + processingRunnable.setActive(true); + processingThread.start(); + return this; + } + + public synchronized void stop() { + processingRunnable.setActive(false); + if (processingThread != null) { + try { + + processingThread.join(); + } catch (InterruptedException e) { + Log.d(TAG, "Frame processing thread interrupted on release."); + } + processingThread = null; + } + + if (camera != null) { + camera.stopPreview(); + camera.setPreviewCallbackWithBuffer(null); + try { + camera.setPreviewTexture(null); + dummySurfaceTexture = null; + camera.setPreviewDisplay(null); + } catch (Exception e) { + Log.e(TAG, "Failed to clear camera preview: " + e); + } + camera.release(); + camera = null; + } + + bytesToByteBuffer.clear(); + } + + public synchronized void setFacing(int facing) { + if ((facing != CAMERA_FACING_BACK) && (facing != CAMERA_FACING_FRONT)) { + throw new IllegalArgumentException("Invalid camera: " + facing); + } + this.facing = facing; + } + + public Size getPreviewSize() { + return previewSize; + } + + public int getCameraFacing() { + return facing; + } + + public boolean setZoom(float zoomRatio) { + Log.d(TAG, "setZoom: " + zoomRatio); + if (camera == null) { + return false; + } + + Parameters parameters = camera.getParameters(); + parameters.setZoom(getZoomValue(parameters, zoomRatio)); + camera.setParameters(parameters); + return true; + } + + @SuppressLint("InlinedApi") + private Camera createCamera() throws IOException { + int requestedCameraId = getIdForRequestedCamera(facing); + if (requestedCameraId == -1) { + throw new IOException("Could not find requested camera."); + } + Camera camera = Camera.open(requestedCameraId); + + SizePair sizePair = PreferenceUtils.getCameraPreviewSizePair(activity, requestedCameraId); + if (sizePair == null) { + sizePair = + selectSizePair( + camera, + DEFAULT_REQUESTED_CAMERA_PREVIEW_WIDTH, + DEFAULT_REQUESTED_CAMERA_PREVIEW_HEIGHT); + } + + if (sizePair == null) { + throw new IOException("Could not find suitable preview size."); + } + + previewSize = sizePair.preview; + Log.v(TAG, "Camera preview size: " + previewSize); + + int[] previewFpsRange = selectPreviewFpsRange(camera, REQUESTED_FPS); + if (previewFpsRange == null) { + throw new IOException("Could not find suitable preview frames per second range."); + } + + Parameters parameters = camera.getParameters(); + + Size pictureSize = sizePair.picture; + if (pictureSize != null) { + Log.v(TAG, "Camera picture size: " + pictureSize); + parameters.setPictureSize(pictureSize.getWidth(), pictureSize.getHeight()); + } + parameters.setPreviewSize(previewSize.getWidth(), previewSize.getHeight()); + parameters.setPreviewFpsRange( + previewFpsRange[Parameters.PREVIEW_FPS_MIN_INDEX], + previewFpsRange[Parameters.PREVIEW_FPS_MAX_INDEX]); + parameters.setPreviewFormat(IMAGE_FORMAT); + + setRotation(camera, parameters, requestedCameraId); + + if (REQUESTED_AUTO_FOCUS) { + if (parameters + .getSupportedFocusModes() + .contains(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { + parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); + } else { + Log.i(TAG, "Camera auto focus is not supported on this device."); + } + } + + camera.setParameters(parameters); + + + camera.setPreviewCallbackWithBuffer(new CameraPreviewCallback()); + camera.addCallbackBuffer(createPreviewBuffer(previewSize)); + camera.addCallbackBuffer(createPreviewBuffer(previewSize)); + camera.addCallbackBuffer(createPreviewBuffer(previewSize)); + camera.addCallbackBuffer(createPreviewBuffer(previewSize)); + + return camera; + } + + private void setRotation(Camera camera, Parameters parameters, int cameraId) { + WindowManager windowManager = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE); + int degrees = 0; + int rotation = windowManager.getDefaultDisplay().getRotation(); + switch (rotation) { + case Surface.ROTATION_0: + degrees = 0; + break; + case Surface.ROTATION_90: + degrees = 90; + break; + case Surface.ROTATION_180: + degrees = 180; + break; + case Surface.ROTATION_270: + degrees = 270; + break; + default: + Log.e(TAG, "Bad rotation value: " + rotation); + } + + CameraInfo cameraInfo = new CameraInfo(); + Camera.getCameraInfo(cameraId, cameraInfo); + + int displayAngle; + if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) { + this.rotationDegrees = (cameraInfo.orientation + degrees) % 360; + displayAngle = (360 - this.rotationDegrees) % 360; + } else { + this.rotationDegrees = (cameraInfo.orientation - degrees + 360) % 360; + displayAngle = this.rotationDegrees; + } + Log.d(TAG, "Display rotation is: " + rotation); + Log.d(TAG, "Camera face is: " + cameraInfo.facing); + Log.d(TAG, "Camera rotation is: " + cameraInfo.orientation); + Log.d(TAG, "RotationDegrees is: " + this.rotationDegrees); + + camera.setDisplayOrientation(displayAngle); + parameters.setRotation(this.rotationDegrees); + } + + @SuppressLint("InlinedApi") + private byte[] createPreviewBuffer(Size previewSize) { + int bitsPerPixel = ImageFormat.getBitsPerPixel(IMAGE_FORMAT); + long sizeInBits = (long) previewSize.getHeight() * previewSize.getWidth() * bitsPerPixel; + int bufferSize = (int) Math.ceil(sizeInBits / 8.0d) + 1; + + + byte[] byteArray = new byte[bufferSize]; + ByteBuffer buffer = ByteBuffer.wrap(byteArray); + if (!buffer.hasArray() || (buffer.array() != byteArray)) { + + throw new IllegalStateException("Failed to create valid buffer for camera source."); + } + + bytesToByteBuffer.put(byteArray, buffer); + return byteArray; + } + + public void setMachineLearningFrameProcessor(VisionImageProcessor processor) { + synchronized (processorLock) { + cleanScreen(); + if (frameProcessor != null) { + frameProcessor.stop(); + } + frameProcessor = processor; + } + } + + private void cleanScreen() { + graphicOverlay.clear(); + } + + public static class SizePair { + public final Size preview; + @Nullable + public final Size picture; + + SizePair(Camera.Size previewSize, @Nullable Camera.Size pictureSize) { + preview = new Size(previewSize.width, previewSize.height); + picture = pictureSize != null ? new Size(pictureSize.width, pictureSize.height) : null; + } + + public SizePair(Size previewSize, @Nullable Size pictureSize) { + preview = previewSize; + picture = pictureSize; + } + } + + private class CameraPreviewCallback implements Camera.PreviewCallback { + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + processingRunnable.setNextFrame(data, camera); + } + } + + private class FrameProcessingRunnable implements Runnable { + + private final Object lock = new Object(); + private boolean active = true; + + private ByteBuffer pendingFrameData; + + FrameProcessingRunnable() { + } + + void setActive(boolean active) { + synchronized (lock) { + this.active = active; + lock.notifyAll(); + } + } + + + @SuppressWarnings("ByteBufferBackingArray") + void setNextFrame(byte[] data, Camera camera) { + synchronized (lock) { + if (pendingFrameData != null) { + camera.addCallbackBuffer(pendingFrameData.array()); + pendingFrameData = null; + } + + if (!bytesToByteBuffer.containsKey(data)) { + Log.d( + TAG, + "Skipping frame. Could not find ByteBuffer associated with the image " + + "data from the camera."); + return; + } + + pendingFrameData = bytesToByteBuffer.get(data); + + lock.notifyAll(); + } + } + + @SuppressLint("InlinedApi") + @SuppressWarnings({"GuardedBy", "ByteBufferBackingArray"}) + @Override + public void run() { + ByteBuffer data; + + while (true) { + synchronized (lock) { + while (active && (pendingFrameData == null)) { + try { + + lock.wait(); + } catch (InterruptedException e) { + Log.d(TAG, "Frame processing loop terminated.", e); + return; + } + } + + if (!active) { + + return; + } + + data = pendingFrameData; + pendingFrameData = null; + } + + + try { + synchronized (processorLock) { + frameProcessor.processByteBuffer( + data, + new FrameMetadata.Builder() + .setWidth(previewSize.getWidth()) + .setHeight(previewSize.getHeight()) + .setRotation(rotationDegrees) + .build(), + graphicOverlay); + } + } catch (Exception t) { + Log.e(TAG, "Exception thrown from receiver.", t); + } finally { + camera.addCallbackBuffer(data.array()); + } + } + } + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/Utils/CameraSourcePreview.java b/app/src/main/java/com/modarb/android/posedetection/Utils/CameraSourcePreview.java new file mode 100644 index 0000000..173a24b --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/Utils/CameraSourcePreview.java @@ -0,0 +1,181 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.modarb.android.posedetection.Utils; + +import android.content.Context; +import android.content.res.Configuration; +import android.util.AttributeSet; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.ViewGroup; + +import com.google.android.gms.common.images.Size; +import com.modarb.android.posedetection.GraphicOverlay; + +import java.io.IOException; + +/** + * Preview the camera image in the screen. + */ +public class CameraSourcePreview extends ViewGroup { + private static final String TAG = "MIDemoApp:Preview"; + + private final Context context; + private final SurfaceView surfaceView; + private boolean startRequested; + private boolean surfaceAvailable; + private CameraSource cameraSource; + + private GraphicOverlay overlay; + + public CameraSourcePreview(Context context, AttributeSet attrs) { + super(context, attrs); + this.context = context; + startRequested = false; + surfaceAvailable = false; + + surfaceView = new SurfaceView(context); + surfaceView.getHolder().addCallback(new SurfaceCallback()); + addView(surfaceView); + } + + private void start(CameraSource cameraSource) throws IOException { + this.cameraSource = cameraSource; + + if (this.cameraSource != null) { + startRequested = true; + startIfReady(); + } + } + + public void start(CameraSource cameraSource, GraphicOverlay overlay) throws IOException { + this.overlay = overlay; + start(cameraSource); + } + + public void stop() { + if (cameraSource != null) { + cameraSource.stop(); + } + } + + public void release() { + if (cameraSource != null) { + cameraSource.release(); + cameraSource = null; + } + surfaceView.getHolder().getSurface().release(); + } + + private void startIfReady() throws IOException, SecurityException { + if (startRequested && surfaceAvailable) { + if (PreferenceUtils.isCameraLiveViewportEnabled(context)) { + cameraSource.start(surfaceView.getHolder()); + } else { + cameraSource.start(); + } + requestLayout(); + + if (overlay != null) { + Size size = cameraSource.getPreviewSize(); + int min = Math.min(size.getWidth(), size.getHeight()); + int max = Math.max(size.getWidth(), size.getHeight()); + boolean isImageFlipped = cameraSource.getCameraFacing() == CameraSource.CAMERA_FACING_FRONT; + if (isPortraitMode()) { + // Swap width and height sizes when in portrait, since it will be rotated by 90 degrees. + // The camera preview and the image being processed have the same size. + overlay.setImageSourceInfo(min, max, isImageFlipped); + } else { + overlay.setImageSourceInfo(max, min, isImageFlipped); + } + overlay.clear(); + } + startRequested = false; + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int width = 320; + int height = 240; + if (cameraSource != null) { + Size size = cameraSource.getPreviewSize(); + if (size != null) { + width = size.getWidth(); + height = size.getHeight(); + } + } + + // Swap width and height sizes when in portrait, since it will be rotated 90 degrees + if (isPortraitMode()) { + int tmp = width; + width = height; + height = tmp; + } + + float previewAspectRatio = (float) width / height; + int layoutWidth = right - left; + int layoutHeight = bottom - top; + float layoutAspectRatio = (float) layoutWidth / layoutHeight; + if (previewAspectRatio > layoutAspectRatio) { + // The preview input is wider than the layout area. Fit the layout height and crop + // the preview input horizontally while keep the center. + int horizontalOffset = (int) (previewAspectRatio * layoutHeight - layoutWidth) / 2; + surfaceView.layout(-horizontalOffset, 0, layoutWidth + horizontalOffset, layoutHeight); + } else { + // The preview input is taller than the layout area. Fit the layout width and crop the preview + // input vertically while keep the center. + int verticalOffset = (int) (layoutWidth / previewAspectRatio - layoutHeight) / 2; + surfaceView.layout(0, -verticalOffset, layoutWidth, layoutHeight + verticalOffset); + } + } + + private boolean isPortraitMode() { + int orientation = context.getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + return false; + } + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + return true; + } + + Log.d(TAG, "isPortraitMode returning false by default"); + return false; + } + + private class SurfaceCallback implements SurfaceHolder.Callback { + @Override + public void surfaceCreated(SurfaceHolder surface) { + surfaceAvailable = true; + try { + startIfReady(); + } catch (IOException e) { + Log.e(TAG, "Could not start camera source.", e); + } + } + + @Override + public void surfaceDestroyed(SurfaceHolder surface) { + surfaceAvailable = false; + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + } + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/Utils/PermissionResultCallback.kt b/app/src/main/java/com/modarb/android/posedetection/Utils/PermissionResultCallback.kt new file mode 100644 index 0000000..9f6282a --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/Utils/PermissionResultCallback.kt @@ -0,0 +1,5 @@ +package com.modarb.android.posedetection.Utils + +interface PermissionResultCallback { + fun onPermissionGranted() +} diff --git a/app/src/main/java/com/modarb/android/posedetection/Utils/PreferenceUtils.java b/app/src/main/java/com/modarb/android/posedetection/Utils/PreferenceUtils.java new file mode 100644 index 0000000..93e61ee --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/Utils/PreferenceUtils.java @@ -0,0 +1,134 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.modarb.android.posedetection.Utils; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import com.google.android.gms.common.images.Size; +import com.google.common.base.Preconditions; +import com.google.mlkit.vision.pose.PoseDetectorOptionsBase; +import com.google.mlkit.vision.pose.accurate.AccuratePoseDetectorOptions; +import com.google.mlkit.vision.pose.defaults.PoseDetectorOptions; +import com.modarb.android.R; + +public class PreferenceUtils { + + private static final int POSE_DETECTOR_PERFORMANCE_MODE_FAST = 1; + + private PreferenceUtils() { + } + + public static void saveString(Context context, @StringRes int prefKeyId, @Nullable String value) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putString(context.getString(prefKeyId), value) + .apply(); + } + + @Nullable + public static CameraSource.SizePair getCameraPreviewSizePair(Context context, int cameraId) { + Preconditions.checkArgument( + cameraId == CameraSource.CAMERA_FACING_BACK + || cameraId == CameraSource.CAMERA_FACING_FRONT); + String previewSizePrefKey; + String pictureSizePrefKey; + if (cameraId == CameraSource.CAMERA_FACING_BACK) { + previewSizePrefKey = context.getString(R.string.pref_key_rear_camera_preview_size); + pictureSizePrefKey = context.getString(R.string.pref_key_rear_camera_picture_size); + } else { + previewSizePrefKey = context.getString(R.string.pref_key_front_camera_preview_size); + pictureSizePrefKey = context.getString(R.string.pref_key_front_camera_picture_size); + } + + try { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + return new CameraSource.SizePair( + Size.parseSize(sharedPreferences.getString(previewSizePrefKey, null)), + Size.parseSize(sharedPreferences.getString(pictureSizePrefKey, null))); + } catch (Exception e) { + return null; + } + } + + public static PoseDetectorOptionsBase getPoseDetectorOptionsForLivePreview(Context context) { + int performanceMode = + getModeTypePreferenceValue( + context, + R.string.pref_key_live_preview_pose_detection_performance_mode, + POSE_DETECTOR_PERFORMANCE_MODE_FAST); + boolean preferGPU = preferGPUForPoseDetection(context); + if (performanceMode == POSE_DETECTOR_PERFORMANCE_MODE_FAST) { + PoseDetectorOptions.Builder builder = + new PoseDetectorOptions.Builder().setDetectorMode(PoseDetectorOptions.STREAM_MODE); + if (preferGPU) { + builder.setPreferredHardwareConfigs(PoseDetectorOptions.CPU_GPU); + } + return builder.build(); + } else { + AccuratePoseDetectorOptions.Builder builder = + new AccuratePoseDetectorOptions.Builder() + .setDetectorMode(AccuratePoseDetectorOptions.STREAM_MODE); + if (preferGPU) { + builder.setPreferredHardwareConfigs(AccuratePoseDetectorOptions.CPU_GPU); + } + return builder.build(); + } + } + + public static boolean preferGPUForPoseDetection(Context context) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + String prefKey = context.getString(R.string.pref_key_pose_detector_prefer_gpu); + return sharedPreferences.getBoolean(prefKey, true); + } + + public static boolean shouldShowPoseDetectionInFrameLikelihoodLivePreview(Context context) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + String prefKey = + context.getString(R.string.pref_key_live_preview_pose_detector_show_in_frame_likelihood); + return sharedPreferences.getBoolean(prefKey, true); + } + + public static boolean shouldPoseDetectionVisualizeZ(Context context) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + String prefKey = context.getString(R.string.pref_key_pose_detector_visualize_z); + return sharedPreferences.getBoolean(prefKey, true); + } + + public static boolean shouldPoseDetectionRescaleZForVisualization(Context context) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + String prefKey = context.getString(R.string.pref_key_pose_detector_rescale_z); + return sharedPreferences.getBoolean(prefKey, true); + } + + private static int getModeTypePreferenceValue( + Context context, @StringRes int prefKeyResId, int defaultValue) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + String prefKey = context.getString(prefKeyResId); + return Integer.parseInt(sharedPreferences.getString(prefKey, String.valueOf(defaultValue))); + } + + public static boolean isCameraLiveViewportEnabled(Context context) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + String prefKey = context.getString(R.string.pref_key_camera_live_viewport); + return sharedPreferences.getBoolean(prefKey, false); + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/Utils/ScopedExecutor.java b/app/src/main/java/com/modarb/android/posedetection/Utils/ScopedExecutor.java new file mode 100644 index 0000000..561c2a1 --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/Utils/ScopedExecutor.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.modarb.android.posedetection.Utils; + +import androidx.annotation.NonNull; + +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; + + +public class ScopedExecutor implements Executor { + + private final Executor executor; + private final AtomicBoolean shutdown = new AtomicBoolean(); + + public ScopedExecutor(@NonNull Executor executor) { + this.executor = executor; + } + + @Override + public void execute(@NonNull Runnable command) { + if (shutdown.get()) { + return; + } + executor.execute( + () -> { + + if (shutdown.get()) { + return; + } + command.run(); + }); + } + + + public void shutdown() { + shutdown.set(true); + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/VisionImageProcessor.java b/app/src/main/java/com/modarb/android/posedetection/VisionImageProcessor.java new file mode 100644 index 0000000..45e0259 --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/VisionImageProcessor.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.modarb.android.posedetection; + +import android.graphics.Bitmap; + +import androidx.camera.core.ImageProxy; + +import com.google.mlkit.common.MlKitException; + +import java.nio.ByteBuffer; + +public interface VisionImageProcessor { + + void processBitmap(Bitmap bitmap, GraphicOverlay graphicOverlay); + + void processByteBuffer( + ByteBuffer data, FrameMetadata frameMetadata, GraphicOverlay graphicOverlay) + throws MlKitException; + + void processImageProxy(ImageProxy image, GraphicOverlay graphicOverlay) throws MlKitException; + + void stop(); +} diff --git a/app/src/main/java/com/modarb/android/posedetection/VisionProcessorBase.kt b/app/src/main/java/com/modarb/android/posedetection/VisionProcessorBase.kt new file mode 100644 index 0000000..bcb1bb6 --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/VisionProcessorBase.kt @@ -0,0 +1,348 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.mlkit.vision.demo.kotlin + +import android.app.ActivityManager +import android.content.Context +import android.graphics.Bitmap +import android.os.Build.VERSION_CODES +import android.os.SystemClock +import android.util.Log +import android.widget.Toast +import androidx.annotation.GuardedBy +import androidx.annotation.RequiresApi +import androidx.camera.core.ExperimentalGetImage +import androidx.camera.core.ImageProxy +import com.google.android.gms.tasks.OnFailureListener +import com.google.android.gms.tasks.OnSuccessListener +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.TaskExecutors +import com.google.android.gms.tasks.Tasks +import com.google.android.odml.image.BitmapMlImageBuilder +import com.google.android.odml.image.ByteBufferMlImageBuilder +import com.google.android.odml.image.MediaMlImageBuilder +import com.google.android.odml.image.MlImage +import com.google.mlkit.common.MlKitException +import com.google.mlkit.vision.common.InputImage + +import com.modarb.android.posedetection.FrameMetadata +import com.modarb.android.posedetection.GraphicOverlay +import com.modarb.android.posedetection.Utils.BitmapUtils +import com.modarb.android.posedetection.Utils.CameraImageGraphic +import com.modarb.android.posedetection.Utils.PreferenceUtils +import com.modarb.android.posedetection.Utils.ScopedExecutor +import com.modarb.android.posedetection.VisionImageProcessor +import java.lang.Math.max +import java.lang.Math.min +import java.nio.ByteBuffer +import java.util.Timer +import java.util.TimerTask + + +abstract class VisionProcessorBase(context: Context) : VisionImageProcessor { + + companion object { + const val MANUAL_TESTING_LOG = "LogTagForTest" + private const val TAG = "VisionProcessorBase" + } + + private var activityManager: ActivityManager = + context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + private val fpsTimer = Timer() + private val executor = ScopedExecutor(TaskExecutors.MAIN_THREAD) + + private var isShutdown = false + + private var numRuns = 0 + private var totalFrameMs = 0L + private var maxFrameMs = 0L + private var minFrameMs = Long.MAX_VALUE + private var totalDetectorMs = 0L + private var maxDetectorMs = 0L + private var minDetectorMs = Long.MAX_VALUE + + private var frameProcessedInOneSecondInterval = 0 + private var framesPerSecond = 0 + + @GuardedBy("this") + private var latestImage: ByteBuffer? = null + + @GuardedBy("this") + private var latestImageMetaData: FrameMetadata? = null + + @GuardedBy("this") + private var processingImage: ByteBuffer? = null + + @GuardedBy("this") + private var processingMetaData: FrameMetadata? = null + + init { + fpsTimer.scheduleAtFixedRate( + object : TimerTask() { + override fun run() { + framesPerSecond = frameProcessedInOneSecondInterval + frameProcessedInOneSecondInterval = 0 + } + }, 0, 1000 + ) + } + + override fun processBitmap(bitmap: Bitmap?, graphicOverlay: GraphicOverlay) { + val frameStartMs = SystemClock.elapsedRealtime() + + if (isMlImageEnabled(graphicOverlay.context)) { + val mlImage = BitmapMlImageBuilder(bitmap!!).build() + requestDetectInImage( + mlImage, graphicOverlay, null, false, frameStartMs + ) + mlImage.close() + return + } + + requestDetectInImage( + InputImage.fromBitmap(bitmap!!, 0), graphicOverlay, null, false, frameStartMs + ) + } + + @Synchronized + override fun processByteBuffer( + data: ByteBuffer?, frameMetadata: FrameMetadata?, graphicOverlay: GraphicOverlay + ) { + latestImage = data + latestImageMetaData = frameMetadata + if (processingImage == null && processingMetaData == null) { + processLatestImage(graphicOverlay) + } + } + + @Synchronized + private fun processLatestImage(graphicOverlay: GraphicOverlay) { + processingImage = latestImage + processingMetaData = latestImageMetaData + latestImage = null + latestImageMetaData = null + if (processingImage != null && processingMetaData != null && !isShutdown) { + processImage(processingImage!!, processingMetaData!!, graphicOverlay) + } + } + + private fun processImage( + data: ByteBuffer, frameMetadata: FrameMetadata, graphicOverlay: GraphicOverlay + ) { + val frameStartMs = SystemClock.elapsedRealtime() + + val bitmap = if (PreferenceUtils.isCameraLiveViewportEnabled(graphicOverlay.context)) null + else BitmapUtils.getBitmap(data, frameMetadata) + + if (isMlImageEnabled(graphicOverlay.context)) { + val mlImage = ByteBufferMlImageBuilder( + data, frameMetadata.width, frameMetadata.height, MlImage.IMAGE_FORMAT_NV21 + ).setRotation(frameMetadata.rotation).build() + requestDetectInImage( + mlImage, graphicOverlay, bitmap, true, frameStartMs + ).addOnSuccessListener(executor) { processLatestImage(graphicOverlay) } + + mlImage.close() + return + } + + requestDetectInImage( + InputImage.fromByteBuffer( + data, + frameMetadata.width, + frameMetadata.height, + frameMetadata.rotation, + InputImage.IMAGE_FORMAT_NV21 + ), graphicOverlay, bitmap,/* shouldShowFps= */ true, frameStartMs + ).addOnSuccessListener(executor) { processLatestImage(graphicOverlay) } + } + + @RequiresApi(VERSION_CODES.LOLLIPOP) + @ExperimentalGetImage + override fun processImageProxy(image: ImageProxy, graphicOverlay: GraphicOverlay) { + val frameStartMs = SystemClock.elapsedRealtime() + if (isShutdown) { + return + } + var bitmap: Bitmap? = null + if (!PreferenceUtils.isCameraLiveViewportEnabled(graphicOverlay.context)) { + bitmap = BitmapUtils.getBitmap(image) + } + + if (isMlImageEnabled(graphicOverlay.context)) { + val mlImage = + MediaMlImageBuilder(image.image!!).setRotation(image.imageInfo.rotationDegrees) + .build() + requestDetectInImage( + mlImage, graphicOverlay,/* originalCameraImage= */ + bitmap,/* shouldShowFps= */ + true, frameStartMs + ) + // When the image is from CameraX analysis use case, must call image.close() on received + // images when finished using them. Otherwise, new images may not be received or the camera + // may stall. + // Currently MlImage doesn't support ImageProxy directly, so we still need to call + // ImageProxy.close() here. + .addOnCompleteListener { image.close() } + + return + } + + requestDetectInImage( + InputImage.fromMediaImage(image.image!!, image.imageInfo.rotationDegrees), + graphicOverlay, + bitmap, + true, + frameStartMs + ) + + .addOnCompleteListener { image.close() } + } + + private fun requestDetectInImage( + image: InputImage, + graphicOverlay: GraphicOverlay, + originalCameraImage: Bitmap?, + shouldShowFps: Boolean, + frameStartMs: Long + ): Task { + return setUpListener( + detectInImage(image), graphicOverlay, originalCameraImage, shouldShowFps, frameStartMs + ) + } + + private fun requestDetectInImage( + image: MlImage, + graphicOverlay: GraphicOverlay, + originalCameraImage: Bitmap?, + shouldShowFps: Boolean, + frameStartMs: Long + ): Task { + return setUpListener( + detectInImage(image), graphicOverlay, originalCameraImage, shouldShowFps, frameStartMs + ) + } + + private fun setUpListener( + task: Task, + graphicOverlay: GraphicOverlay, + originalCameraImage: Bitmap?, + shouldShowFps: Boolean, + frameStartMs: Long + ): Task { + val detectorStartMs = SystemClock.elapsedRealtime() + return task.addOnSuccessListener(executor, OnSuccessListener { results: T -> + val endMs = SystemClock.elapsedRealtime() + val currentFrameLatencyMs = endMs - frameStartMs + val currentDetectorLatencyMs = endMs - detectorStartMs + if (numRuns >= 500) { + resetLatencyStats() + } + numRuns++ + frameProcessedInOneSecondInterval++ + totalFrameMs += currentFrameLatencyMs + maxFrameMs = max(currentFrameLatencyMs, maxFrameMs) + minFrameMs = min(currentFrameLatencyMs, minFrameMs) + totalDetectorMs += currentDetectorLatencyMs + maxDetectorMs = max(currentDetectorLatencyMs, maxDetectorMs) + minDetectorMs = min(currentDetectorLatencyMs, minDetectorMs) + + + if (frameProcessedInOneSecondInterval == 1) { + Log.d(TAG, "Num of Runs: $numRuns") + Log.d( + TAG, + "Frame latency: max=" + maxFrameMs + ", min=" + minFrameMs + ", avg=" + totalFrameMs / numRuns + ) + Log.d( + TAG, + "Detector latency: max=" + maxDetectorMs + ", min=" + minDetectorMs + ", avg=" + totalDetectorMs / numRuns + ) + val mi = ActivityManager.MemoryInfo() + activityManager.getMemoryInfo(mi) + val availableMegs: Long = mi.availMem / 0x100000L + Log.d(TAG, "Memory available in system: $availableMegs MB") + } + graphicOverlay.clear() + if (originalCameraImage != null) { + graphicOverlay.add(CameraImageGraphic(graphicOverlay, originalCameraImage)) + } + this@VisionProcessorBase.onSuccess(results, graphicOverlay) + // TODO fix that +// if (!PreferenceUtils.shouldHideDetectionInfo(graphicOverlay.context)) { +// graphicOverlay.add( +// InferenceInfoGraphic( +// graphicOverlay, +// currentFrameLatencyMs, +// currentDetectorLatencyMs, +// if (shouldShowFps) framesPerSecond else null +// ) +// ) +// } + graphicOverlay.postInvalidate() + }).addOnFailureListener(executor, OnFailureListener { e: Exception -> + graphicOverlay.clear() + graphicOverlay.postInvalidate() + val error = "Failed to process. Error: " + e.localizedMessage + Toast.makeText( + graphicOverlay.context, """ + $error + Cause: ${e.cause} + """.trimIndent(), Toast.LENGTH_SHORT + ).show() + Log.d(TAG, error) + e.printStackTrace() + this@VisionProcessorBase.onFailure(e) + }) + } + + override fun stop() { + executor.shutdown() + isShutdown = true + resetLatencyStats() + fpsTimer.cancel() + } + + private fun resetLatencyStats() { + numRuns = 0 + totalFrameMs = 0 + maxFrameMs = 0 + minFrameMs = Long.MAX_VALUE + totalDetectorMs = 0 + maxDetectorMs = 0 + minDetectorMs = Long.MAX_VALUE + } + + protected abstract fun detectInImage(image: InputImage): Task + + protected open fun detectInImage(image: MlImage): Task { + return Tasks.forException( + MlKitException( + "MlImage is currently not demonstrated for this feature", + MlKitException.INVALID_ARGUMENT + ) + ) + } + + protected abstract fun onSuccess(results: T, graphicOverlay: GraphicOverlay) + + protected abstract fun onFailure(e: Exception) + + protected open fun isMlImageEnabled(context: Context?): Boolean { + return false + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/classification/ClassificationResult.java b/app/src/main/java/com/modarb/android/posedetection/classification/ClassificationResult.java new file mode 100644 index 0000000..5839bea --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/classification/ClassificationResult.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.modarb.android.posedetection.classification; + +import static java.util.Collections.max; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + + +public class ClassificationResult { + + private final Map classConfidences; + + public ClassificationResult() { + classConfidences = new HashMap<>(); + } + + public Set getAllClasses() { + return classConfidences.keySet(); + } + + public float getClassConfidence(String className) { + return classConfidences.containsKey(className) ? classConfidences.get(className) : 0; + } + + public String getMaxConfidenceClass() { + return max( + classConfidences.entrySet(), + (entry1, entry2) -> (int) (entry1.getValue() - entry2.getValue())) + .getKey(); + } + + public void incrementClassConfidence(String className) { + classConfidences.put(className, + classConfidences.containsKey(className) ? classConfidences.get(className) + 1 : 1); + } + + public void putClassConfidence(String className, float confidence) { + classConfidences.put(className, confidence); + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/classification/EMASmoothing.java b/app/src/main/java/com/modarb/android/posedetection/classification/EMASmoothing.java new file mode 100644 index 0000000..52fca51 --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/classification/EMASmoothing.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.modarb.android.posedetection.classification; + +import android.os.SystemClock; + +import java.util.Deque; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.LinkedBlockingDeque; + + +public class EMASmoothing { + private static final int DEFAULT_WINDOW_SIZE = 10; + private static final float DEFAULT_ALPHA = 0.2f; + + private static final long RESET_THRESHOLD_MS = 100; + + private final int windowSize; + private final float alpha; + + private final Deque window; + + private long lastInputMs; + + public EMASmoothing() { + this(DEFAULT_WINDOW_SIZE, DEFAULT_ALPHA); + } + + public EMASmoothing(int windowSize, float alpha) { + this.windowSize = windowSize; + this.alpha = alpha; + this.window = new LinkedBlockingDeque<>(windowSize); + } + + public ClassificationResult getSmoothedResult(ClassificationResult classificationResult) { + long nowMs = SystemClock.elapsedRealtime(); + if (nowMs - lastInputMs > RESET_THRESHOLD_MS) { + window.clear(); + } + lastInputMs = nowMs; + + + if (window.size() == windowSize) { + window.pollLast(); + } + window.addFirst(classificationResult); + + Set allClasses = new HashSet<>(); + for (ClassificationResult result : window) { + allClasses.addAll(result.getAllClasses()); + } + + ClassificationResult smoothedResult = new ClassificationResult(); + + for (String className : allClasses) { + float factor = 1; + float topSum = 0; + float bottomSum = 0; + for (ClassificationResult result : window) { + float value = result.getClassConfidence(className); + + topSum += factor * value; + bottomSum += factor; + + factor = (float) (factor * (1.0 - alpha)); + } + smoothedResult.putClassConfidence(className, topSum / bottomSum); + } + + return smoothedResult; + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/classification/PoseClassifier.java b/app/src/main/java/com/modarb/android/posedetection/classification/PoseClassifier.java new file mode 100644 index 0000000..3a41336 --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/classification/PoseClassifier.java @@ -0,0 +1,134 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.modarb.android.posedetection.classification; + +import static com.modarb.android.posedetection.classification.PoseEmbedding.getPoseEmbedding; +import static com.modarb.android.posedetection.classification.Utils.maxAbs; +import static com.modarb.android.posedetection.classification.Utils.multiply; +import static com.modarb.android.posedetection.classification.Utils.multiplyAll; +import static com.modarb.android.posedetection.classification.Utils.subtract; +import static com.modarb.android.posedetection.classification.Utils.sumAbs; +import static java.lang.Math.max; +import static java.lang.Math.min; + +import android.util.Pair; + +import com.google.mlkit.vision.common.PointF3D; +import com.google.mlkit.vision.pose.Pose; +import com.google.mlkit.vision.pose.PoseLandmark; + +import java.util.ArrayList; +import java.util.List; +import java.util.PriorityQueue; + + +public class PoseClassifier { + private static final String TAG = "PoseClassifier"; + private static final int MAX_DISTANCE_TOP_K = 30; + private static final int MEAN_DISTANCE_TOP_K = 10; + private static final PointF3D AXES_WEIGHTS = PointF3D.from(1, 1, 0.2f); + + private final List poseSamples; + private final int maxDistanceTopK; + private final int meanDistanceTopK; + private final PointF3D axesWeights; + + public PoseClassifier(List poseSamples) { + this(poseSamples, MAX_DISTANCE_TOP_K, MEAN_DISTANCE_TOP_K, AXES_WEIGHTS); + } + + public PoseClassifier(List poseSamples, int maxDistanceTopK, int meanDistanceTopK, PointF3D axesWeights) { + this.poseSamples = poseSamples; + this.maxDistanceTopK = maxDistanceTopK; + this.meanDistanceTopK = meanDistanceTopK; + this.axesWeights = axesWeights; + } + + private static List extractPoseLandmarks(Pose pose) { + List landmarks = new ArrayList<>(); + for (PoseLandmark poseLandmark : pose.getAllPoseLandmarks()) { + landmarks.add(poseLandmark.getPosition3D()); + } + return landmarks; + } + + + public int confidenceRange() { + return min(maxDistanceTopK, meanDistanceTopK); + } + + public ClassificationResult classify(Pose pose) { + return classify(extractPoseLandmarks(pose)); + } + + public ClassificationResult classify(List landmarks) { + ClassificationResult result = new ClassificationResult(); + // Return early if no landmarks detected. + if (landmarks.isEmpty()) { + return result; + } + + List flippedLandmarks = new ArrayList<>(landmarks); + multiplyAll(flippedLandmarks, PointF3D.from(-1, 1, 1)); + + List embedding = getPoseEmbedding(landmarks); + List flippedEmbedding = getPoseEmbedding(flippedLandmarks); + + + PriorityQueue> maxDistances = new PriorityQueue<>(maxDistanceTopK, (o1, o2) -> -Float.compare(o1.second, o2.second)); + for (PoseSample poseSample : poseSamples) { + List sampleEmbedding = poseSample.getEmbedding(); + + float originalMax = 0; + float flippedMax = 0; + for (int i = 0; i < embedding.size(); i++) { + originalMax = max(originalMax, maxAbs(multiply(subtract(embedding.get(i), sampleEmbedding.get(i)), axesWeights))); + flippedMax = max(flippedMax, maxAbs(multiply(subtract(flippedEmbedding.get(i), sampleEmbedding.get(i)), axesWeights))); + } + maxDistances.add(new Pair<>(poseSample, min(originalMax, flippedMax))); + + if (maxDistances.size() > maxDistanceTopK) { + maxDistances.poll(); + } + } + + PriorityQueue> meanDistances = new PriorityQueue<>(meanDistanceTopK, (o1, o2) -> -Float.compare(o1.second, o2.second)); + for (Pair sampleDistances : maxDistances) { + PoseSample poseSample = sampleDistances.first; + List sampleEmbedding = poseSample.getEmbedding(); + + float originalSum = 0; + float flippedSum = 0; + for (int i = 0; i < embedding.size(); i++) { + originalSum += sumAbs(multiply(subtract(embedding.get(i), sampleEmbedding.get(i)), axesWeights)); + flippedSum += sumAbs(multiply(subtract(flippedEmbedding.get(i), sampleEmbedding.get(i)), axesWeights)); + } + float meanDistance = min(originalSum, flippedSum) / (embedding.size() * 2); + meanDistances.add(new Pair<>(poseSample, meanDistance)); + if (meanDistances.size() > meanDistanceTopK) { + meanDistances.poll(); + } + } + + for (Pair sampleDistances : meanDistances) { + String className = sampleDistances.first.getClassName(); + result.incrementClassConfidence(className); + } + + return result; + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/classification/PoseClassifierProcessor.java b/app/src/main/java/com/modarb/android/posedetection/classification/PoseClassifierProcessor.java new file mode 100644 index 0000000..ec2f576 --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/classification/PoseClassifierProcessor.java @@ -0,0 +1,110 @@ + + +package com.modarb.android.posedetection.classification; + +import android.content.Context; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.WorkerThread; + +import com.google.common.base.Preconditions; +import com.google.mlkit.vision.pose.Pose; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + + +public class PoseClassifierProcessor { + private static final String TAG = "PoseClassifierProcessor"; + private static final String POSE_SAMPLES_FILE = "pose/fitness_pose_samples.csv"; + + + private static final String PUSHUPS_CLASS = "pushups_down"; + private static final String[] POSE_CLASSES = {PUSHUPS_CLASS}; + + private final boolean isStreamMode; + + private EMASmoothing emaSmoothing; + private List repCounters; + private PoseClassifier poseClassifier; + private String lastRepResult; + + @WorkerThread + public PoseClassifierProcessor(Context context, boolean isStreamMode) { + Preconditions.checkState(Looper.myLooper() != Looper.getMainLooper()); + this.isStreamMode = isStreamMode; + if (isStreamMode) { + emaSmoothing = new EMASmoothing(); + repCounters = new ArrayList<>(); + lastRepResult = ""; + } + loadPoseSamples(context); + } + + private void loadPoseSamples(Context context) { + List poseSamples = new ArrayList<>(); + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(context.getAssets().open(POSE_SAMPLES_FILE))); + String csvLine = reader.readLine(); + while (csvLine != null) { + PoseSample poseSample = PoseSample.getPoseSample(csvLine, ","); + if (poseSample != null) { + poseSamples.add(poseSample); + } + csvLine = reader.readLine(); + } + } catch (IOException e) { + Log.e(TAG, "Error when loading pose samples.\n" + e); + } + poseClassifier = new PoseClassifier(poseSamples); + if (isStreamMode) { + for (String className : POSE_CLASSES) { + repCounters.add(new RepetitionCounter(className)); + } + } + } + + + @WorkerThread + public List getPoseResult(Pose pose) { + Preconditions.checkState(Looper.myLooper() != Looper.getMainLooper()); + List result = new ArrayList<>(); + ClassificationResult classification = poseClassifier.classify(pose); + + if (isStreamMode) { + classification = emaSmoothing.getSmoothedResult(classification); + + if (pose.getAllPoseLandmarks().isEmpty()) { + result.add(lastRepResult); + return result; + } + +// for (RepetitionCounter repCounter : repCounters) { +// int repsBefore = repCounter.getNumRepeats(); +// int repsAfter = repCounter.addClassificationResult(classification); +// if (repsAfter > repsBefore) { +//// ToneGenerator tg = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100); +//// tg.startTone(ToneGenerator.TONE_PROP_BEEP); +//// lastRepResult = String.format(Locale.US, "%s : %d reps", repCounter.getClassName(), repsAfter); +// break; +// } +// } + //result.add(lastRepResult); + } + + if (!pose.getAllPoseLandmarks().isEmpty()) { + String maxConfidenceClass = classification.getMaxConfidenceClass(); + if (maxConfidenceClass.contains("push")) { + String maxConfidenceClassResult = String.format(Locale.US, "%s : %.2f confidence", maxConfidenceClass, classification.getClassConfidence(maxConfidenceClass) / poseClassifier.confidenceRange()); + result.add(maxConfidenceClassResult); + } + } + return result; + } + +} diff --git a/app/src/main/java/com/modarb/android/posedetection/classification/PoseEmbedding.java b/app/src/main/java/com/modarb/android/posedetection/classification/PoseEmbedding.java new file mode 100644 index 0000000..697bcf0 --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/classification/PoseEmbedding.java @@ -0,0 +1,131 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.modarb.android.posedetection.classification; + + +import static com.modarb.android.posedetection.classification.Utils.average; +import static com.modarb.android.posedetection.classification.Utils.l2Norm2D; +import static com.modarb.android.posedetection.classification.Utils.multiplyAll; +import static com.modarb.android.posedetection.classification.Utils.subtract; +import static com.modarb.android.posedetection.classification.Utils.subtractAll; + +import com.google.mlkit.vision.common.PointF3D; +import com.google.mlkit.vision.pose.PoseLandmark; + +import java.util.ArrayList; +import java.util.List; + + +public class PoseEmbedding { + private static final float TORSO_MULTIPLIER = 2.5f; + + private PoseEmbedding() { + } + + public static List getPoseEmbedding(List landmarks) { + List normalizedLandmarks = normalize(landmarks); + return getEmbedding(normalizedLandmarks); + } + + private static List normalize(List landmarks) { + List normalizedLandmarks = new ArrayList<>(landmarks); + PointF3D center = average( + landmarks.get(PoseLandmark.LEFT_HIP), landmarks.get(PoseLandmark.RIGHT_HIP)); + subtractAll(center, normalizedLandmarks); + + multiplyAll(normalizedLandmarks, 1 / getPoseSize(normalizedLandmarks)); + multiplyAll(normalizedLandmarks, 100); + return normalizedLandmarks; + } + + private static float getPoseSize(List landmarks) { + + PointF3D hipsCenter = average( + landmarks.get(PoseLandmark.LEFT_HIP), landmarks.get(PoseLandmark.RIGHT_HIP)); + + PointF3D shouldersCenter = average( + landmarks.get(PoseLandmark.LEFT_SHOULDER), + landmarks.get(PoseLandmark.RIGHT_SHOULDER)); + + float torsoSize = l2Norm2D(subtract(hipsCenter, shouldersCenter)); + + float maxDistance = torsoSize * TORSO_MULTIPLIER; + + for (PointF3D landmark : landmarks) { + float distance = l2Norm2D(subtract(hipsCenter, landmark)); + if (distance > maxDistance) { + maxDistance = distance; + } + } + return maxDistance; + } + + private static List getEmbedding(List lm) { + List embedding = new ArrayList<>(); + + + embedding.add(subtract( + average(lm.get(PoseLandmark.LEFT_HIP), lm.get(PoseLandmark.RIGHT_HIP)), + average(lm.get(PoseLandmark.LEFT_SHOULDER), lm.get(PoseLandmark.RIGHT_SHOULDER)) + )); + + embedding.add(subtract( + lm.get(PoseLandmark.LEFT_SHOULDER), lm.get(PoseLandmark.LEFT_ELBOW))); + embedding.add(subtract( + lm.get(PoseLandmark.RIGHT_SHOULDER), lm.get(PoseLandmark.RIGHT_ELBOW))); + + embedding.add(subtract(lm.get(PoseLandmark.LEFT_ELBOW), lm.get(PoseLandmark.LEFT_WRIST))); + embedding.add(subtract(lm.get(PoseLandmark.RIGHT_ELBOW), lm.get(PoseLandmark.RIGHT_WRIST))); + + embedding.add(subtract(lm.get(PoseLandmark.LEFT_HIP), lm.get(PoseLandmark.LEFT_KNEE))); + embedding.add(subtract(lm.get(PoseLandmark.RIGHT_HIP), lm.get(PoseLandmark.RIGHT_KNEE))); + + embedding.add(subtract(lm.get(PoseLandmark.LEFT_KNEE), lm.get(PoseLandmark.LEFT_ANKLE))); + embedding.add(subtract(lm.get(PoseLandmark.RIGHT_KNEE), lm.get(PoseLandmark.RIGHT_ANKLE))); + + // Two joints. + embedding.add(subtract( + lm.get(PoseLandmark.LEFT_SHOULDER), lm.get(PoseLandmark.LEFT_WRIST))); + embedding.add(subtract( + lm.get(PoseLandmark.RIGHT_SHOULDER), lm.get(PoseLandmark.RIGHT_WRIST))); + + embedding.add(subtract(lm.get(PoseLandmark.LEFT_HIP), lm.get(PoseLandmark.LEFT_ANKLE))); + embedding.add(subtract(lm.get(PoseLandmark.RIGHT_HIP), lm.get(PoseLandmark.RIGHT_ANKLE))); + + // Four joints. + embedding.add(subtract(lm.get(PoseLandmark.LEFT_HIP), lm.get(PoseLandmark.LEFT_WRIST))); + embedding.add(subtract(lm.get(PoseLandmark.RIGHT_HIP), lm.get(PoseLandmark.RIGHT_WRIST))); + + // Five joints. + embedding.add(subtract( + lm.get(PoseLandmark.LEFT_SHOULDER), lm.get(PoseLandmark.LEFT_ANKLE))); + embedding.add(subtract( + lm.get(PoseLandmark.RIGHT_SHOULDER), lm.get(PoseLandmark.RIGHT_ANKLE))); + + embedding.add(subtract(lm.get(PoseLandmark.LEFT_HIP), lm.get(PoseLandmark.LEFT_WRIST))); + embedding.add(subtract(lm.get(PoseLandmark.RIGHT_HIP), lm.get(PoseLandmark.RIGHT_WRIST))); + + // Cross body. + embedding.add(subtract(lm.get(PoseLandmark.LEFT_ELBOW), lm.get(PoseLandmark.RIGHT_ELBOW))); + embedding.add(subtract(lm.get(PoseLandmark.LEFT_KNEE), lm.get(PoseLandmark.RIGHT_KNEE))); + + embedding.add(subtract(lm.get(PoseLandmark.LEFT_WRIST), lm.get(PoseLandmark.RIGHT_WRIST))); + embedding.add(subtract(lm.get(PoseLandmark.LEFT_ANKLE), lm.get(PoseLandmark.RIGHT_ANKLE))); + + return embedding; + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/classification/PoseSample.java b/app/src/main/java/com/modarb/android/posedetection/classification/PoseSample.java new file mode 100644 index 0000000..420986f --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/classification/PoseSample.java @@ -0,0 +1,80 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.modarb.android.posedetection.classification; + +import android.util.Log; + +import com.google.common.base.Splitter; +import com.google.mlkit.vision.common.PointF3D; + +import java.util.ArrayList; +import java.util.List; + + +public class PoseSample { + private static final String TAG = "PoseSample"; + private static final int NUM_LANDMARKS = 33; + private static final int NUM_DIMS = 3; + + private final String name; + private final String className; + private final List embedding; + + public PoseSample(String name, String className, List landmarks) { + this.name = name; + this.className = className; + this.embedding = PoseEmbedding.getPoseEmbedding(landmarks); + } + + public static PoseSample getPoseSample(String csvLine, String separator) { + List tokens = Splitter.onPattern(separator).splitToList(csvLine); + + if (tokens.size() != (NUM_LANDMARKS * NUM_DIMS) + 2) { + Log.e(TAG, "Invalid number of tokens for PoseSample"); + return null; + } + String name = tokens.get(0); + String className = tokens.get(1); + List landmarks = new ArrayList<>(); + + for (int i = 2; i < tokens.size(); i += NUM_DIMS) { + try { + landmarks.add( + PointF3D.from( + Float.parseFloat(tokens.get(i)), + Float.parseFloat(tokens.get(i + 1)), + Float.parseFloat(tokens.get(i + 2)))); + } catch (NullPointerException | NumberFormatException e) { + Log.e(TAG, "Invalid value " + tokens.get(i) + " for landmark position."); + return null; + } + } + return new PoseSample(name, className, landmarks); + } + + public String getName() { + return name; + } + + public String getClassName() { + return className; + } + + public List getEmbedding() { + return embedding; + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/classification/RepetitionCounter.java b/app/src/main/java/com/modarb/android/posedetection/classification/RepetitionCounter.java new file mode 100644 index 0000000..186cb0c --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/classification/RepetitionCounter.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.modarb.android.posedetection.classification; + + +public class RepetitionCounter { + + private static final float DEFAULT_ENTER_THRESHOLD = 6f; + private static final float DEFAULT_EXIT_THRESHOLD = 4f; + + private final String className; + private final float enterThreshold; + private final float exitThreshold; + + private int numRepeats; + private boolean poseEntered; + + public RepetitionCounter(String className) { + this(className, DEFAULT_ENTER_THRESHOLD, DEFAULT_EXIT_THRESHOLD); + } + + public RepetitionCounter(String className, float enterThreshold, float exitThreshold) { + this.className = className; + this.enterThreshold = enterThreshold; + this.exitThreshold = exitThreshold; + numRepeats = 0; + poseEntered = false; + } + + + public int addClassificationResult(ClassificationResult classificationResult) { + float poseConfidence = classificationResult.getClassConfidence(className); + + if (!poseEntered) { + poseEntered = poseConfidence > enterThreshold; + return numRepeats; + } + + if (poseConfidence < exitThreshold) { + numRepeats++; + poseEntered = false; + } + + return numRepeats; + } + + public String getClassName() { + return className; + } + + public int getNumRepeats() { + return numRepeats; + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/classification/Utils.java b/app/src/main/java/com/modarb/android/posedetection/classification/Utils.java new file mode 100644 index 0000000..00ca0c5 --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/classification/Utils.java @@ -0,0 +1,91 @@ +/* + * Copyright 2020 Google LLC. 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.modarb.android.posedetection.classification; + +import static com.google.common.primitives.Floats.max; + +import com.google.mlkit.vision.common.PointF3D; + +import java.util.List; +import java.util.ListIterator; + +public class Utils { + private Utils() { + } + + public static PointF3D add(PointF3D a, PointF3D b) { + return PointF3D.from(a.getX() + b.getX(), a.getY() + b.getY(), a.getZ() + b.getZ()); + } + + public static PointF3D subtract(PointF3D b, PointF3D a) { + return PointF3D.from(a.getX() - b.getX(), a.getY() - b.getY(), a.getZ() - b.getZ()); + } + + public static PointF3D multiply(PointF3D a, float multiple) { + return PointF3D.from(a.getX() * multiple, a.getY() * multiple, a.getZ() * multiple); + } + + public static PointF3D multiply(PointF3D a, PointF3D multiple) { + return PointF3D.from( + a.getX() * multiple.getX(), a.getY() * multiple.getY(), a.getZ() * multiple.getZ()); + } + + public static PointF3D average(PointF3D a, PointF3D b) { + return PointF3D.from( + (a.getX() + b.getX()) * 0.5f, (a.getY() + b.getY()) * 0.5f, (a.getZ() + b.getZ()) * 0.5f); + } + + public static float l2Norm2D(PointF3D point) { + return (float) Math.hypot(point.getX(), point.getY()); + } + + public static float maxAbs(PointF3D point) { + return max(Math.abs(point.getX()), Math.abs(point.getY()), Math.abs(point.getZ())); + } + + public static float sumAbs(PointF3D point) { + return Math.abs(point.getX()) + Math.abs(point.getY()) + Math.abs(point.getZ()); + } + + public static void addAll(List pointsList, PointF3D p) { + ListIterator iterator = pointsList.listIterator(); + while (iterator.hasNext()) { + iterator.set(add(iterator.next(), p)); + } + } + + public static void subtractAll(PointF3D p, List pointsList) { + ListIterator iterator = pointsList.listIterator(); + while (iterator.hasNext()) { + iterator.set(subtract(p, iterator.next())); + } + } + + public static void multiplyAll(List pointsList, float multiple) { + ListIterator iterator = pointsList.listIterator(); + while (iterator.hasNext()) { + iterator.set(multiply(iterator.next(), multiple)); + } + } + + public static void multiplyAll(List pointsList, PointF3D multiple) { + ListIterator iterator = pointsList.listIterator(); + while (iterator.hasNext()) { + iterator.set(multiply(iterator.next(), multiple)); + } + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/posedetector/PoseDetectorProcessor.kt b/app/src/main/java/com/modarb/android/posedetection/posedetector/PoseDetectorProcessor.kt new file mode 100644 index 0000000..3dd090d --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/posedetector/PoseDetectorProcessor.kt @@ -0,0 +1,361 @@ +package com.modarb.android.posedetection.posedetector + +import android.content.Context +import android.os.Build +import android.speech.tts.TextToSpeech +import android.util.Log +import androidx.annotation.RequiresApi +import com.google.android.gms.tasks.Task +import com.google.android.odml.image.MlImage +import com.google.mlkit.vision.common.InputImage +import com.google.mlkit.vision.demo.kotlin.VisionProcessorBase +import com.google.mlkit.vision.pose.Pose +import com.google.mlkit.vision.pose.PoseDetection +import com.google.mlkit.vision.pose.PoseDetector +import com.google.mlkit.vision.pose.PoseDetectorOptionsBase +import com.google.mlkit.vision.pose.PoseLandmark +import com.modarb.android.posedetection.GraphicOverlay +import com.modarb.android.posedetection.classification.PoseClassifierProcessor +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import java.util.Locale +import java.util.concurrent.Executor +import java.util.concurrent.Executors +import kotlin.coroutines.CoroutineContext +import kotlin.math.atan2 + +class PoseDetectorProcessor( + private val context: Context, + options: PoseDetectorOptionsBase, + private val showInFrameLikelihood: Boolean, + private val visualizeZ: Boolean, + private val rescaleZForVisualization: Boolean, + private val runClassification: Boolean, + private val isStreamMode: Boolean +) : VisionProcessorBase(context), CoroutineScope { + + private val detector: PoseDetector + private val classificationExecutor: Executor + + private var poseClassifierProcessor: PoseClassifierProcessor? = null + + class PoseWithClassification(val pose: Pose, val classificationResult: List) + + init { + detector = PoseDetection.getClient(options) + classificationExecutor = Executors.newSingleThreadExecutor() + } + + override fun stop() { + super.stop() + detector.close() + stopPushUpFormCheck() + } + + override fun detectInImage(image: InputImage): Task { + return detector.process(image).continueWith(classificationExecutor) { task -> + val pose = task.getResult() + var classificationResult: List = ArrayList() + if (runClassification) { + if (poseClassifierProcessor == null) { + poseClassifierProcessor = PoseClassifierProcessor(context, isStreamMode) + } + classificationResult = poseClassifierProcessor!!.getPoseResult(pose) + } + PoseWithClassification(pose, classificationResult) + } + } + + + override fun detectInImage(image: MlImage): Task { + return detector.process(image).continueWith( + classificationExecutor + ) { task -> + val pose = task.getResult() + var classificationResult: List = ArrayList() + if (runClassification) { + if (poseClassifierProcessor == null) { + poseClassifierProcessor = PoseClassifierProcessor(context, isStreamMode) + } + classificationResult = poseClassifierProcessor!!.getPoseResult(pose) + } + PoseWithClassification(pose, classificationResult) + } + } + + private var pushUpCheckJob: Job? = null + + override fun onSuccess( + results: PoseWithClassification, graphicOverlay: GraphicOverlay + ) { + graphicOverlay.add( + PoseGraphic( + graphicOverlay, + results.pose, + showInFrameLikelihood, + visualizeZ, + rescaleZForVisualization, + results.classificationResult + ) + ) + Log.d("Result", results.classificationResult.toString()) + + val containsPushup = results.classificationResult.any { "pushup" in it } + + if (containsPushup) { + startPushUpFormCheck(results) + } else { + stopPushUpFormCheck() + } + } + + private val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable -> + println("Coroutine exception occurred: ${throwable.message}") + } + + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + coroutineExceptionHandler + + private fun startPushUpFormCheck(results: PoseWithClassification) { + pushUpCheckJob?.cancel() + pushUpCheckJob = launch { + while (isActive) { + results.pose.allPoseLandmarks.let { + checkPushUpForm(it) + } + delay(1000) + } + } + } + + private fun stopPushUpFormCheck() { + pushUpCheckJob?.cancel() + } + + override fun onFailure(e: Exception) { + Log.e(TAG, "Pose detection failed!", e) + } + + override fun isMlImageEnabled(context: Context?): Boolean { + return true + } + + + private fun debugPose(pose: Pose) { + val landmarkTypes = listOf( + PoseLandmark.NOSE, + PoseLandmark.LEFT_EYE_INNER, + PoseLandmark.LEFT_EYE, + PoseLandmark.LEFT_EYE_OUTER, + PoseLandmark.RIGHT_EYE_INNER, + PoseLandmark.RIGHT_EYE, + PoseLandmark.RIGHT_EYE_OUTER, + PoseLandmark.LEFT_EAR, + PoseLandmark.RIGHT_EAR, + PoseLandmark.LEFT_SHOULDER, + PoseLandmark.RIGHT_SHOULDER, + PoseLandmark.LEFT_ELBOW, + PoseLandmark.RIGHT_ELBOW, + PoseLandmark.LEFT_WRIST, + PoseLandmark.RIGHT_WRIST, + PoseLandmark.LEFT_HIP, + PoseLandmark.RIGHT_HIP, + PoseLandmark.LEFT_KNEE, + PoseLandmark.RIGHT_KNEE, + PoseLandmark.LEFT_ANKLE, + PoseLandmark.RIGHT_ANKLE + ) + + for (landmarkType in landmarkTypes) { + val landmark = pose.getPoseLandmark(landmarkType) + if (landmark != null) { + Log.d( + TAG, + "Landmark: ${landmark.landmarkType}, Position: ${landmark.position3D.x}, ${landmark.position3D.y}, ${landmark.position3D.z}" + ) + } else { + Log.d(TAG, "Landmark: $landmarkType, Not detected") + } + } + + } + + private fun getAngle( + firstPoint: PoseLandmark, midPoint: PoseLandmark, lastPoint: PoseLandmark + ): Double { + var result = Math.toDegrees( + (atan2( + lastPoint.position.y - midPoint.position.y, + lastPoint.position.x - midPoint.position.x + ) - atan2( + firstPoint.position.y - midPoint.position.y, + firstPoint.position.x - midPoint.position.x + )).toDouble() + ) + result = Math.abs(result) + if (result > 180) { + result = 360.0 - result + } + return result + } + + + private var textToSpeech: TextToSpeech? = null + private var isSpeaking: Boolean = false + + init { + textToSpeech = TextToSpeech(context) { status -> + if (status == TextToSpeech.SUCCESS) { + textToSpeech?.language = Locale.US + textToSpeech?.setOnUtteranceCompletedListener { + isSpeaking = false + } + } else { + + } + } + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + fun isBodyStraight(landmarks: List): Pair { + val leftShoulder = + landmarks.find { it.landmarkType == PoseLandmark.LEFT_SHOULDER } ?: return Pair( + false, "Left shoulder not found" + ) + val rightShoulder = + landmarks.find { it.landmarkType == PoseLandmark.RIGHT_SHOULDER } ?: return Pair( + false, "Right shoulder not found" + ) + val leftHip = landmarks.find { it.landmarkType == PoseLandmark.LEFT_HIP } ?: return Pair( + false, "Left hip not found" + ) + val rightHip = landmarks.find { it.landmarkType == PoseLandmark.RIGHT_HIP } ?: return Pair( + false, "Right hip not found" + ) + val leftKnee = landmarks.find { it.landmarkType == PoseLandmark.LEFT_KNEE } ?: return Pair( + false, "Left knee not found" + ) + val rightKnee = + landmarks.find { it.landmarkType == PoseLandmark.RIGHT_KNEE } ?: return Pair( + false, "Right knee not found" + ) + + val leftBodyAngle = getAngle(leftShoulder, leftHip, leftKnee) + val rightBodyAngle = getAngle(rightShoulder, rightHip, rightKnee) + + return if (leftBodyAngle > 160 && rightBodyAngle > 160) { + Pair(true, "Body is straight") + } else { + speak("Straight your body.") + Pair( + false, + "Body is not straight. Left body angle: $leftBodyAngle, right body angle: $rightBodyAngle" + ) + } + } + + private fun speak(text: String) { + if (!isSpeaking) { + isSpeaking = true + val params = HashMap() + params[TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID] = "uniqueId" + textToSpeech?.speak(text, TextToSpeech.QUEUE_FLUSH, params) + } + } + + fun shutdown() { + textToSpeech?.shutdown() + } + + + private fun areShouldersAboveWrists(landmarks: List): Pair { + val leftShoulder = + landmarks.find { it.landmarkType == PoseLandmark.LEFT_SHOULDER } ?: return Pair( + false, "Left shoulder not found" + ) + val rightShoulder = + landmarks.find { it.landmarkType == PoseLandmark.RIGHT_SHOULDER } ?: return Pair( + false, "Right shoulder not found" + ) + val leftWrist = + landmarks.find { it.landmarkType == PoseLandmark.LEFT_WRIST } ?: return Pair( + false, "Left wrist not found" + ) + val rightWrist = + landmarks.find { it.landmarkType == PoseLandmark.RIGHT_WRIST } ?: return Pair( + false, "Right wrist not found" + ) + + return if (leftShoulder.position.y < leftWrist.position.y && rightShoulder.position.y < rightWrist.position.y) { + Pair(true, "Shoulders are above wrists") + } else { + speak("Shoulders are not above wrists.") + Pair( + false, + "Shoulders are not above wrists. Left shoulder y: ${leftShoulder.position.y}, left wrist y: ${leftWrist.position.y}, right shoulder y: ${rightShoulder.position.y}, right wrist y: ${rightWrist.position.y}" + ) + } + } + + private fun areElbowsBendingCorrectly(landmarks: List): Pair { + val leftElbow = + landmarks.find { it.landmarkType == PoseLandmark.LEFT_ELBOW } ?: return Pair( + false, "Left elbow not found" + ) + val rightElbow = + landmarks.find { it.landmarkType == PoseLandmark.RIGHT_ELBOW } ?: return Pair( + false, "Right elbow not found" + ) + val leftShoulder = + landmarks.find { it.landmarkType == PoseLandmark.LEFT_SHOULDER } ?: return Pair( + false, "Left shoulder not found" + ) + val rightShoulder = + landmarks.find { it.landmarkType == PoseLandmark.RIGHT_SHOULDER } ?: return Pair( + false, "Right shoulder not found" + ) + val leftWrist = + landmarks.find { it.landmarkType == PoseLandmark.LEFT_WRIST } ?: return Pair( + false, "Left wrist not found" + ) + val rightWrist = + landmarks.find { it.landmarkType == PoseLandmark.RIGHT_WRIST } ?: return Pair( + false, "Right wrist not found" + ) + + val leftElbowAngle = getAngle(leftShoulder, leftElbow, leftWrist) + val rightElbowAngle = getAngle(rightShoulder, rightElbow, rightWrist) + + return if (leftElbowAngle < 170 && rightElbowAngle < 170) { + Pair(true, "Elbows are bending correctly") + } else { + //speak("Elbows are not bending correctly.") + Pair( + false, + "Elbows are not bending correctly. Left elbow angle: $leftElbowAngle, right elbow angle: $rightElbowAngle" + ) + } + } + + private fun checkPushUpForm(landmarks: List) { + val (_, shouldersAboveWristsMsg) = areShouldersAboveWrists(landmarks) + val (_, bodyStraightMsg) = isBodyStraight(landmarks) + val (_, elbowsBendingCorrectlyMsg) = areElbowsBendingCorrectly( + landmarks + ) + + println(shouldersAboveWristsMsg) + println(bodyStraightMsg) + println(elbowsBendingCorrectlyMsg) + } + + + companion object { + private val TAG = "PoseDetectorProcessor" + } +} diff --git a/app/src/main/java/com/modarb/android/posedetection/posedetector/PoseGraphic.kt b/app/src/main/java/com/modarb/android/posedetection/posedetector/PoseGraphic.kt new file mode 100644 index 0000000..a3c5839 --- /dev/null +++ b/app/src/main/java/com/modarb/android/posedetection/posedetector/PoseGraphic.kt @@ -0,0 +1,217 @@ +package com.modarb.android.posedetection.posedetector + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint + +import com.google.mlkit.vision.pose.Pose +import com.google.mlkit.vision.pose.PoseLandmark +import com.modarb.android.posedetection.GraphicOverlay +import java.lang.Math.max +import java.lang.Math.min +import java.util.Locale + +class PoseGraphic +internal constructor( + overlay: GraphicOverlay, + private val pose: Pose, + private val showInFrameLikelihood: Boolean, + private val visualizeZ: Boolean, + private val rescaleZForVisualization: Boolean, + private val poseClassification: List +) : GraphicOverlay.Graphic(overlay) { + private var zMin = java.lang.Float.MAX_VALUE + private var zMax = java.lang.Float.MIN_VALUE + private val classificationTextPaint: Paint + private val leftPaint: Paint + private val rightPaint: Paint + private val whitePaint: Paint + + init { + classificationTextPaint = Paint() + classificationTextPaint.color = Color.WHITE + classificationTextPaint.textSize = POSE_CLASSIFICATION_TEXT_SIZE + classificationTextPaint.setShadowLayer(5.0f, 0f, 0f, Color.BLACK) + + whitePaint = Paint() + whitePaint.strokeWidth = STROKE_WIDTH + whitePaint.color = Color.WHITE + whitePaint.textSize = IN_FRAME_LIKELIHOOD_TEXT_SIZE + leftPaint = Paint() + leftPaint.strokeWidth = STROKE_WIDTH + leftPaint.color = Color.GREEN + rightPaint = Paint() + rightPaint.strokeWidth = STROKE_WIDTH + rightPaint.color = Color.YELLOW + } + + override fun draw(canvas: Canvas) { + val landmarks = pose.allPoseLandmarks + if (landmarks.isEmpty()) { + return + } + + // Draw pose classification text. + val classificationX = POSE_CLASSIFICATION_TEXT_SIZE * 0.5f + for (i in poseClassification.indices) { + val classificationY = + canvas.height - + (POSE_CLASSIFICATION_TEXT_SIZE * 1.5f * (poseClassification.size - i).toFloat()) + canvas.drawText( + poseClassification[i], + classificationX, + classificationY, + classificationTextPaint + ) + } + + // Draw all the points + for (landmark in landmarks) { + drawPoint(canvas, landmark, whitePaint) + if (visualizeZ && rescaleZForVisualization) { + zMin = min(zMin, landmark.position3D.z) + zMax = max(zMax, landmark.position3D.z) + } + } + + val nose = pose.getPoseLandmark(PoseLandmark.NOSE) + val lefyEyeInner = pose.getPoseLandmark(PoseLandmark.LEFT_EYE_INNER) + val lefyEye = pose.getPoseLandmark(PoseLandmark.LEFT_EYE) + val leftEyeOuter = pose.getPoseLandmark(PoseLandmark.LEFT_EYE_OUTER) + val rightEyeInner = pose.getPoseLandmark(PoseLandmark.RIGHT_EYE_INNER) + val rightEye = pose.getPoseLandmark(PoseLandmark.RIGHT_EYE) + val rightEyeOuter = pose.getPoseLandmark(PoseLandmark.RIGHT_EYE_OUTER) + val leftEar = pose.getPoseLandmark(PoseLandmark.LEFT_EAR) + val rightEar = pose.getPoseLandmark(PoseLandmark.RIGHT_EAR) + val leftMouth = pose.getPoseLandmark(PoseLandmark.LEFT_MOUTH) + val rightMouth = pose.getPoseLandmark(PoseLandmark.RIGHT_MOUTH) + + val leftShoulder = pose.getPoseLandmark(PoseLandmark.LEFT_SHOULDER) + val rightShoulder = pose.getPoseLandmark(PoseLandmark.RIGHT_SHOULDER) + val leftElbow = pose.getPoseLandmark(PoseLandmark.LEFT_ELBOW) + val rightElbow = pose.getPoseLandmark(PoseLandmark.RIGHT_ELBOW) + val leftWrist = pose.getPoseLandmark(PoseLandmark.LEFT_WRIST) + val rightWrist = pose.getPoseLandmark(PoseLandmark.RIGHT_WRIST) + val leftHip = pose.getPoseLandmark(PoseLandmark.LEFT_HIP) + val rightHip = pose.getPoseLandmark(PoseLandmark.RIGHT_HIP) + val leftKnee = pose.getPoseLandmark(PoseLandmark.LEFT_KNEE) + val rightKnee = pose.getPoseLandmark(PoseLandmark.RIGHT_KNEE) + val leftAnkle = pose.getPoseLandmark(PoseLandmark.LEFT_ANKLE) + val rightAnkle = pose.getPoseLandmark(PoseLandmark.RIGHT_ANKLE) + + val leftPinky = pose.getPoseLandmark(PoseLandmark.LEFT_PINKY) + val rightPinky = pose.getPoseLandmark(PoseLandmark.RIGHT_PINKY) + val leftIndex = pose.getPoseLandmark(PoseLandmark.LEFT_INDEX) + val rightIndex = pose.getPoseLandmark(PoseLandmark.RIGHT_INDEX) + val leftThumb = pose.getPoseLandmark(PoseLandmark.LEFT_THUMB) + val rightThumb = pose.getPoseLandmark(PoseLandmark.RIGHT_THUMB) + val leftHeel = pose.getPoseLandmark(PoseLandmark.LEFT_HEEL) + val rightHeel = pose.getPoseLandmark(PoseLandmark.RIGHT_HEEL) + val leftFootIndex = pose.getPoseLandmark(PoseLandmark.LEFT_FOOT_INDEX) + val rightFootIndex = pose.getPoseLandmark(PoseLandmark.RIGHT_FOOT_INDEX) + + // Face + drawLine(canvas, nose, lefyEyeInner, whitePaint) + drawLine(canvas, lefyEyeInner, lefyEye, whitePaint) + drawLine(canvas, lefyEye, leftEyeOuter, whitePaint) + drawLine(canvas, leftEyeOuter, leftEar, whitePaint) + drawLine(canvas, nose, rightEyeInner, whitePaint) + drawLine(canvas, rightEyeInner, rightEye, whitePaint) + drawLine(canvas, rightEye, rightEyeOuter, whitePaint) + drawLine(canvas, rightEyeOuter, rightEar, whitePaint) + drawLine(canvas, leftMouth, rightMouth, whitePaint) + + drawLine(canvas, leftShoulder, rightShoulder, whitePaint) + drawLine(canvas, leftHip, rightHip, whitePaint) + + // Left body + drawLine(canvas, leftShoulder, leftElbow, leftPaint) + drawLine(canvas, leftElbow, leftWrist, leftPaint) + drawLine(canvas, leftShoulder, leftHip, leftPaint) + drawLine(canvas, leftHip, leftKnee, leftPaint) + drawLine(canvas, leftKnee, leftAnkle, leftPaint) + drawLine(canvas, leftWrist, leftThumb, leftPaint) + drawLine(canvas, leftWrist, leftPinky, leftPaint) + drawLine(canvas, leftWrist, leftIndex, leftPaint) + drawLine(canvas, leftIndex, leftPinky, leftPaint) + drawLine(canvas, leftAnkle, leftHeel, leftPaint) + drawLine(canvas, leftHeel, leftFootIndex, leftPaint) + + // Right body + drawLine(canvas, rightShoulder, rightElbow, rightPaint) + drawLine(canvas, rightElbow, rightWrist, rightPaint) + drawLine(canvas, rightShoulder, rightHip, rightPaint) + drawLine(canvas, rightHip, rightKnee, rightPaint) + drawLine(canvas, rightKnee, rightAnkle, rightPaint) + drawLine(canvas, rightWrist, rightThumb, rightPaint) + drawLine(canvas, rightWrist, rightPinky, rightPaint) + drawLine(canvas, rightWrist, rightIndex, rightPaint) + drawLine(canvas, rightIndex, rightPinky, rightPaint) + drawLine(canvas, rightAnkle, rightHeel, rightPaint) + drawLine(canvas, rightHeel, rightFootIndex, rightPaint) + + // Draw inFrameLikelihood for all points + if (showInFrameLikelihood) { + for (landmark in landmarks) { + canvas.drawText( + String.format(Locale.US, "%.2f", landmark.inFrameLikelihood), + translateX(landmark.position.x), + translateY(landmark.position.y), + whitePaint + ) + } + } + } + + internal fun drawPoint(canvas: Canvas, landmark: PoseLandmark, paint: Paint) { + val point = landmark.position3D + updatePaintColorByZValue( + paint, + canvas, + visualizeZ, + rescaleZForVisualization, + point.z, + zMin, + zMax + ) + canvas.drawCircle(translateX(point.x), translateY(point.y), DOT_RADIUS, paint) + } + + internal fun drawLine( + canvas: Canvas, + startLandmark: PoseLandmark?, + endLandmark: PoseLandmark?, + paint: Paint + ) { + val start = startLandmark!!.position3D + val end = endLandmark!!.position3D + + // Gets average z for the current body line + val avgZInImagePixel = (start.z + end.z) / 2 + updatePaintColorByZValue( + paint, + canvas, + visualizeZ, + rescaleZForVisualization, + avgZInImagePixel, + zMin, + zMax + ) + + canvas.drawLine( + translateX(start.x), + translateY(start.y), + translateX(end.x), + translateY(end.y), + paint + ) + } + + companion object { + + private val DOT_RADIUS = 8.0f + private val IN_FRAME_LIKELIHOOD_TEXT_SIZE = 30.0f + private val STROKE_WIDTH = 10.0f + private val POSE_CLASSIFICATION_TEXT_SIZE = 60.0f + } +} diff --git a/app/src/main/java/com/modarb/android/ui/helpers/ViewUtils.kt b/app/src/main/java/com/modarb/android/ui/helpers/ViewUtils.kt index b74df6f..99997aa 100644 --- a/app/src/main/java/com/modarb/android/ui/helpers/ViewUtils.kt +++ b/app/src/main/java/com/modarb/android/ui/helpers/ViewUtils.kt @@ -9,12 +9,8 @@ object ViewUtils { fun loadImage(context: Context, imageUrl: String, imageView: ImageView) { - Glide - .with(context) - .load(imageUrl) - .placeholder(R.drawable.baseline_broken_image_24) - .centerCrop() - .into(imageView) + Glide.with(context).load(imageUrl).placeholder(R.drawable.baseline_broken_image_24) + .centerCrop().into(imageView) } diff --git a/app/src/main/java/com/modarb/android/ui/home/ui/home/HomeFragment.kt b/app/src/main/java/com/modarb/android/ui/home/ui/home/HomeFragment.kt index c8d146e..74bba06 100644 --- a/app/src/main/java/com/modarb/android/ui/home/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/modarb/android/ui/home/ui/home/HomeFragment.kt @@ -19,6 +19,7 @@ import com.modarb.android.R import com.modarb.android.databinding.FragmentHomeBinding import com.modarb.android.network.ApiResult import com.modarb.android.network.NetworkHelper +import com.modarb.android.posedetection.RequestPermissionsActivity import com.modarb.android.ui.ChatBotWebView import com.modarb.android.ui.helpers.WorkoutData import com.modarb.android.ui.home.HomeActivity @@ -247,6 +248,9 @@ class HomeFragment : Fragment() { startActivity(Intent(requireContext(), ChatBotWebView::class.java)) } + binding.cameraBtn.setOnClickListener { + startActivity(Intent(requireContext(), RequestPermissionsActivity::class.java)) + } binding.userName.text = "Hey, \n" + UserPrefUtil.getUserData(requireContext())!!.user.name } diff --git a/app/src/main/java/com/modarb/android/ui/onboarding/activities/WelcomeScreenActivity.kt b/app/src/main/java/com/modarb/android/ui/onboarding/activities/WelcomeScreenActivity.kt index bac47ff..b44e79f 100644 --- a/app/src/main/java/com/modarb/android/ui/onboarding/activities/WelcomeScreenActivity.kt +++ b/app/src/main/java/com/modarb/android/ui/onboarding/activities/WelcomeScreenActivity.kt @@ -1,9 +1,11 @@ package com.modarb.android.ui.onboarding.activities +import android.app.AlertDialog import android.content.Intent import android.os.Bundle import android.view.View import android.widget.Button +import android.widget.EditText import android.widget.ProgressBar import android.widget.Toast import androidx.appcompat.app.AppCompatActivity @@ -36,10 +38,35 @@ class WelcomeScreenActivity : AppCompatActivity() { val view = binding.root setContentView(view) init() + initServerDialog() initViewModels() onRegister() } + private fun initServerDialog() { + binding.titleTv.setOnClickListener { + val builder = AlertDialog.Builder(this) + builder.setTitle("Change Server Link") + + val input = EditText(this) + input.hint = "Enter new server link" + input.setText(RetrofitService.BASE_URL) + builder.setView(input) + builder.setPositiveButton("OK") { dialog, which -> + val newUrl = input.text.toString() + if (newUrl.isNotEmpty()) { + RetrofitService.changeBaseUrl(newUrl) + var i = Intent(this, WelcomeScreenActivity::class.java) + startActivity(i) + finish() + } + } + builder.setNegativeButton("Cancel") { dialog, which -> dialog.cancel() } + + builder.show() + } + } + private fun init() { initBottomSheet() diff --git a/app/src/main/res/drawable/baseline_cameraswitch_24.xml b/app/src/main/res/drawable/baseline_cameraswitch_24.xml new file mode 100644 index 0000000..e58967f --- /dev/null +++ b/app/src/main/res/drawable/baseline_cameraswitch_24.xml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index 07d5da9..08f3f13 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,7 +1,8 @@ - + + + diff --git a/app/src/main/res/layout/activity_app_appearance.xml b/app/src/main/res/layout/activity_app_appearance.xml index 8d92eec..a2abdfb 100644 --- a/app/src/main/res/layout/activity_app_appearance.xml +++ b/app/src/main/res/layout/activity_app_appearance.xml @@ -131,7 +131,7 @@ android:layout_height="wrap_content"> diff --git a/app/src/main/res/layout/activity_camera_setting.xml b/app/src/main/res/layout/activity_camera_setting.xml new file mode 100644 index 0000000..9c37b46 --- /dev/null +++ b/app/src/main/res/layout/activity_camera_setting.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/layout/activity_camera_view.xml b/app/src/main/res/layout/activity_camera_view.xml new file mode 100644 index 0000000..081d9c7 --- /dev/null +++ b/app/src/main/res/layout/activity_camera_view.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_injury_today_workout.xml b/app/src/main/res/layout/activity_injury_today_workout.xml index 0f7cd4f..51004b3 100644 --- a/app/src/main/res/layout/activity_injury_today_workout.xml +++ b/app/src/main/res/layout/activity_injury_today_workout.xml @@ -23,7 +23,7 @@ tools:ignore="ContentDescription" /> diff --git a/app/src/main/res/layout/activity_request_permissions.xml b/app/src/main/res/layout/activity_request_permissions.xml new file mode 100644 index 0000000..984fadc --- /dev/null +++ b/app/src/main/res/layout/activity_request_permissions.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_welcome_screen.xml b/app/src/main/res/layout/activity_welcome_screen.xml index bf29408..4841fc7 100644 --- a/app/src/main/res/layout/activity_welcome_screen.xml +++ b/app/src/main/res/layout/activity_welcome_screen.xml @@ -40,7 +40,7 @@ app:layout_constraintVertical_bias="0.445" /> + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6f3b755..00f9eaa 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,5 @@ - - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 6f3b755..00f9eaa 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,6 +1,5 @@ - - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index c209e78ecd372343283f4157dcfd918ec5165bb3..5236603d2650d1a190de37d5ba4d72713cb9a118 100644 GIT binary patch literal 1374 zcmV-k1)=&Ff5JouqFE&ZAy+3$T4(!NSyueV{A0%A;qI1^ zIW$4&Hq0<^+end@_n6@ck|4=8ZL8Pb*Z14DZQHhO+qUihXSQwI=F_)*t-aUy-81_m zh;185lBn8y@(+016x>L1BuVth8o65U-rfISsINQILy_A?N`@A11Al

kbeS;ZL=} zB*+A~jRZ7t7806@f<3GaI_kWtr>&YEtdvQ(J_QNX01;`+X~6Y;hX8p%Fh&s43IM4W z(jXS3AyCJj!`1*p!PC8UP9%YK{19P-MW&Tlu;E6*0D>?W!bWK-lZMs2gCPiGl@TPT zHK|y{Khhv^;70%ihK4jciVyVQwfrTwtdj!u_f9LvyvmivKvFW4O0tp92+TC?@pBLN zcanPAcvt!sVkAkL!YQ??Ei;up82GWZLG9oR4w zQkpRUCmk%=&^^8CxoFQ>;(h08{k6x^t-NwW-v(UVu23>_v&2vvA1NqosH7;Zq=B*A z5)y@rUvgPT@Bot$8le#FB_&A>x}3f5{PR82J*VG)Ik%IUJN@0+{i?cn15?PZTjI&a zklBzF{FVYnNQI@nER*gFHPQI;fC6L?mre3ZpU~Y7v)3L#kygrk8@bn?J!Q0uFWKt1 zwNouSt&@TFyGL@v)Gwy#+1+074tyG!ARP*3ePNhP7kIsgzs#s2Xz8wZ!Ok(gyL=|)P@CcW62H=_ki2!i}FjX~0M z?U@g+ICcOP{xRepa<@_CYd3Mb%*EOZOxO%19W3Pyj+9(GY+UFbu&&w@PI%DU5Tse{N3o z8k3iZ&5nWrgv3M;NdzK@Mk!DVqAt=y(Z*u?G`0M-Q#dA)%x}yvHXQtcF%iJYOI#Sz zFT_}5Dr!q$bAH)_1_4lwqisC>Yz&z^@HR^R}qeIq`O0nkr}!9ckKu;PXGVly`PVP3)Vc` gOzdnpG=CAkuI?S{im{;ZbR^;7x5y_h1>REt0I)xn%>V!Z literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..c75edaec97ba18abb7273a8e48835364e7573ed2 GIT binary patch literal 3058 zcmVG(0 zS6O6OaNC56&KglLfRmx8VLT?u;HsKx&urzwOxrf*kFsdiUA@h;ZJRTMojn6!V!>qo zl!0{ge*!SK^I@O?1CDNh7!lOsiDMj*0-j)Tc%yCGn51pn=3ILpM<64Oa-Ftq+qP}n zwr$(CZ5w4*Wo9HJ;=tbPGNak|ax#B-=t#v*`Q$|F;f+n{rG5 z_|yM?`v13X2_e8hEFeJm7YL~-3;B`GduGX@AC%Ru??57Hm#6=W`@#;CBy=VAk+>-%xNe)v!fY$ zxIiyMK7@n}*Lt=*b5LQby~yV_Q(kmAw3?d@%b3t!@JY6f8_Te2A~2t}^rc^s(%OSMSPg5Gh~HH_?*y$xA&`g~s`k!-a? zz{=W}7W(9P#2!*g3e^=;akU_o=kf>SdjnpS5MGeh+>+88}-UGzD#tZT--JUU{- zr@fe-`7+#x0Wwt0pmcMdn&^T!fTtdx;=GlQ?xw9kFir2s)@vo+*nlD=1k?;*5=Ur5 zdZ=T70a7e2EYAFV)%x#eT-LxDtTKjz^+~~IyJl(zI9bmswIB15;Bhun35Faw`ZcWA z61%zS#c8~?8ut(nH&5+=PG(lGBWO1&FozD4O36$mGNDbSNd@X?p#U2j9C+zy!#?Y?zyUNG00hto0DurU zQgDDkt7Zg%4C`>3tOz?j^IAQ%)@#3>H@6Z%4@=RAd93+IVSnP0u8bMGu~&O#bc3JV z5BKREZ3dM(T9Dawn0>P3$PhvRFeWChacKaE*gM>XcFaTgvfKrtNLcpdvT`7v| z`IhV1K#_afi7WvaMB3a&0fGs!X1W;T5)i}_8;od29Y8SJ!J?z6Bl7Ygh?tkkVi5ua zAXo$n4jsl-#x%}UYy>c%IuiwW`X~{Qknk2e(P&33Q=_#qS$@gqqev1gpjqqfs_!F> zcXUVJRp~ws02u6Lut4&G7v0>fNsCH+R+FX1%{c_5000y~0FWJBi?Pe>FbjZT0mYXu zvkBEn01oA6J|m@$Z^>{Pdn>Ha=B#bEHoa@7*N@0(iDl+A8Z6j-#`lRY`PiGrFKHrf zen*=MV#nS~OIyy_FQa$6c%H9GBSTkFR0JkCC}R#F4r@bQs0&z2>|h8N2c#XRszeL` zfKGdrDWptdJ*Mz326pTu%B8qn&23e711GhW=H}|$k`_DOWXHYX9q#|=hpvAh-Qk#W zP8w@)T%Q4z-3A&Q4oi0+7-Ke-5F2Ii<{c~9g2VueM~a1l;D9PE0HVbDkS~boWDPzd zwa`h00881v)N%Y^pp}!gt@Q>gaFX{2|1Nk(ysxRR!OS*&##vv~%X^JA(xDzYQLDg? z-3`piwKlZ^rcx9lDp+30Hjp~C(;{LJ`Ko9BHEI=vceIG8Isruio=#q&k8YOY6Y6A! zjxoiPkZ_G%N5wYMnVQS`2_4_DJ%ygt6^VVp-PkumQ9ZTOe?RYA$F|zE0)1>B6EQ## zPi{8XDhVyr_}!QCiLjklbJNMIwuwf~S{?QHPR2&fsp|%A)QOsVIHt!}^#aP5yhS68 zd`;eh(n-mw-!c|6LHpBVXKQ^TS`>vnX3wGD^RiQMlUwArd!q`EJx8~llXyNw$sv_!u#8cwFC)0v{BYjIr6G#XKltXEy^A`Rl`a~sZ#&2 z%4c+Z%n4>0kFbz?r}GtOqP)!7j(1QSz4O8TdV#48Y)A^1oIFF*TtfEC$Tu?PL z{J=Dt8BV1JT#h9o;0d~HH&vKs3-T(jYGn(|-&yHBuX*$_r{t;|(Mk&yHg&#sXD;7v z6?Qvo!!=h+(VZo2LMExvU5QSn>ptC=C-F40IN=be zYC#Z*sPB*^>7|Skn4y=)xsOX(h`eu%=X4TAPi(=fgK3VtN1mo_>d?(ukdiZu&SLX? z7pu-=(}*X^g1g5k-+t+ILx3`r;lmol%y*N2S#xuu4kdLjvunt2yJ1A-@dDKwf&hxD z*edGYD#&h%V0kDf&Ve~%V2|pOEHxJAnjPnft&n6ViE-^_NE9%xTAX^6b@kyiXJAcI z-ptCn7g|maJvUQ2A&pNnwljPJ9Hr54k<+w43104Kz6U-I9O3JjZn>dIp`eXI1Ys=R1>qy?&oE_ZsPB-P1Nv z^K^D6?Btx7Z@JZ$Odszujb`TIsr61vF?dA0tduiqzi}$NZ<4SDlJ9vLF4?JSfcCYu z&%r#4t3cL@Jk0TUnw44^Wka+NyR!S%z9rnboKlbUH|xEHPUY^D?zsD9@6L3hw${vV ztL7c1?`d-fBRj9L4*S>QldA&7HE1#Y%T-EBKw=bhVveRq-Iik*5mVa3Z_^*fzg07X z;`o8@?Q{m2!5DjqL4M*TJeCw9%xllOo+W+Uf1g_$`{uKK* zsQ5zv-}xKkH)S)`sNvpWy$v4kBs+90T{J=}2q^_DF#!$_Pz))SL2^{cVIW?bED^wx zOnvf62IGmSW)=sD2pHfa<|IoaAw+QiizkIx1r4N#&+XI6P+7hc7Vd>8NGVYu0$7NU z;1L855fVZ?LX5+ZLGi(g2=LfAJORxXm^3kKuqtLCAsGw?fe0al2m%3ug%}5p!MX5p zoU93IY2ek4F5`L(4u=6a6oV2FApj*L0svxAJeCw9I1C1l11N;3sX$dH0ssU60ssL3 z5CYI2{UdO_FWndT$xecCiSxj#Lvz7qNiYC~C>DnyMo<)iI241&0~|ncC@Fv_04N^C zS#|pl!ar>P6#QfE4ie3IIR> zvt9I8*FT5K_!@UBzG+*7+fH}S7+b))X_y(UX@oM0Vo1s12}tRnAHxh8S9 zw@d{Q-sXQ-Sn5YQ#ViND44g^FY9u2QubRj1-ufiZ)yOhY$b&2LAN_pZ@<|BMM6+kP&iDC2><{uN5ByfO)zL1$+*gCpVK!h!ci|0e*y zN;f47Pa;PI$J$WPwhiO*hrJ6SA|}9VD$@T@yi#yv+qIqa52X1I_dEX=@0&owAvs*J zFPM0LVR;|*TbVa$Ej#!W;{> zT&6NdiZ4Z^psE>KmKuikQkaZ^5g8-gV~p_Cv2xyzD6^=|y*Fj)Yxj#G4z_fH*~A{BJ%s4Lv^mk{ z%MZqmfJE_uIuzxW(l8#gc6t_t4fL;>IqHfFKB~fbo2*n*;)qs!{Kk%ir&y9#R3=#7z*=X6hv?? z9)bY?u=7bt90skgkVMq817IExt6&8DnZm&E5QWP^0}P+`M2%!uM3H0&z>}G=CI-T; zh#|&7u`K5vDjt;!LW&3>J~1S|hgt(wye$ERNw|p&jl1O$C~8c*p0@Rv6wK&)K+~H* zm0wES(H^B;oY9Fc&}{`Z!nzg5l&jz=IfiW2ee119>lo4*XwMQ`dTpj(dZ2}yAX?%f zlmeMmZk=&Sz4X@Q(jrgfJu?G2M=YJ{yEd7clz?AM{{~W{88!|Vcfzk!#9dO5Wvd9WK|Xy<6fB*{UL@tCzgrdzPp`U8pm@vkcdL5;UkPd zvkWkFtV=}63FdZX1V|t|3+DCqH*cz6!-ar$-;`(}9hNBl=t`Cn<@Pc&Y%g6N#@r$B}vZcr&vkIOF&M8~7PkzE#g^%%D;$>pk+SNF4=QVeKs3m71@z88e!DqZ1VJjTYEWI?HVT z*rTwF5w=MoHKW5&k;#IHMJon(lYn73)yPGzfMuzb01_1GOXXUN3AiuSyf4<}aTrl; zFHhlgW@hKHVqM(P7D1we_Pl097_%nrEen^aapt^_)W8A6&Q5Rv%o<1I34GPeC1%$j*W}c5FeDIPjWj1!qu?>QUpiq`y#P+`=v!(X-P5Qmdx^Dlk*f$aM;^n; zqo@mS6{!ly0ZI&ehh!vYXgs<~M1FMXO;I9*1lJJZ6IU~c<8Td5J|2>YR!T@Pom`Fy zW#(N&Iwy0tJ)7*FkFqDjH2L=3e9B|I&fFu8|6gS1f99U!p>13kby#7>M2alu&1{({ zW0>y)jcf2S^)g5E3g^IP*&hwdfConfg5r;W zbzPF!W_=e<oXB8z06*^yP6dpUD8BaM?rQ^@5>9M9LWdep^sTn(F; z2j$ojQZ!K}-@t(Z79`Ib03@?=Wp02$%DLkb?u>=wi6sl|k?HS?zdy(LzUq56-mU!l zIr|L%H+*|;j}y8_MhII=M{K#NV=P;igK;cg#j4TF9k?=IHE9`*N6Xs~D1)~FqyfdX zAnODS2E!IO3$Jn+JS|sJLCc^xVR<~C`9945^Le;?_54ok|KE-8&%D9bS{?i=eLB^0EWe-vBomE zBL+nOU-OLNYhyGPH*XL&t%23;-^$9`w-3a_C5o&Z3YM3%TQO4^?p11zO`^ z12`K9I0o&Bs*U^E#gvL(kqcxP4wef5J7)uS06WLftQ0BPLQ<;Zs__pve=bt4gHWAr z_5bd{T|}-4+7vnl0o*s}gbV@~2z>S7hZO)Yf(qWB9Xc!E5SIkGLJ#?Lhte8t zcmHlO9}eogz#-ONM|BEpMHueW{?G>ZVNZo9z6o5B^#gS}Py};z!ojHnb|9nU2o06_ zU0$Syl4wLFiJspGa2N^LZ+d>iUxSFU0g^!&=&~vbIy#OWpeBdWM%i9MpxN25f~o^o zxSL=>dVhc+H7J9)w(=)TjUD(hB!CE{j_XqXAZ4ZL;XLnlFPlWevQ)$9S(5rT%J^1^-nN!G=|0jQ=+?@kGdGkkx`xp(!(_z@!Vjt1;!|51KZR|zCAAp{5Zs}rW&+0qMT$IDp9mIL%o+H5e zE%D>qsu-3TRkcM#R5#f>o4WB05t83xUmDJJn|}7=HboHH>d;yfI;&qYc{XwL9O0`c z!%1dU-2Q*Abt~W(?9q2ye(-R5czSU2>i&}_ufmB2M0ow|`dsC<^$)kq9-)o`(u?iY zedzo={dn^F;s*QGlehQZ4ksK9?Ai74!EyNngMFLmw}B@R)Cn>!@OJg;>bF1N4kzE2 e2gjw4t5@l5_`z+d$aeLrrH2Ysm0s={;eG)*M!I_d literal 2898 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 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d64e58ba64d180ce43ee13bf9a17835fbca..0c9a13b99afcab2bbe047c519ac54edc450acda4 100644 GIT binary patch literal 804 zcmV+<1Ka#kNk&E-0{{S5MM6+kP&iBw0{{RoFTe{BRR%$}P3O=3GekuCJ<2$48_AKF zf7a`_XU}}AcVYru*S2js&$exZRV2S@&m9<5S)V>?+_sIRHdC)D?i=LTwr$$hS!+Mr zwr$(CZ6mX7TbXS{wtw`qUz-=5e@Y;TZ5v6FsG8d+|9o#P$+q3Lksdw(ieTk1%5j== z)&95Djs7cW+lKKDf|f+SMR16+IR~tB)7x+B1f7VmXwcW^olH1}9JrS47w$MZjDaCC z1Kk4-g24&k#jh-{=8T)&VGyC@4)E);vw6^3*6yL=HVE1$j4E{gJ^%y($1w=ND0pfdq{MCo%*44h3L@BLl#iCW0zD;nfXI(X~)sh={U<`>#d*|KTPG{54L#GJhey zMiB74IJGRbISgFW%n3l3^%SKIP4&;Ju{_jYmAP{4H1p2nvUU+YNesHVA`~vFN)7c#U8!B`$xi#7b$`B=Kd)DRw|yP#tNHdQ0pT~mdOuyN}Yk4Ynrbf!=p*)_8 zi<(BQtSDt9Z3J@Ag)t1l8o`8_HEb)Bh7tXaAsG|TaYo#*;MFU6Gk|a zD3AdO2w(_cyztli(sO;D#;8Jf4>$+`moYiTm26MIV3-3#WDR@FO>o-mxbfsMOJ=_8 i>4KjGGb|}R24(4!zNf(t1&>rVP#1p*2*|QCUt$1Ah=D8s literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!Tb{(Z041PBDklmrO^ME@rMN697;(1HyyNcakB*tVUGwEKDA zq~6Zjwr$(CZQHhO+qP}nw%M8Ow8{H?n19XwUWRSkhM|i6|M6mqjU+jeB#W%-xu^Q8 zx}>Wj7`ct4$U{rFl^VVP{;2U+cLX5*3<5w95)FwY$^(M1I)(sla*>L(48ueLBn+@P z2_VwYibCAsIm8JF!h``5!qPN|qI4u0i}CRb7jemk&NE&p5GvH#Hzl=?#+L0&KtqsJ z23TgWdfXeUchz>qYU!1aQRTA9u$Dcu-zj`=(V zmVvAF#uLbwNur*vtG}pC#>z-~x9iG#4U@ZE)7PBgs)0HtUE|Rci|_}wFjj`9pgoHK z29XGo;brEIt)nkSc|T7EjU3G-EDYjocc-Qp8k8SMoT?g+&=uWqXWX5`qTpdHeZ-t0 zrN|(JS_I_!^)C0-uS*tEtH&!rCHi9n5K5o2owK|b5Gf1<0)bkXrc4SQODYi((0j5R zgL}(&;R0bHSm3e?my```1iNsf{8}i%{Erln0sw))03y_J*vm3W3d}Q^mK^JbyI(6U zh>Ex%9QeR5-78n)efa&sh4xK59I@d>wMa2=ScR$v0757P5Mq&A(nuo~G4dh~O(^5A zos}jTelsn_Wa|23&B!mgU)7k=NFvFXn>#cNRXECpz@o)`(AM1SqjQT07(fsZ2#^qv zbQGT0!i7auLQW`i8E9$R@}SuvS!ML3U)IJqO?gyFJhM)+dmHUNwQx}z;fkC~M z+L}qly*s|#Uqo0~2>~Dg8!p{tEqR3ng%%Yvp`?gs#__RddqLPhP$hBY2aK4RLs9cIgRbQ5Qh0Yri&_CgIr zETN5ARV0HcQTlSn&ahc0@%4i=;uZDWPXrtCN%1~KKIGW%#N7R8)_3Cg8adJOj7NNd zl5p)_GL%R?BlBoL1I}rFIlW0FRd#DG^?_{FP556z3MMWUP1jP3ms{!0Ms&bQjgU8T zM$%aB_mu0;MdNcK_a@e?@=MeI=VMlB(i&I&i=B4)qJ1S1%jrjE?JN;`L-ua+QPoYb zE|s?0P}a-yK@BMznVb32SuRSJWvx8cXHrL#=mr&1cw)WrywA-S?fPXmW+#ypu%2{2 zxkHef_9ec$C9}LLsdm3hMBOsKq<3A;n2A_sQdQ^4k6K`kH_&q9WG1bob_}1e*mGX3D+&W)^vsfyq zHvu#vGzbk!NMkYKWUO%0Z_?<7xuuOug`w!08#r(8|Cp|~6?mV#{W&*3c24aiGFh^8 zbuaI_A2#1P+p2bL!LP(#MriJ(XNK!sp;=Z+FMX0#K0N2=n~=Z_aU99O4C)d_H(oIv z$u}gA>fnCQedqVS_Ut?Um3Y@ncW}6Q3}=XfP%c6B9H#!To%@?`M7uldzq{ zvPsYw80avBhC(A8;6$NdGzgR+BzgiU?vEjcg3#U&0)+xdObCY2Fh#Vr33`QtQG&`I zf|MZu0|bPD05A$dpb#hoj6lH<0IJkFNn7-{6q_GozsBfY!zPakdue3=00MF0 zi~>X*P^fh{-rs4Bw%73ceK%dXXr*ZTHT-@(8V+M(hp<6J=QR#YOmrTjVYtA0gY4`j zpmu;)?i1fcbDd6Lz9C>dNW45CA>k-|B*4d21Xd(fAcc)#=@=@MU6=R}C>UgdF#-09CchP8 zB*}4`HuH}fAR*Eb91dRu9033eNy%hzwz+0?_FZS&w$<5ufGXr z^rk!_egcFLJs#+*gYgkCcvLXsOqgRw#esc}kts1{m(HD+5o`BJ_AP>lV5Y#D4}242 zE~d8$MZ6g#mTH>J#9X*VIfK~%3)c-Aer)99IE5fA3L~44)dI~%=-JDRh2E1|J{7Y|I3pGP9 zu%4BiCK@0XG~W4$anDngSe%@>Q~fP#1QVL9=S|=VHIkM^4d9^wzw?jx63_@bKmwlprAIafc>x>N<$gQ0|A3*Y!g;*H&6i|07wKi;E1sY${M9$Jh?)4 zLDQX-6KzPG59;!C9iZ00ud}c_^Nb8;B1}c@%DH>-JUlvWQb{qQE2$PJj~-|MK++lJ zQ0ZCeRB5)JcUR54dZdC3++w0o3Qq2wC zAVY-X(iM;cy9SchvCN9(3-guv$ecGuLu*4BG=LGoz#4FX0ys>R(Z_fVADL}T&ml7? zLL4uUKm!3JlxUz0Fdzg5WcV5k5dzu>Y`7x;9H0bF6NWbMlAIPMm#R&w&Lj!XB)KXJ ziFZWa6WeG9eg|(bSALUzn`qQUo1ZF)O9gyX}9gQi)EJH3p=Hir6!d%m9>>c z4xd-{E$vxoGU36BdtxA1)7=D2hO8Dk3FT{FwFegcKzrkA-u+_T|DV0Tzuw$0cAvL? zXv1AS=w_{pN6N{T+ovt9y{v6KZJ_qaGG*ob!iI$iu^r(61j^z4%`q9tjVo1@-Y$(? zx-Jb_csjT2zPI83-|pXEZ|+BX&THRoW4n6jYK@C~wL`Uqr;V&VZ|Txf%ko*9<#1_b zon*r)699h9+=ZYDEMUqr8;8nFS*9wpYNw9pX4&tnJ^x?Uem-b;?xqTR3cCuUgs$99 zZU@(mTg%nw%9XZM)=Rr94NJsXv50^#P<`TNEr>u>s0P)+tpb&y`bO1UgXgC0d8pe~ zMW{-u6VIzc!qD~HDut@0YC@$#RA2(gIy-G(xre;G-2wsyMNvpVQ5+IXvXl|hMrtET zl7i+4;F~ms<%}pwU|;$g_iQ9Bq>44l8U4{DMS3y zgb4u{L#+SE9!mf$1sY1SB@t2~SE*2~QnFIBP={0?l}HUzlT;yLc~Y4$vAj!L4L_9! zxYK5ve+h^o;xh!EAz;iC|Jnm|1UI6AGt%A&M4xV^o^xZq5u9EA50AO#_MqZ}lI$;m zD8HKe@N^WhxW?DhwF;43c4NaT(AXJ3d%bUj4NA%-juR zW}a(+Y9iOme*%gVudCgj*KDGXtDV<+p7)zf1Y?fm;;t^MPU+lu>~lGdt1BrEW5maw m%VNQifRc|0k!$ivg2MsQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 948a3070fe34c611c42c0d3ad3013a0dce358be0..d39afd7135cf26bb044054949c4cd467acb94cac 100644 GIT binary patch literal 2076 zcmV+%2;=usNk&E#2mk#*9o9SZJQxU3S?$i zwYvvxk8NXZ+qP}n)|GAB9@}nPWn@OIu4?mg@t>}lsO~wFF>0+Dwrv}R%KkrjHN{4f z97&QzR?pl$C;CfzGnEk&0H~MMK&}9Pescg8?8 zwrnw!_w3|$7OZ|v z*>kE)QmTN*Ttr{)k_7e2W4so!P|=*eew)+d(FP@LOmEcN@Gt`=)6;oU0?}m+kJ}aE zt4^F9|9FZ@!K=Ub^1X=%dr)I8kkIpnv3z!;he2)e@`}E@+Dekm#nnuCjcv&m358)U zn&-^sAP^oL7}7_#W2R5tzH95J4nwsu4&53C6?4H(T5}1*PzVw+NF!n2S^FS545qo7 zhYVAM$s(cBk86`LB+76Y0b8z9b*0B{Q3CA4X%fp{E~bc2#^}FPP$Alc*@A_Ecah>C2lo^8M&@q;NFdyV6|KOF;6Mw& z7J;S{WQzF-O5bM~T zw}Xd6TC^{Bxs_lD?@^Y48mB8kgO!UA5+n#Dggzwel2WxG!>dKX2^|Z=a7&7)Oy@{> z4NZoWi$+-0NiEe{Yp_K!bT&-X9hf3v2<@e0vT2Hea|S{Wea`&M!_Y>rBf*k)uYm08#6?7C!f zHD~D{xjS!9w%v)2C(Fid=P=D)q9(*JL8>%QQkELUig3uAvn!Kwk47Oy9byHnUsR-1AyT!y!Gdq*{0Qmo$JqGWk>b1xc)F3buzi%icXy?vD!Y`n(9 zI;iaTGNY3qfh0lCM!1h})%nyZ?6?TQB{7 z^4=wOj!)oy%j=}gh1F+gY>=Ym^QQE*P5tA8_}ntiq1Zf*|6wEj~`J<+7oh0W{y zNE=&RC?+HJsZA<`b&a%QLJL|2q>xG-t*sG`6+s|XFdWuP>!k{>!9RwJT(ERhf?$LI z2>=igLU4m3L26SR?=Wc!EkOa!LlhaX3~6OZ0U&-Nr6qt% zq*WmR)Z(u+AqHT91ZhbCX-U+Jdhpipq3haBtcUmD4RVDP5-gCwphO~;1R+I4fD{mc zUo!S1y0_xS_|-V-P;uX*dJKYFs=yHf0X8CjG=fNsjL7w1hNM?cuZ&f9D!??8HShe!xyOtHBu76tE;pd=rmzC$@SVmua!N-F6*jt)#TdL_9cX z4c8M^sig{F))X*glrV#g;i#<+U8IKBp=yW#AgT)pA(3KXKoeI}01#4e5fUqGX%Z5C z772tQ48K~>RvyHQDXbLado9Y7y!z()a=G$ZL!`#3(9UI^Ps&BEuMz&tj~= z7$>I0Mk{%tEso_0q*vfBX1I3ZofbTJ(t0wGqstLATvT|$n#NOh=<(tnY2wS7+b|&d zq<=JnhRQbY*>X7T_nanWj4yV-zr*UeV}R0QvDn|fB8MbT)50z+6pOo+J*Kl&Jg_y_ G(2GlmjL|m$ literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..907bac8bd19ed8e383117d2fc4b90880e72a852b GIT binary patch literal 4894 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 literal 3918 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 literal 2884 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 EA870BLjV8( literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 9287f5083623b375139afb391af71cc533a7dd37..c95e65fcd589961902bee933a4bf08814f54c6d9 100644 GIT binary patch literal 6660 zcmV+f8vEr^Nk&He82|uRMM6+kP&iER82|t;kH8}kO*o7sHEjNXv890eFIbqg5Yhh$ zV0!w?kpL-fz}?k<_(aV!Q@G>)p5H*>NE9`Ymx?T#SXC5cR;D0iW`;005Sel++mP?%B3&+xFcXzkAuXZQplo+qUhoJ8hB) zz(4K3Dz}l&xzOEWGTV}w!puxFv&_uQ%*@RD<1jNbg$B*cb}Shsi=pw}d(QHgt(qQd$T=0?)qpzIt~Jof+1gXerlgFj(gVGFcbsxK+8w+#opp_&DyR*Dwr%6? z{3$;a+%^&{xc7`A>wdh0= zK}6w&ri&t8@rg%V!KKYHCr&zm43cp&StiOD86^WHKn!0LCX*`Bo`^$iVv=@gl?JJk zX6XlLIS)=c61n&ksN80jt(s+FVprT*(xuj@I73tWiiHEf*{BQ zc+cazk36z1Z1bg)By3b&6UAvO-SFevx4*4z?CR-10|O+PO|mqaz#TyljG{bKrdeBwjY#v?;MW z?~<#s9hPPeKNdiu{+@16MqBv2oRw2VUpdw#K+b%grA>pua#;3&pvEzG_`{bBo`76< z*GpnxNsc@g9?6k+vEAILDencVFBrReby1dqN&mlHPgvBrGF$}_K5Vl26IX0#(k##gb1oMU$7GV2v2GE$n?nwMJt`Kfb% z4f!I!ibT9jH$ZHxi+>m#bd+C-`iEy8z zZb^`6FZ*!F9a?M~044z-!#Ja;fcOXT>*Lw)?H4kJVOxM?G8IVfwMV}LNsRcYTh!81*9`!9a(f(UP9Qy5tgJ_M8u&Z zpM*K>U_LEX)Ha^BuRe?~R=>_sk)=wvPn2HNYBuvA!-ff{;AJ z0W=W@kUd_0>+t>WRAM}NPbOUtsYS{`QiTaH7Ja!hH4>WcPr&>T@rH7+w)FRw-?%{ z5J)#37LlajWRFQ?f;fl~SNtI7vxqv8sNy;LD(OiPz{bYCi>0<`ONNAce(UpsWP3{Q z$vnS@R;1BL6o^aaSm-gWWIn}6Nrp-7QjHW!_99jeoDG;!B`45W29jqwh#C`#Eui9mr2k_m3x%u|A~q*Sa+C67aq3BsMI9}LT2 zyneIIH0pkE)!O^JE6I|z#iRKhj*1`%_nQ+hH+Hsf|BR!c4b9G>f*IzyOELfF-XCfhHoWbynG=ef(FwnQcS7>FG|~G zMXT5%W3*KLWI7UxWxyg+M@D!=2_wVV5!-#Tos}Xz7C9lJSb(WY3{g#QU8OT+uC9}j z1lYtUu81dhVJHy^WLT?hScc$HOj0EM^$I!2xNxFwKsbpS`#-Z-MI}K|#L-Ab1u_s% z#-b6(yxj0*S;C0KctuqUvg&%HGlYLTU_h|asWN%K%%ew?;o~|;;w4G5>x$Se)I;7s zAQp`jAUYAYIYi1G;sDH9j&rC0HCc#6Vm{B7CLK*3@2-)gC2cG$SJ2@*5VvE4oMXcU#es46}b}^Aj}4`$s&kL*KQuI#quRQDX-L9C**L=7N1_c zscyUZ&6cM+1?Du#gI>d2sBwM}J6(wgBCcNAC1# zq>DFSLtg7;g}l%PXpt*bph6LG04ziq2SXSd;*2jpyd0EYt1BS7Wi=3q3b&_s9a=8yLGPbGIgS4Dd~pChM~ z^7tfsj2%ndEc;KnNNa~IwEB-hw06_v_4>C3 zMK)N+Gp%{*inibF-L<|`XLo*Jl59|~QI$x5lKN$M+Oo?ksYBu{sp0AT32r9pK@$5~ zL@sS3388|;;4JgiWRoeyJY0U5Q>UvFX_!7Ij%^_@#J(4R!DVdZo<0p7T1g~5K&(QO zlrc#UwNs^BM$3BLB=d95BeR*#5Ob0pN~!HyE!9jWPbKir zv3D+Bbs@NDtW=1%_g>0pj-L9usaB#aFO&wWr(n(^bGnt1Mafi9T0-}ha}ev;ex44I z)0f#0<019_XEKv2X(+|^08v?I%JokkYng_~WEvwu14zOVh)EFJg$n6npRt$oXsW*)!fM>zIfCfbJ_U4sF zjvTL)F1!8uLRF2tuE`XJDWQpM#>ghnX2|(5b~kFS6yoJCm|K5ujuo8`UBvCU!f@jj z(Cp*+E$zDXdvZ7Q4UH>OAiUBiIAcqMbl|;k$l9DMKcI&EroOIVgZWZ^ivO?WN7bL7 znG?wJf@St{!y)8XE3oElGTSYNE_;pSPWf*K95qCmIF3frlJy>04CU(D zn0G69Vv(hRnY{lG^kC6pD4EA06^V@Hw@>A_RnxKbe4ECZHYrM21oBNGj0|bs6EJmi z6hA2w_(gen?U(8oEI-2pt;*eV_!jvdn=jS6B>8J`K}PcQ$d|xQZ-&wdl>-@9`Aew; zS64QPvKd00m@{-4UPiJxmE$HIU<*Tp#%Eu#mKB0)$y;t+FQfTOd!#Y&VQ70NHnK%x z>bye#Lf3Mb{&-HZsyg1ZwS`HV#t05+6`x}-vSJy|FUuD#zr&O8#Xc)Py&j%*{J_kR z!usROSmxJ}t6B=#`5sTH+JA5O^KJX$WkXk8{5z+JZ!=%?-nC8lUGOJICz%XUyq}4= zo2*q{dB-DUYO+#_wS;keQ#LL7@`*8o^gB{AewA4(51O z?dQ&4Rw5NC;RuWJ)2MBNN;;*7Jpcf6ll}qoUHsFZu^Kp+pJgId-2FOO340sxs66eh zrc1T$67L|*#pk#-vQxL~UesER?z{1G`F?uMdWeo>NRAtl<;AzjwmNztOKFxYC)fNO zzL{=w2;$Z^f3IZJKcBg2ey`a0Dy_aTwW?$ zYOdt8^8I@G=k1M~={8vE)<)B?R7M()gEqJ+l?zyb=*pBL-eFL{WB_V(wmh(vkl6a+ zY65te0PMa~5eZ|(vO%9z*2S1AG$yv$;?s?$T$@yHNz20lQY)%Z!FItV#pFJKa|*xX zY{v2mmSA&CI)KSdmNtc#b?D}Pb+Y!V|66`r{?{lux7lg+li|98H)v_YrG`o#vR$|- zCxn3tXQ>7T9~!jSpw+6=YOx*$*G5BAN3o? zNM_Q(xnqemH!YKjy7myONis>zz(9aKsvrj^h7ARiO0YC+<2FyVrdXP9>kz39aBLr( zrF+bnw0K2QIoe{&s!40tTGq!x!{Su`aUk{LYXZFugjpqfFRcQYX){x#ZEgy|00eY! z=+K;dIuucrX`Mbden;SynwyC*S#8tqaV%~M+p^8H10aA4W&!K4Ou;OcjALj)tPa3L789Y~uF0Ot-c#=$hFvmuww?qyf1nT$ zMj=RplIc zW<#o^Kv3*}g-zjPWcWakV8^{VMc#Bzw)IK_#ke6dgJSu2X*Z`gPrtD5YTByVDy|va zI*Fy6Kn?knvykJdAaTtdeS9<^Ho*lnyTFj4V&vBBYNShMI1U6jmNBMk89_&l?4`mR zY1Dl2k*yh8lhf=9WNy3>Q$PcD`o}HTw=0uWL>MMXmbas}#i+6xdIypsKJVBj*j8FYq|@63Z~B_*4GZII!S;yZ)Y~q z?zr7;^>stMxl$&J1+%+pzmULIJ`jL-5&%gI;sPlX(7av9LVxqPnhX+C!@CiQbjyT1 zoCxZaF=D0#yPL>=7BS1SQz?4s&BD6@Mp}NHvlA&Jq?2U75$zXo>T}air{5AL_5{SM zGK89!VlYrD%j~_@J2B6XxWwCPf6+_oB%NhW76$phQ$4OA64~{HnvY`OlY`OX3W(lh z>c+!)*?9Fgtp`^bqRM!gA&K<4=nOLc?Y%d_RYsHymx;3c9rStV47z2_Yrm|Bs{mHv z*L$Y^C7uKGzZ)~xkFLR0w{>mjre}Ei9Oo#R*4o;x&e_N_V<=CZi*sX=mOg}B?U$Ls zKWIK>nybU@luZo8v;~jWT$?!naAxy;;-{H-k<(46G>2(tT7-4l)}Ohh&7s=H+~BD) zH#lr_IN0XyLl9FGAnxyAn*#!Dd*Bs28PgNVFaE){IVfXH)c@d^nlS&<17i+9Dq^Fp zGr0paXs@W?D5H8lLkiN&%kRUCQPnZ7*( z?ot2tOyFRwFj@ErDjEYt5;ndK93KerErNQ)(Sb8FPiM$|jzRAk{b0l?sq{fbIT?5) zZR6iYr3~U2hF_Y5ft`;M36A(9Hb0_H<~_xDBrocSt^)J<4C`B zpM6mJqjQ_j%@7GH99I<)PWarQauB}}{q8@B<#XS;Te{$w40F+u?WKFPc4qRqwdaU7x-BQ8C21L;E0g&y?w6C9=`x=*42agJH za#8vQ^{))_HmXFc&(wFm^PRi9WvQ4PE>zwAOc)-Z>~jOpOqH2uCJjcN8zcty`xWVz z_Ko`A*{B(q1jh$h-t(QC-j@g>b5zvr>2&(&>IZ=B+cQJYjgpZv^vpmBdi2MqTcJ}x z^}D+#U-{<^)8D>h&_3vWTSR;&MElOo&CSw#FyRqRn07j?>+LDw0cacNIp${{Jx3?? z|KEAf|5p(a4-u%FyJfk#5wsMZG{Tv=wsmc5Ti2Eu@Bjb<0iOX^b znVGF+Yb`VF%)|s$6;V~a_v*c>Du@{XjBRJ8FS!WBrBxMG6%j!&r^eX$BIaTc7l|MU OX4!xN3CP}*Q=lvp4$ZXrTZQHhO+w%wJn3c8j%+5C3UAFD&%8dBl_qi9D5g8fry}6Ev z2_Q~)5^N$!IU`BPh1O|=BxQ#*C5*}`lluC515$lxc-vNC)IgW=K|=z7o%cWFpndn= zX}f{`!VK02_kU+Q5a3m37J;c} zTzbxteE{GNf?yLt5X=Bzc-mio^Up0nunMCgp*ZJ;%MJvPM3QK)BryP(_v@ei4UvHr z6+sbCifQaOkL6-;5fL8$W($zZ_;CZp305C;~$hhRquZr-r)jjd1z z31%ZK{-(`P#|Um_Sivn@p$-vz46uqT>QG0B1w9znfS9A8PB2LaHdzA|_)yjXVR*l{ zkcu3@vEf7bxH0nkh`q?8FmoO_Ucui*>_a~P?qQrlZ9@+D7%MTpSnztpylXrt5!-k8_QPB?YL8Kx_On8WD zgT+111d(Op$^$&KLAN5+@?>f7F4~wFi(8TL8+szgVmcMDTp5l&k6~=rA{Dt}!gb^r zSWY<)M7D|Z2P0cEodj6E42PV>&>DFmQpgt)E-|#sSUU@uKed+F680H@<;-x{p|nuH4!_mn85rx>wz;0mPi2ZkL#k6;sznu?cXh!T0S>{w6 zL^gvR05NY64l*<+_L>On$rjx9!US;l;LX6@z}yi#2XHh)F@Oo+l)h%fq$v}DNmF2> zfs^_t0)3N-W<9-N?uedVv{)-J0W5mh#29QM5R5h&KuiRM=0Zvnf#lF=K#WlCgc#9c zS;qvh(P$!_a8JwyhI^ZJV2k+B6Z^64?w|1?5gyo6y{}923CRZfYVe1#?F% z7h2SUiNO3;T#JUOyovSs@@C1GtwipycA=*x5{BpIZ_#GCMuV8XK=x;qCNy{d7?wA~ zC+=vjls;ci&zW=6$H~4^K%v{p}Ab?U%C6Z4p%eC<3ExqU$XR<}LLF67A$Sr20DR_pJ3yeBa~ z^sw{V0FI5;UpwXsScYuhbqGQ`YQ25;6p6W^+tgL&;Ml;>S3CGpSZ>VrTn0m1$y$HU z&65)I!c?oREz};c=nLCliriqQX->4uivHTgd${GqeAlf*!P^B|jkU|*IdNP(&6C>4 zqOW$)Nw9nvjy^&`?E|gotDV{JmJ9Q~vuhy<`^C4XIUDt|j4o6rK^e8_(=YqC zuaR6TRVf@tUFHB079o4MBIh{M~4>WwnGgesQH*3?w(RA%hCZ*7)b!aNV=yOQ%o_Y=Lt0Sl*(9^jfRnC210Om$=y>*o|3z} zAR&vAdrB#mWoaB0fJSw9xw|Am$fzK>rx-~R#7IFSAwdu_EI|SRfB*yl0w8oX09H^q zAjl2?0I)v*odGJ40FVGaF&2qJq9Gv`>V>2r0|c`GX8h>CX8eHcOy>S0@<;M3<_6UM z7yCEpug5NZL!H_0>Hg_HasQGxR`rY&Z{geOy?N92Z z{lER^um|$*?*G63*njwc(R?NT)Bei*3jVzR>FWUDb^gKhtL4A=kE_1p-%Fo2`!8M} z(0AjuCiS;G{?*^1tB-uY%=)SRx&D)pK4u@>f6@KPe3}2j_har$>HqzH;UCR^ssFD0 z7h+VLO4o@_Yt>>AeaZKUxqyvxWCAjKB>qjQ30UA)#w z&=RmdwlT`7a8J8Yae=7*c8XL|{@%wA8uvCqfsNX^?UZsS>wX}QD{K}ad4y~iO*p%4 z_cS{u7Ek%?WV6em2(U9#d8(&JDirb^u~7wK4+xP$iiI6IlD|a&S)6o=kG;59N|>K1 zn(0mUqbG3YIY7dQd+*4~)`!S9m7H6HP6YcKHhBc#b%1L}VIisp%;TckEkcu0>lo@u995$<*Em;XNodjTiCdC%R+TX|_ZR#|1`RR|`^@Teh zl#w@8fI1FTx2Dy+{blUT{`^kY*V-AZUd?ZZqCS4gW(kY5?retkLbF=>p=59Nl|=sf zo1Pc|{{N4>5nt#627ylGF`3n>X%`w%bw-Y~zWM_{Si$dc82|=YhISal{N7OY?O`C4 zD|qb}6nLWJ`hUyL+E>-;ricg9J@ZNYP(x(Sct&OI$Y!QWr*=^VN;G3#i>^1n4e#Je zOVhbFbLpXVu*16enDM+ic;97@R~u&kh__kgP#!R`*rQEnA+_dLkNP~L`0alC|J;c; zeiK=s8;BsLE)KbG3BD&Br@(Ha@SBT&$?xX`=$;eeel=|R_dIr6-Ro?=HEjnsJ_b`1 zK6Yg^-6;^2aW!xeTK)A~3Rm|L^FCHB_I>jIju7ZGo&N_1*QHkxH2!!%@o4iZ?vntS;&zJdPe1dH#04YD93A44o-MpfD zP{rn_aq>U%RDvC2+bp;xPlsOzauIi3*Lf42`jVKKZCRuKdYhi>FDuL2l=v{$BCN#Q6796s%r-AG$Q^t(3c@ zD?w0UhYr11@feiyl9kY_@H8~|xlmO<8PfQmj1!$@WieW@VxR@Psxfe-v9WCi1+f>F4VL?0O~K7T?m4-u|pSkBpUJZZe*16_wAp zSYZ@;k`3;W3UHKUWc8QeI}0jH5Ly=cGWQPw(Kr2fm=-5L(d`lcXofy8tJY3@Tuadz zYWXR{mW7XT!RF#RVCe%}=tM*O6!AD3^(!8un~opNI%Uko7$5t@<8+?; zTxDys(MyyGsUjtSu9$+|_-t!U3fVb1dkK?l`17<+jfl=hrBHnDSV>^R1=TnQeyqbW z>ov#l%!1|S!1>8UUxIdhQq`_klcHVx0{?#>K3#$4GlXncwldt!g17TcvKq-jo_996 z>oA=tH9CqRl6Yw?Uc`am!V?lHJbizOJaVaScf1UP5e7Dbgabq=b!B~T&_F6?ooU>w%x0A zH~&MHJ=q`fCH{U<7MDXE4SD32cDZA)WJeWkllJ`UspWaS#eDe^kg^oU_A14UE9zG-a^g{xaXf$})Wik>gT zl#dkzGr(;h0JZDuFn(+k8wNq?PZ5grQ<+sM?wBGt@JnH6v0#or-5wBQWKU~(S_> zkE!tc*ZJ1Y&*p(xX84POb3cClRMd!^qJ#CAZfIepEj-<`VURS_yCz0(?*Ixcj4 z-!zV1_QZhpm=0<;*(nm+F>T=)o?ep@CK5I%g^VAA+RB25ab?7)A~z~egru=I1S|@v zH7tXV!0wmGS^qj#e+MY;C5eUjEAp$Y?LDkS^QPZ}8WN85?r$u<-Epi;yZ1|J2J`se z$D6DpH~2F=eI0B&=UFAUnJvZAmClJlK)sutJ?M>xpZiWV&0=G4MZP+x+p>EX=HbCz zxls%Mw?*u^;LbHWIWCyq+yi)`GmFn9J112CZda_u@YIP%i;srFg_paU02Ifij*7}l z&CF-(3|>*a|+vbNR`^RP=9G?ymEJ0Z~)d&c*UE$UMepZ zcITr{0WqhxkjUnM15js_gW=e3Uh|y6ZReaXHIz-=p`x5VvB&rH9y>Amv@^WmXFEw) zQXYrk3feir=a{jMQ+wDIkkFnZ$k{sJakHn*?u za%4b!00ev8NVLM1TY=cl?KB&55BY_MU-sg?c>=Dbz_W{(Z~c?HJi*XpYL)C6Bd8WH zt+v-#0&o~@t4qESi*)+eW%@VD0|o^yF)n0hME$UtXF$*Lvh}7sso{`|pn*JDIy5^Fm3s$5*zEE=?u5<=l8FJc3r%+H} zdfoNl2J0^~!-*mOL5o-x32|e0Im*E!yY7F7E5N)W3>+v_LBydlEx?4$RL5f2oYRD# zaR0wv(-p~wO0eLDl3K=%`{5+0Gd$ktO=W)gWlGZJ0`K z$_RNA=ckrfa;H0KA~dR^p�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp 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 KJOYOKz6Bc#)4TTo literal 3844 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%0ANVcG{>H` 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000..f4e888d --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,13 @@ + + + + @string/pref_entries_pose_detector_performance_mode_fast + @string/pref_entries_pose_detector_performance_mode_accurate + + + + @string/pref_entry_values_pose_detector_performance_mode_fast + @string/pref_entry_values_pose_detector_performance_mode_accurate + + + \ No newline at end of file diff --git a/app/src/main/res/values/refs.xml b/app/src/main/res/values/refs.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/main/res/values/refs.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8b00141..465add0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -246,4 +246,57 @@ Please Enroll on a new program + + ps + Live preview settings + Still image settings + CameraX live preview settings + CameraXSource live preview settings + Detection Info + Pose Detection + + + + pckc + Camera + rcpvs + rcpts + fcpvs + fcpts + crctas + cfctas + clv + Rear camera preview size + Front camera preview size + CameraX rear camera target resolution + CameraX front camera target resolution + Enable live viewport + Do not block camera preview drawing on detection + + + Performance mode + lppdpm + sipdpm + Fast + Accurate + 1 + 2 + + + Prefer using GPU + pdpg + If enabled, GPU will be used as long as it is available, stable and returns correct results. In this case, the detector will not check if GPU is really faster than CPU. + + + Show in-frame likelihood + lppdsifl + sipdsifl + + + Visualize z value + pdvz + Rescale z value for visualization + pdrz + + \ No newline at end of file diff --git a/app/src/main/res/xml/pref_camera_view.xml b/app/src/main/res/xml/pref_camera_view.xml new file mode 100644 index 0000000..a2bcf7e --- /dev/null +++ b/app/src/main/res/xml/pref_camera_view.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +