From 126d8fa4d0ea5d05ed56d2b318e747426317808d Mon Sep 17 00:00:00 2001 From: Adrian Iain Lam Date: Fri, 2 Oct 2020 02:05:59 +0100 Subject: [PATCH] Mouse tracking with lip sync - initial commit --- .gitmodules | 3 - CMakeLists.txt | 22 +- README.md | 107 ++---- block_diagram.png | Bin 19687 -> 0 bytes build.sh | 2 +- config.txt | 196 +++------- example/demo.patch | 227 +++++------ include/facial_landmark_detector.h | 150 -------- include/mouse_cursor_tracker.h | 108 ++++++ lib/dlib | 1 - src/faceXAngle.png | Bin 72607 -> 0 bytes src/facial_landmark_detector.cpp | 762 ------------------------------------- src/math_utils.h | 108 ------ src/mouse_cursor_tracker.cpp | 352 +++++++++++++++++ 14 files changed, 659 insertions(+), 1379 deletions(-) delete mode 100644 block_diagram.png delete mode 100644 include/facial_landmark_detector.h create mode 100644 include/mouse_cursor_tracker.h delete mode 160000 lib/dlib delete mode 100644 src/faceXAngle.png delete mode 100644 src/facial_landmark_detector.cpp delete mode 100644 src/math_utils.h create mode 100644 src/mouse_cursor_tracker.cpp diff --git a/.gitmodules b/.gitmodules index 34321e9..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "lib/dlib"] - path = lib/dlib - url = https://github.com/davisking/dlib.git diff --git a/CMakeLists.txt b/CMakeLists.txt index eb72aa8..8780521 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,14 +1,20 @@ cmake_minimum_required(VERSION 3.16) -project(FacialLandmarksForCubism_project) +project(MouseTrackerForCubism_project) -add_subdirectory(lib/dlib/dlib dlib_build) -find_package(OpenCV REQUIRED) -include_directories(${OpenCV_INCLUDE_DIRS}) +find_library(xdo_LIBS NAMES xdo libxdo PATHS /usr/lib REQUIRED) +find_library(pulse_LIBS NAMES pulse PATHS /usr/lib REQUIRED) -add_library(FacialLandmarksForCubism STATIC src/facial_landmark_detector.cpp) -set_target_properties(FacialLandmarksForCubism PROPERTIES PUBLIC_HEADER include/facial_landmark_detector.h) +include_directories(include) -target_include_directories(FacialLandmarksForCubism PRIVATE include lib/dlib) -target_link_libraries(FacialLandmarksForCubism ${OpenCV_LIBS} dlib::dlib) +add_library( + MouseTrackerForCubism STATIC + src/mouse_cursor_tracker.cpp +) +set_target_properties( + MouseTrackerForCubism PROPERTIES PUBLIC_HEADER + include/mouse_cursor_tracker.h +) + +target_link_libraries(MouseTrackerForCubism ${xdo_LIBS} ${pulse_LIBS} pulse-simple) diff --git a/README.md b/README.md index 62c80f5..c3eb23c 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,45 @@ -# Facial Landmarks for Cubism +# Mouse Tracker for Cubism -A library that extracts facial landmarks from a webcam feed and converts them -into Live2D® Cubism SDK parameters. +A library that tracks mouse cursor location and microphone input and +converts them into Live2D® Cubism SDK parameters. + +This is a spin-off project from [Facial Landmarks for Cubism](https://github.com/adrianiainlam/facial-landmarks-for-cubism). +The objective is to provide similar functionality, but requiring much +less CPU load, which can be critical if the processor does not support +AVX instructions. It also does not require the use of a dataset which +restricts commercial use, and it does not require a webcam. *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 (``) +This library is designed for Linux (or other *nix systems) with X11 and +PulseAudio. + +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 "build.sh" -and "example/demo.patch" to remove the `-D USE_AVX_INSTRUCTIONS=1` variable -(or change AVX to SSE4 or SSE2). 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 + libxdo, and PulseAudio. 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 + libxdo-dev libpulse-dev 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 + git clone https://github.com/adrianiainlam/mouse-tracker-for-cubism.git 3. To build the library only: (Skip this step if you want to build the example program. It will be done automatically.) @@ -58,22 +47,9 @@ performance. 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: +4. Download "Cubism 4 SDK for Native R1" from the Live2D website: . Extract the archive -- put the "CubismSdkForNative-4-r.1" folder under @@ -82,17 +58,17 @@ To build the example program: 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 +5. 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 +6. Go back to the "example" directory and run ./build.sh -9. Now try running the example program. From the "example" directory: +7. Now try running the example program. From the "example" directory: cd ./demo_build/build/make_gcc/bin/Demo/ ./Demo @@ -115,52 +91,33 @@ for the Facial Landmarks for Cubism library. * `--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 + * `--config`, `-c`: Path to the configuration file for the Mouse Tracker 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 +There are fewer tweakable parameters compared to the Facial Landmarks +library, but I have still kept the configuration file to allow some +customization. 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 the `-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). -## Troubleshooting - -1. Example program crashes with SIGILL (Illegal instruction). - - Your CPU probably doesn't support AVX instructions which is used by dlib. - You can confirm this by running - - grep avx /proc/cpuinfo - - If this is the case, try to find out if your CPU supports SSE4 or SSE2, - then edit "build.sh" and "example/demo.patch" to change - `USE_AVX_INSTRUCTIONS=1` to `USE_SSE4_INSTRUCTIONS=1` or - `USE_SSE2_INSTRUCTIONS=1`. - ## 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 + * src/mouse_cursor_tracker.cpp + * include/mouse_cursor_tracker.cpp * and if you decide to build the binary for the library, the resulting - binary file (typically build/libFacialLandmarksForCubism.a) + binary file (typically build/libMouseTrackerForCubism.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 @@ -184,12 +141,6 @@ 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 :). @@ -197,7 +148,7 @@ 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 +really have many environments 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 deleted file mode 100644 index b4bfdd4436b8823acf1e7008034eef66f296febb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19687 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 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-09-27 17:43:12.069477246 +0100 -+++ ./demo_dev/scripts/make_gcc 2020-07-14 15:33:09.865020790 +0100 -@@ -9,5 +9,6 @@ BUILD_PATH=$SCRIPT_PATH/../build/make_gc - # Run CMake. +--- ./demo_clean/scripts/make_gcc 2020-10-02 02:01:04.825787688 +0100 ++++ ./demo_dev/scripts/make_gcc 2020-10-01 23:43:42.213875065 +0100 +@@ -10,4 +10,4 @@ BUILD_PATH=$SCRIPT_PATH/../build/make_gc cmake -S "$CMAKE_PATH" \ -B "$BUILD_PATH" \ -- -D CMAKE_BUILD_TYPE=Release + -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-09-27 17:43:12.081477263 +0100 -+++ ./demo_dev/src/CMakeLists.txt 2020-07-11 17:39:18.358435702 +0100 +--- ./demo_clean/src/CMakeLists.txt 2020-10-02 02:01:04.829787750 +0100 ++++ ./demo_dev/src/CMakeLists.txt 2020-10-01 22:47:24.842846271 +0100 @@ -19,6 +19,4 @@ target_sources(${APP_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/LAppView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/LAppView.hpp @@ -83,8 +62,8 @@ diff -pruN --exclude build ./demo_clean/src/CMakeLists.txt ./demo_dev/src/CMakeL - ${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-09-27 17:43:12.081477263 +0100 -+++ ./demo_dev/src/LAppDelegate.cpp 2020-07-11 17:35:02.414902548 +0100 +--- ./demo_clean/src/LAppDelegate.cpp 2020-10-02 02:01:04.829787750 +0100 ++++ ./demo_dev/src/LAppDelegate.cpp 2020-10-01 22:47:24.698848890 +0100 @@ -45,7 +45,8 @@ void LAppDelegate::ReleaseInstance() s_instance = NULL; } @@ -213,8 +192,8 @@ diff -pruN --exclude build ./demo_clean/src/LAppDelegate.cpp ./demo_dev/src/LApp 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-09-27 17:43:12.081477263 +0100 -+++ ./demo_dev/src/LAppDelegate.hpp 2020-07-11 17:34:40.778602504 +0100 +--- ./demo_clean/src/LAppDelegate.hpp 2020-10-02 02:01:04.829787750 +0100 ++++ ./demo_dev/src/LAppDelegate.hpp 2020-10-01 22:47:24.842846271 +0100 @@ -40,7 +40,8 @@ public: /** * @brief APPに必要なものを初期化する。 @@ -289,8 +268,8 @@ diff -pruN --exclude build ./demo_clean/src/LAppDelegate.hpp ./demo_dev/src/LApp - -}; diff -pruN --exclude build ./demo_clean/src/LAppLive2DManager.cpp ./demo_dev/src/LAppLive2DManager.cpp ---- ./demo_clean/src/LAppLive2DManager.cpp 2020-09-27 17:43:12.081477263 +0100 -+++ ./demo_dev/src/LAppLive2DManager.cpp 2020-07-11 23:20:11.548419176 +0100 +--- ./demo_clean/src/LAppLive2DManager.cpp 2020-10-02 02:01:04.829787750 +0100 ++++ ./demo_dev/src/LAppLive2DManager.cpp 2020-10-02 02:00:49.961556700 +0100 @@ -52,9 +52,10 @@ void LAppLive2DManager::ReleaseInstance( LAppLive2DManager::LAppLive2DManager() @@ -377,11 +356,11 @@ diff -pruN --exclude build ./demo_clean/src/LAppLive2DManager.cpp ./demo_dev/src return _models.GetSize(); } + -+void LAppLive2DManager::SetFacialLandmarkDetector(FacialLandmarkDetector *detector) ++void LAppLive2DManager::SetTracker(MouseCursorTracker *tracker) +{ + for (auto it = _models.Begin(); it != _models.End(); ++it) + { -+ (*it)->SetFacialLandmarkDetector(detector); ++ (*it)->SetTracker(tracker); + } +} + @@ -394,8 +373,8 @@ diff -pruN --exclude build ./demo_clean/src/LAppLive2DManager.cpp ./demo_dev/src + _translateY = translateY; +} diff -pruN --exclude build ./demo_clean/src/LAppLive2DManager.hpp ./demo_dev/src/LAppLive2DManager.hpp ---- ./demo_clean/src/LAppLive2DManager.hpp 2020-09-27 17:43:12.069477246 +0100 -+++ ./demo_dev/src/LAppLive2DManager.hpp 2020-07-11 23:21:17.969484538 +0100 +--- ./demo_clean/src/LAppLive2DManager.hpp 2020-10-02 02:01:04.825787688 +0100 ++++ ./demo_dev/src/LAppLive2DManager.hpp 2020-10-01 23:36:24.583055381 +0100 @@ -6,12 +6,15 @@ */ #pragma once @@ -407,7 +386,7 @@ diff -pruN --exclude build ./demo_clean/src/LAppLive2DManager.hpp ./demo_dev/src class LAppModel; -+class FacialLandmarkDetector; ++class MouseCursorTracker; + /** * @brief サンプルアプリケーションにおいてCubismModelを管理するクラス
@@ -440,11 +419,11 @@ diff -pruN --exclude build ./demo_clean/src/LAppLive2DManager.hpp ./demo_dev/src Csm::csmUint32 GetModelNum() const; + /** -+ * @brief Set the pointer to the FacialLandmarkDetector instance ++ * @brief Set the pointer to the MouseCursorTracker instance + * -+ * @param[in] detector : Pointer to FacialLandmarkDetector instance ++ * @param[in] tracker : Pointer to MouseCursorTracker instance + */ -+ void SetFacialLandmarkDetector(FacialLandmarkDetector *detector); ++ void SetTracker(MouseCursorTracker *tracker); + + /** + * @brief Set projection scale factor and translation parameters @@ -471,18 +450,26 @@ diff -pruN --exclude build ./demo_clean/src/LAppLive2DManager.hpp ./demo_dev/src + float _translateY; }; diff -pruN --exclude build ./demo_clean/src/LAppModel.cpp ./demo_dev/src/LAppModel.cpp ---- ./demo_clean/src/LAppModel.cpp 2020-09-27 17:43:12.069477246 +0100 -+++ ./demo_dev/src/LAppModel.cpp 2020-09-27 17:40:16.401166244 +0100 +--- ./demo_clean/src/LAppModel.cpp 2020-10-02 02:01:04.825787688 +0100 ++++ ./demo_dev/src/LAppModel.cpp 2020-10-01 23:34:43.482626010 +0100 @@ -21,6 +21,8 @@ #include "LAppTextureManager.hpp" #include "LAppDelegate.hpp" -+#include "facial_landmark_detector.h" ++#include "mouse_cursor_tracker.h" + using namespace Live2D::Cubism::Framework; using namespace Live2D::Cubism::Framework::DefaultParameterId; using namespace LAppDefine; -@@ -128,30 +130,6 @@ void LAppModel::SetupModel(ICubismModelS +@@ -49,6 +51,7 @@ LAppModel::LAppModel() + : CubismUserModel() + , _modelSetting(NULL) + , _userTimeSeconds(0.0f) ++ , _tracker(nullptr) + { + if (DebugLogEnable) + { +@@ -128,30 +131,6 @@ void LAppModel::SetupModel(ICubismModelS DeleteBuffer(buffer, path.GetRawString()); } @@ -513,23 +500,7 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.cpp ./demo_dev/src/LAppMod //Physics if (strcmp(_modelSetting->GetPhysicsFileName(), "") != 0) { -@@ -214,15 +192,6 @@ void LAppModel::SetupModel(ICubismModelS - } - } - -- // LipSyncIds -- { -- csmInt32 lipSyncIdCount = _modelSetting->GetLipSyncParameterCount(); -- for (csmInt32 i = 0; i < lipSyncIdCount; ++i) -- { -- _lipSyncIds.PushBack(_modelSetting->GetLipSyncParameterId(i)); -- } -- } -- - //Layout - csmMap layout; - _modelSetting->GetLayoutMap(layout); -@@ -335,59 +304,57 @@ void LAppModel::Update() +@@ -335,59 +314,72 @@ void LAppModel::Update() const csmFloat32 deltaTimeSeconds = LAppPal::GetDeltaTime(); _userTimeSeconds += deltaTimeSeconds; @@ -543,19 +514,19 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.cpp ./demo_dev/src/LAppMod - //----------------------------------------------------------------- - _model->LoadParameters(); // 前回セーブされた状態をロード - if (_motionManager->IsFinished()) -- { ++ if (_tracker) + { - // モーションの再生がない場合、待機モーションの中からランダムで再生する - StartRandomMotion(MotionGroupIdle, PriorityIdle); - } - else -+ if (_detector) - { +- { - motionUpdated = _motionManager->UpdateMotion(_model, deltaTimeSeconds); // モーションを更新 - } - _model->SaveParameters(); // 状態を保存 - //----------------------------------------------------------------- + auto idMan = CubismFramework::GetIdManager(); -+ auto params = _detector->getParams(); ++ auto params = _tracker->getParams(); - // まばたき - if (!motionUpdated) @@ -572,49 +543,61 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.cpp ./demo_dev/src/LAppMod + StartRandomMotion(MotionGroupIdle, PriorityIdle); } - } -- ++ else ++ { ++ _motionManager->UpdateMotion(_model, deltaTimeSeconds); // モーションを更新 ++ } ++ _model->SaveParameters(); // 状態を保存 + - if (_expressionManager != NULL) - { - _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の値を加える ++ if (params.autoBlink && _eyeBlink) ++ { ++ _eyeBlink->UpdateParameters(_model, deltaTimeSeconds); ++ } + else + { -+ _motionManager->UpdateMotion(_model, deltaTimeSeconds); // モーションを更新 ++ _model->SetParameterValue(idMan->GetId("ParamEyeLOpen"), ++ params.leftEyeOpenness); ++ _model->SetParameterValue(idMan->GetId("ParamEyeROpen"), ++ params.rightEyeOpenness); + } -+ _model->SaveParameters(); // 状態を保存 + +- //ドラッグによる体の向きの調整 +- _model->AddParameterValue(_idParamBodyAngleX, _dragX * 10); // -10から10の値を加える ++ _model->SetParameterValue(idMan->GetId("ParamMouthForm"), ++ params.mouthForm); - //ドラッグによる目の向きの調整 - _model->AddParameterValue(_idParamEyeBallX, _dragX); // -1から1の値を加える - _model->AddParameterValue(_idParamEyeBallY, _dragY); ++ if (params.useLipSync && _lipSync) ++ { ++ csmFloat32 value = params.lipSyncParam; // 0 to 1 - // 呼吸など - if (_breath != NULL) - { - _breath->UpdateParameters(_model, deltaTimeSeconds); -+ if (params.autoBlink && _eyeBlink) -+ { -+ _eyeBlink->UpdateParameters(_model, deltaTimeSeconds); ++ for (csmUint32 i = 0; i < _lipSyncIds.GetSize(); ++i) ++ { ++ _model->AddParameterValue(_lipSyncIds[i], value, 0.8f); ++ } + } + else + { -+ _model->SetParameterValue(idMan->GetId("ParamEyeLOpen"), -+ params.leftEyeOpenness); -+ _model->SetParameterValue(idMan->GetId("ParamEyeROpen"), -+ params.rightEyeOpenness); ++ _model->SetParameterValue(idMan->GetId("ParamMouthOpenY"), ++ params.mouthOpenness); + } -+ _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"), @@ -634,7 +617,7 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.cpp ./demo_dev/src/LAppMod } // 物理演算の設定 -@@ -396,17 +363,6 @@ void LAppModel::Update() +@@ -396,17 +388,6 @@ void LAppModel::Update() _physics->Evaluate(_model, deltaTimeSeconds); } @@ -652,24 +635,24 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.cpp ./demo_dev/src/LAppMod // ポーズの設定 if (_pose != NULL) { -@@ -626,3 +582,9 @@ Csm::Rendering::CubismOffscreenFrame_Ope +@@ -626,3 +607,9 @@ Csm::Rendering::CubismOffscreenFrame_Ope { return _renderBuffer; } + -+void LAppModel::SetFacialLandmarkDetector(FacialLandmarkDetector *detector) ++void LAppModel::SetTracker(MouseCursorTracker *tracker) +{ -+ _detector = detector; ++ _tracker = tracker; +} + diff -pruN --exclude build ./demo_clean/src/LAppModel.hpp ./demo_dev/src/LAppModel.hpp ---- ./demo_clean/src/LAppModel.hpp 2020-09-27 17:43:12.081477263 +0100 -+++ ./demo_dev/src/LAppModel.hpp 2020-07-11 15:40:18.977286166 +0100 +--- ./demo_clean/src/LAppModel.hpp 2020-10-02 02:01:04.829787750 +0100 ++++ ./demo_dev/src/LAppModel.hpp 2020-10-01 23:35:39.254849094 +0100 @@ -13,6 +13,7 @@ #include #include -+#include "facial_landmark_detector.h" ++#include "mouse_cursor_tracker.h" /** * @brief ユーザーが実際に使用するモデルの実装クラス
@@ -678,11 +661,11 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.hpp ./demo_dev/src/LAppMod Csm::Rendering::CubismOffscreenFrame_OpenGLES2& GetRenderBuffer(); + /** -+ * @brief Set the pointer to the FacialLandmarkDetector instance ++ * @brief Set the pointer to the MouseCursorTracker instance + * -+ * @param[in] detector : Pointer to FacialLandmarkDetector instance ++ * @param[in] tracker : Pointer to MouseCursorTracker instance + */ -+ void SetFacialLandmarkDetector(FacialLandmarkDetector *detector); ++ void SetTracker(MouseCursorTracker *tracker); + protected: /** @@ -692,13 +675,13 @@ diff -pruN --exclude build ./demo_clean/src/LAppModel.hpp ./demo_dev/src/LAppMod Csm::Rendering::CubismOffscreenFrame_OpenGLES2 _renderBuffer; ///< フレームバッファ以外の描画先 + -+ FacialLandmarkDetector *_detector; ++ MouseCursorTracker *_tracker; }; diff -pruN --exclude build ./demo_clean/src/LAppPal.cpp ./demo_dev/src/LAppPal.cpp ---- ./demo_clean/src/LAppPal.cpp 2020-09-27 17:43:12.081477263 +0100 -+++ ./demo_dev/src/LAppPal.cpp 2020-07-11 23:29:09.084910139 +0100 +--- ./demo_clean/src/LAppPal.cpp 2020-10-02 02:01:04.829787750 +0100 ++++ ./demo_dev/src/LAppPal.cpp 2020-10-01 22:47:24.722848453 +0100 @@ -6,6 +6,7 @@ */ @@ -720,8 +703,8 @@ diff -pruN --exclude build ./demo_clean/src/LAppPal.cpp ./demo_dev/src/LAppPal.c } file.read(buf, size); diff -pruN --exclude build ./demo_clean/src/LAppTextureManager.cpp ./demo_dev/src/LAppTextureManager.cpp ---- ./demo_clean/src/LAppTextureManager.cpp 2020-09-27 17:43:12.085477268 +0100 -+++ ./demo_dev/src/LAppTextureManager.cpp 2020-07-11 22:22:18.004965003 +0100 +--- ./demo_clean/src/LAppTextureManager.cpp 2020-10-02 02:01:04.833787812 +0100 ++++ ./demo_dev/src/LAppTextureManager.cpp 2020-10-01 22:47:24.654849690 +0100 @@ -96,6 +96,46 @@ LAppTextureManager::TextureInfo* LAppTex } @@ -770,8 +753,8 @@ diff -pruN --exclude build ./demo_clean/src/LAppTextureManager.cpp ./demo_dev/sr { 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-09-27 17:43:12.069477246 +0100 -+++ ./demo_dev/src/LAppTextureManager.hpp 2020-07-11 17:36:31.180131039 +0100 +--- ./demo_clean/src/LAppTextureManager.hpp 2020-10-02 02:01:04.825787688 +0100 ++++ ./demo_dev/src/LAppTextureManager.hpp 2020-10-01 22:47:24.786847290 +0100 @@ -72,6 +72,8 @@ public: */ TextureInfo* CreateTextureFromPngFile(std::string fileName); @@ -782,8 +765,8 @@ diff -pruN --exclude build ./demo_clean/src/LAppTextureManager.hpp ./demo_dev/sr * @brief 画像の解放 * diff -pruN --exclude build ./demo_clean/src/LAppView.cpp ./demo_dev/src/LAppView.cpp ---- ./demo_clean/src/LAppView.cpp 2020-09-27 17:43:12.085477268 +0100 -+++ ./demo_dev/src/LAppView.cpp 2020-07-11 17:38:06.905451955 +0100 +--- ./demo_clean/src/LAppView.cpp 2020-10-02 02:01:04.833787812 +0100 ++++ ./demo_dev/src/LAppView.cpp 2020-10-01 22:47:24.602850636 +0100 @@ -13,7 +13,6 @@ #include "LAppLive2DManager.hpp" #include "LAppTextureManager.hpp" @@ -959,8 +942,8 @@ diff -pruN --exclude build ./demo_clean/src/LAppView.cpp ./demo_dev/src/LAppView - } } diff -pruN --exclude build ./demo_clean/src/LAppView.hpp ./demo_dev/src/LAppView.hpp ---- ./demo_clean/src/LAppView.hpp 2020-09-27 17:43:12.069477246 +0100 -+++ ./demo_dev/src/LAppView.hpp 2020-07-11 17:38:25.541708705 +0100 +--- ./demo_clean/src/LAppView.hpp 2020-10-02 02:01:04.825787688 +0100 ++++ ./demo_dev/src/LAppView.hpp 2020-10-01 22:47:24.802846999 +0100 @@ -14,7 +14,6 @@ #include "CubismFramework.hpp" #include @@ -1015,9 +998,9 @@ diff -pruN --exclude build ./demo_clean/src/LAppView.hpp ./demo_dev/src/LAppView // レンダリング先を別ターゲットにする方式の場合に使用 LAppSprite* _renderSprite; ///< モードによっては_renderBufferのテクスチャを描画 diff -pruN --exclude build ./demo_clean/src/main.cpp ./demo_dev/src/main.cpp ---- ./demo_clean/src/main.cpp 2020-09-27 17:43:12.069477246 +0100 -+++ ./demo_dev/src/main.cpp 2020-07-12 15:06:29.194034887 +0100 -@@ -5,18 +5,156 @@ +--- ./demo_clean/src/main.cpp 2020-10-02 02:01:04.825787688 +0100 ++++ ./demo_dev/src/main.cpp 2020-10-01 23:42:12.845205308 +0100 +@@ -5,18 +5,154 @@ * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. */ @@ -1033,10 +1016,9 @@ diff -pruN --exclude build ./demo_clean/src/main.cpp ./demo_dev/src/main.cpp +namespace fs = std::experimental::filesystem; +#endif + -+ #include "LAppDelegate.hpp" +#include "LAppLive2DManager.hpp" -+#include "facial_landmark_detector.h" ++#include "mouse_cursor_tracker.h" + +struct CmdArgs +{ @@ -1048,7 +1030,7 @@ diff -pruN --exclude build ./demo_clean/src/main.cpp ./demo_dev/src/main.cpp + float translateX; + float translateY; + std::string modelName; -+ std::string cfgPath; // Path to config file for FacialLandmarkDetector ++ std::string cfgPath; // Path to config file for MouseCursorTracker +}; + +CmdArgs parseArgv(int argc, char *argv[]) @@ -1058,7 +1040,7 @@ diff -pruN --exclude build ./demo_clean/src/main.cpp ./demo_dev/src/main.cpp + // Set default values + cmdArgs.windowWidth = 600; + cmdArgs.windowHeight = 600; -+ cmdArgs.windowTitle = "FacialLandmarksForCubism example"; ++ cmdArgs.windowTitle = "MouseTrackerForCubism example"; + cmdArgs.rootDir = fs::current_path(); + cmdArgs.scaleFactor = 8.0f; + cmdArgs.translateX = 0.0f; @@ -1158,10 +1140,9 @@ diff -pruN --exclude build ./demo_clean/src/main.cpp ./demo_dev/src/main.cpp - LAppDelegate::GetInstance()->Run(); + delegate->SetRootDirectory(cmdArgs.rootDir); + -+ FacialLandmarkDetector detector(cmdArgs.cfgPath); ++ MouseCursorTracker tracker(cmdArgs.cfgPath); + -+ std::thread detectorThread(&FacialLandmarkDetector::mainLoop, -+ &detector); ++ std::thread trackerThread(&MouseCursorTracker::mainLoop, &tracker); + + LAppLive2DManager *manager = LAppLive2DManager::GetInstance(); + manager->SetModel(cmdArgs.modelName); @@ -1169,12 +1150,12 @@ diff -pruN --exclude build ./demo_clean/src/main.cpp ./demo_dev/src/main.cpp + manager->SetProjectionScaleTranslate(cmdArgs.scaleFactor, + cmdArgs.translateX, + cmdArgs.translateY); -+ manager->SetFacialLandmarkDetector(&detector); ++ manager->SetTracker(&tracker); + + delegate->Run(); + -+ detector.stop(); -+ detectorThread.join(); ++ tracker.stop(); ++ trackerThread.join(); return 0; } diff --git a/include/facial_landmark_detector.h b/include/facial_landmark_detector.h deleted file mode 100644 index 5163fe9..0000000 --- a/include/facial_landmark_detector.h +++ /dev/null @@ -1,150 +0,0 @@ -// -*- 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; - bool autoBlink; - bool autoBreath; - bool randomMotion; - // 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; - bool autoBlink; - bool autoBreath; - bool randomMotion; - } m_cfg; -}; - -#endif - diff --git a/include/mouse_cursor_tracker.h b/include/mouse_cursor_tracker.h new file mode 100644 index 0000000..a53713f --- /dev/null +++ b/include/mouse_cursor_tracker.h @@ -0,0 +1,108 @@ +// -*- mode: c++ -*- + +#ifndef __MOUSE_CURSOR_TRACKER_H__ +#define __MOUSE_CURSOR_TRACKER_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 +extern "C" +{ +#include +#include +} + +class MouseCursorTracker +{ +public: + MouseCursorTracker(std::string cfgPath); + ~MouseCursorTracker(); + + struct Params + { + double leftEyeOpenness; + double rightEyeOpenness; + double leftEyeSmile; + double rightEyeSmile; + double mouthOpenness; + double mouthForm; + double faceXAngle; + double faceYAngle; + double faceZAngle; + bool autoBlink; + bool autoBreath; + bool randomMotion; + bool useLipSync; + double lipSyncParam; + }; + + Params getParams(void) const; + + void stop(void); + + void mainLoop(void); + +private: + struct Coord + { + int x; + int y; + }; + + struct Config + { + int sleepMs; + bool autoBlink; + bool autoBreath; + bool randomMotion; + bool useLipSync; + double lipSyncGain; + double lipSyncCutOff; + unsigned int audioBufSize; + double mouthForm; + int top; + int bottom; + int left; + int right; + int screen; + Coord middle; + } m_cfg; + + bool m_stop; + + Coord m_curPos; + + xdo_t *m_xdo; + + std::thread m_getVolumeThread; + void audioLoop(void); + double m_currentVol; + pa_simple *m_pulse; + + void populateDefaultConfig(void); + void parseConfig(std::string cfgPath); +}; + +#endif diff --git a/lib/dlib b/lib/dlib deleted file mode 160000 index 23b9abd..0000000 --- a/lib/dlib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 23b9abd07a56f9fef560aa9e263b37f82543a0cc diff --git a/src/faceXAngle.png b/src/faceXAngle.png deleted file mode 100644 index c35e26eae46f9251ceb2b93507b0b41c548265db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72607 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

Fi?h)925dS-|x442ky8 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 Z5Yg+EU-p(Uh01n>jvc~lXKI}f`Y%glf;0dC diff --git a/src/facial_landmark_detector.cpp b/src/facial_landmark_detector.cpp deleted file mode 100644 index fb536b6..0000000 --- a/src/facial_landmark_detector.cpp +++ /dev/null @@ -1,762 +0,0 @@ -/**** -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; - } - - params.autoBlink = m_cfg.autoBlink; - params.autoBreath = m_cfg.autoBreath; - params.randomMotion = m_cfg.randomMotion; - - 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 if (paramName == "autoBlink") - { - if (!(ss >> m_cfg.autoBlink)) - { - throwConfigError(paramName, "bool", - line, lineNum); - } - } - else if (paramName == "autoBreath") - { - if (!(ss >> m_cfg.autoBreath)) - { - throwConfigError(paramName, "bool", - line, lineNum); - } - } - else if (paramName == "randomMotion") - { - if (!(ss >> m_cfg.randomMotion)) - { - throwConfigError(paramName, "bool", - 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; - m_cfg.autoBlink = false; - m_cfg.autoBreath = false; - m_cfg.randomMotion = false; -} - -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 deleted file mode 100644 index d465c87..0000000 --- a/src/math_utils.h +++ /dev/null @@ -1,108 +0,0 @@ -// -*- 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 diff --git a/src/mouse_cursor_tracker.cpp b/src/mouse_cursor_tracker.cpp new file mode 100644 index 0000000..8ccdb35 --- /dev/null +++ b/src/mouse_cursor_tracker.cpp @@ -0,0 +1,352 @@ +/**** +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 + +extern "C" +{ +#include +#include +} +#include "mouse_cursor_tracker.h" + +static double rms(float *buf, std::size_t count) +{ + double sum = 0; + for (std::size_t i = 0; i < count; i++) + { + sum += buf[i] * buf[i]; + } + return std::sqrt(sum / count); +} + +MouseCursorTracker::MouseCursorTracker(std::string cfgPath) + : m_stop(false) +{ + parseConfig(cfgPath); + m_xdo = xdo_new(nullptr); + + const pa_sample_spec ss = + { + .format = PA_SAMPLE_FLOAT32NE, + .rate = 44100, + .channels = 2 + }; + m_pulse = pa_simple_new(nullptr, "MouseCursorTracker", PA_STREAM_RECORD, + nullptr, "LipSync", &ss, nullptr, nullptr, nullptr); + if (!m_pulse) + { + throw std::runtime_error("Unable to create pulse"); + } + + m_getVolumeThread = std::thread(&MouseCursorTracker::audioLoop, this); +} + +void MouseCursorTracker::audioLoop(void) +{ + float *buf = new float[m_cfg.audioBufSize]; + + std::size_t audioBufByteSize = m_cfg.audioBufSize * sizeof *buf; + + while (!m_stop) + { + if (pa_simple_read(m_pulse, buf, audioBufByteSize, nullptr) < 0) + { + throw std::runtime_error("Unable to get audio data"); + } + m_currentVol = rms(buf, m_cfg.audioBufSize); + } + + delete[] buf; +} + +MouseCursorTracker::~MouseCursorTracker() +{ + xdo_free(m_xdo); + m_getVolumeThread.join(); + pa_simple_free(m_pulse); +} + +void MouseCursorTracker::stop(void) +{ + m_stop = true; +} + +MouseCursorTracker::Params MouseCursorTracker::getParams(void) const +{ + Params params = Params(); + + int xOffset = m_curPos.x - m_cfg.middle.x; + int leftRange = m_cfg.middle.x - m_cfg.left; + int rightRange = m_cfg.right - m_cfg.middle.x; + + if (xOffset > 0) // i.e. to the right + { + params.faceXAngle = 30.0 * xOffset / rightRange; + } + else // to the left + { + params.faceXAngle = 30.0 * xOffset / leftRange; + } + + int yOffset = m_curPos.y - m_cfg.middle.y; + int topRange = m_cfg.middle.y - m_cfg.top; + int bottomRange = m_cfg.bottom - m_cfg.middle.y; + + if (yOffset > 0) // downwards + { + params.faceYAngle = -30.0 * yOffset / bottomRange; + } + else // upwards + { + params.faceYAngle = -30.0 * yOffset / topRange; + } + + params.faceZAngle = 0; + + params.leftEyeOpenness = 1; + params.rightEyeOpenness = 1; + + params.autoBlink = m_cfg.autoBlink; + params.autoBreath = m_cfg.autoBreath; + params.randomMotion = m_cfg.randomMotion; + params.useLipSync = m_cfg.useLipSync; + + params.mouthForm = m_cfg.mouthForm; + + if (m_cfg.useLipSync) + { + params.lipSyncParam = m_currentVol * m_cfg.lipSyncGain; + if (params.lipSyncParam < m_cfg.lipSyncCutOff) + { + params.lipSyncParam = 0; + } + else if (params.lipSyncParam > 1) + { + params.lipSyncParam = 1; + } + } + + + // Leave everything else as zero + + + return params; +} + +void MouseCursorTracker::mainLoop(void) +{ + while (!m_stop) + { + int x; + int y; + int screenNum; + + xdo_get_mouse_location(m_xdo, &x, &y, &screenNum); + + if (screenNum == m_cfg.screen) + { + m_curPos.x = x; + m_curPos.y = y; + } + // else just silently ignore for now + std::this_thread::sleep_for(std::chrono::milliseconds(m_cfg.sleepMs)); + } +} + +void MouseCursorTracker::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)) + { + if (line[0] == '#') + { + continue; + } + + std::istringstream ss(line); + std::string paramName; + + if (ss >> paramName) + { + if (paramName == "sleep_ms") + { + if (!(ss >> m_cfg.sleepMs)) + { + throw std::runtime_error("Error parsing sleep_ms"); + } + } + else if (paramName == "autoBlink") + { + if (!(ss >> m_cfg.autoBlink)) + { + throw std::runtime_error("Error parsing autoBlink"); + } + } + else if (paramName == "autoBreath") + { + if (!(ss >> m_cfg.autoBreath)) + { + throw std::runtime_error("Error parsing autoBreath"); + } + } + else if (paramName == "randomMotion") + { + if (!(ss >> m_cfg.randomMotion)) + { + throw std::runtime_error("Error parsing randomMotion"); + } + } + else if (paramName == "useLipSync") + { + if (!(ss >> m_cfg.useLipSync)) + { + throw std::runtime_error("Error parsing useLipSync"); + } + } + else if (paramName == "lipSyncGain") + { + if (!(ss >> m_cfg.lipSyncGain)) + { + throw std::runtime_error("Error parsing lipSyncGain"); + } + } + else if (paramName == "lipSyncCutOff") + { + if (!(ss >> m_cfg.lipSyncCutOff)) + { + throw std::runtime_error("Error parsing lipSyncCutOff"); + } + } + else if (paramName == "audioBufSize") + { + if (!(ss >> m_cfg.audioBufSize)) + { + throw std::runtime_error("Error parsing audioBufSize"); + } + } + else if (paramName == "mouthForm") + { + if (!(ss >> m_cfg.mouthForm)) + { + throw std::runtime_error("Error parsing mouthForm"); + } + } + else if (paramName == "screen") + { + if (!(ss >> m_cfg.screen)) + { + throw std::runtime_error("Error parsing screen"); + } + } + else if (paramName == "middle_x") + { + if (!(ss >> m_cfg.middle.x)) + { + throw std::runtime_error("Error parsing middle_x"); + } + } + else if (paramName == "middle_y") + { + if (!(ss >> m_cfg.middle.y)) + { + throw std::runtime_error("Error parsing middle_y"); + } + } + else if (paramName == "top") + { + if (!(ss >> m_cfg.top)) + { + throw std::runtime_error("Error parsing top"); + } + } + else if (paramName == "bottom") + { + if (!(ss >> m_cfg.bottom)) + { + throw std::runtime_error("Error parsing bottom"); + } + } + else if (paramName == "left") + { + if (!(ss >> m_cfg.left)) + { + throw std::runtime_error("Error parsing left"); + } + } + else if (paramName == "right") + { + if (!(ss >> m_cfg.right)) + { + throw std::runtime_error("Error parsing right"); + } + } + else + { + throw std::runtime_error("Unrecognized config parameter"); + } + } + } + } +} + +void MouseCursorTracker::populateDefaultConfig(void) +{ + m_cfg.sleepMs = 5; + m_cfg.autoBlink = true; + m_cfg.autoBreath = true; + m_cfg.randomMotion = false; + m_cfg.useLipSync = true; + m_cfg.lipSyncGain = 10; + m_cfg.lipSyncCutOff = 0.15; + m_cfg.audioBufSize = 4096; + m_cfg.mouthForm = 0; + m_cfg.top = 0; + m_cfg.bottom = 1079; + m_cfg.left = 0; + m_cfg.right = 1919; // These will be the full screen for 1920x1080 + + m_cfg.screen = 0; + m_cfg.middle = {1600, 870}; // Somewhere near the bottom right +} + -- 2.7.4