From 830d0ba4cbaad94c10181246fefe8e34bf858894 Mon Sep 17 00:00:00 2001 From: Adrian Iain Lam Date: Sun, 12 Jul 2020 16:27:24 +0100 Subject: [PATCH] Initial commit - it should now be more or less working --- CMakeLists.txt | 14 + LICENSE-MIT.txt | 19 + LICENSE-WTFPL.txt | 14 + README.md | 187 ++++++ block_diagram.png | Bin 0 -> 19687 bytes build.sh | 6 + config.txt | 140 +++++ example/build.sh | 6 + example/demo.patch | 1209 ++++++++++++++++++++++++++++++++++++ example/generate_patch.sh | 5 + include/facial_landmark_detector.h | 144 +++++ src/faceXAngle.png | Bin 0 -> 72607 bytes src/facial_landmark_detector.cpp | 731 ++++++++++++++++++++++ src/math_utils.h | 108 ++++ 14 files changed, 2583 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 LICENSE-MIT.txt create mode 100644 LICENSE-WTFPL.txt create mode 100644 README.md create mode 100644 block_diagram.png create mode 100755 build.sh create mode 100644 config.txt create mode 100755 example/build.sh create mode 100644 example/demo.patch create mode 100755 example/generate_patch.sh create mode 100644 include/facial_landmark_detector.h create mode 100644 src/faceXAngle.png create mode 100644 src/facial_landmark_detector.cpp create mode 100644 src/math_utils.h diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..eb72aa8 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.16) + +project(FacialLandmarksForCubism_project) + +add_subdirectory(lib/dlib/dlib dlib_build) +find_package(OpenCV REQUIRED) +include_directories(${OpenCV_INCLUDE_DIRS}) + +add_library(FacialLandmarksForCubism STATIC src/facial_landmark_detector.cpp) +set_target_properties(FacialLandmarksForCubism PROPERTIES PUBLIC_HEADER include/facial_landmark_detector.h) + +target_include_directories(FacialLandmarksForCubism PRIVATE include lib/dlib) +target_link_libraries(FacialLandmarksForCubism ${OpenCV_LIBS} dlib::dlib) + diff --git a/LICENSE-MIT.txt b/LICENSE-MIT.txt new file mode 100644 index 0000000..ac4d357 --- /dev/null +++ b/LICENSE-MIT.txt @@ -0,0 +1,19 @@ +Copyright (c) 2020 Adrian I. Lam + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE-WTFPL.txt b/LICENSE-WTFPL.txt new file mode 100644 index 0000000..ee7d6a5 --- /dev/null +++ b/LICENSE-WTFPL.txt @@ -0,0 +1,14 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..d59ef66 --- /dev/null +++ b/README.md @@ -0,0 +1,187 @@ +# Facial Landmarks for Cubism + +A library that extracts facial landmarks from a webcam feed and converts them +into Live2D® Cubism SDK parameters. + +*Disclaimer: This library is designed for use with the Live2D® Cubism SDK. +It is not part of the SDK itself, and is not affiliated in any way with Live2D +Inc. The Live2D® Cubism SDK belongs solely to Live2D Inc. You will need to +agree to Live2D Inc.'s license agreements to use the Live2D® Cubism SDK.* + +This block diagram shows the intended usage of this library: + +![Block diagram showing interaction of this library with other components](block_diagram.png) + +Video showing me using the example program: + + + +## Supporting environments + +This library was developed and tested only on Ubuntu 18.04 using GCC 7.5.0. +However I don't think I've used anything that prevents it from being +cross-platform compatible -- it should still work as long as you have a +recent C/C++ compiler. (The library should only require C++11. The Cubism +SDK requires C++14. I have made use of one C++17 library (``) +in the example program, but it should be straightforward to change this +if you don't have C++17 support. + +I have provided some shell scripts for convenience when building. In an +environment without a `/bin/sh` shell you may have to run the commands +manually. Hereafter, all build instructions will assume a Linux environment +where a shell is available. + +If your CPU does not support AVX instructions you may want to edit the +relevant build scripts to remove the `-D USE_AVX_INSTRUCTIONS=1` variable +(or change it the SSE3 etc). However there could be a penalty in performance. + +## Build instructions + +1. Install dependencies. + + You will require a recent C/C++ compiler, `make`, `patch`, CMake >= 3.16, + and the OpenCV library (I'm using version 4.3.0). To compile the example + program you will also require the OpenGL library (and its dev headers) + among other libraries required for the example program. The libraries I + had to install (this list may not be exhaustive) are: + + libgl1-mesa-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libglu1-mesa-dev + +2. Clone this repository including its submodule (dlib) + + git clone --recurse-submodules https://github.com/adrianiainlam/facial-landmarks-for-cubism.git + +3. To build the library only: + + cd + ./build.sh + +4. You will require a facial landmark dataset to use with dlib. I have + downloaded mine from + . + Extract the file and edit the "config.txt" file to point to the + path to this file. + + Note: The license for this dataset excludes commercial use. If you want + to use this library in a commercial product you will need to obtain a + dataset in some other way. + +To build the example program: + +5. Copy the extracted dlib dataset from step 4 to the "example" folder + of this repo. + +6. Download "Cubism 4 SDK for Native R1" from the Live2D website: + . + + Extract the archive -- put the "CubismSdkForNative-4-r.1" folder under + the "example" folder of this repo. + + Note: The Cubism SDK is the property of Live2D and is not part of this + project. You must agree to Live2D's license agreements to use it. + +7. Go into the + "example/CubismSdkForNative-4-r.1/Samples/OpenGL/thirdParty/scripts" + directory and run + + ./setup_glew_glfw + +8. Go back to the "example" directory and run + + ./build.sh + +9. Now try running the example program. From the "example" directory: + + cd ./demo_build/build/make_gcc/bin/Demo/ + ./Demo + + +## Command-line arguments for the example program + +Most command-line arguments are to control the Cubism side of the program. +Only one argument (`--config`) is used to specify the configuration file +for the Facial Landmarks for Cubism library. + + * `--window-width`, `-W`: Specify the window width + * `--window-height`, `-H`: Specify the window height + * `--window-title`, `-t`: Specify the window title + * `--root-dir`, `-d`: The directory at which the "Resources" folder will + be found. This is where the model data will be located. + * `--scale-factor`, `-f`: How the model should be scaled + * `--translate-x`, `-x`: Horizontal translation of the model within the + window + * `--translate-y`, `-y`: Vertical translation of the model within the window + * `--model`, `-m`: Name of the model to be used. This must be located inside + the "Resources" folder. + * `--config`, `-c`: Path to the configuration file for the Facial Landmarks + for Cubism library. See below for more details. + + +## Configuration file + +Due to the differences in hardware and differences in each person's face, +I have decided to make pretty much every parameter tweakable. The file +"config.txt" lists and documents all parameters and their default values. +You can change the values there and pass it to the example program using +to `-c` argument. If using the library directly, the path to this file +should be passed to the constructor (or pass an empty string to use +default values). + +## License + +The library itself is provided under the MIT license. By "the library itself" +I refer to the following files that I have provided under this repo: + + * src/facial_landmark_detector.cpp + * src/math_utils.h + * include/facial_landmark_detector.h + * and if you decide to build the binary for the library, the resulting + binary file (typically build/libFacialLandmarksForCubism.a) + +The license text can be found in LICENSE-MIT.txt, and also at the top of +the .cpp and .h files. + +The library makes use of the dlib library, provided here as a Git +submodule, which is used under the Boost Software License, version 1.0. +The full license text can be found under lib/dlib/dlib/LICENSE.txt. + +The example program is a patched version of the sample program provided +by Live2D (because there's really no point in reinventing the wheel), +and as such, as per the licensing restrictions by Live2D, is still the +property of Live2D. + +The patch file (example/demo.patch) contains lines showing additions by +me, as well as deleted lines and unchanged lines for context. The deleted +and unchanged lines are obviously still owned by Live2D. For my additions, +where substantial enough for me to claim ownership, I release them under +the Do What the Fuck You Want to Public License, version 2. The full license +text can be found in LICENSE-WTFPL.txt. + +All other files not mentioned above that I have provided in this repo +(i.e. not downloaded and placed here by you), *excluding* the two license +documents and files generated by Git, are also released under the Do What +the Fuck You Want to Public License, version 2, whose full license text +can be found in LICENSE-WTFPL.txt. + +In order to use example program, or in any other way use this library +with the Live2D® Cubism SDK, you must agree to the license by Live2D Inc. +Their licenses can be found here: +. + +The library requires a facial landmark dataset, and the one provided by +dlib (which is derived from a dataset owned by Imperial College London) +has been used in development. The license for this dataset excludes +commercial use. You must obtain an alternative dataset if you wish to +use this library commercially. + +This is not a license requirement, but if you find my library useful, +I'd love to hear from you! Send me an email at spam(at)adrianiainlam.tk -- +replacing "spam" with the name of this repo :). + +## Contributions + +Contributions welcome! This is only a hobby weekend project so I don't +really have many environments / faces to test it on. Feel free to submit +issues or pull requests on GitHub, or send questions or patches to me +(see my email address above) if you prefer email. Thanks :) + diff --git a/block_diagram.png b/block_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..b4bfdd4436b8823acf1e7008034eef66f296febb GIT binary patch literal 19687 zcmd_ScT`mA_AOdUi)}#6C?FWx9LPz@IcG@{q)1hSDxiQO2W=zCj7nA%F`*)YL<0hn z6_p@C$x6;a!kt?@_jI3g-ub;T-n)NY#^^Rw)vmq2Z+&acIoDjC(bZOATgthVL?W@N zsVeG|NGw(O{lni`@&6`TdjW~W+U22S;^E?LPj$8|;v&@jG#xp22|Gs-GTBo>+g(k5{yH>k z7rXh3=;D_43(Q|c%*dT=GE6aid!ps8<;R%6e&8}1?4^Eb+q^eUC8!y7h@)! zE^ni!rb8x+NthXHX~|Kn-7 zrfS~a0uFQ|nyQzllbn(^70)EEz)%+F*L3i7wNqA8(Nj{kR<~0(ad7YwQ!+4iqKm5O zInrFH8Z>PkQzw35BS#lEGhtuF!)9h`-a58qBbth*hmfzaf|{|ghN_mSFPYy{TuI!T zswe1f=4_;IgD)`ES631Bu+|X~WjHGs_y{-~)9v&Q8_=}Plq5v8DH8frCog@53&T-R z*VRiKw@7v;Ya7{_=m=^Fix`+{dg&;6iF;`&>pN)KI|%tQ?R3R`?HP1WJBGWKhA@*R z>ZMfNZz7R%n3pCWFmU~fm)Fg`3!rz&fk8v4*(we7SFoi&t14Ea5@ ztUdTCuKF%|9u9^~ioF&-WaO)?;v_7rY^|ZDE~X+%b+J>z>WNadX`%+M?rt6~+P>P( zc7`fuZu~CZhGH%v9yTKSbXRX3L3xUoIGv)PVys6Ip?L8#6*ZLPygW_SU8%mFMpT-E zsszQ>grTD#=SsHm7BW=SveR=Cu$6EVkdxOo^078{cEcCB^6QD~YZxieL?nC}WFu>L zEn^1{y0eCdlA@fZ3x)1rr>sfw(8Y~(SK=-yOs zFJ*>}kfX7uyS9nCtFn)}z2aeeWxBJgwS>}P1AbcrF(V@(4`Uk-F&!FH)k9s;PMMl|t zA9W=+FL^nJsgt3Wk+7zMhN6qUqMC_<4wb4b=&HbUmUmNpK)t#A&-iL*~1%2=tRs83n zD{M@o3({4UY1#&6nu_Wo63P;Kp7Ltq`c8TR-rk;6OPFGPS(iu))f7AtO~?T>Up{}#wE=csn`w%pEbnZr>_vAKvR0B8!%bV` zt%qHi_+oi&FF`FsFFQM16$w=_bvI>kGj#Hz|Pp!#MwvC zNZrlNQ_;srf-YicZRD$H;G!+6r^*mgHS^WiFxAn*=k*v=A$MVShPRQts*{bTsw)Fu zVxTEPqv?7%`g+^hNVv;et20y%i!n58#fire(e`G@Y4{jgt84O`nTWe8D2dVZ9bI(v zorH{a+#S5^1dP?4X^s-=u7*PTMmo0kHcra&CWdBCR7HI^riPcZfuWfe^w;hn5f%QyljcUlhEc@*2k|Z z23};5!*W74TJl0vYcB;aABGW4*OV;iN;mWsQl>Li`1ySgi+O1ZIXNh3n(Fz`l||^P zzWlz1-b`mRbtO+VPZw1YCnakNzFA8^%$RQEMbn@=c@tZ}FQg};YA5e5!SK-aHuPXJ z^{K`*Az@7k19y87yv~#t5|=|Uz(@a44}KO0{P|Z=6HKH${Bjc^fvmK z8;;Hvxs@1dmM=Q3Wx7^G&v0diyHZ2C{#*0!9UnFIH5lbjG*iWL?d_E8Za<^UJlZD` zRZ>>=h~_1faiL*+*;4ggo0pe$k1uAg@FcI2Dias(?`#!_JQX8UdTo5NZ1Bgmv@ou% z^FQu~X=1($6z6|RSVXFR{r#;3*7_xx&!2y@AKFpVubi5l|Ekx_xhdzG`sF=eTU%`y zhZr4YB2qfe&igLz)H&c4NTVH)mXFGDR~A2B zN%JVreBbNa*j1_S>wD0p{^?h%xv3fvK0kH!rOXDmA`zL2CYs0o%=ZuO#~$=N`||kW zH5)dl%F2>h7O!Y4KEKUw_8`e`tS8Yp)pFv;4}%Pv1=+?%O+!PdWAUOzZe3MdvU5c5 zXvaRDEx5AUYvCRb#-(_PlFAsTa{6#bxkq0^-AMP2?c3ePdg{oXQ5mcU#l)gMd@wvE zT15`Bch7qle>t2>OwhUZQMYfo6E$I(WAiG*@d&+763Mr3>nCe(YVse}I$)Kj>E$Kb zpZDWHUS3|!^ojQ$dm6JjtgNg)-8WUh8blqiAeUVg80r07lNq^D)Nrj$Uwr^LHR$t;#qlpV5!Tsk@us>o*=cDaA>-_pv0?t#x2{c$={BC0yPn-7MtEkNf6X z9%mjs;`jIW_in#zTwGjywBYor-+udT<*HRL?;gGv+|un^l_1yP+x;MHqtMCG?iwNg z3D?%=%N(aG)2$kudYW>hPoMsMdS-_D`Pu%RI&nFDnumWbv`(~WIKGDC`fQm{_JV+5dQfE_B`OY=V*4)uMj)&nYee65x*q1xr)P2z7*;7QF^|O!z-_xEj z`z?J(y5zLAS4c=m2ve$p3knJZ=kA-=O{^E*Q&(3PBk6PD#S7U8ZV7#73#;dgvyxb+ zO6vLgL|EB43*O(-Sa4M)NE*S~=jm`aiy8K$;yLcpf3iL5#O=9(*X&Q7>n;=&DDE-5 z^CB*RSYN&6Wm)iQVgyY-KSL7r%eJm*!OSaDL6a zx~f8oi&byjym=-fqOhpw)W>_qEJ`(=RIaYQPWhQNCfI*#YwMipp;G0vfZX}R-|Sgq z;avCRR@F%CA?9jMPIWSQE8bK%a{vB5TKvvb{vU-*s;}a2iB#vp5^deLjmnnJ(k{vOYt+it#>BKi>XXrLq^zrk97)bB}{9$njGhQ zwme!)$gX7V`UB?0XV>lZZac3M_TyG^^2N9~n((4GQhknW9&VAxr&+U(>L|Z_@%=>z z3-<86cU)PSrsK!Ei`vAh7DY;Vbgk_DoFUd{TqVnAo_TJp_j8)ZH~q2x=Ezg079;A4 z3JNxi^Gf@R3)z=_nHg_Rwke9JxzwAk`}5Ry_21gBYC81AKRP~;M=+LR8CF~xf>Ceg?y~c5@6TMG0u5z?z!*mWGD60{f&J2(?g!^ zOYZuq?Cy@?_3IIc3!M5UUv1_6bI^*&V8HYn`yX$Lw!%OekH_?hvG2yW~z)Ig;ou8Ai$7kfvuYT&N-ohVn=I-``rs zmoux%Fd{(`V0;N56kmHqdQoefUY1V_bZf?@_zQCciITO8zm{OWJ3i zS!K^Y@{zOa&YfM-bJL8?b1j-5?;d73OG~IpwTk@c*#0oPkNoEBx|&OLYD(Xx*F0<^ zJ#}f%$n112Xr9b22dDYa^-hA>zZQHVC zOW}{xVPSvp@kJ&j{b8fi6iK$S>o*w{Ui) zJJsmt2L=RA52|dQ+sX{(;QPeW<*qnc7ys9S_{1@R&nmC%IJ>l}x3~DFLtbsjpvgYN z9e?cHnMQuIlv9kRiK>gJc;lGPsU_zYiDIvLhRs7Qb-Xj=Fp6o&w7uBAePM`_9ojL) zjm&1YOAWFajvvqB?$ZKieCyMlRFDC$N%{yXoLXU55hL+Nw2Fi;sOgN;)-A#i5Of8gcf!ho}P zyBiwJk+L@;d`(SFmv1_FsW6!0o(Z}Q_Kz6>>`@J&YquZ*^#;acn+ zu+bw%r*?uXi^ZeZ#U>fFO%IM_6xP&44Gg#v=?*p3o_lE1+O>a1Mn;y8E}8w&Ja?te z?{Mt(n}07p5kq_9pEhwTNF3sLz+U9)QE}$X8Br_qE6lqFfGU4W3wnX=7e7VY>~iS5 zG5U3bV?w#!|8OfrCM_>p?qQYE?Y%P`Hi`D#xnKO>;V<1ud+P5!+b=XJ|)SYSlZxTy5BHG|HLzw z&qL*6&F14y7JMIb=gaKhPUwV6a8$3ZIpBIHyS1UY$dTmj7_;?Na&2;``5phuaOcy{#GL z)`}M0zI%7dK9179k{`Z%`ONosUb?ilb=)KNypV7e4*X4O^=(@_JG}>HBB+?;vhH^` z7t?b`*I&DKEsRHslTN3?0!~tMEr7}1vyCZ};Vxprp93Agk#QiYir1%|Baf?X_VLg53%-8f9OP0-2Qn5f& z7cN|Q^^{tji7F1HwOK%5m5huG*8Jof^4GF!LWK;AOxF-pT-WIdw}dWMUsP6M=h`Yu zdYWqc^AKrfUxSEDkSU|ESJ~&rzl5XI@Q;bsNNPi-!0ha-#w||L`5>-d^0<+S}{Wz<1z4#Giluc9M;& z@X~IBuI7B1OQ5SVx z-IWR_mwyFjBWcDSyo@kw>F=)$<;$aFl&NcY_HP&~^TE2~c~321VXb=Ras>F8^vLW% z7?;=ztcSdU!bX`uNuzt0aUZ}wil{7C4>1pEZafr#^8w zSu5ACw`a6z>alr{$v09M4^vM`xz7$idv<5)$TR1SKrk#{n&)Q42%=QVmtWA_9FK(S z)l(~HYx_r0#3q(6D8-7($_rO++EiYxBNLvSyc3vE%;U(z-ZY%U*`5mn8O?!5y0~g{ zawLJ)YcuG);8}~iYf||}GLi2eyMBEoXcc!%YTp~vbjNj%A3wI6&AN3XWOsDWAJ|kx^c1!Tm8_VyvVe)Z{l- zR`-swjZdCDp;X^y1Mnu7T-q%;JJCwwmiFHOf~Bvoe>PSc)Ck3;=uIfcW0QU09FIJ^ zjYu68u^QOU1)o{7Ku>|dc!IP0^P-Wcj;5QP2@4ah5Wz(<2PJmZ?sZ23nx3An%@2}! z{`|S*cmqRndf@dmBKyNVu_fCFf-!>ZfV8&S-w&5&++CUUQjnAWlaiY7t*2#EzO~lb*Shf ze(AuIcJ^j8KlABp8Iu*GRK3vde?l2+VzDrvS>Vy%6k93%BMc~cTj<_#*{Bb)GS~u> z2cNj|_ScNn#XKqLuS!&aE-=`3QRm9{**Bxin!5=VNNlbQhEQGsbB9-o?!Axdva-Qi z7azHN76lt+eQ|!9%Gu=#Y){Q2L81s~IxFovvTMPD1^y%NRFaaDx#rH8hdQ|=Aa^+l zxd7F8eE+Z{7>ut4NC;ObE-!CKIsuz3+>k?%frFmkYb66;1TR2>>A4=2Eq+&q(R10# z-(M04On9~eiE{7WP@@NCM18lJ3SPi6fr>UCKC`2#K?^4R$PhZhewgQYKQ3lHxMxpE zW+f_LSctK*GHY|-kNtePBdbw;1fSIyulVj*^H9sfLl_*I$XmknrkI`{XHHS);;wf$ zCwtN=ohz(z^azB}&pLHcM_OPuaOmw$AMES`r2BnbqRvNg?hR8dW5E*6pF1au{@?pf zhrsca-W(SkOuBE>eYoGsJc#*V>32zpC?KW!xzBFaJ&;eZe*t5r66IoNCyx=-U>N6E#-ii-mzs%$knSGkUM@~ zwyd?rDrivxNW*=L>??m)3c@ChMXQVtT}rvbI)wxw#A-rl1}hHjJNnpUg8=zAP?3Ef zWI;n1H5Sav-*}e(%uaNiNXen%ZH3KPPekjQjT_aFumu9Xya<6>lvw+1QMdr$>tYIM zqFekgDpo66B_JSB7_gMHYZ=FZs!TVVIN$HPMd?jtSfHgMRWj29$K8J4(H3;=dTcCP zTg@nH{REJswyNrBH}NmwH|7%$7V`1nc-z~HKE3HR6SF`1m$0$fy*#$4r}pu2urp4! zk<011u3zP`tCct+bx3PGJUp_q4v7iRfOc(%gH+{Mq?mo?9hLEP&C2rfH{{2)!c|hf zBS%p}3Ijl2i%U!QKILANIy^mGS$*GB$TD!UT~PDw6(P|odd?s#5b$zt@v5uqxwxFT zT|8R~mO#6z%kdGKo9PcqF-zZrP45_LFD-m7`U`SlU1pn0DE^I8L!S^vB8FSV;KXd9pe zthXr)+X4k`qmccjw|@DY*@wGQ^Jg9XZLHJHQ1e%Sdo7yf7>K6lsX+jHTT!t?LgE^3 z0~Fk(Cvi70%bUu|%1m-Al(40njC9{`IeSw%T=3zCiN5T9&QpOFP+cI%Cl21`I+g9K0D zgmr7_cEC`mQ-^zo2y(P^-5y<_7ieWEx`_)dvOIPVk#%b{Mjaw@hYlf!zI^#I<`8p5 z=o@)(6(j`v_epA+*M;9^MxwmDef#!}cK(x)XQ-5m7CnY!xH&&$mm|A}9@_{IBvEJ2 ztM*g%6M;PjG@b>M*Lc#5Q)GR+vp}1bTW6+!7QNL{PNNb7D!24Gl{hk;GUa zl3d<%_!TPg5qj=rQJ4DZTY-zSjvhH;K4ZFXy;%mWTt{X`5f}^Qh-Ij#bkQmA40>}D zMH*b!#Fi!CW=4CxbhN?x$r+$$WnMAsSHDH`*+NwcpLOc2hZ)axte>y1`{atqBITpN_oyXb#^p+r-Y=AW~yH?#J>*M9t1dZ7Jl$n~Ouy%N8sP;KyCp~{gv@y%G zwaKr4(Nf-579X})j3u$1khmqaxE@m1)Mz(Xv-XV7N+DW<`1cPf3XYC@@WqDrP0l~G z$maCKy4qF5%g}|ZZb@AFR@o_wD z$d?H^Kqv{wftmi3g6z^0o59;XdTP%jna6gNM_-JIDFfO?^4ossQ1t#I&#H2K7{`ts zBXAYS+GhMEGRW8dex0L7H@6l=)<{$xc-6!?nLD>` zJ%)5W)WE2GhvX%np8Fpqi|wLS56seAM!r>(2uV=b@xy84g;seZ;6^|;L5SL@IR7G1 z$bOyk6Qwn%s;d5uv-#(#Q(ULs)gLF+j?2TBKiA**PSnxD6MeH_0V|;<8X2wMYjmFw zIDkUgiIRoHN#qPmzg`iW%DKyI!mbJ#?NA6xw&oY{_?6wc=kod4*KgmbVT_vyIX;K? z@85^rq(LwFG1~1QX^hTE*tQKjG_M}@zh>|uMO4>=#o|5HaZ!y*@HC&qronrgy&hC? zSoxnvGUlhg|D#n#bi(Qdq0%6F3P(VZ308nWGtCdoEvHZRHfBcwNY&-otDV{f4KaQ( zk9RO6@IR50?-aBCGxv?I>{}SbQvd+^8_+rk_OXwSZ>p=SJ7%N!tXQ4dzvk5^Y6dm2 z#70M#g3xX_(Iwvgn^x%k!hlj`G{z#+k9A~G z5<^sLvCVE}8@VRrFfcfnXqoFfJ)S=&a+~9~=}$-K?|_MLJ&n<^!w}358u|nqPPN*O9Y(CmRF+h^l!G z3Ul>?BVwd~D0MFo%S)CmW0|dyjq1?7eWM~|cB(t|8`Mh@Wa?W8gF>EDP`K9fS(^9o zP>N;rZ+;mlc#<$Bq%wI~$BZ74$XfsWPTwX438I}O5Zw&Yz0~4)U`xT7!rc-0Vh-kA zw&{MuMH5ym7T2pkeBpa9OVHZ~RZUN1tQ7C#xv};qQ9>gicd_r9s@hdd_W*sSRwXO| z)RG}UUt-O^zLjsl4_EQ*}py6>9VU#;InM} zA!Y<_gy3%^Xm~tLKAoHXsh^3;NO`tzY50Y^(8nXPo$*?W|0JXpqAodCggiuH2IV2> zUt!b0UmrgvHuTde=wd`~&wiLo(Dros`4{JcT<$?sIDh`U-klIyQ;vjg7OO9oq@bvX z!|yIzZQNbmD%q$p;dBJ@`Is2z?=w*+_k37FQLv5}!oos_FWLD(tVp+_LaF<;z$Jn_ zL%)HDwczZ*qEU6ClW6tUFlg-7bj-!?O%l5tiqIfi+-@%TF(qJk{soH%v+4><4#|Qq z5UC#MOsRseR2~?;6Z&z>v(L1fY8Q8CUK6~f88mcf7w~NYV1>80_ZNs9$ZFxCROtSZ z*qe2}Wv5qdJ3Adv+?~j#^p@K+$o_$8TA?^!F0umAcH_&Nc$K;S!9U_IB6mK~)cnj^>)1tPFbCJ&3jm(GL9} z*fspsWUQ$P4FT8TaxpFuE7!f56FNS^C~2*CyX}-btprsfG=K*=e8>C3qoO#F+Ht@O zGOt|71@+~3`m_&{836+E>|~tMLhCYg-CmVgxaF{gh|$dwmv*+1l(r8#4J}y))#QPn5MWbK023e&pYE@L-_#BhM^LW}ph-q+&`;Wi?*Le)H(Xkx$wi72{+pb-wkqkL}`z9v5p_9BJx(`?!N^_o6{_ND=bNO0v{MHKz zc?wJsn<|Lp3Y`YcXB)tk!l>KEq9boyT9*Y{R@T&T@bdCn`n0cuCWPm?jW0waU?fZE zedp`J9@FvdB2|mht#Akc9BZH0ErWKQi1(mQJo4%_BLQ|6Tote(DmNhl!lAIwDlY~P zgzpP$E$uRMqU)FV)%^HS0Xs@&*{^6oQPFH)(H){wv^en$yR-Zy@kWF#EXU;^Le?KVu7h zLkQ7msx2H^>;#TeX!rFLJ8uO(`?jo%XiV*KrT3Jhe|2@-@r5AlI05Me^on*kxLV9NNE-~NS?_RCn^DI_eFD`OqXi&=!9BRCuhfvD77_v2b?gN2xRW zQ(91LR=ydFlx^`!oBLIdtQD>tus93N9hB~t-c4oMK$hNb-_)QM;rbu$n_h&B!!iK* zoXdD*21NRKYs4XO2zQY96qS?&{KlMt(@-6DZrgSo!j^unuP0I}+Y6c5L3TptTfrl# z3bZ+aZeKAtsHo_bEea<$^e3VU_k7M^zxUBmxQeL1n>PpFREfM^^1I=mwt4(aLX5vV!#lbmXo=f2CCDB3M!1@0d87=+FO&81diL1uOlWA@wegNU4f#}>f7wN#qNBwRcvl|zpn`O{hvtAoI#bbhsN(h z=*SP58LpIR?eFieJu$oHWBl+FW+O+9Y=2!^(L^>%ds0$T?T0~Va^JuTkez?q3Yvv-W!VFsDbx@W^-h*V|@?bRKaRpQuV8nPLT_{IzQzb!I7b%rnUubpvN{voRLHe4SaXo{{0VX*!WYue%oDksdt>7%C*$aFbN!f!cDnP`KK0q+tS|i3Vwp?n02RWBIKTefn%g0HL&b?u`iJ|2=D31E zjyAG+fZC&P{4}KoAbkPh!8K-$&$lR#ACDm7@UeS|mW%*;D6K2|Ap2|_hAdD7+0X~*ZxZ4oO|!DJRH!03A~e*dA=9le z=NKy|kx@@n@WE$y6TJy;N$;wwV~EDpP;}6ADhCpwPR&@Vah<&&abxzOv5Cpm@)tp~ z)04(72@tUz8-lQKcC#nS()DK~b^?e|4{S9uWC7VkW8#gG@!_p_ zji(qK8EHcg;3fI-03qW-KuBm~^~EU!*?QvmaSvM1YTDWzXuYF+@Q9X<74Zh?raK-c zT9XyAhf3RK)>oapVH}J<^~P&H%Z>x`?u_$Z2!McUeh0WIaqlbmt6Doc3eiGVwzTAdS(GScg;Wt^+Qq1S zrt&zOYFb+Dus#7(6xQC<|9Lz+E8}(0Xd&=E!4ti`AK4VGfaXj2{OmzW*vO>oKEIj( z*tGyE4bZ1}UtK*3@1%FjnVm$nhc0uAmeAVTdgk=$FAdKb(CQhLQsWysuh^sO58w`C z%kmeHFICeWKZY6jqidI9ku93d>@Org=J)C`X`Q=r193F|)V`b(pwnimQ|9pacE*Ry zwzNv=*KqrQ=ARoG@xX!=p(Cq_D6K2`uxlSGj zkXRTrP9z~ODOs90qi0bV6lO>CPiVG*i;TaH4GrA@49%Gt`z$$FutxSB+Xx=E6dOZD z8_LqsayhSbPE|%u#OX)V4MCl|_T7G{ic%UEv}=4w0H@w4Ix)K7uxbXy*~Dy#dlO zp*M*pmZqenzibB{!2GKC*A(%e*^2s^1uGaAh)JZe|?v2pfcIXzUyL zj|P{ox&Q)#w)JWro(Pm`!paaSGfNtZ?CYsYWP!u#HkOOf?@17ykzry9Rc_)C9%^U2 z{{>JRJ(y+Cpj4EXUkJLs71(|z-c7kKZ+DK^HXIrro<5oKF@NF5dCw4Q zJvU?+!WEZ4^tP_%h^cTDQVW#bceT;(+r<4U9xBhP8ib8#=5JOo?sjA8=dXN2ab93; z$x1B!vQ#2!rx|ZVV$8hF?uWrEZP^pF|3N1Crz`KDULQ(;_(^xspW@2j9JqfmK{;6R z{+kx^+GM;um=SWM}&i@|ipe28~?B z%d3MX7xE~4*U>P60RpxSjJMvGh~oHfcieDi6n>LXa;KfWy*oM;mnZz%q3$Q)@J^3Z z>)1LttN{@`w?!cs{?7l6`%91?|IPek)X|^v$6dd9gZtm?<%j{b?s8~Y47WnoDU_>Z z0POA3fe@v_xFz0C9Qt>diJCM77a>z4;W+PV@I8#GIgQ|wc zUST@gEpTR^#mNwWCKT=SH{kPIFr1VC|2yC!huq7VfwLRc%ayhCf3l0Y2mG;P2PfP8 z?Ck8pesr1T#{qP9qjaQQyK?1( zMAD{XV`9u(zJ47`$eQ=v{y&Cf0}I$KV~&juDqb10q_C{;WBA@3H@ATl=TuE+##Mm5?VO+1~(OM3Njt58!i; z#4LV+SoJG$MYXlrBCW?>xl;JZ`ZeG~A)G)_mcA)H%~gl*{P%tO7d`Vo9i;yk2NiC` z|L^|7r_yjf&JRKS%_H{@Vf(Mj?!WlU{~6T3k|3el{#zoW=1QD&_d4)@BzpmJHEK4s zO*M9TjvHsHO)_pe6<(&bNd6pG(R$b%ZpjQf5nUU#di|>8%c$+}b|42oe;4|5oaP3r znPN;$j?eHN!<5xfA#(rv_r~odm)#hQn>eRpzIs0gaX!d~o@PMti_>Cxko$8Y$NgqX z2rg|{CCSn7l*5d87fl$$IxQY>SvUz-fmMiccA>*MtIT)(G=4Jqf4&N>^-svQD8UdM zzjk&Op_*#nQol?axdD)L`t&jbmi=3`+wZ03LB~A~M$EDy-vKQ+cNAYtK2(RBOG1)b zIuHdYNkF(>}} zm;L_JS5;KB7@A;1VE+M++^XM+TN0)E{_7;fXdJUVM@{x&TGhPQgtCM}%WpN5Zo_#;X4IGjMy_pQvVo0D)E2%~!k$w~#K%9LRKGZ$ ztBde+P7GL+uIw{?1$}LSI9mOfHzGW!NO0&c{{BLPFSh`WB~lkaNof>ckz@MMM`NpZ zFr%T71vv$@aa}NUN5Z;mWBS*vS~|&KX}o957d?4u#U?VM59Zf;idU+0!aeAgm~J9D zeY(E|k^&Oi9;5rJFvDS9V#mRQ*C81V18=WhwaQv@9P+@&`2a9@j{qR9x@pZCo3bOB zu3HGtESg$9&H230MqfMwpXTm3##--~4y~R#d=PN01;%VJ-n5K0T7r& zdU`+sEL^%TXIOi-e{PmN7)HJ07@0GGnF{?p!j?u7510}LM&E{s2#6N%0LyQNSUx(s z06kNpn+>$HaQ{=K}k1bk8@*b;mAZD`wv(d_^f@Vqr;e3UyUqOhsgBWMp zzWoF+Lp&-u3o(mED8~>#-T+2v!8w6OD#%wgg3Rc?N#cBTmn_18Pf~@NJq?CR+GqOY z&u`VqEayxW>MuR!fdpIHy$lNfJm6@WNu3xt9Kdme){nsgXnuHy=Ii$B=IHlimwg!9z@jorc6M<+~Og?F$z#ZWOfr9qD_~7bM!-kY;-7 z(?ii{sk))G3{(^ef6JadXCar)C$;nLazr5NtFd_ygDyeM%;o?>Lx72wu%S%%ZzUrM zBw6JLf^Be~^2NN&KTHP5TFbk(l8F?mFa3WcYyOuM-O>VB=ON^Ssf<;tY7)-JpAI!( zp+pQ3e|Fo%Ie8{iTqGFd13zC*{J;6-y)G=ZI8?->6fvy@!q)Rl5{?pLP?6{f5S?;# zS!|jJLlH)8z@{ZfsuW;TtGk+kkt}F(Dln8UTfcAD&~wOIn_zx&z728WEG8r>Ck~Ivow zA^o5S6WvI{^9tdHgs5D$ zZm-cF+qT^_EWP*hC>MBA+IF_IsDCCE78E>!xP(Lg62pt7Na=*2La322Ymo@MKH5GT zBCi3;3^L)TKV65q?EbY=;!V1@krhq2e}~ zRUAwywh=9{#sY{@7@M%h>4WQ(=m3*hq+GwqVj2{k9c$>207>iWWI0r|wCqrHuzv7m zm%vs1913{&B-E*7NT6sh5v2r{Pis{jICov-{+aj3uQv5m^+AUYFM zitw5xK)NOoqZ$w*ZWvxeFPu2E#y5yVn;i$KJ(RY95Y5rEkY^i#WOhx!h84b0XkDcp z0#HurXV`>aKwbn`B+R<7-(JO?at)*PTe&}@f!q^h5Ti)KPM^ZinS>ja zy)AyR#Vz%9E4amj^Ydj$c%~a5&v3E-iG6oR(^22 ziMzM`PTIYDH{`}u(6+L?dP}(IIWO@XXydtR(lG0cJz4&R*| z#^zFUhRzD~qp1YNV*LlKc5!nE4b$d$s=Z^(gk^-G1E>F7FEumMfwTkf5UKMxSib0;TIH_o)|JB^wny3rc@{RxcoDPw!k8rpV5GMfB zf98F|J9rLml%GO$YY7X=<;!ojMM}OxLv+DssjBUbbgVy7DA1+_Ap(k9^ac3369V!V z2>lqxUIhUaTGxW!pQP;kllfZH%{qY0K#ckk{arIW^XbsgR)i?PG;1&pCE?seW63kW*o)BPYLtY#V#5b-Y(B>cjICyDKUc%SP9ssf(eR>%cD z zF?={Yv{9-;+J>OWJ{Sk_V|lIg&}z-FLLV1vQ0r+A6^Asl;%L#RQb{^%^jdO8d<0Wt1` z=-I%36kq/Resources + ) + ++# Copy shape predictor trained dataset to build directory ++set(DLIB_SHAPE_PREDICTOR_DATA ${CMAKE_CURRENT_SOURCE_DIR}/../shape_predictor_68_face_landmarks.dat ++ CACHE FILEPATH "Path to dlib shape predictor trained dataset") ++add_custom_command( ++ TARGET ${APP_NAME} ++ POST_BUILD ++ COMMAND ++ ${CMAKE_COMMAND} -E ++ copy ${DLIB_SHAPE_PREDICTOR_DATA} $/ ++) ++ + # You can change target that renderer draws by enabling following definition. + # + # * USE_RENDER_TARGET +diff -pruN --exclude build ./demo_clean/scripts/make_gcc ./demo_dev/scripts/make_gcc +--- ./demo_clean/scripts/make_gcc 2020-07-12 16:16:33.999809687 +0100 ++++ ./demo_dev/scripts/make_gcc 2020-07-11 21:22:23.615043956 +0100 +@@ -9,5 +9,6 @@ BUILD_PATH=$SCRIPT_PATH/../build/make_gc + # Run CMake. + cmake -S "$CMAKE_PATH" \ + -B "$BUILD_PATH" \ +- -D CMAKE_BUILD_TYPE=Release +-cd "$BUILD_PATH" && make ++ -D CMAKE_BUILD_TYPE=Release \ ++ -D USE_AVX_INSTRUCTIONS=1 ++cd "$BUILD_PATH" && make -j4 +diff -pruN --exclude build ./demo_clean/src/CMakeLists.txt ./demo_dev/src/CMakeLists.txt +--- ./demo_clean/src/CMakeLists.txt 2020-07-12 16:16:33.999809687 +0100 ++++ ./demo_dev/src/CMakeLists.txt 2020-07-11 17:39:18.358435702 +0100 +@@ -19,6 +19,4 @@ target_sources(${APP_NAME} + ${CMAKE_CURRENT_SOURCE_DIR}/LAppView.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/LAppView.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp +- ${CMAKE_CURRENT_SOURCE_DIR}/TouchManager.cpp +- ${CMAKE_CURRENT_SOURCE_DIR}/TouchManager.hpp + ) +diff -pruN --exclude build ./demo_clean/src/LAppDelegate.cpp ./demo_dev/src/LAppDelegate.cpp +--- ./demo_clean/src/LAppDelegate.cpp 2020-07-12 16:16:33.999809687 +0100 ++++ ./demo_dev/src/LAppDelegate.cpp 2020-07-11 17:35:02.414902548 +0100 +@@ -45,7 +45,8 @@ void LAppDelegate::ReleaseInstance() + s_instance = NULL; + } + +-bool LAppDelegate::Initialize() ++bool LAppDelegate::Initialize(int initWindowWidth, int initWindowHeight, ++ const char *windowTitle) + { + if (DebugLogEnable) + { +@@ -63,7 +64,13 @@ bool LAppDelegate::Initialize() + } + + // Windowの生成_ +- _window = glfwCreateWindow(RenderTargetWidth, RenderTargetHeight, "SAMPLE", NULL, NULL); ++ _window = glfwCreateWindow( ++ initWindowWidth ? initWindowWidth : RenderTargetWidth, ++ initWindowHeight ? initWindowHeight : RenderTargetHeight, ++ windowTitle ? windowTitle : "SAMPLE", ++ NULL, ++ NULL); ++ + if (_window == NULL) + { + if (DebugLogEnable) +@@ -95,10 +102,6 @@ bool LAppDelegate::Initialize() + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + +- //コールバック関数の登録 +- glfwSetMouseButtonCallback(_window, EventHandler::OnMouseCallBack); +- glfwSetCursorPosCallback(_window, EventHandler::OnMouseCallBack); +- + // ウィンドウサイズ記憶 + int width, height; + glfwGetWindowSize(LAppDelegate::GetInstance()->GetWindow(), &width, &height); +@@ -111,8 +114,6 @@ bool LAppDelegate::Initialize() + // Cubism3の初期化 + InitializeCubism(); + +- SetRootDirectory(); +- + //load model + LAppLive2DManager::GetInstance(); + +@@ -214,49 +215,6 @@ void LAppDelegate::InitializeCubism() + LAppPal::UpdateTime(); + } + +-void LAppDelegate::OnMouseCallBack(GLFWwindow* window, int button, int action, int modify) +-{ +- if (_view == NULL) +- { +- return; +- } +- if (GLFW_MOUSE_BUTTON_LEFT != button) +- { +- return; +- } +- +- if (GLFW_PRESS == action) +- { +- _captured = true; +- _view->OnTouchesBegan(_mouseX, _mouseY); +- } +- else if (GLFW_RELEASE == action) +- { +- if (_captured) +- { +- _captured = false; +- _view->OnTouchesEnded(_mouseX, _mouseY); +- } +- } +-} +- +-void LAppDelegate::OnMouseCallBack(GLFWwindow* window, double x, double y) +-{ +- _mouseX = static_cast(x); +- _mouseY = static_cast(y); +- +- if (!_captured) +- { +- return; +- } +- if (_view == NULL) +- { +- return; +- } +- +- _view->OnTouchesMoved(_mouseX, _mouseY); +-} +- + GLuint LAppDelegate::CreateShader() + { + //バーテックスシェーダのコンパイル +@@ -299,29 +257,9 @@ GLuint LAppDelegate::CreateShader() + return programId; + } + +-void LAppDelegate::SetRootDirectory() ++void LAppDelegate::SetRootDirectory(std::string rootDir) + { +- char path[1024]; +- ssize_t len = readlink("/proc/self/exe", path, 1024 - 1); +- +- if (len != -1) +- { +- path[len] = '\0'; +- } +- +- std::string pathString(path); +- +- pathString = pathString.substr(0, pathString.rfind("Demo")); +- Csm::csmVector splitStrings = this->Split(pathString, '/'); +- +- this->_rootDirectory = ""; +- +- for(int i = 0; i < splitStrings.GetSize(); i++) +- { +- this->_rootDirectory = this->_rootDirectory + "/" +splitStrings[i]; +- } +- +- this->_rootDirectory += "/"; ++ this->_rootDirectory = rootDir + "/"; + } + + Csm::csmVector LAppDelegate::Split(const std::string& baseString, char delimiter) +diff -pruN --exclude build ./demo_clean/src/LAppDelegate.hpp ./demo_dev/src/LAppDelegate.hpp +--- ./demo_clean/src/LAppDelegate.hpp 2020-07-12 16:16:33.999809687 +0100 ++++ ./demo_dev/src/LAppDelegate.hpp 2020-07-11 17:34:40.778602504 +0100 +@@ -40,7 +40,8 @@ public: + /** + * @brief APPに必要なものを初期化する。 + */ +- bool Initialize(); ++ bool Initialize(int initWindowWidth = 0, int initWindowHeight = 0, ++ const char *windowTitle = "SAMPLE"); + + /** + * @brief 解放する。 +@@ -53,25 +54,6 @@ public: + void Run(); + + /** +- * @brief OpenGL用 glfwSetMouseButtonCallback用関数。 +- * +- * @param[in] window コールバックを呼んだWindow情報 +- * @param[in] button ボタン種類 +- * @param[in] action 実行結果 +- * @param[in] modify +- */ +- void OnMouseCallBack(GLFWwindow* window, int button, int action, int modify); +- +- /** +- * @brief OpenGL用 glfwSetCursorPosCallback用関数。 +- * +- * @param[in] window コールバックを呼んだWindow情報 +- * @param[in] x x座標 +- * @param[in] y x座標 +- */ +- void OnMouseCallBack(GLFWwindow* window, double x, double y); +- +- /** + * @brief シェーダーを登録する。 + */ + GLuint CreateShader(); +@@ -98,8 +80,10 @@ public: + + /** + * @brief ルートディレクトリを設定する。 ++ * ++ * @param[in] rootDir : The root directory to set to. + */ +- void SetRootDirectory(); ++ void SetRootDirectory(std::string rootDir); + + /** + * @brief ルートディレクトリを取得する。 +@@ -146,24 +130,3 @@ private: + int _windowWidth; ///< Initialize関数で設定したウィンドウ幅 + int _windowHeight; ///< Initialize関数で設定したウィンドウ高さ + }; +- +-class EventHandler +-{ +-public: +- /** +- * @brief glfwSetMouseButtonCallback用コールバック関数。 +- */ +- static void OnMouseCallBack(GLFWwindow* window, int button, int action, int modify) +- { +- LAppDelegate::GetInstance()->OnMouseCallBack(window, button, action, modify); +- } +- +- /** +- * @brief glfwSetCursorPosCallback用コールバック関数。 +- */ +- static void OnMouseCallBack(GLFWwindow* window, double x, double y) +- { +- LAppDelegate::GetInstance()->OnMouseCallBack(window, x, y); +- } +- +-}; +diff -pruN --exclude build ./demo_clean/src/LAppLive2DManager.cpp ./demo_dev/src/LAppLive2DManager.cpp +--- ./demo_clean/src/LAppLive2DManager.cpp 2020-07-12 16:16:33.999809687 +0100 ++++ ./demo_dev/src/LAppLive2DManager.cpp 2020-07-11 23:20:11.548419176 +0100 +@@ -52,9 +52,10 @@ void LAppLive2DManager::ReleaseInstance( + + LAppLive2DManager::LAppLive2DManager() + : _viewMatrix(NULL) +- , _sceneIndex(0) ++ , _projScaleFactor(1.0f) ++ , _translateX(0.0f) ++ , _translateY(0.0f) + { +- ChangeScene(_sceneIndex); + } + + LAppLive2DManager::~LAppLive2DManager() +@@ -98,26 +99,6 @@ void LAppLive2DManager::OnTap(csmFloat32 + { + LAppPal::PrintLog("[APP]tap point: {x:%.2f y:%.2f}", x, y); + } +- +- for (csmUint32 i = 0; i < _models.GetSize(); i++) +- { +- if (_models[i]->HitTest(HitAreaNameHead, x, y)) +- { +- if (DebugLogEnable) +- { +- LAppPal::PrintLog("[APP]hit area: [%s]", HitAreaNameHead); +- } +- _models[i]->SetRandomExpression(); +- } +- else if (_models[i]->HitTest(HitAreaNameBody, x, y)) +- { +- if (DebugLogEnable) +- { +- LAppPal::PrintLog("[APP]hit area: [%s]", HitAreaNameBody); +- } +- _models[i]->StartRandomMotion(MotionGroupTapBody, PriorityNormal, FinishedMotion); +- } +- } + } + + void LAppLive2DManager::OnUpdate() const +@@ -125,7 +106,9 @@ void LAppLive2DManager::OnUpdate() const + CubismMatrix44 projection; + int width, height; + glfwGetWindowSize(LAppDelegate::GetInstance()->GetWindow(), &width, &height); +- projection.Scale(1.0f, static_cast(width) / static_cast(height)); ++ projection.Scale(_projScaleFactor, ++ _projScaleFactor * static_cast(width) / static_cast(height)); ++ projection.Translate(_translateX, _translateY); + + if (_viewMatrix != NULL) + { +@@ -148,26 +131,10 @@ void LAppLive2DManager::OnUpdate() const + } + } + +-void LAppLive2DManager::NextScene() +-{ +- csmInt32 no = (_sceneIndex + 1) % ModelDirSize; +- ChangeScene(no); +-} +- +-void LAppLive2DManager::ChangeScene(Csm::csmInt32 index) ++void LAppLive2DManager::SetModel(std::string modelName) + { +- _sceneIndex = index; +- if (DebugLogEnable) +- { +- LAppPal::PrintLog("[APP]model index: %d", _sceneIndex); +- } +- +- // ModelDir[]に保持したディレクトリ名から +- // model3.jsonのパスを決定する. +- // ディレクトリ名とmodel3.jsonの名前を一致させておくこと. +- std::string model = ModelDir[index]; +- std::string modelPath = LAppDelegate::GetInstance()->GetRootDirectory() + ResourcesPath + model + "/"; +- std::string modelJsonName = ModelDir[index]; ++ std::string modelPath = LAppDelegate::GetInstance()->GetRootDirectory() + ResourcesPath + modelName + "/"; ++ std::string modelJsonName = modelName; + modelJsonName += ".model3.json"; + + ReleaseAllModel(); +@@ -215,3 +182,20 @@ csmUint32 LAppLive2DManager::GetModelNum + { + return _models.GetSize(); + } ++ ++void LAppLive2DManager::SetFacialLandmarkDetector(FacialLandmarkDetector *detector) ++{ ++ for (auto it = _models.Begin(); it != _models.End(); ++it) ++ { ++ (*it)->SetFacialLandmarkDetector(detector); ++ } ++} ++ ++void LAppLive2DManager::SetProjectionScaleTranslate(float scaleFactor, ++ float translateX, ++ float translateY) ++{ ++ _projScaleFactor = scaleFactor; ++ _translateX = translateX; ++ _translateY = translateY; ++} +diff -pruN --exclude build ./demo_clean/src/LAppLive2DManager.hpp ./demo_dev/src/LAppLive2DManager.hpp +--- ./demo_clean/src/LAppLive2DManager.hpp 2020-07-12 16:16:33.999809687 +0100 ++++ ./demo_dev/src/LAppLive2DManager.hpp 2020-07-11 23:21:17.969484538 +0100 +@@ -6,12 +6,15 @@ + */ + #pragma once + ++#include + #include + #include + #include + + class LAppModel; + ++class FacialLandmarkDetector; ++ + /** + * @brief サンプルアプリケーションにおいてCubismModelを管理するクラス
+ * モデル生成と破棄、タップイベントの処理、モデル切り替えを行う。 +@@ -72,16 +75,12 @@ public: + void OnUpdate() const; + + /** +- * @brief 次のシーンに切り替える
+- * サンプルアプリケーションではモデルセットの切り替えを行う。 +- */ +- void NextScene(); +- +- /** +- * @brief シーンを切り替える
+- * サンプルアプリケーションではモデルセットの切り替えを行う。 +- */ +- void ChangeScene(Csm::csmInt32 index); ++ * @brief Set model data ++ * ++ * @param[in] modelName : Name of model, should be the same for both ++ * the directory and the model3.json file ++ */ ++ void SetModel(std::string modelName); + + /** + * @brief モデル個数を得る +@@ -89,6 +88,24 @@ public: + */ + Csm::csmUint32 GetModelNum() const; + ++ /** ++ * @brief Set the pointer to the FacialLandmarkDetector instance ++ * ++ * @param[in] detector : Pointer to FacialLandmarkDetector instance ++ */ ++ void SetFacialLandmarkDetector(FacialLandmarkDetector *detector); ++ ++ /** ++ * @brief Set projection scale factor and translation parameters ++ * ++ * @param[in] scaleFactor : Scale factor applied in both X and Y directions ++ * @param[in] translateX : Translation in X direction ++ * @param[in] translateY : Translation in Y direction ++ */ ++ void SetProjectionScaleTranslate(float scaleFactor, ++ float translateX, ++ float translateY); ++ + private: + /** + * @brief コンストラクタ +@@ -102,5 +119,8 @@ private: + + Csm::CubismMatrix44* _viewMatrix; ///< モデル描画に用いるView行列 + Csm::csmVector _models; ///< モデルインスタンスのコンテナ +- Csm::csmInt32 _sceneIndex; ///< 表示するシーンのインデックス値 ++ ++ float _projScaleFactor; ++ float _translateX; ++ float _translateY; + }; +diff -pruN --exclude build ./demo_clean/src/LAppModel.cpp ./demo_dev/src/LAppModel.cpp +--- ./demo_clean/src/LAppModel.cpp 2020-07-12 16:16:33.999809687 +0100 ++++ ./demo_dev/src/LAppModel.cpp 2020-07-11 15:57:43.784019311 +0100 +@@ -21,6 +21,8 @@ + #include "LAppTextureManager.hpp" + #include "LAppDelegate.hpp" + ++#include "facial_landmark_detector.h" ++ + using namespace Live2D::Cubism::Framework; + using namespace Live2D::Cubism::Framework::DefaultParameterId; + using namespace LAppDefine; +@@ -128,30 +130,6 @@ void LAppModel::SetupModel(ICubismModelS + DeleteBuffer(buffer, path.GetRawString()); + } + +- //Expression +- if (_modelSetting->GetExpressionCount() > 0) +- { +- const csmInt32 count = _modelSetting->GetExpressionCount(); +- for (csmInt32 i = 0; i < count; i++) +- { +- csmString name = _modelSetting->GetExpressionName(i); +- csmString path = _modelSetting->GetExpressionFileName(i); +- path = _modelHomeDir + path; +- +- buffer = CreateBuffer(path.GetRawString(), &size); +- ACubismMotion* motion = LoadExpression(buffer, size, name.GetRawString()); +- +- if (_expressions[name] != NULL) +- { +- ACubismMotion::Delete(_expressions[name]); +- _expressions[name] = NULL; +- } +- _expressions[name] = motion; +- +- DeleteBuffer(buffer, path.GetRawString()); +- } +- } +- + //Physics + if (strcmp(_modelSetting->GetPhysicsFileName(), "") != 0) + { +@@ -174,27 +152,6 @@ void LAppModel::SetupModel(ICubismModelS + DeleteBuffer(buffer, path.GetRawString()); + } + +- //EyeBlink +- if (_modelSetting->GetEyeBlinkParameterCount() > 0) +- { +- _eyeBlink = CubismEyeBlink::Create(_modelSetting); +- } +- +- //Breath +- { +- _breath = CubismBreath::Create(); +- +- csmVector breathParameters; +- +- breathParameters.PushBack(CubismBreath::BreathParameterData(_idParamAngleX, 0.0f, 15.0f, 6.5345f, 0.5f)); +- breathParameters.PushBack(CubismBreath::BreathParameterData(_idParamAngleY, 0.0f, 8.0f, 3.5345f, 0.5f)); +- breathParameters.PushBack(CubismBreath::BreathParameterData(_idParamAngleZ, 0.0f, 10.0f, 5.5345f, 0.5f)); +- breathParameters.PushBack(CubismBreath::BreathParameterData(_idParamBodyAngleX, 0.0f, 4.0f, 15.5345f, 0.5f)); +- breathParameters.PushBack(CubismBreath::BreathParameterData(CubismFramework::GetIdManager()->GetId(ParamBreath), 0.5f, 0.5f, 3.2345f, 0.5f)); +- +- _breath->SetParameters(breathParameters); +- } +- + //UserData + if (strcmp(_modelSetting->GetUserDataFile(), "") != 0) + { +@@ -205,24 +162,6 @@ void LAppModel::SetupModel(ICubismModelS + DeleteBuffer(buffer, path.GetRawString()); + } + +- // EyeBlinkIds +- { +- csmInt32 eyeBlinkIdCount = _modelSetting->GetEyeBlinkParameterCount(); +- for (csmInt32 i = 0; i < eyeBlinkIdCount; ++i) +- { +- _eyeBlinkIds.PushBack(_modelSetting->GetEyeBlinkParameterId(i)); +- } +- } +- +- // LipSyncIds +- { +- csmInt32 lipSyncIdCount = _modelSetting->GetLipSyncParameterCount(); +- for (csmInt32 i = 0; i < lipSyncIdCount; ++i) +- { +- _lipSyncIds.PushBack(_modelSetting->GetLipSyncParameterId(i)); +- } +- } +- + //Layout + csmMap layout; + _modelSetting->GetLayoutMap(layout); +@@ -230,14 +169,6 @@ void LAppModel::SetupModel(ICubismModelS + + _model->SaveParameters(); + +- for (csmInt32 i = 0; i < _modelSetting->GetMotionGroupCount(); i++) +- { +- const csmChar* group = _modelSetting->GetMotionGroupName(i); +- PreloadMotionGroup(group); +- } +- +- _motionManager->StopAllMotions(); +- + _updating = false; + _initialized = true; + } +@@ -335,59 +266,29 @@ void LAppModel::Update() + const csmFloat32 deltaTimeSeconds = LAppPal::GetDeltaTime(); + _userTimeSeconds += deltaTimeSeconds; + +- _dragManager->Update(deltaTimeSeconds); +- _dragX = _dragManager->GetX(); +- _dragY = _dragManager->GetY(); +- +- // モーションによるパラメータ更新の有無 +- csmBool motionUpdated = false; +- +- //----------------------------------------------------------------- +- _model->LoadParameters(); // 前回セーブされた状態をロード +- if (_motionManager->IsFinished()) +- { +- // モーションの再生がない場合、待機モーションの中からランダムで再生する +- StartRandomMotion(MotionGroupIdle, PriorityIdle); +- } +- else +- { +- motionUpdated = _motionManager->UpdateMotion(_model, deltaTimeSeconds); // モーションを更新 +- } +- _model->SaveParameters(); // 状態を保存 +- //----------------------------------------------------------------- +- +- // まばたき +- if (!motionUpdated) +- { +- if (_eyeBlink != NULL) +- { +- // メインモーションの更新がないとき +- _eyeBlink->UpdateParameters(_model, deltaTimeSeconds); // 目パチ +- } +- } +- +- if (_expressionManager != NULL) ++ if (_detector) + { +- _expressionManager->UpdateMotion(_model, deltaTimeSeconds); // 表情でパラメータ更新(相対変化) +- } +- +- //ドラッグによる変化 +- //ドラッグによる顔の向きの調整 +- _model->AddParameterValue(_idParamAngleX, _dragX * 30); // -30から30の値を加える +- _model->AddParameterValue(_idParamAngleY, _dragY * 30); +- _model->AddParameterValue(_idParamAngleZ, _dragX * _dragY * -30); +- +- //ドラッグによる体の向きの調整 +- _model->AddParameterValue(_idParamBodyAngleX, _dragX * 10); // -10から10の値を加える +- +- //ドラッグによる目の向きの調整 +- _model->AddParameterValue(_idParamEyeBallX, _dragX); // -1から1の値を加える +- _model->AddParameterValue(_idParamEyeBallY, _dragY); ++ auto idMan = CubismFramework::GetIdManager(); ++ auto params = _detector->getParams(); + +- // 呼吸など +- if (_breath != NULL) +- { +- _breath->UpdateParameters(_model, deltaTimeSeconds); ++ _model->SetParameterValue(idMan->GetId("ParamEyeLOpen"), ++ params.leftEyeOpenness); ++ _model->SetParameterValue(idMan->GetId("ParamEyeROpen"), ++ params.rightEyeOpenness); ++ _model->SetParameterValue(idMan->GetId("ParamMouthForm"), ++ params.mouthForm); ++ _model->SetParameterValue(idMan->GetId("ParamMouthOpenY"), ++ params.mouthOpenness); ++ _model->SetParameterValue(idMan->GetId("ParamEyeLSmile"), ++ params.leftEyeSmile); ++ _model->SetParameterValue(idMan->GetId("ParamEyeRSmile"), ++ params.rightEyeSmile); ++ _model->SetParameterValue(idMan->GetId("ParamAngleX"), ++ params.faceXAngle); ++ _model->SetParameterValue(idMan->GetId("ParamAngleY"), ++ params.faceYAngle); ++ _model->SetParameterValue(idMan->GetId("ParamAngleZ"), ++ params.faceZAngle); + } + + // 物理演算の設定 +@@ -396,17 +297,6 @@ void LAppModel::Update() + _physics->Evaluate(_model, deltaTimeSeconds); + } + +- // リップシンクの設定 +- if (_lipSync) +- { +- csmFloat32 value = 0; // リアルタイムでリップシンクを行う場合、システムから音量を取得して0〜1の範囲で値を入力します。 +- +- for (csmUint32 i = 0; i < _lipSyncIds.GetSize(); ++i) +- { +- _model->AddParameterValue(_lipSyncIds[i], value, 0.8f); +- } +- } +- + // ポーズの設定 + if (_pose != NULL) + { +@@ -626,3 +516,9 @@ Csm::Rendering::CubismOffscreenFrame_Ope + { + return _renderBuffer; + } ++ ++void LAppModel::SetFacialLandmarkDetector(FacialLandmarkDetector *detector) ++{ ++ _detector = detector; ++} ++ +diff -pruN --exclude build ./demo_clean/src/LAppModel.hpp ./demo_dev/src/LAppModel.hpp +--- ./demo_clean/src/LAppModel.hpp 2020-07-12 16:16:33.999809687 +0100 ++++ ./demo_dev/src/LAppModel.hpp 2020-07-11 15:40:18.977286166 +0100 +@@ -13,6 +13,7 @@ + #include + #include + ++#include "facial_landmark_detector.h" + + /** + * @brief ユーザーが実際に使用するモデルの実装クラス
+@@ -113,6 +114,13 @@ public: + */ + Csm::Rendering::CubismOffscreenFrame_OpenGLES2& GetRenderBuffer(); + ++ /** ++ * @brief Set the pointer to the FacialLandmarkDetector instance ++ * ++ * @param[in] detector : Pointer to FacialLandmarkDetector instance ++ */ ++ void SetFacialLandmarkDetector(FacialLandmarkDetector *detector); ++ + protected: + /** + * @brief モデルを描画する処理。モデルを描画する空間のView-Projection行列を渡す。 +@@ -183,6 +191,8 @@ private: + const Csm::CubismId* _idParamEyeBallY; ///< パラメータID: ParamEyeBallXY + + Csm::Rendering::CubismOffscreenFrame_OpenGLES2 _renderBuffer; ///< フレームバッファ以外の描画先 ++ ++ FacialLandmarkDetector *_detector; + }; + + +diff -pruN --exclude build ./demo_clean/src/LAppPal.cpp ./demo_dev/src/LAppPal.cpp +--- ./demo_clean/src/LAppPal.cpp 2020-07-12 16:16:33.999809687 +0100 ++++ ./demo_dev/src/LAppPal.cpp 2020-07-11 23:29:09.084910139 +0100 +@@ -6,6 +6,7 @@ + */ + + #include "LAppPal.hpp" ++#include + #include + #include + #include +@@ -45,10 +46,7 @@ csmByte* LAppPal::LoadFileAsBytes(const + file.open(path, std::ios::in | std::ios::binary); + if (!file.is_open()) + { +- if (DebugLogEnable) +- { +- PrintLog("file open error"); +- } ++ throw std::runtime_error("Failed to open file " + filePath); + return NULL; + } + file.read(buf, size); +diff -pruN --exclude build ./demo_clean/src/LAppTextureManager.cpp ./demo_dev/src/LAppTextureManager.cpp +--- ./demo_clean/src/LAppTextureManager.cpp 2020-07-12 16:16:33.999809687 +0100 ++++ ./demo_dev/src/LAppTextureManager.cpp 2020-07-11 22:22:18.004965003 +0100 +@@ -96,6 +96,46 @@ LAppTextureManager::TextureInfo* LAppTex + + } + ++LAppTextureManager::TextureInfo* LAppTextureManager::CreateTextureFromColor( ++ uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha ++) ++{ ++ int width = 8, height = 8; ++ ++ uint8_t pixels[height][width][4]; ++ for (std::size_t h = 0; h < height; h++) ++ { ++ for (std::size_t w = 0; w < width; w++) ++ { ++ pixels[h][w][0] = red; ++ pixels[h][w][1] = green; ++ pixels[h][w][2] = blue; ++ pixels[h][w][3] = alpha; ++ } ++ } ++ ++ GLuint textureId; ++ glGenTextures(1, &textureId); ++ glBindTexture(GL_TEXTURE_2D, textureId); ++ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); ++ ++ glGenerateMipmap(GL_TEXTURE_2D); ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); ++ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); ++ glBindTexture(GL_TEXTURE_2D, 0); ++ ++ ++ LAppTextureManager::TextureInfo* textureInfo = new LAppTextureManager::TextureInfo(); ++ textureInfo->fileName = ""; ++ textureInfo->width = width; ++ textureInfo->height = height; ++ textureInfo->id = textureId; ++ ++ _textures.PushBack(textureInfo); ++ ++ return textureInfo; ++} ++ + void LAppTextureManager::ReleaseTextures() + { + for (Csm::csmUint32 i = 0; i < _textures.GetSize(); i++) +diff -pruN --exclude build ./demo_clean/src/LAppTextureManager.hpp ./demo_dev/src/LAppTextureManager.hpp +--- ./demo_clean/src/LAppTextureManager.hpp 2020-07-12 16:16:33.999809687 +0100 ++++ ./demo_dev/src/LAppTextureManager.hpp 2020-07-11 17:36:31.180131039 +0100 +@@ -72,6 +72,8 @@ public: + */ + TextureInfo* CreateTextureFromPngFile(std::string fileName); + ++ TextureInfo *CreateTextureFromColor(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha = 255); ++ + /** + * @brief 画像の解放 + * +diff -pruN --exclude build ./demo_clean/src/LAppView.cpp ./demo_dev/src/LAppView.cpp +--- ./demo_clean/src/LAppView.cpp 2020-07-12 16:16:34.003809759 +0100 ++++ ./demo_dev/src/LAppView.cpp 2020-07-11 17:38:06.905451955 +0100 +@@ -13,7 +13,6 @@ + #include "LAppLive2DManager.hpp" + #include "LAppTextureManager.hpp" + #include "LAppDefine.hpp" +-#include "TouchManager.hpp" + #include "LAppSprite.hpp" + #include "LAppModel.hpp" + +@@ -26,8 +25,6 @@ using namespace LAppDefine; + LAppView::LAppView(): + _programId(0), + _back(NULL), +- _gear(NULL), +- _power(NULL), + _renderSprite(NULL), + _renderTarget(SelectTarget_None) + { +@@ -35,8 +32,6 @@ LAppView::LAppView(): + _clearColor[1] = 1.0f; + _clearColor[2] = 1.0f; + _clearColor[3] = 0.0f; +- // タッチ関係のイベント管理 +- _touchManager = new TouchManager(); + + // デバイス座標からスクリーン座標に変換するための + _deviceToScreen = new CubismMatrix44(); +@@ -52,10 +47,7 @@ LAppView::~LAppView() + + delete _viewMatrix; + delete _deviceToScreen; +- delete _touchManager; + delete _back; +- delete _gear; +- delete _power; + } + + void LAppView::Initialize() +@@ -97,9 +89,6 @@ void LAppView::Initialize() + void LAppView::Render() + { + _back->Render(); +- _gear->Render(); +- _power->Render(); +- + + LAppLive2DManager* Live2DManager = LAppLive2DManager::GetInstance(); + +@@ -139,35 +128,17 @@ void LAppView::InitializeSprite() + glfwGetWindowSize(LAppDelegate::GetInstance()->GetWindow(), &width, &height); + + LAppTextureManager* textureManager = LAppDelegate::GetInstance()->GetTextureManager(); +- const string resourcesPath = LAppDelegate::GetInstance()->GetRootDirectory() + ResourcesPath; + +- string imageName = BackImageName; +- LAppTextureManager::TextureInfo* backgroundTexture = textureManager->CreateTextureFromPngFile(resourcesPath + imageName); ++ ++ LAppTextureManager::TextureInfo* backgroundTexture = ++ textureManager->CreateTextureFromColor(0, 255, 0); + + float x = width * 0.5f; + float y = height * 0.5f; +- float fWidth = static_cast(backgroundTexture->width * 2.0f); +- float fHeight = static_cast(height) * 0.95f; ++ float fWidth = static_cast(width); ++ float fHeight = static_cast(height); + _back = new LAppSprite(x, y, fWidth, fHeight, backgroundTexture->id, _programId); + +- imageName = GearImageName; +- LAppTextureManager::TextureInfo* gearTexture = textureManager->CreateTextureFromPngFile(resourcesPath + imageName); +- +- x = static_cast(width - gearTexture->width * 0.5f); +- y = static_cast(height - gearTexture->height * 0.5f); +- fWidth = static_cast(gearTexture->width); +- fHeight = static_cast(gearTexture->height); +- _gear = new LAppSprite(x, y, fWidth, fHeight, gearTexture->id, _programId); +- +- imageName = PowerImageName; +- LAppTextureManager::TextureInfo* powerTexture = textureManager->CreateTextureFromPngFile(resourcesPath + imageName); +- +- x = static_cast(width - powerTexture->width * 0.5f); +- y = static_cast(powerTexture->height * 0.5f); +- fWidth = static_cast(powerTexture->width); +- fHeight = static_cast(powerTexture->height); +- _power = new LAppSprite(x, y, fWidth, fHeight, powerTexture->id, _programId); +- + // 画面全体を覆うサイズ + x = width * 0.5f; + y = height * 0.5f; +@@ -175,52 +146,6 @@ void LAppView::InitializeSprite() + + } + +-void LAppView::OnTouchesBegan(float px, float py) const +-{ +- _touchManager->TouchesBegan(px, py); +-} +- +-void LAppView::OnTouchesMoved(float px, float py) const +-{ +- float viewX = this->TransformViewX(_touchManager->GetX()); +- float viewY = this->TransformViewY(_touchManager->GetY()); +- +- _touchManager->TouchesMoved(px, py); +- +- LAppLive2DManager* Live2DManager = LAppLive2DManager::GetInstance(); +- Live2DManager->OnDrag(viewX, viewY); +-} +- +-void LAppView::OnTouchesEnded(float px, float py) const +-{ +- // タッチ終了 +- LAppLive2DManager* live2DManager = LAppLive2DManager::GetInstance(); +- live2DManager->OnDrag(0.0f, 0.0f); +- { +- +- // シングルタップ +- float x = _deviceToScreen->TransformX(_touchManager->GetX()); // 論理座標変換した座標を取得。 +- float y = _deviceToScreen->TransformY(_touchManager->GetY()); // 論理座標変換した座標を取得。 +- if (DebugTouchLogEnable) +- { +- LAppPal::PrintLog("[APP]touchesEnded x:%.2f y:%.2f", x, y); +- } +- live2DManager->OnTap(x, y); +- +- // 歯車にタップしたか +- if (_gear->IsHit(px, py)) +- { +- live2DManager->NextScene(); +- } +- +- // 電源ボタンにタップしたか +- if (_power->IsHit(px, py)) +- { +- LAppDelegate::GetInstance()->AppEnd(); +- } +- } +-} +- + float LAppView::TransformViewX(float deviceX) const + { + float screenX = _deviceToScreen->TransformX(deviceX); // 論理座標変換した座標を取得。 +@@ -362,32 +287,4 @@ void LAppView::ResizeSprite() + _back->ResetRect(x, y, fWidth, fHeight); + } + } +- +- if (_power) +- { +- GLuint id = _power->GetTextureId(); +- LAppTextureManager::TextureInfo* texInfo = textureManager->GetTextureInfoById(id); +- if (texInfo) +- { +- x = static_cast(width - texInfo->width * 0.5f); +- y = static_cast(texInfo->height * 0.5f); +- fWidth = static_cast(texInfo->width); +- fHeight = static_cast(texInfo->height); +- _power->ResetRect(x, y, fWidth, fHeight); +- } +- } +- +- if (_gear) +- { +- GLuint id = _gear->GetTextureId(); +- LAppTextureManager::TextureInfo* texInfo = textureManager->GetTextureInfoById(id); +- if (texInfo) +- { +- x = static_cast(width - texInfo->width * 0.5f); +- y = static_cast(height - texInfo->height * 0.5f); +- fWidth = static_cast(texInfo->width); +- fHeight = static_cast(texInfo->height); +- _gear->ResetRect(x, y, fWidth, fHeight); +- } +- } + } +diff -pruN --exclude build ./demo_clean/src/LAppView.hpp ./demo_dev/src/LAppView.hpp +--- ./demo_clean/src/LAppView.hpp 2020-07-12 16:16:33.999809687 +0100 ++++ ./demo_dev/src/LAppView.hpp 2020-07-11 17:38:25.541708705 +0100 +@@ -14,7 +14,6 @@ + #include "CubismFramework.hpp" + #include + +-class TouchManager; + class LAppSprite; + class LAppModel; + +@@ -66,30 +65,6 @@ public: + void ResizeSprite(); + + /** +- * @brief タッチされたときに呼ばれる。 +- * +- * @param[in] pointX スクリーンX座標 +- * @param[in] pointY スクリーンY座標 +- */ +- void OnTouchesBegan(float pointX, float pointY) const; +- +- /** +- * @brief タッチしているときにポインタが動いたら呼ばれる。 +- * +- * @param[in] pointX スクリーンX座標 +- * @param[in] pointY スクリーンY座標 +- */ +- void OnTouchesMoved(float pointX, float pointY) const; +- +- /** +- * @brief タッチが終了したら呼ばれる。 +- * +- * @param[in] pointX スクリーンX座標 +- * @param[in] pointY スクリーンY座標 +- */ +- void OnTouchesEnded(float pointX, float pointY) const; +- +- /** + * @brief X座標をView座標に変換する。 + * + * @param[in] deviceX デバイスX座標 +@@ -147,13 +122,10 @@ public: + void SetRenderTargetClearColor(float r, float g, float b); + + private: +- TouchManager* _touchManager; ///< タッチマネージャー + Csm::CubismMatrix44* _deviceToScreen; ///< デバイスからスクリーンへの行列 + Csm::CubismViewMatrix* _viewMatrix; ///< viewMatrix + GLuint _programId; ///< シェーダID + LAppSprite* _back; ///< 背景画像 +- LAppSprite* _gear; ///< ギア画像 +- LAppSprite* _power; ///< 電源画像 + + // レンダリング先を別ターゲットにする方式の場合に使用 + LAppSprite* _renderSprite; ///< モードによっては_renderBufferのテクスチャを描画 +diff -pruN --exclude build ./demo_clean/src/main.cpp ./demo_dev/src/main.cpp +--- ./demo_clean/src/main.cpp 2020-07-12 16:16:33.999809687 +0100 ++++ ./demo_dev/src/main.cpp 2020-07-12 15:06:29.194034887 +0100 +@@ -5,18 +5,156 @@ + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + ++#include ++#include ++#include ++ ++#ifdef __cpp_lib_filesystem ++#include ++namespace fs = std::filesystem; ++#else ++#include ++namespace fs = std::experimental::filesystem; ++#endif ++ ++ + #include "LAppDelegate.hpp" ++#include "LAppLive2DManager.hpp" ++#include "facial_landmark_detector.h" ++ ++struct CmdArgs ++{ ++ int windowWidth; ++ int windowHeight; ++ std::string windowTitle; ++ std::string rootDir; ++ float scaleFactor; ++ float translateX; ++ float translateY; ++ std::string modelName; ++ std::string cfgPath; // Path to config file for FacialLandmarkDetector ++}; ++ ++CmdArgs parseArgv(int argc, char *argv[]) ++{ ++ // I think the command-line args are simple enough to not justify using a library... ++ CmdArgs cmdArgs; ++ // Set default values ++ cmdArgs.windowWidth = 600; ++ cmdArgs.windowHeight = 600; ++ cmdArgs.windowTitle = "FacialLandmarksForCubism example"; ++ cmdArgs.rootDir = fs::current_path(); ++ cmdArgs.scaleFactor = 8.0f; ++ cmdArgs.translateX = 0.0f; ++ cmdArgs.translateY = -2.8f; ++ cmdArgs.modelName = "Haru"; ++ cmdArgs.cfgPath = ""; ++ ++ int i = 1; ++ while (i < argc) ++ { ++ std::string arg = argv[i]; ++ std::stringstream ss; ++ ++ if (arg == "--window-width" || arg == "-W") // capital W for consistency with height ++ { ++ ss << argv[i + 1]; ++ if (!(ss >> cmdArgs.windowWidth)) ++ { ++ throw std::runtime_error("Invalid argument for window width"); ++ } ++ } ++ else if (arg == "--window-height" || arg == "-H") // avoiding "-h", typically for help ++ { ++ ss << argv[i + 1]; ++ if (!(ss >> cmdArgs.windowHeight)) ++ { ++ throw std::runtime_error("Invalid argument for window height"); ++ } ++ } ++ else if (arg == "--window-title" || arg == "-t") ++ { ++ cmdArgs.windowTitle = argv[i + 1]; ++ } ++ else if (arg == "--root-dir" || arg == "-d") ++ { ++ cmdArgs.rootDir = argv[i + 1]; ++ } ++ else if (arg == "--scale-factor" || arg == "-f") ++ { ++ ss << argv[i + 1]; ++ if (!(ss >> cmdArgs.scaleFactor)) ++ { ++ throw std::runtime_error("Invalid argument for scale factor"); ++ } ++ } ++ else if (arg == "--translate-x" || arg == "-x") ++ { ++ ss << argv[i + 1]; ++ if (!(ss >> cmdArgs.translateX)) ++ { ++ throw std::runtime_error("Invalid argument for translate X"); ++ } ++ } ++ else if (arg == "--translate-y" || arg == "-y") ++ { ++ ss << argv[i + 1]; ++ if (!(ss >> cmdArgs.translateY)) ++ { ++ throw std::runtime_error("Invalid argument for translate Y"); ++ } ++ } ++ else if (arg == "--model" || arg == "-m") ++ { ++ cmdArgs.modelName = argv[i + 1]; ++ } ++ else if (arg == "--config" || arg == "-c") ++ { ++ cmdArgs.cfgPath = argv[i + 1]; ++ } ++ else ++ { ++ throw std::runtime_error("Unrecognized argument: " + arg); ++ } ++ ++ i += 2; ++ } ++ ++ return cmdArgs; ++} + + int main(int argc, char* argv[]) + { +- // create the application instance +- if (LAppDelegate::GetInstance()->Initialize() == GL_FALSE) ++ auto cmdArgs = parseArgv(argc, argv); ++ ++ LAppDelegate *delegate = LAppDelegate::GetInstance(); ++ ++ if (!delegate->Initialize(cmdArgs.windowWidth, ++ cmdArgs.windowHeight, ++ cmdArgs.windowTitle.c_str())) + { +- return 1; ++ throw std::runtime_error("Unable to initialize LAppDelegate"); + } + +- LAppDelegate::GetInstance()->Run(); ++ delegate->SetRootDirectory(cmdArgs.rootDir); ++ ++ FacialLandmarkDetector detector(cmdArgs.cfgPath); ++ ++ std::thread detectorThread(&FacialLandmarkDetector::mainLoop, ++ &detector); ++ ++ LAppLive2DManager *manager = LAppLive2DManager::GetInstance(); ++ manager->SetModel(cmdArgs.modelName); ++ ++ manager->SetProjectionScaleTranslate(cmdArgs.scaleFactor, ++ cmdArgs.translateX, ++ cmdArgs.translateY); ++ manager->SetFacialLandmarkDetector(&detector); ++ ++ delegate->Run(); ++ ++ detector.stop(); ++ detectorThread.join(); + + return 0; + } +- diff --git a/example/generate_patch.sh b/example/generate_patch.sh new file mode 100755 index 0000000..068f4b0 --- /dev/null +++ b/example/generate_patch.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +mkdir -p demo_clean +cp -r CubismSdkForNative-4-r.1/Samples/OpenGL/Demo/proj.linux.cmake/* ./demo_clean/ +diff -pruN --exclude build ./demo_clean ./demo_dev > ./demo.patch diff --git a/include/facial_landmark_detector.h b/include/facial_landmark_detector.h new file mode 100644 index 0000000..7c6f639 --- /dev/null +++ b/include/facial_landmark_detector.h @@ -0,0 +1,144 @@ +// -*- mode: c++ -*- + +#ifndef __FACIAL_LANDMARK_DETECTOR_H__ +#define __FACIAL_LANDMARK_DETECTOR_H__ + +/**** +Copyright (c) 2020 Adrian I. Lam + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +****/ + +#include +#include +#include +#include +#include +#include + +class FacialLandmarkDetector +{ +public: + struct Params + { + double leftEyeOpenness; + double rightEyeOpenness; + double leftEyeSmile; + double rightEyeSmile; + double mouthOpenness; + double mouthForm; + double faceXAngle; + double faceYAngle; + double faceZAngle; + // TODO eyebrows currently not supported... + // I'd like to include them, but the dlib detection is very + // noisy and inaccurate (at least for my face). + }; + + FacialLandmarkDetector(std::string cfgPath); + + Params getParams(void) const; + + void stop(void); + + void mainLoop(void); + +private: + enum LeftRight : bool + { + LEFT, + RIGHT + }; + + cv::VideoCapture webcam; + dlib::image_window win; + dlib::frontal_face_detector detector; + dlib::shape_predictor predictor; + bool m_stop; + + double calcEyeAspectRatio(dlib::point& p1, dlib::point& p2, + dlib::point& p3, dlib::point& p4, + dlib::point& p5, dlib::point& p6) const; + + double calcRightEyeAspectRatio(dlib::full_object_detection& shape) const; + double calcLeftEyeAspectRatio(dlib::full_object_detection& shape) const; + + double calcEyeOpenness(LeftRight eye, + dlib::full_object_detection& shape, + double faceYAngle) const; + + double calcMouthForm(dlib::full_object_detection& shape) const; + double calcMouthOpenness(dlib::full_object_detection& shape, double mouthForm) const; + + double calcFaceXAngle(dlib::full_object_detection& shape) const; + double calcFaceYAngle(dlib::full_object_detection& shape, double faceXAngle, double mouthForm) const; + double calcFaceZAngle(dlib::full_object_detection& shape) const; + + void populateDefaultConfig(void); + void parseConfig(std::string cfgPath); + void throwConfigError(std::string paramName, std::string expectedType, + std::string line, unsigned int lineNum); + + + std::deque m_leftEyeOpenness; + std::deque m_rightEyeOpenness; + + std::deque m_mouthOpenness; + std::deque m_mouthForm; + + std::deque m_faceXAngle; + std::deque m_faceYAngle; + std::deque m_faceZAngle; + + struct Config + { + int cvVideoCaptureId; + std::string predictorPath; + double faceYAngleCorrection; + double eyeSmileEyeOpenThreshold; + double eyeSmileMouthFormThreshold; + double eyeSmileMouthOpenThreshold; + bool showWebcamVideo; + bool renderLandmarksOnVideo; + bool lateralInversion; + std::size_t faceXAngleNumTaps; + std::size_t faceYAngleNumTaps; + std::size_t faceZAngleNumTaps; + std::size_t mouthFormNumTaps; + std::size_t mouthOpenNumTaps; + std::size_t leftEyeOpenNumTaps; + std::size_t rightEyeOpenNumTaps; + int cvWaitKeyMs; + double eyeClosedThreshold; + double eyeOpenThreshold; + double mouthNormalThreshold; + double mouthSmileThreshold; + double mouthClosedThreshold; + double mouthOpenThreshold; + double mouthOpenLaughCorrection; + double faceYAngleXRotCorrection; + double faceYAngleSmileCorrection; + double faceYAngleZeroValue; + double faceYAngleUpThreshold; + double faceYAngleDownThreshold; + } m_cfg; +}; + +#endif + diff --git a/src/faceXAngle.png b/src/faceXAngle.png new file mode 100644 index 0000000000000000000000000000000000000000..c35e26eae46f9251ceb2b93507b0b41c548265db GIT binary patch literal 72607 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

Fi?h)925dS-|x442ky8 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 Z5Yg+EU-p(Uh01n>jvc~lXKI}f`Y%glf;0dC literal 0 HcmV?d00001 diff --git a/src/facial_landmark_detector.cpp b/src/facial_landmark_detector.cpp new file mode 100644 index 0000000..20ec7c8 --- /dev/null +++ b/src/facial_landmark_detector.cpp @@ -0,0 +1,731 @@ +/**** +Copyright (c) 2020 Adrian I. Lam + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +****/ + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "facial_landmark_detector.h" +#include "math_utils.h" + + +static void filterPush(std::deque& buf, double newval, + std::size_t numTaps) +{ + buf.push_back(newval); + while (buf.size() > numTaps) + { + buf.pop_front(); + } +} + +FacialLandmarkDetector::FacialLandmarkDetector(std::string cfgPath) + : m_stop(false) +{ + parseConfig(cfgPath); + + if (!webcam.open(m_cfg.cvVideoCaptureId)) + { + throw std::runtime_error("Unable to open webcam"); + } + + detector = dlib::get_frontal_face_detector(); + dlib::deserialize(m_cfg.predictorPath) >> predictor; +} + +FacialLandmarkDetector::Params FacialLandmarkDetector::getParams(void) const +{ + Params params; + + params.faceXAngle = avg(m_faceXAngle); + params.faceYAngle = avg(m_faceYAngle) + m_cfg.faceYAngleCorrection; + // + 10 correct for angle between computer monitor and webcam + params.faceZAngle = avg(m_faceZAngle); + params.mouthOpenness = avg(m_mouthOpenness); + params.mouthForm = avg(m_mouthForm); + + double leftEye = avg(m_leftEyeOpenness, 1); + double rightEye = avg(m_rightEyeOpenness, 1); + // Just combine the two to get better synchronized blinks + // This effectively disables winks, so if we want to + // support winks in the future (see below) we will need + // a better way to handle this out-of-sync blinks. + double bothEyes = (leftEye + rightEye) / 2; + leftEye = bothEyes; + rightEye = bothEyes; + // Detect winks and make them look better + // Commenting out - winks are difficult to be detected by the + // dlib data set anyway... maybe in the future we can + // add a runtime option to enable/disable... + /*if (right == 0 && left > 0.2) + { + left = 1; + } + else if (left == 0 && right > 0.2) + { + right = 1; + } + */ + params.leftEyeOpenness = leftEye; + params.rightEyeOpenness = rightEye; + + if (leftEye <= m_cfg.eyeSmileEyeOpenThreshold && + rightEye <= m_cfg.eyeSmileEyeOpenThreshold && + params.mouthForm > m_cfg.eyeSmileMouthFormThreshold && + params.mouthOpenness > m_cfg.eyeSmileMouthOpenThreshold) + { + params.leftEyeSmile = 1; + params.rightEyeSmile = 1; + } + else + { + params.leftEyeSmile = 0; + params.rightEyeSmile = 0; + } + + return params; +} + +void FacialLandmarkDetector::stop(void) +{ + m_stop = true; +} + +void FacialLandmarkDetector::mainLoop(void) +{ + while (!m_stop) + { + cv::Mat frame; + if (!webcam.read(frame)) + { + throw std::runtime_error("Unable to read from webcam"); + } + cv::Mat flipped; + if (m_cfg.lateralInversion) + { + cv::flip(frame, flipped, 1); + } + else + { + flipped = frame; + } + dlib::cv_image cimg(flipped); + + if (m_cfg.showWebcamVideo) + { + win.set_image(cimg); + } + + std::vector faces = detector(cimg); + + if (faces.size() > 0) + { + dlib::rectangle face = faces[0]; + dlib::full_object_detection shape = predictor(cimg, face); + + /* The coordinates seem to be rather noisy in general. + * We will push everything through some moving average filters + * to reduce noise. The number of taps is determined empirically + * until we get something good. + * An alternative method would be to get some better dataset + * for dlib - perhaps even to train on a custom data set just for the user. + */ + + // Face rotation: X direction (left-right) + double faceXRot = calcFaceXAngle(shape); + filterPush(m_faceXAngle, faceXRot, m_cfg.faceXAngleNumTaps); + + // Mouth form (smile / laugh) detection + double mouthForm = calcMouthForm(shape); + filterPush(m_mouthForm, mouthForm, m_cfg.mouthFormNumTaps); + + // Face rotation: Y direction (up-down) + double faceYRot = calcFaceYAngle(shape, faceXRot, mouthForm); + filterPush(m_faceYAngle, faceYRot, m_cfg.faceYAngleNumTaps); + + // Face rotation: Z direction (head tilt) + double faceZRot = calcFaceZAngle(shape); + filterPush(m_faceZAngle, faceZRot, m_cfg.faceZAngleNumTaps); + + // Mouth openness + double mouthOpen = calcMouthOpenness(shape, mouthForm); + filterPush(m_mouthOpenness, mouthOpen, m_cfg.mouthOpenNumTaps); + + // Eye openness + double eyeLeftOpen = calcEyeOpenness(LEFT, shape, faceYRot); + filterPush(m_leftEyeOpenness, eyeLeftOpen, m_cfg.leftEyeOpenNumTaps); + double eyeRightOpen = calcEyeOpenness(RIGHT, shape, faceYRot); + filterPush(m_rightEyeOpenness, eyeRightOpen, m_cfg.rightEyeOpenNumTaps); + + // TODO eyebrows? + + if (m_cfg.showWebcamVideo && m_cfg.renderLandmarksOnVideo) + { + win.clear_overlay(); + win.add_overlay(dlib::render_face_detections(shape)); + } + } + else + { + if (m_cfg.showWebcamVideo && m_cfg.renderLandmarksOnVideo) + { + win.clear_overlay(); + } + } + + cv::waitKey(m_cfg.cvWaitKeyMs); + } +} + +double FacialLandmarkDetector::calcEyeAspectRatio( + dlib::point& p1, dlib::point& p2, + dlib::point& p3, dlib::point& p4, + dlib::point& p5, dlib::point& p6) const +{ + double eyeWidth = dist(p1, p4); + double eyeHeight1 = dist(p2, p6); + double eyeHeight2 = dist(p3, p5); + + return (eyeHeight1 + eyeHeight2) / (2 * eyeWidth); +} + +double FacialLandmarkDetector::calcEyeOpenness( + LeftRight eye, + dlib::full_object_detection& shape, + double faceYAngle) const +{ + double eyeAspectRatio; + if (eye == LEFT) + { + eyeAspectRatio = calcEyeAspectRatio(shape.part(42), shape.part(43), shape.part(44), + shape.part(45), shape.part(46), shape.part(47)); + } + else + { + eyeAspectRatio = calcEyeAspectRatio(shape.part(36), shape.part(37), shape.part(38), + shape.part(39), shape.part(40), shape.part(41)); + } + + // Apply correction due to faceYAngle + double corrEyeAspRat = eyeAspectRatio / std::cos(degToRad(faceYAngle)); + + return linearScale01(corrEyeAspRat, m_cfg.eyeClosedThreshold, m_cfg.eyeOpenThreshold); +} + + + +double FacialLandmarkDetector::calcMouthForm(dlib::full_object_detection& shape) const +{ + /* Mouth form parameter: 0 for normal mouth, 1 for fully smiling / laughing. + * Compare distance between the two corners of the mouth + * to the distance between the two eyes. + */ + + /* An alternative (my initial attempt) was to compare the corners of + * the mouth to the top of the upper lip - they almost lie on a + * straight line when smiling / laughing. But that is only true + * when facing straight at the camera. When looking up / down, + * the angle changes. So here we'll use the distance approach instead. + */ + + auto eye1 = centroid(shape.part(36), shape.part(37), shape.part(38), + shape.part(39), shape.part(40), shape.part(41)); + auto eye2 = centroid(shape.part(42), shape.part(43), shape.part(44), + shape.part(45), shape.part(46), shape.part(47)); + double distEyes = dist(eye1, eye2); + double distMouth = dist(shape.part(48), shape.part(54)); + + double form = linearScale01(distMouth / distEyes, + m_cfg.mouthNormalThreshold, + m_cfg.mouthSmileThreshold); + + return form; +} + +double FacialLandmarkDetector::calcMouthOpenness( + dlib::full_object_detection& shape, + double mouthForm) const +{ + // Use points for the bottom of the upper lip, and top of the lower lip + // We have 3 pairs of points available, which give the mouth height + // on the left, in the middle, and on the right, resp. + // First let's try to use an average of all three. + double heightLeft = dist(shape.part(63), shape.part(65)); + double heightMiddle = dist(shape.part(62), shape.part(66)); + double heightRight = dist(shape.part(61), shape.part(67)); + + double avgHeight = (heightLeft + heightMiddle + heightRight) / 3; + + // Now, normalize it with the width of the mouth. + double width = dist(shape.part(60), shape.part(64)); + + double normalized = avgHeight / width; + + double scaled = linearScale01(normalized, + m_cfg.mouthClosedThreshold, + m_cfg.mouthOpenThreshold, + true, false); + + // Apply correction according to mouthForm + // Notice that when you smile / laugh, width is increased + scaled *= (1 + m_cfg.mouthOpenLaughCorrection * mouthForm); + + return scaled; +} + +double FacialLandmarkDetector::calcFaceXAngle(dlib::full_object_detection& shape) const +{ + // This function will be easier to understand if you refer to the + // diagram in faceXAngle.png + + // Construct the y-axis using (1) average of four points on the nose and + // (2) average of four points on the upper lip. + + auto y0 = centroid(shape.part(27), shape.part(28), shape.part(29), + shape.part(30)); + auto y1 = centroid(shape.part(50), shape.part(51), shape.part(52), + shape.part(62)); + + // Now drop a perpedicular from the left and right edges of the face, + // and calculate the ratio between the lengths of these perpendiculars + + auto left = centroid(shape.part(14), shape.part(15), shape.part(16)); + auto right = centroid(shape.part(0), shape.part(1), shape.part(2)); + + // Constructing a perpendicular: + // Join the left/right point and the upper lip. The included angle + // can now be determined using cosine rule. + // Then sine of this angle is the perpendicular divided by the newly + // created line. + double opp = dist(right, y0); + double adj1 = dist(y0, y1); + double adj2 = dist(y1, right); + double angle = solveCosineRuleAngle(opp, adj1, adj2); + double perpRight = adj2 * std::sin(angle); + + opp = dist(left, y0); + adj2 = dist(y1, left); + angle = solveCosineRuleAngle(opp, adj1, adj2); + double perpLeft = adj2 * std::sin(angle); + + // Model the head as a sphere and look from above. + double theta = std::asin((perpRight - perpLeft) / (perpRight + perpLeft)); + + theta = radToDeg(theta); + if (theta < -30) theta = -30; + if (theta > 30) theta = 30; + return theta; +} + +double FacialLandmarkDetector::calcFaceYAngle(dlib::full_object_detection& shape, double faceXAngle, double mouthForm) const +{ + // Use the nose + // angle between the two left/right points and the tip + double c = dist(shape.part(31), shape.part(35)); + double a = dist(shape.part(30), shape.part(31)); + double b = dist(shape.part(30), shape.part(35)); + + double angle = solveCosineRuleAngle(c, a, b); + + // This probably varies a lot from person to person... + + // Best is probably to work out some trigonometry again, + // but just linear interpolation seems to work ok... + + // Correct for X rotation + double corrAngle = angle * (1 + (std::abs(faceXAngle) / 30 + * m_cfg.faceYAngleXRotCorrection)); + + // Correct for smiles / laughs - this increases the angle + corrAngle *= (1 - mouthForm * m_cfg.faceYAngleSmileCorrection); + + if (corrAngle >= m_cfg.faceYAngleZeroValue) + { + return -30 * linearScale01(corrAngle, + m_cfg.faceYAngleZeroValue, + m_cfg.faceYAngleDownThreshold, + false, false); + } + else + { + return 30 * (1 - linearScale01(corrAngle, + m_cfg.faceYAngleUpThreshold, + m_cfg.faceYAngleZeroValue, + false, false)); + } +} + +double FacialLandmarkDetector::calcFaceZAngle(dlib::full_object_detection& shape) const +{ + // Use average of eyes and nose + + auto eyeRight = centroid(shape.part(36), shape.part(37), shape.part(38), + shape.part(39), shape.part(40), shape.part(41)); + auto eyeLeft = centroid(shape.part(42), shape.part(43), shape.part(44), + shape.part(45), shape.part(46), shape.part(47)); + + auto noseLeft = shape.part(35); + auto noseRight = shape.part(31); + + double eyeYDiff = eyeRight.y() - eyeLeft.y(); + double eyeXDiff = eyeRight.x() - eyeLeft.x(); + + double angle1 = std::atan(eyeYDiff / eyeXDiff); + + double noseYDiff = noseRight.y() - noseLeft.y(); + double noseXDiff = noseRight.x() - noseLeft.x(); + + double angle2 = std::atan(noseYDiff / noseXDiff); + + return radToDeg((angle1 + angle2) / 2); +} + +void FacialLandmarkDetector::parseConfig(std::string cfgPath) +{ + populateDefaultConfig(); + if (cfgPath != "") + { + std::ifstream file(cfgPath); + + if (!file) + { + throw std::runtime_error("Failed to open config file"); + } + + std::string line; + unsigned int lineNum = 0; + + while (std::getline(file, line)) + { + lineNum++; + + if (line[0] == '#') + { + continue; + } + + std::istringstream ss(line); + std::string paramName; + if (ss >> paramName) + { + if (paramName == "cvVideoCaptureId") + { + if (!(ss >> m_cfg.cvVideoCaptureId)) + { + throwConfigError(paramName, "int", + line, lineNum); + } + } + else if (paramName == "predictorPath") + { + if (!(ss >> m_cfg.predictorPath)) + { + throwConfigError(paramName, "std::string", + line, lineNum); + } + } + else if (paramName == "faceYAngleCorrection") + { + if (!(ss >> m_cfg.faceYAngleCorrection)) + { + throwConfigError(paramName, "double", + line, lineNum); + } + } + else if (paramName == "eyeSmileEyeOpenThreshold") + { + if (!(ss >> m_cfg.eyeSmileEyeOpenThreshold)) + { + throwConfigError(paramName, "double", + line, lineNum); + } + } + else if (paramName == "eyeSmileMouthFormThreshold") + { + if (!(ss >> m_cfg.eyeSmileMouthFormThreshold)) + { + throwConfigError(paramName, "double", + line, lineNum); + } + } + else if (paramName == "eyeSmileMouthOpenThreshold") + { + if (!(ss >> m_cfg.eyeSmileMouthOpenThreshold)) + { + throwConfigError(paramName, "double", + line, lineNum); + } + } + else if (paramName == "showWebcamVideo") + { + if (!(ss >> m_cfg.showWebcamVideo)) + { + throwConfigError(paramName, "bool", + line, lineNum); + } + } + else if (paramName == "renderLandmarksOnVideo") + { + if (!(ss >> m_cfg.renderLandmarksOnVideo)) + { + throwConfigError(paramName, "bool", + line, lineNum); + } + } + else if (paramName == "lateralInversion") + { + if (!(ss >> m_cfg.lateralInversion)) + { + throwConfigError(paramName, "bool", + line, lineNum); + } + } + else if (paramName == "faceXAngleNumTaps") + { + if (!(ss >> m_cfg.faceXAngleNumTaps)) + { + throwConfigError(paramName, "std::size_t", + line, lineNum); + } + } + else if (paramName == "faceYAngleNumTaps") + { + if (!(ss >> m_cfg.faceYAngleNumTaps)) + { + throwConfigError(paramName, "std::size_t", + line, lineNum); + } + } + else if (paramName == "faceZAngleNumTaps") + { + if (!(ss >> m_cfg.faceZAngleNumTaps)) + { + throwConfigError(paramName, "std::size_t", + line, lineNum); + } + } + else if (paramName == "mouthFormNumTaps") + { + if (!(ss >> m_cfg.mouthFormNumTaps)) + { + throwConfigError(paramName, "std::size_t", + line, lineNum); + } + } + else if (paramName == "mouthOpenNumTaps") + { + if (!(ss >> m_cfg.mouthOpenNumTaps)) + { + throwConfigError(paramName, "std::size_t", + line, lineNum); + } + } + else if (paramName == "leftEyeOpenNumTaps") + { + if (!(ss >> m_cfg.leftEyeOpenNumTaps)) + { + throwConfigError(paramName, "std::size_t", + line, lineNum); + } + } + else if (paramName == "rightEyeOpenNumTaps") + { + if (!(ss >> m_cfg.rightEyeOpenNumTaps)) + { + throwConfigError(paramName, "std::size_t", + line, lineNum); + } + } + else if (paramName == "cvWaitKeyMs") + { + if (!(ss >> m_cfg.cvWaitKeyMs)) + { + throwConfigError(paramName, "int", + line, lineNum); + } + } + else if (paramName == "eyeClosedThreshold") + { + if (!(ss >> m_cfg.eyeClosedThreshold)) + { + throwConfigError(paramName, "double", + line, lineNum); + } + } + else if (paramName == "eyeOpenThreshold") + { + if (!(ss >> m_cfg.eyeOpenThreshold)) + { + throwConfigError(paramName, "double", + line, lineNum); + } + } + else if (paramName == "mouthNormalThreshold") + { + if (!(ss >> m_cfg.mouthNormalThreshold)) + { + throwConfigError(paramName, "double", + line, lineNum); + } + } + else if (paramName == "mouthSmileThreshold") + { + if (!(ss >> m_cfg.mouthSmileThreshold)) + { + throwConfigError(paramName, "double", + line, lineNum); + } + } + else if (paramName == "mouthClosedThreshold") + { + if (!(ss >> m_cfg.mouthClosedThreshold)) + { + throwConfigError(paramName, "double", + line, lineNum); + } + } + else if (paramName == "mouthOpenThreshold") + { + if (!(ss >> m_cfg.mouthOpenThreshold)) + { + throwConfigError(paramName, "double", + line, lineNum); + } + } + else if (paramName == "mouthOpenLaughCorrection") + { + if (!(ss >> m_cfg.mouthOpenLaughCorrection)) + { + throwConfigError(paramName, "double", + line, lineNum); + } + } + else if (paramName == "faceYAngleXRotCorrection") + { + if (!(ss >> m_cfg.faceYAngleXRotCorrection)) + { + throwConfigError(paramName, "double", + line, lineNum); + } + } + else if (paramName == "faceYAngleSmileCorrection") + { + if (!(ss >> m_cfg.faceYAngleSmileCorrection)) + { + throwConfigError(paramName, "double", + line, lineNum); + } + } + else if (paramName == "faceYAngleZeroValue") + { + if (!(ss >> m_cfg.faceYAngleZeroValue)) + { + throwConfigError(paramName, "double", + line, lineNum); + } + } + else if (paramName == "faceYAngleUpThreshold") + { + if (!(ss >> m_cfg.faceYAngleUpThreshold)) + { + throwConfigError(paramName, "double", + line, lineNum); + } + } + else if (paramName == "faceYAngleDownThreshold") + { + if (!(ss >> m_cfg.faceYAngleDownThreshold)) + { + throwConfigError(paramName, "double", + line, lineNum); + } + } + else + { + std::ostringstream oss; + oss << "Unrecognized parameter name at line " << lineNum + << ": " << paramName; + throw std::runtime_error(oss.str()); + } + } + } + } +} + +void FacialLandmarkDetector::populateDefaultConfig(void) +{ + // These are values that I've personally tested to work OK for my face. + // Your milage may vary - hence the config file. + + m_cfg.cvVideoCaptureId = 0; + m_cfg.predictorPath = "shape_predictor_68_face_landmarks.dat"; + m_cfg.faceYAngleCorrection = 10; + m_cfg.eyeSmileEyeOpenThreshold = 0.6; + m_cfg.eyeSmileMouthFormThreshold = 0.75; + m_cfg.eyeSmileMouthOpenThreshold = 0.5; + m_cfg.showWebcamVideo = true; + m_cfg.renderLandmarksOnVideo = true; + m_cfg.lateralInversion = true; + m_cfg.cvWaitKeyMs = 5; + m_cfg.faceXAngleNumTaps = 11; + m_cfg.faceYAngleNumTaps = 11; + m_cfg.faceZAngleNumTaps = 11; + m_cfg.mouthFormNumTaps = 3; + m_cfg.mouthOpenNumTaps = 3; + m_cfg.leftEyeOpenNumTaps = 3; + m_cfg.rightEyeOpenNumTaps = 3; + m_cfg.eyeClosedThreshold = 0.2; + m_cfg.eyeOpenThreshold = 0.25; + m_cfg.mouthNormalThreshold = 0.75; + m_cfg.mouthSmileThreshold = 1.0; + m_cfg.mouthClosedThreshold = 0.1; + m_cfg.mouthOpenThreshold = 0.4; + m_cfg.mouthOpenLaughCorrection = 0.2; + m_cfg.faceYAngleXRotCorrection = 0.15; + m_cfg.faceYAngleSmileCorrection = 0.075; + m_cfg.faceYAngleZeroValue = 1.8; + m_cfg.faceYAngleDownThreshold = 2.3; + m_cfg.faceYAngleUpThreshold = 1.3; +} + +void FacialLandmarkDetector::throwConfigError(std::string paramName, + std::string expectedType, + std::string line, + unsigned int lineNum) +{ + std::ostringstream ss; + ss << "Error parsing config file for parameter " << paramName + << "\nAt line " << lineNum << ": " << line + << "\nExpecting value of type " << expectedType; + + throw std::runtime_error(ss.str()); +} + diff --git a/src/math_utils.h b/src/math_utils.h new file mode 100644 index 0000000..d465c87 --- /dev/null +++ b/src/math_utils.h @@ -0,0 +1,108 @@ +// -*- mode: c++ -*- + +#ifndef __FACE_DETECTOR_MATH_UTILS_H__ +#define __FACE_DETECTOR_MATH_UTILS_H__ + +/**** +Copyright (c) 2020 Adrian I. Lam + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +****/ + +#include +#include +#include + +static const double PI = 3.14159265358979; + +template +static double avg(T container, double defaultValue = 0) +{ + if (container.size() == 0) + { + return defaultValue; + } + + double sum = 0; + for (auto it = container.begin(); it != container.end(); ++it) + { + sum += *it; + } + return sum / container.size(); +} + +template +static dlib::point centroid(Args&... args) +{ + std::size_t numArgs = sizeof...(args); + if (numArgs == 0) return dlib::point(0, 0); + + double sumX = 0, sumY = 0; + for (auto point : {args...}) + { + sumX += point.x(); + sumY += point.y(); + } + + return dlib::point(sumX / numArgs, sumY / numArgs); +} + +static inline double sq(double x) +{ + return x * x; +} + +static double solveCosineRuleAngle(double opposite, + double adjacent1, + double adjacent2) +{ + // c^2 = a^2 + b^2 - 2 a b cos(C) + double cosC = (sq(opposite) - sq(adjacent1) - sq(adjacent2)) / + (-2 * adjacent1 * adjacent2); + return std::acos(cosC); +} + +static inline double radToDeg(double rad) +{ + return rad * 180 / PI; +} + +static inline double degToRad(double deg) +{ + return deg * PI / 180; +} + +double dist(dlib::point& p1, dlib::point& p2) +{ + double xDist = p1.x() - p2.x(); + double yDist = p1.y() - p2.y(); + + return std::hypot(xDist, yDist); +} + +/*! Scale linearly from 0 to 1 (both end-points inclusive) */ +double linearScale01(double num, double min, double max, + bool clipMin = true, bool clipMax = true) +{ + if (num < min && clipMin) return 0.0; + if (num > max && clipMax) return 1.0; + return (num - min) / (max - min); +} + +#endif -- 2.7.4