├── ci ├── ci_includes.cmd.in ├── forum-update-info.json ├── ci_includes.sh.in ├── macos │ ├── install-packagesbuild.sh │ └── change-rpath.py ├── windows │ └── package-windows.cmd ├── download-dlib-models.sh └── plugin.spec ├── .gitignore ├── data └── locale │ └── en-US.ini ├── .gitmodules ├── src ├── source_list.h ├── face-detector-dlib-hog-datagen.cpp ├── face-tracker-preset.h ├── ptz-backend.cpp ├── dummy-backend.hpp ├── texture-object.h ├── face-detector-dlib-cnn.h ├── face-detector-dlib-hog.h ├── face-tracker-dlib.h ├── obsptz-backend.hpp ├── dummy-backend.cpp ├── face-detector-base.h ├── ptz-backend.hpp ├── plugin-macros.h.in ├── face-tracker-base.h ├── face-tracker.hpp ├── face-tracker-ptz.hpp ├── libvisca-thread.hpp ├── source_list.cc ├── face-detector-base.cpp ├── module-main.c ├── face-tracker-base.cpp ├── face-tracker-manager.hpp ├── texture-object.cpp ├── face-detector-dlib-hog.cpp ├── obsptz-backend.cpp ├── face-detector-dlib-cnn.cpp ├── helper.hpp ├── helper.cpp ├── face-tracker-dlib.cpp ├── face-tracker-preset.cpp ├── libvisca-thread.cpp ├── face-tracker-monitor.cpp └── face-tracker-manager.cpp ├── .github ├── containers │ ├── fedora-template │ │ ├── build.sh │ │ └── Dockerfile │ └── fedora-common │ │ └── build.sh ├── workflows │ ├── clang-format.yml │ ├── docker-build.yml │ └── main.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── support-request.md │ └── bug_report.md ├── cmake ├── bundle │ └── macos │ │ ├── entitlements.plist │ │ └── Plugin-Info.plist.in └── ObsPluginHelpers.cmake ├── ui ├── face-tracker-dock-internal.hpp ├── face-tracker-widget.hpp ├── obsgui-helper.hpp ├── face-tracker-dock.hpp └── face-tracker-widget.cpp ├── installer └── installer-Windows.iss.in ├── .clang-format ├── CMakeLists.txt ├── README.md ├── doc ├── properties.md └── properties-ptz.md └── LICENSE /ci/ci_includes.cmd.in: -------------------------------------------------------------------------------- 1 | set PluginName=@CMAKE_PROJECT_NAME@ 2 | set PluginVersion=@CMAKE_PROJECT_VERSION@ 3 | -------------------------------------------------------------------------------- /ci/forum-update-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin_url": "https://obsproject.com/forum/resources/face-tracker.1294/" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | /build/ 4 | /build32/ 5 | /build64/ 6 | /release/ 7 | /installer/Output/ 8 | 9 | .vscode 10 | .idea 11 | -------------------------------------------------------------------------------- /data/locale/en-US.ini: -------------------------------------------------------------------------------- 1 | Detector.dlib.hog="HOG, dlib" 2 | Detector.dlib.cnn="CNN, dlib" 3 | dock.menu.close="Close" 4 | Prop.Automation.InactiveReset="Reset while inactive" 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libvisca"] 2 | path = libvisca 3 | url = https://github.com/norihiro/libvisca-ip.git 4 | [submodule "dlib"] 5 | path = dlib 6 | url = https://github.com/norihiro/dlib.git 7 | -------------------------------------------------------------------------------- /ci/ci_includes.sh.in: -------------------------------------------------------------------------------- 1 | PLUGIN_NAME="@CMAKE_PROJECT_NAME@" 2 | PLUGIN_VERSION="@CMAKE_PROJECT_VERSION@" 3 | MACOS_BUNDLEID="@MACOS_BUNDLEID@" 4 | LINUX_MAINTAINER_EMAIL="@LINUX_MAINTAINER_EMAIL@" 5 | PKG_SUFFIX='@PKG_SUFFIX@' 6 | -------------------------------------------------------------------------------- /src/source_list.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef __cplusplus 4 | extern "C" { 5 | #endif 6 | 7 | void property_list_add_sources(obs_property_t *prop, obs_source_t *self); 8 | 9 | #ifdef __cplusplus 10 | } // extern "C" 11 | #endif 12 | -------------------------------------------------------------------------------- /.github/containers/fedora-template/build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -ex 3 | .github/containers/fedora-common/build.sh obs-plugin-build/fedora%releasever% fedora%releasever%-rpmbuild 4 | echo 'FILE_NAME=fedora%releasever%-rpmbuild/*RPMS/**/*.rpm' >> $GITHUB_ENV 5 | -------------------------------------------------------------------------------- /src/face-detector-dlib-hog-datagen.cpp: -------------------------------------------------------------------------------- 1 | #include "plugin-macros.generated.h" 2 | #include 3 | #include 4 | 5 | int main() 6 | { 7 | std::cout << dlib::get_serialized_frontal_faces(); 8 | return 0; 9 | } 10 | -------------------------------------------------------------------------------- /src/face-tracker-preset.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void ftf_preset_item_to_list(obs_property_t *p, obs_data_t *settings); 4 | bool ftf_preset_load(obs_properties_t *props, obs_property_t *, void *ctx_data); 5 | bool ftf_preset_save(obs_properties_t *props, obs_property_t *, void *ctx_data); 6 | bool ftf_preset_delete(obs_properties_t *props, obs_property_t *, void *ctx_data); 7 | -------------------------------------------------------------------------------- /src/ptz-backend.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "plugin-macros.generated.h" 4 | #include "ptz-backend.hpp" 5 | 6 | ptz_backend::ptz_backend() 7 | { 8 | ref = 1; 9 | } 10 | 11 | ptz_backend::~ptz_backend() {} 12 | 13 | void ptz_backend::release() 14 | { 15 | if (os_atomic_dec_long(&ref) == 0) 16 | delete this; 17 | } 18 | -------------------------------------------------------------------------------- /.github/containers/fedora-template/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fedora:%releasever% 2 | 3 | RUN dnf install -y rpm-build python3-dnf-plugins-core && dnf clean all 4 | RUN dnf install -y --setopt=install_weak_deps=false obs-studio obs-studio-devel cmake gcc gcc-c++ && dnf clean all 5 | RUN dnf install -y qt6-qtbase-devel qt6-qtbase-private-devel && dnf clean all 6 | 7 | RUN useradd -s /bin/bash -m rpm 8 | RUN echo >> /etc/sudoers 9 | RUN echo "rpm ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers 10 | 11 | USER rpm 12 | WORKDIR /home/rpm 13 | -------------------------------------------------------------------------------- /src/dummy-backend.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ptz-backend.hpp" 4 | 5 | class dummy_backend : public ptz_backend { 6 | int prev_pan = 0; 7 | int prev_tilt = 0; 8 | int prev_zoom = 0; 9 | 10 | public: 11 | dummy_backend(); 12 | ~dummy_backend() override; 13 | 14 | void set_config(struct obs_data *data) override; 15 | void set_pantilt_speed(int pan, int tilt) override; 16 | void set_zoom_speed(int zoom) override; 17 | void recall_preset(int preset) override; 18 | float get_zoom() override; 19 | }; 20 | -------------------------------------------------------------------------------- /src/texture-object.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "plugin-macros.generated.h" 7 | 8 | class texture_object { 9 | struct texture_object_private_s *data; 10 | 11 | public: 12 | texture_object(); 13 | ~texture_object(); 14 | 15 | void set_texture_obsframe(const struct obs_source_frame *frame, int scale); 16 | bool get_dlib_rgb_image(dlib::matrix &img) const; 17 | 18 | public: 19 | int tick; 20 | float scale; 21 | }; 22 | -------------------------------------------------------------------------------- /src/face-detector-dlib-cnn.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "plugin-macros.generated.h" 5 | #include "face-detector-base.h" 6 | 7 | class face_detector_dlib_cnn : public face_detector_base { 8 | struct private_s *p; 9 | 10 | void detect_main() override; 11 | 12 | public: 13 | face_detector_dlib_cnn(); 14 | virtual ~face_detector_dlib_cnn(); 15 | void set_texture(std::shared_ptr &, int crop_l, int crop_r, int crop_t, int crop_b) override; 16 | void get_faces(std::vector &) override; 17 | 18 | void set_model(const char *filename); 19 | }; 20 | -------------------------------------------------------------------------------- /src/face-detector-dlib-hog.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "plugin-macros.generated.h" 5 | #include "face-detector-base.h" 6 | 7 | class face_detector_dlib_hog : public face_detector_base { 8 | struct face_detector_dlib_private_s *p; 9 | 10 | void detect_main() override; 11 | 12 | public: 13 | face_detector_dlib_hog(); 14 | virtual ~face_detector_dlib_hog(); 15 | void set_texture(std::shared_ptr &, int crop_l, int crop_r, int crop_t, int crop_b) override; 16 | void get_faces(std::vector &) override; 17 | 18 | void set_model(const char *filename); 19 | }; 20 | -------------------------------------------------------------------------------- /ci/macos/install-packagesbuild.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | if which packagesbuild; then 6 | exit 0 7 | fi 8 | 9 | packages_url='http://www.nagater.net/obs-studio/Packages.dmg' 10 | packages_hash='6afdd25386295974dad8f078b8f1e41cabebd08e72d970bf92f707c7e48b16c9' 11 | 12 | for ((retry=5; retry>0; retry--)); do 13 | curl -o Packages.dmg $packages_url 14 | sha256sum -c <<<"$packages_hash Packages.dmg" && break 15 | done 16 | 17 | hdiutil attach -noverify Packages.dmg 18 | packages_volume="$(hdiutil info -plist | grep '/Volumes/Packages' | sed 's/.*\(\/Volumes\/[^<]*\)<\/string>/\1/')" 19 | 20 | sudo installer -pkg "${packages_volume}/packages/Packages.pkg" -target / 21 | hdiutil detach "${packages_volume}" 22 | -------------------------------------------------------------------------------- /ci/windows/package-windows.cmd: -------------------------------------------------------------------------------- 1 | call "build\ci\ci_includes.generated.cmd" 2 | 3 | mkdir package 4 | cd package 5 | 6 | git describe --tags --always > package-version.txt 7 | set /p PackageVersion= 2 | #include 3 | #include 4 | #include "plugin-macros.generated.h" 5 | #include "face-tracker-base.h" 6 | 7 | class face_tracker_dlib : public face_tracker_base { 8 | struct face_tracker_dlib_private_s *p; 9 | 10 | void track_main() override; 11 | 12 | public: 13 | face_tracker_dlib(); 14 | virtual ~face_tracker_dlib(); 15 | 16 | void set_texture(std::shared_ptr &) override; 17 | void set_position(const rect_s &rect) override; 18 | void set_upsize_info(const rectf_s &upsize) override; 19 | void set_landmark_detection(const char *data_file_path) override; 20 | bool get_face(struct rect_s &) override; 21 | bool get_landmark(std::vector &) override; 22 | }; 23 | -------------------------------------------------------------------------------- /cmake/bundle/macos/entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.device.camera 8 | 9 | com.apple.security.device.audio-input 10 | 11 | com.apple.security.cs.disable-library-validation 12 | 13 | 14 | com.apple.security.cs.allow-dyld-environment-variables 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/clang-format.yml: -------------------------------------------------------------------------------- 1 | name: Clang Format Check 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | clang: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Clang 17 | run: | 18 | sudo apt-get install -y clang-format-18 19 | clang-format -i -fallback-style=none $(git ls-files src/ ui/ | grep '[^/]\.[ch]') 20 | 21 | - name: Check 22 | # Build your program with the given configuration 23 | run: | 24 | dirty=$(git ls-files --modified) 25 | set +x 26 | if [[ $dirty ]]; then 27 | git diff 28 | echo "Error: File(s) are not properly formatted." 29 | echo "$dirty" 30 | exit 1 31 | fi 32 | -------------------------------------------------------------------------------- /ui/face-tracker-dock-internal.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include "util/threading.h" 5 | 6 | struct face_tracker_dock_s 7 | { 8 | obs_display_t *disp; 9 | pthread_mutex_t mutex; 10 | volatile long ref; 11 | 12 | obs_source_t *src_monitor; 13 | }; 14 | 15 | static inline void face_tracker_dock_addref(struct face_tracker_dock_s *data) 16 | { 17 | if (!data) 18 | return; 19 | os_atomic_inc_long(&data->ref); 20 | } 21 | 22 | struct face_tracker_dock_s *face_tracker_dock_create(); 23 | void face_tracker_dock_destroy(struct face_tracker_dock_s *data); 24 | 25 | static inline void face_tracker_dock_release(struct face_tracker_dock_s *data) 26 | { 27 | if (!data) 28 | return; 29 | if (os_atomic_dec_long(&data->ref) == 0) 30 | face_tracker_dock_destroy(data); 31 | } 32 | -------------------------------------------------------------------------------- /src/obsptz-backend.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ptz-backend.hpp" 3 | 4 | class obsptz_backend : public ptz_backend { 5 | uint64_t available_ns = 0; 6 | int device_id = -1; 7 | int ptz_max_x = 0, ptz_max_y = 0, ptz_max_z = 0; 8 | proc_handler_t *ptz_ph = NULL; 9 | proc_handler_t *get_ptz_ph(); 10 | int prev_pan = 0; 11 | int prev_tilt = 0; 12 | int prev_zoom = 0; 13 | int same_pantilt_cnt = 0; 14 | int same_zoom_cnt = 0; 15 | 16 | public: 17 | obsptz_backend(); 18 | ~obsptz_backend() override; 19 | 20 | void set_config(struct obs_data *data) override; 21 | bool can_send() override; 22 | void tick() override; 23 | void set_pantilt_speed(int pan, int tilt) override; 24 | void set_zoom_speed(int zoom) override; 25 | void recall_preset(int preset) override; 26 | float get_zoom() override; 27 | 28 | static bool ptz_type_modified(obs_properties_t *group_output, obs_data_t *settings); 29 | }; 30 | -------------------------------------------------------------------------------- /src/dummy-backend.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "plugin-macros.generated.h" 3 | #include "dummy-backend.hpp" 4 | 5 | #define debug(...) blog(LOG_INFO, __VA_ARGS__) 6 | 7 | dummy_backend::dummy_backend() {} 8 | 9 | dummy_backend::~dummy_backend() {} 10 | 11 | void dummy_backend::set_config(struct obs_data *) {} 12 | 13 | void dummy_backend::set_pantilt_speed(int pan, int tilt) 14 | { 15 | if (pan == prev_pan && tilt == prev_tilt) 16 | return; 17 | 18 | blog(LOG_INFO, "set_pantilt_speed: %d %d", pan, tilt); 19 | 20 | prev_pan = pan; 21 | prev_tilt = tilt; 22 | } 23 | 24 | void dummy_backend::set_zoom_speed(int zoom) 25 | { 26 | if (zoom == prev_zoom) 27 | return; 28 | 29 | blog(LOG_INFO, "set_zoom_speed: %d", zoom); 30 | 31 | prev_zoom = zoom; 32 | } 33 | 34 | void dummy_backend::recall_preset(int preset) 35 | { 36 | blog(LOG_INFO, "recall_preset: %d", preset); 37 | } 38 | 39 | float dummy_backend::get_zoom() 40 | { 41 | return 1.0f; 42 | } 43 | -------------------------------------------------------------------------------- /cmake/bundle/macos/Plugin-Info.plist.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleName 6 | ${MACOSX_PLUGIN_BUNDLE_NAME} 7 | CFBundleIdentifier 8 | ${MACOSX_PLUGIN_GUI_IDENTIFIER} 9 | CFBundleVersion 10 | ${MACOSX_PLUGIN_BUNDLE_VERSION} 11 | CFBundleShortVersionString 12 | ${MACOSX_PLUGIN_SHORT_VERSION_STRING} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleExecutable 16 | ${MACOSX_PLUGIN_EXECUTABLE_NAME} 17 | CFBundlePackageType 18 | ${MACOSX_PLUGIN_BUNDLE_TYPE} 19 | CFBundleSupportedPlatforms 20 | 21 | MacOSX 22 | 23 | LSMinimumSystemVersion 24 | 10.13 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Source/filter types** 11 | 12 | 13 | 14 | 15 | 16 | 17 | **Is your feature request related to a problem? Please describe.** 18 | 19 | 20 | **Describe the solution you'd like** 21 | 22 | 23 | **Describe alternatives you've considered** 24 | 25 | 26 | **Additional context** 27 | 28 | -------------------------------------------------------------------------------- /src/face-detector-base.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "plugin-macros.generated.h" 7 | #include "helper.hpp" 8 | 9 | class face_detector_base { 10 | pthread_t thread; 11 | pthread_mutex_t mutex; 12 | pthread_cond_t cond; 13 | bool running; 14 | volatile bool request_stop; 15 | void *leak_test; 16 | 17 | static void *thread_routine(void *); 18 | virtual void detect_main() = 0; 19 | 20 | public: 21 | face_detector_base(); 22 | virtual ~face_detector_base(); 23 | 24 | int lock() { return pthread_mutex_lock(&mutex); } 25 | int trylock() { return pthread_mutex_trylock(&mutex); } 26 | int unlock() { return pthread_mutex_unlock(&mutex); } 27 | int signal() { return pthread_cond_signal(&cond); } 28 | 29 | virtual void set_texture(std::shared_ptr &, int crop_l, int crop_r, int crop_t, 30 | int crop_b) = 0; 31 | virtual void get_faces(std::vector &) = 0; 32 | 33 | void start(); 34 | void stop(); 35 | }; 36 | -------------------------------------------------------------------------------- /src/ptz-backend.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class ptz_backend { 6 | volatile long ref; 7 | 8 | public: 9 | ptz_backend(); 10 | virtual ~ptz_backend(); 11 | void add_ref() { os_atomic_inc_long(&ref); } 12 | void release(); 13 | inline long get_ref() { return os_atomic_load_long(&ref); } 14 | 15 | virtual void set_config(struct obs_data *data) = 0; 16 | 17 | virtual bool can_send() { return true; } 18 | virtual void tick() {} 19 | virtual void set_pantilt_speed(int pan, int tilt) = 0; 20 | virtual void set_zoom_speed(int zoom) = 0; 21 | virtual void recall_preset(int preset) = 0; 22 | virtual float get_zoom() = 0; 23 | 24 | virtual void set_pantiltzoom_speed(float pan, float tilt, float zoom) 25 | { 26 | (void)pan; 27 | (void)tilt; 28 | (void)zoom; 29 | } 30 | 31 | inline static bool check_data(obs_data_t *) { return true; } 32 | inline static bool ptz_type_modified(obs_properties_t *group_output, obs_data_t *settings) 33 | { 34 | (void)group_output; 35 | (void)settings; 36 | return false; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support request 3 | about: Select this type if you need support or help 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Source/filter types** 11 | 12 | 13 | 14 | 15 | 16 | 17 | **Describe your problem** 18 | 19 | 20 | **Desktop (please complete the following information):** 21 | - OS: [e.g. Windows, macOS, Linux] 22 | - OBS Version: [e.g. 29.1.3, 30.0.0] 23 | - Face Tracker Version: [e.g. 0.6.4, 0.7.1] 24 | 25 | **Additional context** 26 | 27 | 28 | -------------------------------------------------------------------------------- /ui/face-tracker-widget.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #define SCOPE_WIDGET_N_SRC 4 8 | 9 | class OBSEventFilter; 10 | 11 | class FTWidget : public QWidget { 12 | Q_OBJECT 13 | 14 | void CreateDisplay(); 15 | void resizeEvent(QResizeEvent *event) override; 16 | void paintEvent(QPaintEvent *event) override; 17 | class QPaintEngine *paintEngine() const override; 18 | void closeEvent(QCloseEvent *event) override; 19 | 20 | signals: 21 | void removeDock(); 22 | 23 | public: 24 | FTWidget(struct face_tracker_dock_s *data, QWidget *parent); 25 | ~FTWidget(); 26 | void setShown(bool shown); 27 | void openMenu(const class QPoint &pos); 28 | 29 | private: 30 | struct face_tracker_dock_s *data; 31 | }; 32 | 33 | typedef std::function EventFilterFunc; 34 | 35 | class OBSEventFilter : public QObject { 36 | Q_OBJECT 37 | public: 38 | OBSEventFilter(EventFilterFunc filter_) : filter(filter_) {} 39 | 40 | protected: 41 | bool eventFilter(QObject *obj, QEvent *event) { return filter(obj, event); } 42 | 43 | public: 44 | EventFilterFunc filter; 45 | }; 46 | -------------------------------------------------------------------------------- /src/plugin-macros.h.in: -------------------------------------------------------------------------------- 1 | /* 2 | Plugin Name 3 | Copyright (C) 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program. If not, see 17 | */ 18 | 19 | #ifndef PLUGINNAME_H 20 | #define PLUGINNAME_H 21 | 22 | #define PLUGIN_NAME "@CMAKE_PROJECT_NAME@" 23 | #define PLUGIN_VERSION "@CMAKE_PROJECT_VERSION@" 24 | 25 | #cmakedefine WITH_PTZ_TCP 26 | #cmakedefine ENABLE_MONITOR_USER 27 | #cmakedefine WITH_DOCK 28 | 29 | #define blog(level, msg, ...) blog(level, "[" PLUGIN_NAME "] " msg, ##__VA_ARGS__) 30 | 31 | #endif // PLUGINNAME_H 32 | -------------------------------------------------------------------------------- /src/face-tracker-base.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include "plugin-macros.generated.h" 6 | #include "face-detector-base.h" 7 | 8 | class face_tracker_base { 9 | pthread_t thread; 10 | pthread_mutex_t mutex; 11 | pthread_cond_t cond; 12 | bool running; 13 | volatile bool stop_requested; 14 | volatile bool stopped; 15 | volatile bool suspend_requested; 16 | void *leak_test; 17 | 18 | static void *thread_routine(void *); 19 | virtual void track_main() = 0; 20 | 21 | public: 22 | face_tracker_base(); 23 | virtual ~face_tracker_base(); 24 | 25 | int lock() { return pthread_mutex_lock(&mutex); } 26 | int trylock() { return pthread_mutex_trylock(&mutex); } 27 | int unlock() { return pthread_mutex_unlock(&mutex); } 28 | int signal() { return pthread_cond_signal(&cond); } 29 | 30 | virtual void set_texture(std::shared_ptr &) = 0; 31 | virtual void set_position(const rect_s &rect) = 0; 32 | virtual void set_upsize_info(const rectf_s &upsize) = 0; 33 | virtual void set_landmark_detection(const char *data_file_path) = 0; 34 | virtual bool get_face(struct rect_s &) = 0; 35 | virtual bool get_landmark(std::vector &) = 0; 36 | 37 | void start(); 38 | void stop(); 39 | void request_stop(); 40 | void request_suspend(); 41 | bool is_stopped(); 42 | }; 43 | -------------------------------------------------------------------------------- /ui/obsgui-helper.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #if !defined(_WIN32) && !defined(__APPLE__) // if Linux 4 | #include 5 | #include 6 | #include 7 | #endif 8 | 9 | // copied from obs-studio/UI/qt-wrappers.cpp and modified to support OBS-26 10 | static inline bool QTToGSWindow(QWindow *window, gs_window &gswindow) 11 | { 12 | bool success = true; 13 | 14 | #ifdef _WIN32 15 | gswindow.hwnd = (HWND)window->winId(); 16 | #elif __APPLE__ 17 | gswindow.view = (id)window->winId(); 18 | #else 19 | #ifdef ENABLE_WAYLAND 20 | switch (obs_get_nix_platform()) { 21 | case OBS_NIX_PLATFORM_X11_EGL: 22 | #endif // ENABLE_WAYLAND 23 | gswindow.id = window->winId(); 24 | gswindow.display = obs_get_nix_platform_display(); 25 | #ifdef ENABLE_WAYLAND 26 | break; 27 | case OBS_NIX_PLATFORM_WAYLAND: { 28 | QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface(); 29 | gswindow.display = native->nativeResourceForWindow("surface", window); 30 | success = gswindow.display != nullptr; 31 | break; 32 | } 33 | default: 34 | return false; 35 | } 36 | #endif // ENABLE_WAYLAND 37 | #endif 38 | return success; 39 | } 40 | 41 | // copied from obs-studio/UI/display-helpers.hpp 42 | static inline QSize GetPixelSize(QWidget *widget) 43 | { 44 | return widget->size() * widget->devicePixelRatioF(); 45 | } 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report for bugs 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Source/filter types** 11 | 12 | 13 | 14 | 15 | 16 | 17 | **Describe the bug** 18 | 19 | 20 | **To Reproduce** 21 | 28 | 29 | **Expected behavior** 30 | 31 | 32 | **Actual behavior** 33 | 34 | 35 | **Desktop (please complete the following information):** 36 | - OS: [e.g. Windows, macOS, Linux] 37 | - OBS Version: [e.g. 29.1.3, 30.0.0] 38 | - Face Tracker Version: [e.g. 0.6.4, 0.7.1] 39 | 40 | **Additional context** 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/face-tracker.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "helper.hpp" 6 | 7 | struct face_tracker_filter 8 | { 9 | obs_source_t *context; 10 | gs_texrender_t *texrender; 11 | gs_texrender_t *texrender_scaled; 12 | gs_stagesurf_t *stagesurface; 13 | uint32_t known_width; 14 | uint32_t known_height; 15 | uint32_t width_with_aspect; 16 | uint32_t height_with_aspect; 17 | bool target_valid; 18 | bool rendered; 19 | bool is_active; 20 | 21 | f3 detect_err; 22 | f3 range_min, range_max, range_min_out; 23 | 24 | class ft_manager_for_ftf *ftm; 25 | 26 | float track_z, track_x, track_y; 27 | float scale_max; 28 | 29 | f3 kp; 30 | float ki; 31 | f3 klpf; 32 | f3 tlpf; 33 | f3 e_deadband, e_nonlinear; // deadband and nonlinear amount for error input 34 | f3 filter_int_out; 35 | f3 filter_int; 36 | f3 filter_lpf; 37 | f3 u_last; 38 | int aspect_x, aspect_y; 39 | 40 | // face tracker source 41 | char *target_name; 42 | obs_weak_source_t *target_ref; 43 | 44 | bool inactive_reset = false; 45 | bool debug_faces; 46 | bool debug_notrack; 47 | bool debug_always_show; 48 | FILE *debug_data_tracker; 49 | FILE *debug_data_error; 50 | FILE *debug_data_control; 51 | char *debug_data_tracker_last; 52 | char *debug_data_error_last; 53 | char *debug_data_control_last; 54 | 55 | bool is_paused; 56 | obs_hotkey_pair_id hotkey_pause; 57 | obs_hotkey_id hotkey_reset; 58 | }; 59 | -------------------------------------------------------------------------------- /src/face-tracker-ptz.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "helper.hpp" 6 | 7 | struct face_tracker_ptz 8 | { 9 | obs_source_t *context; 10 | uint32_t known_width; 11 | uint32_t known_height; 12 | bool rendered; 13 | bool is_active; 14 | 15 | video_scaler_t *scaler; 16 | uint8_t *scaler_buffer; 17 | struct video_scale_info scaler_src_info; 18 | struct video_scale_info scaler_dst_info; 19 | 20 | f3 detect_err; 21 | bool face_found, face_found_last; 22 | 23 | class ft_manager_for_ftptz *ftm; 24 | 25 | float track_z, track_x, track_y; 26 | 27 | float kp_x, kp_y, kp_z; 28 | f3 ki; 29 | f3 klpf; 30 | f3 tlpf; 31 | f3 e_deadband, e_nonlinear; // deadband and nonlinear amount for error input 32 | f3 filter_int; 33 | f3 filter_lpf; 34 | float f_att_int; 35 | int u[3]; 36 | float u_linear[3]; 37 | float ptz_query[3]; 38 | uint64_t face_found_last_ns; 39 | int face_lost_preset_sent; 40 | 41 | int face_lost_preset_timeout_ms; 42 | int face_lost_ptz_preset; 43 | int face_lost_zoomout_timeout_ms; 44 | 45 | bool debug_faces; 46 | bool debug_notrack; 47 | bool debug_always_show; 48 | FILE *debug_data_tracker; 49 | FILE *debug_data_error; 50 | FILE *debug_data_control; 51 | char *debug_data_tracker_last; 52 | char *debug_data_error_last; 53 | char *debug_data_control_last; 54 | 55 | char *ptz_type; 56 | 57 | bool is_paused; 58 | obs_hotkey_pair_id hotkey_pause; 59 | obs_hotkey_id hotkey_reset; 60 | }; 61 | -------------------------------------------------------------------------------- /ci/download-dlib-models.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | flg_nonfree=0 4 | 5 | while (($# > 0)); do 6 | case "$1" in 7 | --nonfree) 8 | flg_nonfree=1 9 | shift ;; 10 | *) 11 | echo "Error: unknown option $1" >&2 12 | exit 1;; 13 | esac 14 | done 15 | 16 | mkdir -p ${DESTDIR}data 17 | 18 | mkdir ${DESTDIR}data/dlib_hog_model 19 | curl -LO https://github.com/norihiro/obs-face-tracker/releases/download/0.7.0-hogdata/frontal_face_detector.dat.bz2 20 | bunzip2 < frontal_face_detector.dat.bz2 > ${DESTDIR}data/dlib_hog_model/frontal_face_detector.dat 21 | git clone --depth 1 https://github.com/davisking/dlib-models 22 | mkdir ${DESTDIR}data/{dlib_cnn_model,dlib_face_landmark_model} 23 | bunzip2 < dlib-models/mmod_human_face_detector.dat.bz2 > ${DESTDIR}data/dlib_cnn_model/mmod_human_face_detector.dat 24 | bunzip2 < dlib-models/shape_predictor_5_face_landmarks.dat.bz2 > ${DESTDIR}data/dlib_face_landmark_model/shape_predictor_5_face_landmarks.dat 25 | cp dlib/LICENSE.txt ${DESTDIR}data/LICENSE-dlib 26 | cp dlib-models/LICENSE ${DESTDIR}data/LICENSE-dlib-models 27 | 28 | if ((flg_nonfree)); then 29 | bunzip2 < dlib-models/shape_predictor_68_face_landmarks.dat.bz2 > ${DESTDIR}data/dlib_face_landmark_model/shape_predictor_68_face_landmarks.dat 30 | bunzip2 < dlib-models/shape_predictor_68_face_landmarks_GTX.dat.bz2 > ${DESTDIR}data/dlib_face_landmark_model/shape_predictor_68_face_landmarks_GTX.dat 31 | awk '/^##/{p=0} /^##.*shape_predictor_68/{p=1} p' dlib-models/README.md > ${DESTDIR}data/LICENSE-shape_predictor_68_face_landmarks 32 | fi 33 | -------------------------------------------------------------------------------- /src/libvisca-thread.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | // #include "libvisca.h" 5 | #include "ptz-backend.hpp" 6 | 7 | class libvisca_thread : public ptz_backend { 8 | pthread_mutex_t mutex; 9 | struct _VISCA_interface *iface; 10 | struct _VISCA_camera *camera; 11 | struct obs_data *data; 12 | volatile bool data_changed; 13 | volatile bool preset_changed; 14 | volatile long pan_rsvd, tilt_rsvd, zoom_rsvd; 15 | volatile int preset_rsvd; 16 | volatile long zoom_got; 17 | 18 | static void *thread_main(void *); 19 | void thread_connect(); 20 | void thread_loop(); 21 | float raw2zoomfactor(int); 22 | 23 | public: 24 | libvisca_thread(); 25 | ~libvisca_thread() override; 26 | 27 | void set_config(struct obs_data *data) override; // and attempt to connect 28 | 29 | void set_pantilt_speed(int pan, int tilt) override 30 | { 31 | os_atomic_set_long(&pan_rsvd, pan); 32 | os_atomic_set_long(&tilt_rsvd, tilt); 33 | } 34 | void set_zoom_speed(int zoom) override { os_atomic_set_long(&zoom_rsvd, zoom); } 35 | void recall_preset(int preset) override 36 | { 37 | preset_rsvd = preset; 38 | os_atomic_set_bool(&preset_changed, true); 39 | } 40 | float get_zoom() override { return raw2zoomfactor(os_atomic_load_long(&zoom_got)); } 41 | 42 | inline static bool check_data(obs_data_t *data) 43 | { 44 | if (!obs_data_get_string(data, "address")) 45 | return false; 46 | if (obs_data_get_int(data, "port") <= 0) 47 | return false; 48 | return true; 49 | } 50 | static bool ptz_type_modified(obs_properties_t *group_output, obs_data_t *settings); 51 | }; 52 | -------------------------------------------------------------------------------- /src/source_list.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "plugin-macros.generated.h" 7 | #include "source_list.h" 8 | 9 | struct add_sources_s 10 | { 11 | obs_source_t *self; 12 | std::vector source_names; 13 | }; 14 | 15 | static bool add_sources(void *data, obs_source_t *source) 16 | { 17 | auto &ctx = *(add_sources_s *)data; 18 | 19 | if (source == ctx.self) 20 | return true; 21 | 22 | uint32_t caps = obs_source_get_output_flags(source); 23 | if (~caps & OBS_SOURCE_VIDEO) 24 | return true; 25 | 26 | if (obs_source_is_group(source)) 27 | return true; 28 | 29 | const char *name = obs_source_get_name(source); 30 | ctx.source_names.push_back(name); 31 | return true; 32 | } 33 | 34 | void property_list_add_sources(obs_property_t *prop, obs_source_t *self) 35 | { 36 | // scenes, same order as the scene list 37 | obs_frontend_source_list sceneList = {}; 38 | obs_frontend_get_scenes(&sceneList); 39 | for (size_t i = 0; i < sceneList.sources.num; i++) { 40 | obs_source_t *source = sceneList.sources.array[i]; 41 | const char *c_name = obs_source_get_name(source); 42 | std::string name = obs_module_text("Scene: "); 43 | name += c_name; 44 | obs_property_list_add_string(prop, name.c_str(), c_name); 45 | } 46 | obs_frontend_source_list_free(&sceneList); 47 | 48 | // sources, alphabetical order 49 | add_sources_s ctx; 50 | ctx.self = self; 51 | obs_enum_sources(add_sources, &ctx); 52 | 53 | std::sort(ctx.source_names.begin(), ctx.source_names.end()); 54 | 55 | for (size_t i = 0; i < ctx.source_names.size(); i++) { 56 | const std::string name = obs_module_text("Source: ") + ctx.source_names[i]; 57 | obs_property_list_add_string(prop, name.c_str(), ctx.source_names[i].c_str()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/face-detector-base.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "plugin-macros.generated.h" 6 | #include "face-detector-base.h" 7 | #ifndef _WIN32 8 | #include 9 | #include 10 | #else // _WIN32 11 | #include 12 | #endif // _WIN32 13 | 14 | face_detector_base::face_detector_base() 15 | { 16 | pthread_mutex_init(&mutex, NULL); 17 | pthread_cond_init(&cond, NULL); 18 | request_stop = 0; 19 | running = 0; 20 | leak_test = bmalloc(1); 21 | } 22 | 23 | face_detector_base::~face_detector_base() 24 | { 25 | bfree(leak_test); 26 | pthread_cond_destroy(&cond); 27 | pthread_mutex_destroy(&mutex); 28 | } 29 | 30 | void *face_detector_base::thread_routine(void *p) 31 | { 32 | face_detector_base *base = (face_detector_base *)p; 33 | #ifndef _WIN32 34 | setpriority(PRIO_PROCESS, 0, 19); 35 | #else // _WIN32 36 | SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_LOWEST); 37 | #endif // _WIN32 38 | os_set_thread_name("face-det"); 39 | 40 | base->lock(); 41 | while (!base->request_stop) { 42 | try { 43 | base->detect_main(); 44 | } catch (std::exception &e) { 45 | blog(LOG_ERROR, "detect_main: exception %s", e.what()); 46 | } catch (...) { 47 | blog(LOG_ERROR, "detect_main: unknown exception"); 48 | } 49 | pthread_cond_wait(&base->cond, &base->mutex); 50 | } 51 | base->unlock(); 52 | return NULL; 53 | } 54 | 55 | void face_detector_base::start() 56 | { 57 | blog(LOG_INFO, "face_detector_base: starting the thread."); 58 | request_stop = 0; 59 | pthread_create(&thread, NULL, thread_routine, (void *)this); 60 | running = 1; 61 | } 62 | 63 | void face_detector_base::stop() 64 | { 65 | blog(LOG_INFO, "face_detector_base: stopping the thread..."); 66 | lock(); 67 | request_stop = 1; 68 | signal(); 69 | unlock(); 70 | if (running) { 71 | pthread_join(thread, NULL); 72 | running = 0; 73 | } 74 | blog(LOG_INFO, "face_detector_base: stopped the thread..."); 75 | } 76 | -------------------------------------------------------------------------------- /src/module-main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "plugin-macros.generated.h" 5 | #ifdef WITH_DOCK 6 | #include "../ui/face-tracker-dock.hpp" 7 | #endif // WITH_DOCK 8 | 9 | #define CONFIG_SECTION_NAME "face-tracker" 10 | 11 | OBS_DECLARE_MODULE() 12 | OBS_MODULE_USE_DEFAULT_LOCALE(PLUGIN_NAME, "en-US") 13 | 14 | void register_face_tracker_filter(bool hide_filter, bool hide_source); 15 | void register_face_tracker_ptz(bool hide_ptz); 16 | void register_face_tracker_monitor(bool hide_monitor); 17 | 18 | bool obs_module_load(void) 19 | { 20 | blog(LOG_INFO, "registering face_tracker_filter_info (version %s)", PLUGIN_VERSION); 21 | 22 | #if LIBOBS_API_VER < MAKE_SEMANTIC_VERSION(31, 0, 0) 23 | config_t *cfg = obs_frontend_get_global_config(); 24 | #else 25 | config_t *cfg = obs_frontend_get_app_config(); 26 | #endif 27 | 28 | config_set_default_bool(cfg, CONFIG_SECTION_NAME, "ShowFilter", true); 29 | config_set_default_bool(cfg, CONFIG_SECTION_NAME, "ShowSource", true); 30 | config_set_default_bool(cfg, CONFIG_SECTION_NAME, "ShowPTZ", true); 31 | #ifdef ENABLE_MONITOR_USER 32 | config_set_default_bool(cfg, CONFIG_SECTION_NAME, "ShowMonitor", true); 33 | #else 34 | config_set_default_bool(cfg, CONFIG_SECTION_NAME, "ShowMonitor", false); 35 | #endif 36 | 37 | bool show_filter = config_get_bool(cfg, CONFIG_SECTION_NAME, "ShowFilter"); 38 | bool show_source = config_get_bool(cfg, CONFIG_SECTION_NAME, "ShowSource"); 39 | bool show_ptz = config_get_bool(cfg, CONFIG_SECTION_NAME, "ShowPTZ"); 40 | bool show_monitor = config_get_bool(cfg, CONFIG_SECTION_NAME, "ShowMonitor"); 41 | 42 | register_face_tracker_filter(!show_filter, !show_source); 43 | register_face_tracker_ptz(!show_ptz); 44 | register_face_tracker_monitor(!show_monitor); 45 | 46 | #ifdef WITH_DOCK 47 | config_set_default_bool(cfg, CONFIG_SECTION_NAME, "LoadDock", true); 48 | bool load_dock = config_get_bool(cfg, CONFIG_SECTION_NAME, "LoadDock"); 49 | if (load_dock) 50 | ft_docks_init(); 51 | #endif // WITH_DOCK 52 | return true; 53 | } 54 | 55 | void obs_module_unload() 56 | { 57 | #ifdef WITH_DOCK 58 | ft_docks_release(); 59 | #endif // WITH_DOCK 60 | } 61 | -------------------------------------------------------------------------------- /ui/face-tracker-dock.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef __cplusplus 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | class FTDock : public QFrame { 11 | Q_OBJECT 12 | 13 | public: 14 | class FTWidget *widget; 15 | std::string name; 16 | struct face_tracker_dock_s *data; 17 | 18 | class QVBoxLayout *mainLayout; 19 | class QComboBox *targetSelector; 20 | class QPushButton *pauseButton; 21 | class QPushButton *resetButton; 22 | class QPushButton *enableButton; 23 | class QPushButton *propertyButton; 24 | class FTWidget *ftWidget; 25 | class QCheckBox *notrackButton; 26 | 27 | bool updating_widget = false; 28 | bool in_updateState = false; 29 | 30 | public: 31 | FTDock(QWidget *parent = nullptr); 32 | ~FTDock(); 33 | void closeEvent(QCloseEvent *event) override; 34 | 35 | static void default_properties(obs_data_t *); 36 | void save_properties(obs_data_t *); 37 | void load_properties(obs_data_t *); 38 | 39 | private: 40 | void showEvent(QShowEvent *event) override; 41 | void hideEvent(QHideEvent *event) override; 42 | 43 | void frontendEvent(enum obs_frontend_event event); 44 | static void frontendEvent_cb(enum obs_frontend_event event, void *private_data); 45 | 46 | OBSSource get_source(); 47 | signal_handler_t *source_sh = NULL; 48 | 49 | static void onStateChanged(void *data, calldata_t *cd); 50 | 51 | signals: 52 | void scenesMayChanged(); 53 | void dataChanged(); 54 | 55 | public slots: 56 | void checkTargetSelector(); 57 | void updateState(); 58 | void updateWidget(); 59 | void removeDock(); 60 | 61 | private slots: 62 | void targetSelectorChanged(); 63 | void pauseButtonClicked(bool checked); 64 | void resetButtonClicked(bool checked); 65 | void enableButtonClicked(bool checked); 66 | void propertyButtonClicked(bool checked); 67 | void notrackButtonChanged( 68 | #if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) 69 | int state 70 | #else 71 | Qt::CheckState state 72 | #endif 73 | ); 74 | }; 75 | 76 | extern "C" { 77 | #endif // __cplusplus 78 | void ft_dock_add(const char *name, obs_data_t *props, bool show); 79 | void ft_docks_init(); 80 | void ft_docks_release(); 81 | 82 | #ifdef __cplusplus 83 | } // extern "C" 84 | #endif 85 | -------------------------------------------------------------------------------- /.github/containers/fedora-common/build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -ex 4 | 5 | docker_image="$1" 6 | rpmbuild="$2" 7 | 8 | PLUGIN_NAME=$(awk '/^project\(/{print gensub(/project\(([^ ()]*).*/, "\\1", 1, $0)}' CMakeLists.txt) 9 | PLUGIN_NAME_FEDORA="$(sed -e 's/^obs-/obs-studio-plugin-/' <<< "$PLUGIN_NAME")" 10 | OBS_VERSION=$(docker run $docker_image bash -c 'rpm -q --qf "%{version}" obs-studio') 11 | eval $(git describe --tag --always --long | awk ' 12 | BEGIN { 13 | VERSION="unknown"; 14 | RELEASE=0; 15 | } 16 | { 17 | if (match($0, /^(.*)-([0-9]*)-g[0-9a-f]*$/, aa)) { 18 | VERSION = aa[1] 19 | RELEASE = aa[2] 20 | } 21 | } 22 | END { 23 | VERSION = gensub(/-(alpha|beta|rc)/, "~\\1", 1, VERSION); 24 | gsub(/["'\''-]/, ".", VERSION); 25 | printf("VERSION='\''%s'\'' RELEASE=%d\n", VERSION, RELEASE + 1); 26 | }') 27 | 28 | rm -rf $rpmbuild 29 | mkdir -p $rpmbuild/{BUILD,BUILDROOT,SRPMS,SOURCES,SPECS,RPMS} 30 | rpmbuild="$(cd $rpmbuild && pwd -P)" 31 | chmod a+w $rpmbuild/{BUILD,BUILDROOT,SRPMS,RPMS} 32 | test -x /usr/sbin/selinuxenabled && /usr/sbin/selinuxenabled && chcon -Rt container_file_t $rpmbuild 33 | 34 | # Prepare files 35 | sed \ 36 | -e "s/@PLUGIN_NAME@/$PLUGIN_NAME/g" \ 37 | -e "s/@PLUGIN_NAME_FEDORA@/$PLUGIN_NAME_FEDORA/g" \ 38 | -e "s/@VERSION@/$VERSION/g" \ 39 | -e "s/@RELEASE@/$RELEASE/g" \ 40 | -e "s/@OBS_VERSION@/$OBS_VERSION/g" \ 41 | < ci/plugin.spec \ 42 | > $rpmbuild/SPECS/$PLUGIN_NAME_FEDORA.spec 43 | 44 | DESTDIR='dlib-models-data/' ci/download-dlib-models.sh --nonfree 45 | 46 | git archive --format=tar --prefix=$PLUGIN_NAME_FEDORA-$VERSION/ HEAD | bzip2 > $rpmbuild/SOURCES/$PLUGIN_NAME_FEDORA-$VERSION.tar.bz2 47 | (cd libvisca && git archive --format=tar --prefix=libvisca/ HEAD) | bzip2 > $rpmbuild/SOURCES/$PLUGIN_NAME_FEDORA-$VERSION-libvisca.tar.bz2 48 | (cd dlib-models-data && tar cj .) > $rpmbuild/SOURCES/$PLUGIN_NAME_FEDORA-$VERSION-dlib-models.tar.bz2 49 | 50 | docker run -v $rpmbuild:/home/rpm/rpmbuild $docker_image bash -c " 51 | if awk '/Fedora release/{fc=\$3} END{exit(fc>=43 ? 0 : 1)}' /etc/fedora-release; then 52 | sudo dnf copr -y enable kamae/obs-studio-plugins 53 | fi 54 | sudo dnf builddep -y ~/rpmbuild/SPECS/$PLUGIN_NAME_FEDORA.spec && 55 | sudo chown 0:0 ~/rpmbuild/SOURCES/* && 56 | sudo chown 0:0 ~/rpmbuild/SPECS/* && 57 | rpmbuild -ba ~/rpmbuild/SPECS/$PLUGIN_NAME_FEDORA.spec 58 | " 59 | -------------------------------------------------------------------------------- /installer/installer-Windows.iss.in: -------------------------------------------------------------------------------- 1 | #define MyAppName "@CMAKE_PROJECT_NAME@" 2 | #define MyAppVersion "@CMAKE_PROJECT_VERSION@" 3 | #define MyAppPublisher "@PLUGIN_AUTHOR@" 4 | #define MyAppURL "@PLUGIN_URL@" 5 | 6 | [Setup] 7 | ; NOTE: The value of AppId uniquely identifies this application. 8 | ; Do not use the same AppId value in installers for other applications. 9 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 10 | AppId={{723828DB-04B8-41F7-9F7A-AF692B2CDF3D} 11 | AppName={#MyAppName} 12 | AppVersion={#MyAppVersion} 13 | AppPublisher={#MyAppPublisher} 14 | AppPublisherURL={#MyAppURL} 15 | AppSupportURL={#MyAppURL} 16 | AppUpdatesURL={#MyAppURL} 17 | DefaultDirName={code:GetDirName} 18 | DefaultGroupName={#MyAppName} 19 | OutputBaseFilename={#MyAppName}-{#MyAppVersion}-Windows-Installer 20 | Compression=lzma 21 | SolidCompression=yes 22 | 23 | [Languages] 24 | Name: "english"; MessagesFile: "compiler:Default.isl" 25 | 26 | [Files] 27 | Source: "..\release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 28 | Source: "..\LICENSE"; Flags: dontcopy 29 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 30 | 31 | [Icons] 32 | Name: "{group}\{cm:ProgramOnTheWeb,{#MyAppName}}"; Filename: "{#MyAppURL}" 33 | Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" 34 | 35 | [Code] 36 | procedure InitializeWizard(); 37 | var 38 | GPLText: AnsiString; 39 | Page: TOutputMsgMemoWizardPage; 40 | begin 41 | ExtractTemporaryFile('LICENSE'); 42 | LoadStringFromFile(ExpandConstant('{tmp}\LICENSE'), GPLText); 43 | Page := CreateOutputMsgMemoPage(wpWelcome, 44 | 'License Information', 'Please review the license terms before installing {#MyAppName}', 45 | 'Press Page Down to see the rest of the agreement. Once you are aware of your rights, click Next to continue.', 46 | String(GPLText) 47 | ); 48 | end; 49 | 50 | // credit where it's due : 51 | // following function come from https://github.com/Xaymar/obs-studio_amf-encoder-plugin/blob/master/%23Resources/Installer.in.iss#L45 52 | function GetDirName(Value: string): string; 53 | var 54 | InstallPath: string; 55 | begin 56 | // initialize default path, which will be returned when the following registry 57 | // key queries fail due to missing keys or for some different reason 58 | Result := '{pf}\obs-studio'; 59 | // query the first registry value; if this succeeds, return the obtained value 60 | if RegQueryStringValue(HKLM32, 'SOFTWARE\OBS Studio', '', InstallPath) then 61 | Result := InstallPath 62 | end; 63 | 64 | -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Plugin Build on Docker 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | branches: 8 | - main 9 | tags: 10 | - '*' 11 | pull_request: 12 | paths-ignore: 13 | - '**.md' 14 | branches: 15 | - main 16 | 17 | env: 18 | artifactName: ${{ contains(github.ref_name, '/') && 'docker-artifact' || github.ref_name }}-rpm 19 | 20 | jobs: 21 | docker_build: 22 | runs-on: ubuntu-latest 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | target: 27 | - fedora41 28 | - fedora42 29 | - fedora43 30 | defaults: 31 | run: 32 | shell: bash 33 | env: 34 | target: ${{ matrix.target }} 35 | 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v4 39 | with: 40 | fetch-depth: 0 41 | submodules: recursive 42 | 43 | - name: Generate container directory 44 | run: | 45 | cp -a .github/containers/fedora-template .github/containers/$target 46 | releasever="$(cut -b 7- <<< "$target")" 47 | sed -i "s/%releasever%/$releasever/g" .github/containers/$target/* 48 | 49 | - name: Restore docker from cache 50 | id: docker-cache 51 | uses: actions/cache/restore@v4 52 | with: 53 | path: ${{ github.workspace }}/docker-cache 54 | key: docker-cache-${{ matrix.target }}-${{ hashFiles(format('.github/containers/{0}/Dockerfile', matrix.target)) }} 55 | 56 | - name: Build environment 57 | if: ${{ steps.docker-cache.outputs.cache-hit != 'true' }} 58 | run: | 59 | docker build -t obs-plugin-build/$target .github/containers/$target 60 | mkdir -p docker-cache 61 | docker save obs-plugin-build/$target | gzip > docker-cache/obs-plugin-build-$target.tar.gz 62 | 63 | - name: Save docker to cache 64 | uses: actions/cache/save@v4 65 | if: ${{ steps.docker-cache.outputs.cache-hit != 'true' }} 66 | with: 67 | path: ${{ github.workspace }}/docker-cache 68 | key: docker-cache-${{ matrix.target }}-${{ hashFiles(format('.github/containers/{0}/Dockerfile', matrix.target)) }} 69 | 70 | - name: Extract cached environment 71 | if: ${{ steps.docker-cache.outputs.cache-hit == 'true' }} 72 | run: | 73 | zcat docker-cache/obs-plugin-build-$target.tar.gz | docker load 74 | 75 | - name: Build package 76 | run: .github/containers/$target/build.sh 77 | 78 | - name: Upload artifact 79 | uses: actions/upload-artifact@v4 80 | with: 81 | name: ${{ env.artifactName }}-${{ matrix.target }} 82 | path: '${{ env.FILE_NAME }}' 83 | -------------------------------------------------------------------------------- /src/face-tracker-base.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "plugin-macros.generated.h" 6 | #include "face-tracker-base.h" 7 | #ifndef _WIN32 8 | #include 9 | #include 10 | #else // _WIN32 11 | #include 12 | #endif // _WIN32 13 | 14 | face_tracker_base::face_tracker_base() 15 | { 16 | pthread_mutex_init(&mutex, NULL); 17 | pthread_cond_init(&cond, NULL); 18 | stop_requested = 0; 19 | running = 0; 20 | leak_test = bmalloc(1); 21 | } 22 | 23 | face_tracker_base::~face_tracker_base() 24 | { 25 | bfree(leak_test); 26 | pthread_cond_destroy(&cond); 27 | pthread_mutex_destroy(&mutex); 28 | } 29 | 30 | void *face_tracker_base::thread_routine(void *p) 31 | { 32 | face_tracker_base *base = (face_tracker_base *)p; 33 | #ifndef _WIN32 34 | setpriority(PRIO_PROCESS, 0, 17); 35 | #else // _WIN32 36 | SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_LOWEST); 37 | #endif // _WIN32 38 | os_set_thread_name("face-trk"); 39 | 40 | base->lock(); 41 | while (!base->stop_requested) { 42 | if (!base->suspend_requested) { 43 | try { 44 | base->track_main(); 45 | } catch (std::exception &e) { 46 | blog(LOG_ERROR, "track_main: exception %s", e.what()); 47 | } catch (...) { 48 | blog(LOG_ERROR, "track_main: unknown exception"); 49 | } 50 | } 51 | pthread_cond_wait(&base->cond, &base->mutex); 52 | } 53 | base->stopped = 1; 54 | base->unlock(); 55 | return NULL; 56 | } 57 | 58 | void face_tracker_base::start() 59 | { 60 | stop_requested = 0; 61 | stopped = 0; 62 | suspend_requested = 0; 63 | if (!running) { 64 | blog(LOG_INFO, "face_tracker_base: starting a new thread."); 65 | pthread_create(&thread, NULL, thread_routine, (void *)this); 66 | running = 1; 67 | } else { 68 | lock(); 69 | signal(); 70 | unlock(); 71 | } 72 | } 73 | 74 | void face_tracker_base::stop() 75 | { 76 | lock(); 77 | stop_requested = 1; 78 | signal(); 79 | unlock(); 80 | if (running) { 81 | blog(LOG_INFO, "face_tracker_base: joining the thread..."); 82 | pthread_join(thread, NULL); 83 | running = 0; 84 | blog(LOG_INFO, "face_tracker_base: joined the thread."); 85 | } 86 | } 87 | 88 | void face_tracker_base::request_stop() 89 | { 90 | lock(); 91 | stop_requested = 1; 92 | signal(); 93 | unlock(); 94 | } 95 | 96 | bool face_tracker_base::is_stopped() 97 | { 98 | if (stopped) { 99 | if (running) { 100 | blog(LOG_INFO, "face_tracker_base: joining the thread..."); 101 | pthread_join(thread, NULL); 102 | running = 0; 103 | } 104 | return 1; 105 | } else 106 | return 0; 107 | } 108 | 109 | void face_tracker_base::request_suspend() 110 | { 111 | suspend_requested = true; 112 | } 113 | -------------------------------------------------------------------------------- /src/face-tracker-manager.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "face-tracker-base.h" 6 | 7 | class face_tracker_manager { 8 | public: 9 | enum detector_engine_e { 10 | engine_dlib_hog = 0, 11 | engine_dlib_cnn = 1, 12 | engine_uninitialized = -1, 13 | }; 14 | 15 | struct tracker_rect_s 16 | { 17 | rect_s rect; 18 | rectf_s crop_rect; 19 | std::vector landmark; 20 | }; 21 | 22 | struct tracker_inst_s 23 | { 24 | class face_tracker_base *tracker; 25 | rect_s rect; 26 | rectf_s crop_tracker; // crop corresponding to current processing image 27 | rectf_s crop_rect; // crop corresponding to rect 28 | std::vector landmark; 29 | float att; 30 | float score_first; 31 | enum tracker_state_e { 32 | tracker_state_init = 0, 33 | tracker_state_reset_texture, // texture has been set, position is not set. 34 | tracker_state_constructing, // texture and positions have been set, starting to construct correlation_tracker. 35 | tracker_state_first_track, // correlation_tracker has been prepared, running 1st tracking 36 | tracker_state_available, // 1st tracking was done, `rect` is available, can accept next frame. 37 | tracker_state_ending, 38 | } state; 39 | int tick_cnt; 40 | }; 41 | 42 | public: // properties 43 | float upsize_l, upsize_r, upsize_t, upsize_b; 44 | volatile float scale; 45 | volatile bool reset_requested; 46 | float tracking_threshold; 47 | enum detector_engine_e detector_engine = engine_uninitialized; 48 | std::string detector_dlib_hog_model; 49 | std::string detector_dlib_cnn_model; 50 | int detector_crop_l, detector_crop_r, detector_crop_t, detector_crop_b; 51 | char *landmark_detection_data; 52 | 53 | public: // realtime status 54 | rectf_s crop_cur; 55 | int tick_cnt; 56 | 57 | public: // results 58 | std::vector detect_rects; 59 | std::vector tracker_rects; 60 | 61 | public: /* not sure they are necessary to be public */ 62 | class face_detector_base *detect; 63 | int detect_tick; 64 | 65 | // TODO: Just have two pairs 66 | std::deque trackers; 67 | std::deque trackers_idlepool; 68 | 69 | private: 70 | int next_tick_stage_to_detector; 71 | bool detector_in_progress; 72 | 73 | public: 74 | face_tracker_manager(); 75 | virtual ~face_tracker_manager(); 76 | void tick(float second); 77 | void post_render(); 78 | void update(obs_data_t *settings); 79 | static void get_properties(obs_properties_t *); 80 | static void get_defaults(obs_data_t *settings); 81 | 82 | protected: 83 | virtual std::shared_ptr get_cvtex() = 0; 84 | 85 | private: 86 | inline void retire_tracker(int ix); 87 | inline bool is_low_confident(const tracker_inst_s &t, float th1); 88 | void remove_duplicated_tracker(); 89 | void attenuate_tracker(); 90 | void copy_detector_to_tracker(); 91 | void stage_to_detector(); 92 | int stage_surface_to_tracker(struct tracker_inst_s &t); 93 | void stage_to_trackers(); 94 | }; 95 | -------------------------------------------------------------------------------- /ci/plugin.spec: -------------------------------------------------------------------------------- 1 | Name: @PLUGIN_NAME_FEDORA@ 2 | Version: @VERSION@ 3 | Release: @RELEASE@%{?dist} 4 | Summary: OBS Studio plugin as video filters to track face for mainly a speaking person 5 | License: GPLv3+ 6 | 7 | Source0: %{name}-%{version}.tar.bz2 8 | Source1: %{name}-%{version}-libvisca.tar.bz2 9 | Source2: %{name}-%{version}-dlib-models.tar.bz2 10 | Requires: obs-studio >= @OBS_VERSION@ 11 | BuildRequires: cmake, gcc, gcc-c++ 12 | BuildRequires: obs-studio-devel 13 | BuildRequires: qt6-qtbase-devel qt6-qtbase-private-devel 14 | BuildRequires: dlib-devel ffmpeg-free-devel sqlite-devel blas-devel lapack-devel 15 | BuildRequires: flexiblas-devel 16 | BuildRequires: libjxl-devel 17 | # dlib-devel requires /usr/include/ffmpeg so that install ffmpeg-free-devel 18 | 19 | %package data 20 | Summary: Model file for %{name} 21 | BuildArch: noarch 22 | License: CC0-1.0 23 | 24 | %package data-nonfree 25 | Summary: Non-free model file for %{name} 26 | BuildArch: noarch 27 | License: Nonfree 28 | 29 | %description 30 | This plugin tracks face of a person by detecting and tracking a face. 31 | 32 | This plugin employs dlib on face detection and object tracking. The frame of 33 | the source is periodically taken to face detection algorithm. Once a face is 34 | found, the face is tracked. Based on the location and the size of the face 35 | under tracking, the frame will be cropped. 36 | 37 | %description data 38 | Model files for @PLUGIN_NAME_FEDORA@. 39 | The model files came from https://github.com/davisking/dlib-models/. 40 | 41 | %description data-nonfree 42 | Non-free model files for @PLUGIN_NAME_FEDORA@. 43 | The model file came from https://github.com/davisking/dlib-models/. 44 | 45 | %prep 46 | %autosetup -p1 47 | %setup -T -D -a 1 48 | %setup -T -D -a 2 49 | 50 | %build 51 | %{cmake} \ 52 | -DLINUX_PORTABLE=OFF -DLINUX_RPATH=OFF \ 53 | -DQT_VERSION=6 \ 54 | -DWITH_DLIB_SUBMODULE=OFF \ 55 | -DBUILD_SHARED_LIBS:BOOL=OFF 56 | %{cmake_build} 57 | 58 | %install 59 | %{cmake_install} 60 | 61 | mkdir -p %{buildroot}/%{_datadir}/licenses/%{name}/ 62 | mkdir -p %{buildroot}/%{_datadir}/licenses/%{name}-data/ 63 | mkdir -p %{buildroot}/%{_datadir}/licenses/%{name}-data-nonfree/ 64 | cp LICENSE %{buildroot}/%{_datadir}/licenses/%{name}/ 65 | mv %{buildroot}/%{_datadir}/obs/obs-plugins/@PLUGIN_NAME@/LICENSE-dlib %{buildroot}/%{_datadir}/licenses/%{name}/ 66 | mv %{buildroot}/%{_datadir}/obs/obs-plugins/@PLUGIN_NAME@/LICENSE-dlib-models %{buildroot}/%{_datadir}/licenses/%{name}-data/ 67 | mv %{buildroot}/%{_datadir}/obs/obs-plugins/@PLUGIN_NAME@/LICENSE-shape_predictor_68_face_landmarks %{buildroot}/%{_datadir}/licenses/%{name}-data-nonfree/ 68 | 69 | %files 70 | %{_libdir}/obs-plugins/@PLUGIN_NAME@.so 71 | %{_datadir}/obs/obs-plugins/@PLUGIN_NAME@/locale/ 72 | %{_datadir}/licenses/%{name}/* 73 | 74 | %files data 75 | %{_datadir}/obs/obs-plugins/@PLUGIN_NAME@/dlib_cnn_model 76 | %{_datadir}/obs/obs-plugins/@PLUGIN_NAME@/dlib_face_landmark_model/shape_predictor_5_face_landmarks.dat 77 | %{_datadir}/obs/obs-plugins/@PLUGIN_NAME@/dlib_hog_model 78 | %{_datadir}/licenses/%{name}-data/* 79 | 80 | %files data-nonfree 81 | %{_datadir}/obs/obs-plugins/@PLUGIN_NAME@/dlib_face_landmark_model/shape_predictor_68_face_landmarks.dat 82 | %{_datadir}/obs/obs-plugins/@PLUGIN_NAME@/dlib_face_landmark_model/shape_predictor_68_face_landmarks_GTX.dat 83 | %{_datadir}/licenses/%{name}-data-nonfree/* 84 | -------------------------------------------------------------------------------- /src/texture-object.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "plugin-macros.generated.h" 7 | #include "texture-object.h" 8 | 9 | static uint32_t formats_found = 0; 10 | #define TEST_FORMAT(f) (0 <= (uint32_t)(f) && (uint32_t)(f) < 32 && !(formats_found & (1 << (uint32_t)(f)))) 11 | #define SET_FORMAT(f) (0 <= (uint32_t)(f) && (uint32_t)(f) < 32 && (formats_found |= (1 << (uint32_t)(f)))) 12 | 13 | struct texture_object_private_s 14 | { 15 | struct obs_source_frame *obs_frame = NULL; 16 | int scale = 0; 17 | }; 18 | 19 | texture_object::texture_object() 20 | { 21 | data = new texture_object_private_s; 22 | data->obs_frame = NULL; 23 | } 24 | 25 | texture_object::~texture_object() 26 | { 27 | obs_source_frame_destroy(data->obs_frame); 28 | delete data; 29 | } 30 | 31 | static void obsframe2dlib_bgrx(dlib::matrix &img, const struct obs_source_frame *frame, int scale, 32 | int size = 4) 33 | { 34 | const int nr = img.nr(); 35 | const int nc = img.nc(); 36 | const int inc = size * scale; 37 | for (int i = 0; i < nr; i++) { 38 | uint8_t *line = frame->data[0] + frame->linesize[0] * scale * i; 39 | for (int j = 0, js = 0; j < nc; j++, js += inc) { 40 | img(i, j).red = line[js + 2]; 41 | img(i, j).green = line[js + 1]; 42 | img(i, j).blue = line[js + 0]; 43 | } 44 | } 45 | } 46 | 47 | static void obsframe2dlib_rgbx(dlib::matrix &img, const struct obs_source_frame *frame, int scale) 48 | { 49 | const int nr = img.nr(); 50 | const int nc = img.nc(); 51 | for (int i = 0; i < nr; i++) { 52 | uint8_t *line = frame->data[0] + frame->linesize[0] * scale * i; 53 | for (int j = 0, js = 0; j < nc; j++, js += 4 * scale) { 54 | img(i, j).red = line[js + 0]; 55 | img(i, j).green = line[js + 1]; 56 | img(i, j).blue = line[js + 2]; 57 | } 58 | } 59 | } 60 | 61 | static bool need_allocate_frame(const struct obs_source_frame *dst, const struct obs_source_frame *src) 62 | { 63 | if (!dst) 64 | return true; 65 | 66 | if (dst->format != src->format) 67 | return true; 68 | 69 | if (dst->width != src->width || dst->height != src->height) 70 | return true; 71 | 72 | return false; 73 | } 74 | 75 | void texture_object::set_texture_obsframe(const struct obs_source_frame *frame, int scale) 76 | { 77 | if (need_allocate_frame(data->obs_frame, frame)) { 78 | obs_source_frame_destroy(data->obs_frame); 79 | data->obs_frame = obs_source_frame_create(frame->format, frame->width, frame->height); 80 | } 81 | 82 | obs_source_frame_copy(data->obs_frame, frame); 83 | data->scale = scale; 84 | } 85 | 86 | bool texture_object::get_dlib_rgb_image(dlib::matrix &img) const 87 | { 88 | if (!data->obs_frame) 89 | return false; 90 | 91 | const auto *frame = data->obs_frame; 92 | const int scale = data->scale; 93 | if (TEST_FORMAT(frame->format)) 94 | blog(LOG_INFO, "received frame format=%d", frame->format); 95 | img.set_size(frame->height / scale, frame->width / scale); 96 | switch (frame->format) { 97 | case VIDEO_FORMAT_BGRX: 98 | case VIDEO_FORMAT_BGRA: 99 | obsframe2dlib_bgrx(img, frame, scale); 100 | break; 101 | case VIDEO_FORMAT_BGR3: 102 | obsframe2dlib_bgrx(img, frame, scale, 3); 103 | break; 104 | case VIDEO_FORMAT_RGBA: 105 | obsframe2dlib_rgbx(img, frame, scale); 106 | break; 107 | default: 108 | if (TEST_FORMAT(frame->format)) 109 | blog(LOG_ERROR, "Frame format %d has to be RGB", (int)frame->format); 110 | } 111 | SET_FORMAT(frame->format); 112 | 113 | return true; 114 | } 115 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | # please use clang-format version 8 or later 2 | 3 | Standard: Cpp11 4 | AccessModifierOffset: -8 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: false 7 | AlignConsecutiveDeclarations: false 8 | AlignEscapedNewlines: Left 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | #AllowAllArgumentsOnNextLine: false # requires clang-format 9 12 | #AllowAllConstructorInitializersOnNextLine: false # requires clang-format 9 13 | AllowAllParametersOfDeclarationOnNextLine: false 14 | AllowShortBlocksOnASingleLine: false 15 | AllowShortCaseLabelsOnASingleLine: false 16 | AllowShortFunctionsOnASingleLine: Inline 17 | AllowShortIfStatementsOnASingleLine: false 18 | #AllowShortLambdasOnASingleLine: Inline # requires clang-format 9 19 | AllowShortLoopsOnASingleLine: false 20 | AlwaysBreakAfterDefinitionReturnType: None 21 | AlwaysBreakAfterReturnType: None 22 | AlwaysBreakBeforeMultilineStrings: false 23 | AlwaysBreakTemplateDeclarations: false 24 | BinPackArguments: true 25 | BinPackParameters: true 26 | BraceWrapping: 27 | AfterClass: false 28 | AfterControlStatement: false 29 | AfterEnum: false 30 | AfterFunction: true 31 | AfterNamespace: false 32 | AfterObjCDeclaration: false 33 | AfterStruct: true 34 | AfterUnion: false 35 | AfterExternBlock: false 36 | BeforeCatch: false 37 | BeforeElse: false 38 | IndentBraces: false 39 | SplitEmptyFunction: true 40 | SplitEmptyRecord: true 41 | SplitEmptyNamespace: true 42 | BreakBeforeBinaryOperators: None 43 | BreakBeforeBraces: Custom 44 | BreakBeforeTernaryOperators: true 45 | BreakConstructorInitializers: BeforeColon 46 | BreakStringLiterals: false # apparently unpredictable 47 | ColumnLimit: 120 48 | CompactNamespaces: false 49 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 50 | ConstructorInitializerIndentWidth: 8 51 | ContinuationIndentWidth: 8 52 | Cpp11BracedListStyle: true 53 | DerivePointerAlignment: false 54 | DisableFormat: false 55 | FixNamespaceComments: false 56 | ForEachMacros: 57 | - 'json_object_foreach' 58 | - 'json_object_foreach_safe' 59 | - 'json_array_foreach' 60 | - 'HASH_ITER' 61 | IncludeBlocks: Preserve 62 | IndentCaseLabels: false 63 | IndentPPDirectives: None 64 | IndentWidth: 8 65 | IndentWrappedFunctionNames: false 66 | KeepEmptyLinesAtTheStartOfBlocks: true 67 | MaxEmptyLinesToKeep: 1 68 | NamespaceIndentation: None 69 | #ObjCBinPackProtocolList: Auto # requires clang-format 7 70 | ObjCBlockIndentWidth: 8 71 | ObjCSpaceAfterProperty: true 72 | ObjCSpaceBeforeProtocolList: true 73 | 74 | PenaltyBreakAssignment: 10 75 | PenaltyBreakBeforeFirstCallParameter: 30 76 | PenaltyBreakComment: 10 77 | PenaltyBreakFirstLessLess: 0 78 | PenaltyBreakString: 10 79 | PenaltyExcessCharacter: 100 80 | PenaltyReturnTypeOnItsOwnLine: 60 81 | 82 | PointerAlignment: Right 83 | ReflowComments: false 84 | SortIncludes: false 85 | SortUsingDeclarations: false 86 | SpaceAfterCStyleCast: false 87 | #SpaceAfterLogicalNot: false # requires clang-format 9 88 | SpaceAfterTemplateKeyword: false 89 | SpaceBeforeAssignmentOperators: true 90 | #SpaceBeforeCtorInitializerColon: true # requires clang-format 7 91 | #SpaceBeforeInheritanceColon: true # requires clang-format 7 92 | SpaceBeforeParens: ControlStatements 93 | #SpaceBeforeRangeBasedForLoopColon: true # requires clang-format 7 94 | SpaceInEmptyParentheses: false 95 | SpacesBeforeTrailingComments: 1 96 | SpacesInAngles: false 97 | SpacesInCStyleCastParentheses: false 98 | SpacesInContainerLiterals: false 99 | SpacesInParentheses: false 100 | SpacesInSquareBrackets: false 101 | #StatementMacros: # requires clang-format 8 102 | # - 'Q_OBJECT' 103 | TabWidth: 8 104 | #TypenameMacros: # requires clang-format 9 105 | # - 'DARRAY' 106 | UseTab: ForContinuationAndIndentation 107 | --- 108 | Language: ObjC 109 | -------------------------------------------------------------------------------- /src/face-detector-dlib-hog.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "plugin-macros.generated.h" 5 | #include "face-detector-dlib-hog.h" 6 | #include "texture-object.h" 7 | 8 | #include 9 | 10 | #define MAX_ERROR 2 11 | 12 | struct face_detector_dlib_private_s 13 | { 14 | std::shared_ptr tex; 15 | std::vector rects; 16 | dlib::frontal_face_detector detector; 17 | bool detector_loaded = false; 18 | bool has_error = false; 19 | std::string model_filename; 20 | int crop_l = 0, crop_r = 0, crop_t = 0, crop_b = 0; 21 | int n_error = 0; 22 | face_detector_dlib_private_s() {} 23 | ~face_detector_dlib_private_s() {} 24 | }; 25 | 26 | face_detector_dlib_hog::face_detector_dlib_hog() 27 | { 28 | p = new face_detector_dlib_private_s; 29 | } 30 | 31 | face_detector_dlib_hog::~face_detector_dlib_hog() 32 | { 33 | delete p; 34 | } 35 | 36 | void face_detector_dlib_hog::set_texture(std::shared_ptr &tex, int crop_l, int crop_r, int crop_t, 37 | int crop_b) 38 | { 39 | p->tex = tex; 40 | p->crop_l = crop_l; 41 | p->crop_r = crop_r; 42 | p->crop_t = crop_t; 43 | p->crop_b = crop_b; 44 | } 45 | 46 | void face_detector_dlib_hog::detect_main() 47 | { 48 | if (!p->tex) 49 | return; 50 | 51 | dlib::matrix img; 52 | if (!p->tex->get_dlib_rgb_image(img)) 53 | return; 54 | 55 | int x0 = 0, y0 = 0; 56 | if (p->crop_l > 0 || p->crop_r > 0 || p->crop_t > 0 || p->crop_b > 0) { 57 | dlib::matrix img_crop; 58 | x0 = (int)(p->crop_l / p->tex->scale); 59 | int x1 = img.nc() - (int)(p->crop_r / p->tex->scale); 60 | y0 = (int)(p->crop_t / p->tex->scale); 61 | int y1 = img.nr() - (int)(p->crop_b / p->tex->scale); 62 | if (x1 - x0 < 80 || y1 - y0 < 80) { 63 | if (p->n_error++ < MAX_ERROR) 64 | blog(LOG_ERROR, "too small image: %dx%d cropped left=%d right=%d top=%d bottom=%d", 65 | (int)img.nc(), (int)img.nr(), p->crop_l, p->crop_r, p->crop_t, p->crop_b); 66 | return; 67 | } else if (p->n_error) { 68 | p->n_error--; 69 | } 70 | img_crop.set_size(y1 - y0, x1 - x0); 71 | for (int y = y0; y < y1; y++) { 72 | for (int x = x0; x < x1; x++) { 73 | img_crop(y - y0, x - x0) = img(y, x); 74 | } 75 | } 76 | img = img_crop; 77 | } 78 | if (img.nc() < 80 || img.nr() < 80) { 79 | if (p->n_error++ < MAX_ERROR) 80 | blog(LOG_ERROR, "too small image: %dx%d", (int)img.nc(), (int)img.nr()); 81 | return; 82 | } else if (p->n_error) { 83 | p->n_error--; 84 | } 85 | 86 | if (!p->detector_loaded) { 87 | p->detector_loaded = true; 88 | try { 89 | blog(LOG_INFO, "loading file '%s'", p->model_filename.c_str()); 90 | dlib::deserialize(p->model_filename.c_str()) >> p->detector; 91 | p->has_error = false; 92 | } catch (...) { 93 | blog(LOG_ERROR, "failed to load file '%s'", p->model_filename.c_str()); 94 | p->has_error = true; 95 | } 96 | } 97 | 98 | if (!p->has_error) { 99 | std::vector dets = p->detector(img); 100 | p->rects.resize(dets.size()); 101 | for (size_t i = 0; i < dets.size(); i++) { 102 | rect_s &r = p->rects[i]; 103 | r.x0 = (dets[i].left() + x0) * p->tex->scale; 104 | r.y0 = (dets[i].top() + y0) * p->tex->scale; 105 | r.x1 = (dets[i].right() + x0) * p->tex->scale; 106 | r.y1 = (dets[i].bottom() + y0) * p->tex->scale; 107 | r.score = 1.0; // TODO: implement me 108 | } 109 | } 110 | 111 | p->tex.reset(); 112 | } 113 | 114 | void face_detector_dlib_hog::get_faces(std::vector &rects) 115 | { 116 | rects = p->rects; 117 | } 118 | 119 | void face_detector_dlib_hog::set_model(const char *filename) 120 | { 121 | if (p->model_filename != filename) { 122 | p->model_filename = filename; 123 | p->detector_loaded = false; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/obsptz-backend.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "plugin-macros.generated.h" 5 | #include "obsptz-backend.hpp" 6 | #include "helper.hpp" 7 | 8 | #define debug(...) blog(LOG_INFO, __VA_ARGS__) 9 | 10 | #define SAME_CNT_TH 4 11 | #define PTZ_MAX_X 0x18 12 | #define PTZ_MAX_Y 0x14 13 | #define PTZ_MAX_Z 0x07 14 | 15 | obsptz_backend::obsptz_backend() {} 16 | 17 | obsptz_backend::~obsptz_backend() {} 18 | 19 | void obsptz_backend::set_config(struct obs_data *data) 20 | { 21 | device_id = (int)obs_data_get_int(data, "device_id"); 22 | 23 | ptz_max_x = obs_data_get_int(data, "max_x"); 24 | ptz_max_y = obs_data_get_int(data, "max_y"); 25 | ptz_max_z = obs_data_get_int(data, "max_z"); 26 | } 27 | 28 | bool obsptz_backend::can_send() 29 | { 30 | if (!available_ns) 31 | return true; 32 | 33 | uint64_t ns = os_gettime_ns(); 34 | return ns >= available_ns; 35 | } 36 | 37 | void obsptz_backend::tick() {} 38 | 39 | proc_handler_t *obsptz_backend::get_ptz_ph() 40 | { 41 | if (ptz_ph) 42 | return ptz_ph; 43 | 44 | proc_handler_t *ph = obs_get_proc_handler(); 45 | if (!ph) 46 | return NULL; 47 | 48 | CALLDATA_FIXED_DECL(cd, 128); 49 | proc_handler_call(ph, "ptz_get_proc_handler", &cd); 50 | calldata_get_ptr(&cd, "return", &ptz_ph); 51 | 52 | return ptz_ph; 53 | } 54 | 55 | void obsptz_backend::set_pantilt_speed(int pan, int tilt) 56 | { 57 | pan = std::clamp(pan, -ptz_max_x, ptz_max_x); 58 | tilt = std::clamp(tilt, -ptz_max_y, ptz_max_y); 59 | 60 | if (pan == prev_pan && tilt == prev_tilt) { 61 | if (same_pantilt_cnt > SAME_CNT_TH) 62 | return; 63 | same_pantilt_cnt++; 64 | } else { 65 | same_pantilt_cnt = 0; 66 | } 67 | 68 | CALLDATA_FIXED_DECL(cd, 128); 69 | calldata_set_int(&cd, "device_id", device_id); 70 | calldata_set_float(&cd, "pan", pan / 24.0f); 71 | calldata_set_float(&cd, "tilt", -tilt / 20.0f); 72 | proc_handler_t *ph = get_ptz_ph(); 73 | if (ph) 74 | proc_handler_call(ph, "ptz_move_continuous", &cd); 75 | else { 76 | // compatibility 77 | ph = obs_get_proc_handler(); 78 | proc_handler_call(ph, "ptz_pantilt", &cd); 79 | } 80 | uint64_t ns = os_gettime_ns(); 81 | available_ns = std::max(available_ns, ns) + (60 * 1000 * 1000); 82 | prev_pan = pan; 83 | prev_tilt = tilt; 84 | } 85 | 86 | void obsptz_backend::set_zoom_speed(int zoom) 87 | { 88 | zoom = std::clamp(zoom, -ptz_max_z, ptz_max_z); 89 | 90 | if (zoom == prev_zoom) { 91 | if (same_zoom_cnt > SAME_CNT_TH) 92 | return; 93 | same_zoom_cnt++; 94 | } else { 95 | same_zoom_cnt = 0; 96 | } 97 | 98 | proc_handler_t *ph = get_ptz_ph(); 99 | if (!ph) 100 | return; 101 | 102 | CALLDATA_FIXED_DECL(cd, 128); 103 | calldata_set_int(&cd, "device_id", device_id); 104 | calldata_set_float(&cd, "zoom", -zoom / 7.0f); 105 | proc_handler_call(ph, "ptz_move_continuous", &cd); 106 | 107 | uint64_t ns = os_gettime_ns(); 108 | available_ns = std::max(available_ns, ns) + (60 * 1000 * 1000); 109 | prev_zoom = zoom; 110 | } 111 | 112 | void obsptz_backend::recall_preset(int preset) 113 | { 114 | proc_handler_t *ph = get_ptz_ph(); 115 | if (!ph) 116 | return; 117 | 118 | CALLDATA_FIXED_DECL(cd, 128); 119 | calldata_set_int(&cd, "device_id", device_id); 120 | calldata_set_int(&cd, "preset_id", preset); 121 | proc_handler_call(ph, "ptz_preset_recall", &cd); 122 | 123 | uint64_t ns = os_gettime_ns(); 124 | available_ns = std::max(available_ns, ns) + (500 * 1000 * 1000); 125 | } 126 | 127 | float obsptz_backend::get_zoom() 128 | { 129 | // TODO: implement 130 | return 1.0f; 131 | } 132 | 133 | bool obsptz_backend::ptz_type_modified(obs_properties_t *pp, obs_data_t *) 134 | { 135 | if (obs_properties_get(pp, "ptz.obsptz.device_id")) 136 | return false; 137 | 138 | obs_properties_add_int(pp, "ptz.obsptz.device_id", obs_module_text("Device ID"), 0, 99, 1); 139 | 140 | obs_properties_add_int_slider(pp, "ptz.obsptz.max_x", "Max control (pan)", 0, PTZ_MAX_X, 1); 141 | obs_properties_add_int_slider(pp, "ptz.obsptz.max_y", "Max control (tilt)", 0, PTZ_MAX_Y, 1); 142 | obs_properties_add_int_slider(pp, "ptz.obsptz.max_z", "Max control (zoom)", 0, PTZ_MAX_Z, 1); 143 | 144 | return true; 145 | } 146 | -------------------------------------------------------------------------------- /ui/face-tracker-widget.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "plugin-macros.generated.h" 12 | #include "face-tracker-widget.hpp" 13 | #include "face-tracker-dock-internal.hpp" 14 | #include "obsgui-helper.hpp" 15 | 16 | static void draw(void *param, uint32_t cx, uint32_t cy) 17 | { 18 | auto *data = (struct face_tracker_dock_s *)param; 19 | 20 | if (pthread_mutex_trylock(&data->mutex)) 21 | return; 22 | 23 | gs_blend_state_push(); 24 | gs_reset_blend_state(); 25 | 26 | if (data->src_monitor) { 27 | int w_src = obs_source_get_width(data->src_monitor); 28 | int h_src = obs_source_get_height(data->src_monitor); 29 | if (w_src <= 0 || h_src <= 0) 30 | goto err; 31 | int w, h; 32 | if (w_src * cy > h_src * cx) { 33 | w = cx; 34 | h = cx * h_src / w_src; 35 | } else { 36 | h = cy; 37 | w = cy * w_src / h_src; 38 | } 39 | 40 | gs_projection_push(); 41 | gs_viewport_push(); 42 | 43 | gs_set_viewport((cx - w) * 0.5, (cy - h) * 0.5, w, h); 44 | gs_ortho(0.0f, w_src, -1.0f, h_src, -100.0f, 100.0f); 45 | 46 | obs_source_video_render(data->src_monitor); 47 | 48 | gs_viewport_pop(); 49 | gs_projection_pop(); 50 | } 51 | err: 52 | 53 | gs_blend_state_pop(); 54 | 55 | pthread_mutex_unlock(&data->mutex); 56 | } 57 | 58 | FTWidget::FTWidget(struct face_tracker_dock_s *data_, QWidget *parent) : QWidget(parent) 59 | { 60 | face_tracker_dock_addref((data = data_)); 61 | setAttribute(Qt::WA_PaintOnScreen); 62 | setAttribute(Qt::WA_StaticContents); 63 | setAttribute(Qt::WA_NoSystemBackground); 64 | setAttribute(Qt::WA_OpaquePaintEvent); 65 | setAttribute(Qt::WA_DontCreateNativeAncestors); 66 | setAttribute(Qt::WA_NativeWindow); 67 | 68 | setContextMenuPolicy(Qt::CustomContextMenu); 69 | connect(this, &QWidget::customContextMenuRequested, this, &FTWidget::openMenu); 70 | } 71 | 72 | FTWidget::~FTWidget() 73 | { 74 | face_tracker_dock_release(data); 75 | } 76 | 77 | void FTWidget::CreateDisplay() 78 | { 79 | if (!data) 80 | return; 81 | if (data->disp || !windowHandle()->isExposed()) 82 | return; 83 | 84 | blog(LOG_INFO, "FTWidget::CreateDisplay %p", this); 85 | 86 | QSize size = GetPixelSize(this); 87 | gs_init_data info = {}; 88 | info.cx = size.width(); 89 | info.cy = size.height(); 90 | info.format = GS_BGRA; 91 | info.zsformat = GS_ZS_NONE; 92 | QWindow *window = windowHandle(); 93 | if (!window) { 94 | blog(LOG_ERROR, "FTWidget %p: windowHandle() returns NULL", this); 95 | return; 96 | } 97 | if (!QTToGSWindow(window, info.window)) { 98 | blog(LOG_ERROR, "FTWidget %p: QTToGSWindow failed", this); 99 | return; 100 | } 101 | data->disp = obs_display_create(&info, 0); 102 | obs_display_add_draw_callback(data->disp, draw, data); 103 | #ifdef HAVE_DISPLAY_SET_INTERLEAVE 104 | blog(LOG_INFO, "calling obs_display_set_interleave interleave=2"); 105 | obs_display_set_interleave(data->disp, 2); 106 | #endif 107 | } 108 | 109 | void FTWidget::resizeEvent(QResizeEvent *event) 110 | { 111 | QWidget::resizeEvent(event); 112 | CreateDisplay(); 113 | 114 | QSize size = GetPixelSize(this); 115 | obs_display_resize(data->disp, size.width(), size.height()); 116 | } 117 | 118 | void FTWidget::paintEvent(QPaintEvent *) 119 | { 120 | CreateDisplay(); 121 | } 122 | 123 | class QPaintEngine *FTWidget::paintEngine() const 124 | { 125 | return NULL; 126 | } 127 | 128 | void FTWidget::closeEvent(QCloseEvent *) 129 | { 130 | setShown(false); 131 | } 132 | 133 | void FTWidget::setShown(bool shown) 134 | { 135 | if (shown && !data->disp) { 136 | CreateDisplay(); 137 | } 138 | if (!shown && data->disp) { 139 | obs_display_destroy(data->disp); 140 | data->disp = NULL; 141 | } 142 | } 143 | 144 | void FTWidget::openMenu(const class QPoint &pos) 145 | { 146 | QMenu popup(this); 147 | 148 | auto *act = new QAction(obs_module_text("dock.menu.close"), this); 149 | connect(act, &QAction::triggered, this, &FTWidget::removeDock, Qt::QueuedConnection); 150 | popup.addAction(act); 151 | 152 | popup.exec(mapToGlobal(pos)); 153 | } 154 | -------------------------------------------------------------------------------- /src/face-detector-dlib-cnn.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "plugin-macros.generated.h" 6 | #include "face-detector-dlib-cnn.h" 7 | #include "texture-object.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #define MAX_ERROR 2 15 | 16 | using namespace dlib; 17 | template using con5d = con; 18 | template using con5 = con; 19 | template 20 | using downsampler = relu>>>>>>>>; 21 | template using rcon5 = relu>>; 22 | using net_type = 23 | loss_mmod>>>>>>>; 24 | typedef dlib::matrix image_t; 25 | 26 | struct private_s 27 | { 28 | std::shared_ptr tex; 29 | std::vector rects; 30 | std::string model_filename; 31 | net_type net; 32 | bool net_loaded = false; 33 | bool has_error = false; 34 | int crop_l = 0, crop_r = 0, crop_t = 0, crop_b = 0; 35 | int n_error = 0; 36 | }; 37 | 38 | face_detector_dlib_cnn::face_detector_dlib_cnn() 39 | { 40 | p = new private_s; 41 | } 42 | 43 | face_detector_dlib_cnn::~face_detector_dlib_cnn() 44 | { 45 | delete p; 46 | } 47 | 48 | void face_detector_dlib_cnn::set_texture(std::shared_ptr &tex, int crop_l, int crop_r, int crop_t, 49 | int crop_b) 50 | { 51 | p->tex = tex; 52 | p->crop_l = crop_l; 53 | p->crop_r = crop_r; 54 | p->crop_t = crop_t; 55 | p->crop_b = crop_b; 56 | } 57 | 58 | void face_detector_dlib_cnn::detect_main() 59 | { 60 | if (!p->tex) 61 | return; 62 | 63 | dlib::matrix img; 64 | if (!p->tex->get_dlib_rgb_image(img)) 65 | return; 66 | 67 | int x0 = 0, y0 = 0; 68 | if (p->crop_l > 0 || p->crop_r > 0 || p->crop_t > 0 || p->crop_b > 0) { 69 | image_t img_crop; 70 | x0 = (int)(p->crop_l / p->tex->scale); 71 | int x1 = img.nc() - (int)(p->crop_r / p->tex->scale); 72 | y0 = (int)(p->crop_t / p->tex->scale); 73 | int y1 = img.nr() - (int)(p->crop_b / p->tex->scale); 74 | if (x1 - x0 < 80 || y1 - y0 < 80) { 75 | if (p->n_error++ < MAX_ERROR) 76 | blog(LOG_ERROR, "too small image: %dx%d cropped left=%d right=%d top=%d bottom=%d", 77 | (int)img.nc(), (int)img.nr(), p->crop_l, p->crop_r, p->crop_t, p->crop_b); 78 | return; 79 | } else if (p->n_error) { 80 | p->n_error--; 81 | } 82 | img_crop.set_size(y1 - y0, x1 - x0); 83 | for (int y = y0; y < y1; y++) { 84 | for (int x = x0; x < x1; x++) { 85 | img_crop(y - y0, x - x0) = img(y, x); 86 | } 87 | } 88 | img = img_crop; 89 | } 90 | if (img.nc() < 80 || img.nr() < 80) { 91 | if (p->n_error++ < MAX_ERROR) 92 | blog(LOG_ERROR, "too small image: %dx%d", (int)img.nc(), (int)img.nr()); 93 | return; 94 | } else if (p->n_error) { 95 | p->n_error--; 96 | } 97 | 98 | if (!p->net_loaded) { 99 | p->net_loaded = true; 100 | try { 101 | blog(LOG_INFO, "loading file '%s'", p->model_filename.c_str()); 102 | deserialize(p->model_filename.c_str()) >> p->net; 103 | p->has_error = false; 104 | } catch (...) { 105 | blog(LOG_ERROR, "failed to load file '%s'", p->model_filename.c_str()); 106 | p->has_error = true; 107 | } 108 | } 109 | 110 | if (p->has_error) 111 | return; 112 | 113 | auto dets = p->net(img); 114 | p->rects.resize(dets.size()); 115 | for (size_t i = 0; i < dets.size(); i++) { 116 | auto &det = dets[i]; 117 | rect_s &r = p->rects[i]; 118 | r.x0 = (det.rect.left() + x0) * p->tex->scale; 119 | r.y0 = (det.rect.top() + y0) * p->tex->scale; 120 | r.x1 = (det.rect.right() + x0) * p->tex->scale; 121 | r.y1 = (det.rect.bottom() + y0) * p->tex->scale; 122 | r.score = det.detection_confidence; 123 | } 124 | 125 | p->tex.reset(); 126 | } 127 | 128 | void face_detector_dlib_cnn::get_faces(std::vector &rects) 129 | { 130 | rects = p->rects; 131 | } 132 | 133 | void face_detector_dlib_cnn::set_model(const char *filename) 134 | { 135 | if (p->model_filename != filename) { 136 | p->model_filename = filename; 137 | p->net_loaded = false; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/helper.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #ifdef _WIN32 6 | #define DEBUG_DATA_PATH_FILTER "TSV Files (*.tsv);;Data Files (*.dat);;All Files (*.*)" 7 | #else 8 | #define DEBUG_DATA_PATH_FILTER "Data Files (*.dat);;TSV Files (*.tsv);;All Files (*.*)" 9 | #endif 10 | 11 | #define CALLDATA_FIXED_DECL(cd, size) \ 12 | calldata_t cd; \ 13 | uint8_t calldata_##cd##_stack[128]; \ 14 | calldata_init_fixed(&cd, calldata_##cd##_stack, sizeof(calldata_##cd##_stack)); 15 | 16 | struct pointf_s 17 | { 18 | float x; 19 | float y; 20 | }; 21 | 22 | struct rect_s 23 | { 24 | int x0; 25 | int y0; 26 | int x1; 27 | int y1; 28 | float score; 29 | }; 30 | 31 | struct rectf_s 32 | { 33 | float x0; 34 | float y0; 35 | float x1; 36 | float y1; 37 | }; 38 | 39 | struct f3 40 | { 41 | float v[3]; 42 | 43 | f3(const f3 &a) { *this = a; } 44 | f3(float a, float b, float c) 45 | { 46 | v[0] = a; 47 | v[1] = b; 48 | v[2] = c; 49 | } 50 | f3(const rect_s &a) 51 | { 52 | v[0] = (float)(a.x0 + a.x1) * 0.5f; 53 | v[1] = (float)(a.y0 + a.y1) * 0.5f; 54 | v[2] = sqrtf((float)(a.x1 - a.x0) * (float)(a.y1 - a.y0)); 55 | } 56 | f3(const rectf_s &a) 57 | { 58 | v[0] = (a.x0 + a.x1) * 0.5f; 59 | v[1] = (a.y0 + a.y1) * 0.5f; 60 | v[2] = sqrtf((a.x1 - a.x0) * (a.y1 - a.y0)); 61 | } 62 | f3 operator+(const f3 &a) { return f3(v[0] + a.v[0], v[1] + a.v[1], v[2] + a.v[2]); } 63 | f3 operator-(const f3 &a) { return f3(v[0] - a.v[0], v[1] - a.v[1], v[2] - a.v[2]); } 64 | f3 operator*(float a) { return f3(v[0] * a, v[1] * a, v[2] * a); } 65 | f3 &operator+=(const f3 &a) { return *this = f3(v[0] + a.v[0], v[1] + a.v[1], v[2] + a.v[2]); } 66 | f3 &operator=(const f3 &a) 67 | { 68 | v[0] = a.v[0]; 69 | v[1] = a.v[1]; 70 | v[2] = a.v[2]; 71 | return *this; 72 | } 73 | 74 | f3 hp(const f3 &a) const { return f3(v[0] * a.v[0], v[1] * a.v[1], v[2] * a.v[2]); } 75 | }; 76 | 77 | static inline bool isnan(const f3 &a) 78 | { 79 | return isnan(a.v[0]) || isnan(a.v[1]) || isnan(a.v[2]); 80 | } 81 | 82 | static inline int get_width(const rect_s &r) 83 | { 84 | return r.x1 - r.x0; 85 | } 86 | static inline int get_height(const rect_s &r) 87 | { 88 | return r.y1 - r.y0; 89 | } 90 | static inline float get_width(const rectf_s &r) 91 | { 92 | return r.x1 - r.x0; 93 | } 94 | static inline float get_height(const rectf_s &r) 95 | { 96 | return r.y1 - r.y0; 97 | } 98 | 99 | static inline int common_length(int a0, int a1, int b0, int b1) 100 | { 101 | // assumes a0 < a1, b0 < b1 102 | // if (a1 <= b0) return 0; // a0 < a1 < b0 < b1 103 | if (a0 <= b0 && b0 <= a1 && a1 <= b1) 104 | return a1 - b0; // a0 < b0 < a1 < b1 105 | if (a0 <= b0 && b1 <= a1) 106 | return b1 - b0; // a0 < b0 < b1 < a1 107 | if (b0 <= a0 && a1 <= b1) 108 | return a1 - a0; // b0 < a0 < a1 < b1 109 | if (b0 <= a0 && a0 <= b1 && a0 <= b1) 110 | return b1 - a0; // b0 < a0 < b1 < a1 111 | // if (b1 <= a0) return 0; // b0 < b1 < a0 < a1 112 | return 0; 113 | } 114 | 115 | static inline int common_area(const rect_s &a, const rect_s &b) 116 | { 117 | return common_length(a.x0, a.x1, b.x0, b.x1) * common_length(a.y0, a.y1, b.y0, b.y1); 118 | } 119 | 120 | template static inline bool samesign(const T &a, const T &b) 121 | { 122 | if (a > 0 && b > 0) 123 | return true; 124 | if (a < 0 && b < 0) 125 | return true; 126 | return false; 127 | } 128 | 129 | static inline float sqf(float x) 130 | { 131 | return x * x; 132 | } 133 | 134 | static inline rectf_s f3_to_rectf(const f3 &u, float w, float h) 135 | { 136 | const float srwh = sqrtf(w * h); 137 | const float s2h = h / srwh; 138 | const float s2w = w / srwh; 139 | rectf_s r; 140 | r.x0 = u.v[0] - s2w * u.v[2] * 0.5f; 141 | r.x1 = u.v[0] + s2w * u.v[2] * 0.5f; 142 | r.y0 = u.v[1] - s2h * u.v[2] * 0.5f; 143 | r.y1 = u.v[1] + s2h * u.v[2] * 0.5f; 144 | return r; 145 | } 146 | 147 | void draw_rect_upsize(rect_s r, float upsize_l = 0.0f, float upsize_r = 0.0f, float upsize_t = 0.0f, 148 | float upsize_b = 0.0f); 149 | void draw_landmark(const std::vector &landmark); 150 | float landmark_area(const std::vector &landmark); 151 | pointf_s landmark_center(const std::vector &landmark); 152 | 153 | inline double from_dB(double x) 154 | { 155 | return exp(x * (M_LN10 / 20)); 156 | } 157 | 158 | void debug_data_open(FILE **dest, char **last_name, obs_data_t *settings, const char *name); 159 | -------------------------------------------------------------------------------- /src/helper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "plugin-macros.generated.h" 3 | #include "helper.hpp" 4 | 5 | void draw_rect_upsize(rect_s r, float upsize_l, float upsize_r, float upsize_t, float upsize_b) 6 | { 7 | if (r.x0 >= r.x1 || r.y0 >= r.y1) 8 | return; 9 | int w = r.x1 - r.x0; 10 | int h = r.y1 - r.y0; 11 | float dx0 = w * upsize_l; 12 | float dx1 = w * upsize_r; 13 | float dy0 = h * upsize_t; 14 | float dy1 = h * upsize_b; 15 | 16 | gs_render_start(false); 17 | 18 | if (std::abs(dx0) >= 0.5f || std::abs(dy1) >= 0.5f || std::abs(dx1) >= 0.5f || std::abs(dy0) >= 0.5f) { 19 | gs_vertex2f((float)r.x0, (float)r.y0); 20 | gs_vertex2f((float)r.x0, (float)r.y1); 21 | gs_vertex2f((float)r.x0, (float)r.y1); 22 | gs_vertex2f((float)r.x1, (float)r.y1); 23 | gs_vertex2f((float)r.x1, (float)r.y1); 24 | gs_vertex2f((float)r.x1, (float)r.y0); 25 | gs_vertex2f((float)r.x1, (float)r.y0); 26 | gs_vertex2f((float)r.x0, (float)r.y0); 27 | } 28 | r.x0 -= (int)dx0; 29 | r.x1 += (int)dx1; 30 | r.y0 -= (int)dy0; 31 | r.y1 += (int)dy1; 32 | gs_vertex2f((float)r.x0, (float)r.y0); 33 | gs_vertex2f((float)r.x0, (float)r.y1); 34 | gs_vertex2f((float)r.x0, (float)r.y1); 35 | gs_vertex2f((float)r.x1, (float)r.y1); 36 | gs_vertex2f((float)r.x1, (float)r.y1); 37 | gs_vertex2f((float)r.x1, (float)r.y0); 38 | gs_vertex2f((float)r.x1, (float)r.y0); 39 | gs_vertex2f((float)r.x0, (float)r.y0); 40 | 41 | gs_render_stop(GS_LINES); 42 | } 43 | 44 | float landmark_area(const std::vector &landmark) 45 | { 46 | // TODO: implement area calculation for other models 47 | // Maybe, use the area of the maximum convex polygon. 48 | 49 | float ret = 0.0f; 50 | 51 | const static int ii5[] = {1, // center 52 | // 0, 4, 2, 3, 1, 53 | 0, 1, 3, 2, 4, 0, -1}; 54 | 55 | const static int ii68[] = {30, // center 56 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 57 | 15, 16, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 0, -1}; 58 | 59 | const int *ii = landmark.size() == 68 ? ii68 : landmark.size() == 5 ? ii5 : NULL; 60 | 61 | if (!ii) 62 | return 0.0f; 63 | 64 | pointf_s c = landmark[ii[0]]; // center to minimize calculation errors 65 | for (int i = 1; ii[i + 1] >= 0; i++) { 66 | float x1 = landmark[ii[i]].x - c.x; 67 | float y1 = landmark[ii[i]].y - c.y; 68 | float x2 = landmark[ii[i + 1]].x - c.x; 69 | float y2 = landmark[ii[i + 1]].y - c.y; 70 | ret += (x2 * y1 - x1 * y2) * 0.5f; 71 | } 72 | 73 | return ret; 74 | } 75 | 76 | pointf_s landmark_center(const std::vector &landmark) 77 | { 78 | pointf_s ret = {0.0f, 0.0f}; 79 | 80 | for (size_t i = 0; i < landmark.size(); i++) { 81 | ret.x += landmark[i].x; 82 | ret.y += landmark[i].y; 83 | } 84 | 85 | ret.x /= landmark.size(); 86 | ret.y /= landmark.size(); 87 | 88 | return ret; 89 | } 90 | 91 | void draw_landmark(const std::vector &landmark) 92 | { 93 | if (landmark.size() < 2) 94 | return; 95 | 96 | gs_render_start(false); 97 | 98 | if (landmark.size() == 5) { 99 | gs_vertex2f(landmark[0].x, landmark[0].y); 100 | gs_vertex2f(landmark[1].x, landmark[1].y); 101 | 102 | gs_vertex2f(landmark[1].x, landmark[1].y); 103 | gs_vertex2f(landmark[3].x, landmark[3].y); 104 | 105 | gs_vertex2f(landmark[3].x, landmark[3].y); 106 | gs_vertex2f(landmark[2].x, landmark[2].y); 107 | 108 | gs_vertex2f(landmark[2].x, landmark[2].y); 109 | gs_vertex2f(landmark[4].x, landmark[4].y); 110 | 111 | gs_vertex2f(landmark[4].x, landmark[4].y); 112 | gs_vertex2f(landmark[0].x, landmark[0].y); 113 | } else 114 | for (size_t i = 0; i < landmark.size(); i++) { 115 | // points should not be duplicated: 0, 16, 17, 21, 22, 26, 27, 30, 31, 35, 36, 41, 42, 47, 48, 59, 60, 57 116 | if (i == 42) { 117 | gs_vertex2f(landmark[36].x, landmark[36].y); 118 | } else if (i == 48) { 119 | gs_vertex2f(landmark[42].x, landmark[42].y); 120 | } else if (i == 60) { 121 | gs_vertex2f(landmark[48].x, landmark[48].y); 122 | } else if (i == 67) { 123 | gs_vertex2f(landmark[60].x, landmark[60].y); 124 | } else if (i == 0 || i == 16 || i == 17 || i == 21 || i == 22 || i == 26 || i == 27 || 125 | i == 30 || i == 31 || i == 35 || i == 36) { 126 | } else 127 | gs_vertex2f(landmark[i].x, landmark[i].y); 128 | 129 | gs_vertex2f(landmark[i].x, landmark[i].y); 130 | } 131 | 132 | gs_render_stop(GS_LINES); 133 | } 134 | 135 | void debug_data_open(FILE **dest, char **last_name, obs_data_t *settings, const char *name) 136 | { 137 | const char *debug_data = obs_data_get_string(settings, name); 138 | 139 | // If the file name is not changed, just return. 140 | if (*last_name && debug_data && strcmp(*last_name, debug_data) == 0) 141 | return; 142 | 143 | // If both file names are empty, just return. 144 | if (!*last_name && (!debug_data || !*debug_data)) 145 | return; 146 | 147 | if (*dest) 148 | fclose(*dest); 149 | *dest = NULL; 150 | 151 | if (*last_name) 152 | bfree(*last_name); 153 | *last_name = NULL; 154 | 155 | if (debug_data && *debug_data) { 156 | *dest = fopen(debug_data, "a"); 157 | if (!*dest) { 158 | blog(LOG_ERROR, "%s: Failed to open file \"%s\"", name, debug_data); 159 | } 160 | *last_name = bstrdup(debug_data); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /ci/macos/change-rpath.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # pylint: disable=C0103 3 | 'Handle dependencies from homebrew' 4 | 5 | import argparse 6 | import os 7 | import os.path 8 | import re 9 | import shutil 10 | import sys 11 | import subprocess 12 | 13 | def otool_list(f): 14 | 'List depending libraries using otool' 15 | libs = [] 16 | res = subprocess.run(['otool', '-L', f], capture_output=True, check=True) 17 | for line in res.stdout.decode('utf-8').split('\n')[1:]: 18 | lib = line.strip('\t').split(' ')[0] 19 | if lib: 20 | libs.append(lib) 21 | return libs 22 | 23 | def int_set_id(f, name): 24 | 'Set ID using install_name_tool' 25 | subprocess.run(['install_name_tool', '-id', name, f], capture_output=True, check=True) 26 | 27 | def int_change(f, old_lib, new_lib): 28 | 'Change the dependent dylib' 29 | subprocess.run(['install_name_tool', '-change', old_lib, new_lib, f], 30 | capture_output=True, check=True) 31 | 32 | def resolve_dylib_path(dylib, src): 33 | 'Resolve @loader_path and @rpath' 34 | ss = dylib.split('/', 1) 35 | if len(ss) != 2: 36 | return dylib 37 | if ss[0] == '@loader_path' or ss[0] == '@rpath': 38 | cand = os.path.dirname(src) + '/' + ss[1] 39 | if os.path.exists(cand): 40 | return cand 41 | return dylib 42 | 43 | class _CopyDependencies: 44 | 'Copy dependency libraries' 45 | def __init__(self, args): 46 | self.args = args 47 | self.include_re = [re.compile(t) for t in args.include_regex] 48 | self.exclude_re = [re.compile(t) for t in args.exclude_regex] 49 | self.check_invalid_re = [re.compile(t) for t in args.check_invalid_regex] 50 | if args.libdir: 51 | os.makedirs(args.libdir, exist_ok=True) 52 | 53 | def _is_included(self, lib): 54 | for r in self.exclude_re: 55 | if r.match(lib): 56 | return False 57 | if not self.include_re: 58 | return True 59 | for r in self.include_re: 60 | if r.match(lib): 61 | return True 62 | return False 63 | 64 | def _is_invalid(self, lib): 65 | for r in self.check_invalid_re: 66 | if r.match(lib): 67 | return True 68 | return False 69 | 70 | def copy_dependencies(self, f, src=None): 71 | 'Copy depending libraries to dest' 72 | 73 | if not self.args.libdir: 74 | raise ValueError('Error: libdir has to be specified to copy depending libraries.') 75 | libdir = self.args.libdir.rstrip('/') + '/' 76 | 77 | libs = otool_list(f) 78 | for lib in libs: 79 | lib = resolve_dylib_path(lib, src if src else f) 80 | 81 | if not self._is_included(lib): 82 | continue 83 | 84 | if self.args.verbose >= 1: 85 | sys.stderr.write(f'{f}: Copying {lib}\n') 86 | 87 | base = os.path.basename(lib) 88 | dest = libdir + base 89 | 90 | if not os.path.exists(dest): 91 | shutil.copy(lib, dest, follow_symlinks=True) 92 | int_set_id(dest, f'@loader_path/{base}') 93 | self.copy_dependencies(dest, lib) 94 | 95 | relpath = os.path.relpath(dest, os.path.dirname(f)) 96 | int_change(f, lib, '@loader_path/' + relpath) 97 | 98 | def check_dependencies(self, f): 99 | 'Check the depending libraries have been copied.' 100 | libs = otool_list(f) 101 | unknown = [] 102 | for lib in libs: 103 | lib_orig = lib 104 | lib = resolve_dylib_path(lib, f) 105 | 106 | if not self._is_included(lib): 107 | continue 108 | 109 | if self._is_invalid(lib) or not os.path.exists(lib): 110 | unknown.append((lib, lib_orig)) 111 | 112 | for lib, lib_orig in unknown: 113 | msg = f'Error: {f} depends {lib}' 114 | if lib != lib_orig: 115 | msg += f' ({lib_orig})' 116 | print(msg) 117 | 118 | if unknown: 119 | return 1 120 | return 0 121 | 122 | def _get_args(): 123 | parser = argparse.ArgumentParser() 124 | 125 | # Debugging arguments 126 | parser.add_argument('--list-dylib', action='store_true', default=False, 127 | help='List dependencies and exit') 128 | parser.add_argument('-v', '--verbose', action='count', default=0) 129 | parser.add_argument('--check', action='store_true', default=False) 130 | 131 | # Control arguments 132 | parser.add_argument('--libdir', action='store', default=None) 133 | parser.add_argument('--include-regex', action='append', default=[]) 134 | parser.add_argument('--exclude-regex', action='append', default=[]) 135 | parser.add_argument('--check-invalid-regex', action='append', default=[]) 136 | parser.add_argument('files', nargs='*', default=[]) 137 | 138 | return parser.parse_args() 139 | 140 | def main(): 141 | 'main routine' 142 | args = _get_args() 143 | 144 | if args.list_dylib: 145 | for f in args.files: 146 | print('\n'.join(otool_list(f))) 147 | return 0 148 | 149 | ctx = _CopyDependencies(args) 150 | if args.check: 151 | ret = 0 152 | for f in args.files: 153 | if ctx.check_dependencies(f): 154 | ret = 1 155 | return ret 156 | 157 | for f in args.files: 158 | ctx.copy_dependencies(f) 159 | 160 | return 0 161 | 162 | if __name__ == '__main__': 163 | sys.exit(main()) 164 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | project(obs-face-tracker VERSION 0.9.1) 4 | set(PLUGIN_AUTHOR "Norihiro Kamae") 5 | set(MACOS_BUNDLEID "net.nagater.obs-face-tracker") 6 | set(MACOS_PACKAGE_UUID "A2EB2F73-9828-40C4-A307-F47282BB8D3F") 7 | set(MACOS_INSTALLER_UUID "652CF739-5A6F-4DAC-9611-B068FAC15BF3") 8 | set(PLUGIN_URL "https://obsproject.com/forum/resources/face-tracker.1294/") 9 | set(LINUX_MAINTAINER_EMAIL "norihiro@nagater.net") 10 | 11 | option(WITH_PTZ_TCP "Enable to connect PTZ camera through TCP socket" ON) 12 | option(WITH_DLIB_SUBMODULE "Link dlib from submodule" ON) 13 | option(ENABLE_MONITOR_USER "Enable monitor source for user" OFF) 14 | option(ENABLE_DEBUG_DATA "Enable property to save error and control data" OFF) 15 | option(WITH_DOCK "Enable dock" ON) 16 | option(ENABLE_DATAGEN "Enable generating data" OFF) 17 | 18 | set(CMAKE_PREFIX_PATH "${QTDIR}") 19 | 20 | set(CMAKE_CXX_STANDARD 20) 21 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 22 | 23 | set(plugin_additional_libs) 24 | set(plugin_additional_incs) 25 | 26 | if (WITH_PTZ_TCP) 27 | set(CMAKE_POSITION_INDEPENDENT_CODE true) 28 | set(WITH_VISCA_SERIAL OFF) 29 | add_definitions("-DVISCA_API= ") 30 | add_subdirectory(libvisca/visca) 31 | set(plugin_additional_libs ${plugin_additional_libs} visca) 32 | set(plugin_additional_incs ${plugin_additional_incs} libvisca/visca) 33 | endif() 34 | 35 | find_package(libobs REQUIRED) 36 | find_package(obs-frontend-api REQUIRED) 37 | include(cmake/ObsPluginHelpers.cmake) 38 | find_qt(VERSION ${QT_VERSION} COMPONENTS Widgets Core Gui) 39 | 40 | if (WITH_DLIB_SUBMODULE) 41 | set(CMAKE_POSITION_INDEPENDENT_CODE True) 42 | set(DLIB_NO_GUI_SUPPORT ON) 43 | set(DLIB_PNG_SUPPORT OFF) 44 | set(DLIB_GIF_SUPPORT OFF) 45 | set(DLIB_USE_FFMPEG OFF) 46 | set(DLIB_JPEG_SUPPORT OFF) 47 | set(DLIB_LINK_WITH_SQLITE3 OFF) 48 | set(DLIB_WEBP_SUPPORT OFF) 49 | set(DLIB_JXL_SUPPORT OFF) 50 | set(DLIB_TEST_COMPILE_ALL_SOURCE_CPP OFF) 51 | add_subdirectory(dlib) 52 | else() 53 | find_package(dlib REQUIRED) 54 | add_library(dlib ALIAS dlib::dlib) 55 | endif() 56 | 57 | set(CMAKE_AUTOMOC ON) 58 | set(CMAKE_AUTOUIC ON) 59 | 60 | configure_file( 61 | src/plugin-macros.h.in 62 | plugin-macros.generated.h 63 | ) 64 | configure_file( 65 | installer/installer-Windows.iss.in 66 | installer-Windows.generated.iss 67 | ) 68 | 69 | configure_file( 70 | ci/ci_includes.sh.in 71 | ci/ci_includes.generated.sh 72 | ) 73 | configure_file( 74 | ci/ci_includes.cmd.in 75 | ci/ci_includes.generated.cmd 76 | ) 77 | 78 | set(PLUGIN_SOURCES 79 | src/module-main.c 80 | src/face-tracker.cpp 81 | src/source_list.cc 82 | src/face-tracker-preset.cpp 83 | src/face-tracker-manager.cpp 84 | src/face-tracker-ptz.cpp 85 | src/face-tracker-monitor.cpp 86 | src/face-detector-base.cpp 87 | src/face-detector-dlib-hog.cpp 88 | src/face-detector-dlib-cnn.cpp 89 | src/face-tracker-base.cpp 90 | src/face-tracker-dlib.cpp 91 | src/texture-object.cpp 92 | src/helper.cpp 93 | src/ptz-backend.cpp 94 | src/obsptz-backend.cpp 95 | src/dummy-backend.cpp 96 | ) 97 | 98 | if (WITH_PTZ_TCP) 99 | set(PLUGIN_SOURCES ${PLUGIN_SOURCES} src/libvisca-thread.cpp) 100 | endif() 101 | if (WITH_DOCK) 102 | set(PLUGIN_SOURCES 103 | ${PLUGIN_SOURCES} 104 | ui/face-tracker-dock.cpp 105 | ui/face-tracker-widget.cpp 106 | ) 107 | endif() 108 | 109 | add_library(${CMAKE_PROJECT_NAME} MODULE ${PLUGIN_SOURCES}) 110 | 111 | if (WITH_DOCK) 112 | set(plugin_additional_incs ${plugin_additional_incs} src) 113 | set(plugin_additional_libs ${plugin_additional_libs} Qt::Gui Qt::Widgets Qt::GuiPrivate) 114 | endif() 115 | 116 | include_directories( 117 | ${CMAKE_CURRENT_SOURCE_DIR}/dlib 118 | ${plugin_additional_incs} 119 | ) 120 | 121 | target_link_libraries(${CMAKE_PROJECT_NAME} 122 | OBS::libobs 123 | OBS::obs-frontend-api 124 | dlib 125 | ${plugin_additional_libs} 126 | ) 127 | 128 | target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE 129 | ${CMAKE_CURRENT_BINARY_DIR} 130 | ) 131 | 132 | if(OS_WINDOWS) 133 | # Enable Multicore Builds and disable FH4 (to not depend on VCRUNTIME140_1.DLL when building with VS2019) 134 | if (MSVC) 135 | add_definitions(/MP /d2FH4-) 136 | add_definitions("-D_USE_MATH_DEFINES") 137 | add_definitions("-D_CRT_SECURE_NO_WARNINGS") # to avoid a warning for `fopen` 138 | add_definitions("-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR") # TODO: Remove once requiring OBS 30.2 or later. 139 | endif() 140 | 141 | target_link_libraries(${CMAKE_PROJECT_NAME} OBS::w32-pthreads) 142 | endif() 143 | 144 | if(OS_LINUX) 145 | target_compile_options(${CMAKE_PROJECT_NAME} 146 | PRIVATE 147 | -Wall 148 | -Wextra 149 | -Wunused-function 150 | -Werror 151 | -Wno-error=deprecated-declarations # TODO: Replace obs_frontend_add_dock with the new API. 152 | ) 153 | target_link_options(${CMAKE_PROJECT_NAME} PRIVATE "-Wl,-z,defs") 154 | endif() 155 | 156 | if(APPLE) 157 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++ -fvisibility=default") 158 | 159 | set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES PREFIX "") 160 | set(MACOSX_PLUGIN_GUI_IDENTIFIER "${MACOS_BUNDLEID}") 161 | set(MACOSX_PLUGIN_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION}") 162 | set(MACOSX_PLUGIN_SHORT_VERSION_STRING "1") 163 | endif() 164 | 165 | setup_plugin_target(${CMAKE_PROJECT_NAME}) 166 | 167 | configure_file(installer/installer-macOS.pkgproj.in installer-macOS.generated.pkgproj) 168 | 169 | if(ENABLE_DATAGEN) 170 | add_executable(face-detector-dlib-hog-datagen 171 | src/face-detector-dlib-hog-datagen.cpp 172 | ) 173 | target_include_directories(face-detector-dlib-hog-datagen PRIVATE 174 | ${CMAKE_CURRENT_BINARY_DIR} 175 | ) 176 | target_link_libraries(face-detector-dlib-hog-datagen 177 | dlib 178 | ) 179 | endif() 180 | -------------------------------------------------------------------------------- /src/face-tracker-dlib.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "plugin-macros.generated.h" 5 | #include "texture-object.h" 6 | #include "face-tracker-dlib.h" 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | struct face_tracker_dlib_private_s 13 | { 14 | std::shared_ptr tex; 15 | rect_s rect; 16 | dlib::correlation_tracker *tracker; 17 | int tracker_nc, tracker_nr; 18 | dlib::shape_predictor sp; 19 | dlib::full_object_detection shape; 20 | float last_scale; 21 | float score0; 22 | float pslr_max, pslr_min; 23 | bool need_restart; 24 | uint64_t last_ns; 25 | float scale_orig; 26 | int n_track; 27 | rectf_s upsize; 28 | char *landmark_detection_data; 29 | bool landmark_detection_data_updated; 30 | bool sp_available = false; 31 | 32 | face_tracker_dlib_private_s() 33 | { 34 | tracker = NULL; 35 | need_restart = false; 36 | tex = NULL; 37 | rect.score = 0.0f; 38 | landmark_detection_data = NULL; 39 | landmark_detection_data_updated = false; 40 | } 41 | }; 42 | 43 | face_tracker_dlib::face_tracker_dlib() 44 | { 45 | p = new face_tracker_dlib_private_s; 46 | } 47 | 48 | face_tracker_dlib::~face_tracker_dlib() 49 | { 50 | bfree(p->landmark_detection_data); 51 | if (p->tracker) 52 | delete p->tracker; 53 | delete p; 54 | } 55 | 56 | void face_tracker_dlib::set_texture(std::shared_ptr &tex) 57 | { 58 | p->tex = tex; 59 | p->n_track = 0; 60 | } 61 | 62 | void face_tracker_dlib::set_position(const rect_s &rect) 63 | { 64 | if (!p->tex) { 65 | blog(LOG_ERROR, "face_tracker_dlib::set_position: texture was not set. rect=(%d %d %d %d %f)", rect.x0, 66 | rect.y0, rect.x1, rect.y1, rect.score); 67 | return; 68 | } 69 | 70 | p->rect.x0 = rect.x0 / p->tex->scale; 71 | p->rect.y0 = rect.y0 / p->tex->scale; 72 | p->rect.x1 = rect.x1 / p->tex->scale; 73 | p->rect.y1 = rect.y1 / p->tex->scale; 74 | p->rect.score = 1.0f; 75 | p->need_restart = true; 76 | p->n_track = 0; 77 | } 78 | 79 | void face_tracker_dlib::set_upsize_info(const rectf_s &upsize) 80 | { 81 | p->upsize = upsize; 82 | } 83 | 84 | void face_tracker_dlib::set_landmark_detection(const char *data_file_path) 85 | { 86 | if (p->landmark_detection_data && data_file_path && strcmp(p->landmark_detection_data, data_file_path) == 0) 87 | return; 88 | 89 | bfree(p->landmark_detection_data); 90 | p->landmark_detection_data = NULL; 91 | if (data_file_path) { 92 | p->landmark_detection_data = bstrdup(data_file_path); 93 | p->landmark_detection_data_updated = true; 94 | } 95 | } 96 | 97 | template inline Tx internal_division(Tx x0, Tx x1, Ta a0, Ta a1) 98 | { 99 | return (x0 * a1 + x1 * a0) / (a0 + a1); 100 | } 101 | 102 | void face_tracker_dlib::track_main() 103 | { 104 | if (!p->tex) 105 | return; 106 | 107 | uint64_t ns = os_gettime_ns(); 108 | if (p->need_restart) { 109 | if (!p->tracker) 110 | p->tracker = new dlib::correlation_tracker(); 111 | 112 | dlib::matrix img; 113 | if (!p->tex->get_dlib_rgb_image(img)) 114 | return; 115 | 116 | dlib::rectangle r(p->rect.x0, p->rect.y0, p->rect.x1, p->rect.y1); 117 | p->tracker->start_track(img, r); 118 | p->tracker_nc = img.nc(); 119 | p->tracker_nr = img.nr(); 120 | p->score0 = p->rect.score; 121 | p->need_restart = false; 122 | p->pslr_max = 0.0f; 123 | p->pslr_min = 1e9f; 124 | p->scale_orig = p->tex->scale; 125 | p->shape = dlib::full_object_detection(); 126 | } else if (p->tex->scale != p->scale_orig) { 127 | p->rect.score = 0.0f; 128 | } else { 129 | dlib::matrix img; 130 | if (!p->tex->get_dlib_rgb_image(img)) 131 | return; 132 | 133 | if (img.nc() != p->tracker_nc || img.nr() != p->tracker_nr) { 134 | blog(LOG_ERROR, 135 | "face_tracker_dlib::track_main: cannot run correlation-tracker with different image size %dx%d, expected %dx%d", 136 | (int)img.nc(), (int)img.nr(), p->tracker_nc, p->tracker_nr); 137 | p->rect.score = 0; 138 | p->n_track += 1; // to return score=0 139 | return; 140 | } 141 | 142 | float s = p->tracker->update(img); 143 | if (s > p->pslr_max) 144 | p->pslr_max = s; 145 | if (s < p->pslr_min) 146 | p->pslr_min = s; 147 | dlib::rectangle r = p->tracker->get_position(); 148 | p->rect.x0 = r.left() * p->tex->scale; 149 | p->rect.y0 = r.top() * p->tex->scale; 150 | p->rect.x1 = r.right() * p->tex->scale; 151 | p->rect.y1 = r.bottom() * p->tex->scale; 152 | s = p->pslr_max / p->pslr_min * ((ns - p->last_ns) * 1e-9f); 153 | p->rect.score = (p->rect.score /*+ 0.0f*s */) / (1.0f + s); 154 | p->n_track += 1; 155 | 156 | if (p->landmark_detection_data) { 157 | if (p->landmark_detection_data_updated) { 158 | p->landmark_detection_data_updated = false; 159 | blog(LOG_INFO, "loading file %s", p->landmark_detection_data); 160 | try { 161 | p->sp_available = false; 162 | dlib::deserialize(p->landmark_detection_data) >> p->sp; 163 | p->sp_available = true; 164 | } catch (...) { 165 | blog(LOG_ERROR, "Failed to load file %s", p->landmark_detection_data); 166 | } 167 | } 168 | 169 | dlib::rectangle r_face( 170 | internal_division(r.left(), r.right(), p->upsize.x0, p->upsize.x1 + 1.0f), 171 | internal_division(r.top(), r.bottom(), p->upsize.y0, p->upsize.y1 + 1.0f), 172 | internal_division(r.left(), r.right(), p->upsize.x0 + 1.0f, p->upsize.x1), 173 | internal_division(r.top(), r.bottom(), p->upsize.y0 + 1.0f, p->upsize.y1)); 174 | 175 | if (p->sp_available) 176 | p->shape = p->sp(img, r_face); 177 | p->last_scale = p->tex->scale; 178 | } 179 | } 180 | p->last_ns = ns; 181 | 182 | p->tex.reset(); 183 | } 184 | 185 | bool face_tracker_dlib::get_face(struct rect_s &rect) 186 | { 187 | if (p->n_track > 0) { 188 | rect = p->rect; 189 | return true; 190 | } else 191 | return false; 192 | } 193 | 194 | bool face_tracker_dlib::get_landmark(std::vector &results) 195 | { 196 | if (p->shape.num_parts() > 0) { 197 | const auto &shape = p->shape; 198 | results.resize(shape.num_parts()); 199 | 200 | for (unsigned long i = 0; i < shape.num_parts(); i++) { 201 | const dlib::point pnt = shape.part(i); 202 | results[i].x = (float)pnt.x() * p->last_scale; 203 | results[i].y = (float)pnt.y() * p->last_scale; 204 | } 205 | 206 | return true; 207 | } else 208 | return false; 209 | } 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Face Tracker Plugin for OBS Studio 2 | 3 | ## Introduction 4 | 5 | This plugin provide a feature to track face of a person by detecting and tracking a face. 6 | 7 | This plugin employs [dlib](http://dlib.net/) on face detection and object tracking. 8 | The frame of the source is periodically taken to face detection algorithm. 9 | Once a face is found, the face is tracked. 10 | Based on the location and the size of the face under tracking, the frame will be cropped. 11 | 12 | ## Usage 13 | 14 | For several use cases, total 3 methods are provided. 15 | 16 | ### Face Tracker Source 17 | The face tracker is implemented as a source. You can easily have another source that tracks and zooms into a face. 18 | 1. Click the add button on the source list. 19 | 2. Add `Face Tracker`. 20 | 3. Scroll to the bottom and set `Source` property. 21 | 22 | See [Properties](doc/properties.md) for the description of each property. 23 | 24 | ### Face Tracker Filter 25 | The face tracker is implemented as an effect filter so that any video source can have the face tracker. 26 | 1. Open filters for a source on OBS Studio. 27 | 2. Click the add button on `Effect Filters`. 28 | 3. Add `Face Tracker`. 29 | 30 | See [Properties](doc/properties.md) for the description of each property. 31 | 32 | ### Face Tracker PTZ 33 | Experimental version of PTZ control is provided as an video filter. 34 | 1. Open filters for a source on OBS Studio, 35 | 2. Click the add button on `Audio/Video Filters`. 36 | 3. Add `Face Tracker PTZ`. 37 | 38 | See [Properties](doc/properties-ptz.md) for the description of each property. 39 | 40 | See [Limitations](https://github.com/norihiro/obs-face-tracker/wiki/PTZ-Limitation) 41 | for current limitations of PTZ control feature. 42 | 43 | ## Wiki 44 | - [Install procedure for macOS](https://github.com/norihiro/obs-face-tracker/wiki/Install-MacOS) 45 | - [FAQ](https://github.com/norihiro/obs-face-tracker/wiki/FAQ) 46 | 47 | ## Building 48 | 49 | This plugin requires [dlib](http://dlib.net/) to be built. 50 | The `dlib` should be extracted under `obs-face-tracker` so that it will be linked statically. 51 | I modified `dlib` so that `openblasp` won't be linked but `openblas`. 52 | 53 | For macOS, 54 | install openblas and configure the path. 55 | ``` 56 | brew install openblas 57 | export OPENBLAS_HOME=/usr/local/opt/openblas/ 58 | ``` 59 | 60 | For Linux and macOS, 61 | expand `obs-face-tracker` outside `obs-studio` and build. 62 | ``` 63 | d0="$PWD" 64 | git clone https://github.com/obsproject/obs-studio.git 65 | mkdir obs-studio/build && cd obs-studio/build 66 | cmake .. 67 | make 68 | cd "$d0" 69 | 70 | git clone https://github.com/norihiro/obs-face-tracker.git 71 | cd obs-face-tracker 72 | git submodule update --init 73 | mkdir build && cd build 74 | cmake .. \ 75 | -DLIBOBS_INCLUDE_DIR=$d0/obs-studio/libobs \ 76 | -DLIBOBS_LIB=$d0/obs-studio/libobs \ 77 | -DOBS_FRONTEND_LIB="$d0/obs-studio/build/UI/obs-frontend-api/libobs-frontend-api.dylib" \ 78 | -DCMAKE_BUILD_TYPE=RelWithDebInfo 79 | make 80 | ``` 81 | 82 | For Windows, see `.github/workflows/main.yml`. 83 | 84 | ## Preparing data file 85 | 86 | You need to prepare a model file. 87 | 88 | ### HOG model file 89 | Once you have built on Linux or macOS, you will find an executable file `face-detector-dlib-hog-datagen`. 90 | 91 | Assuming your current directory is `obs-face-tracker`, run it like this. 92 | ```shell 93 | mkdir data/dlib_hog_model/ 94 | ./build/face-detector-dlib-hog-datagen > ./data/dlib_hog_model/frontal_face_detector.dat 95 | ``` 96 | 97 | ### CNN model file 98 | The CNN model file `mmod_human_face_detector.dat.bz2` can be downloaded from [dlib-models](https://github.com/davisking/dlib-models/). 99 | 100 | Assuming your current directory is `obs-face-tracker`, run commands like below. 101 | ```shell 102 | mkdir data/dlib_cnn_model/ 103 | git clone --depth 1 https://github.com/davisking/dlib-models 104 | bunzip2 < dlib-models/mmod_human_face_detector.dat.bz2 > data/dlib_cnn_model/mmod_human_face_detector.dat 105 | ``` 106 | 107 | ### 5-point face landmark model file 108 | The 5-point face landmark model file `shape_predictor_5_face_landmarks.dat.bz2` can be downloaded from [dlib-models](https://github.com/davisking/dlib-models/). 109 | 110 | Assuming your current directory is `obs-face-tracker`, run commands like below. 111 | ```shell 112 | mkdir data/dlib_face_landmark_model/ 113 | git clone --depth 1 https://github.com/davisking/dlib-models 114 | bunzip2 < dlib-models/shape_predictor_5_face_landmarks.dat.bz2 > data/dlib_face_landmark_model/shape_predictor_5_face_landmarks.dat 115 | ``` 116 | 117 | ### 68-point face landmark model file 118 | > [!NOTE] 119 | > The 68-point face landmark model is a non-free license. 120 | . Check [README](https://github.com/davisking/dlib-models/#shape_predictor_68_face_landmarksdatbz2) for the restriction. 121 | 122 | If you want to use the 68-point face landmark model file `shape_predictor_68_face_landmarks.dat.bz2`, run commands like below. 123 | ```shell 124 | mkdir data/dlib_face_landmark_model/ 125 | git clone --depth 1 https://github.com/davisking/dlib-models 126 | bunzip2 < dlib-models/shape_predictor_68_face_landmarks.dat.bz2 > data/dlib_face_landmark_model/shape_predictor_68_face_landmarks.dat 127 | ``` 128 | 129 | ### Installing the model files 130 | Once you have prepared the model files under `data` directory, 131 | run `cd build && make install` so that the data file will be installed. 132 | 133 | ## Known issues 134 | This plugin is heavily under development. So far these issues are under investigation. 135 | - Memory usage is gradually increasing when continuously detecting faces. 136 | - It consumes a lot of CPU resource. 137 | - The frame sometimes vibrates because the face detection results vibrates. 138 | 139 | ## License 140 | This plugin is licensed under GPLv2. 141 | 142 | ## Sponsor 143 | - [Jimcom USA](https://www.jimcom.us/?ref=2) - a company of Live Streaming and Content Recording Professionals. 144 | Development of PTZ camera control is supported by Jimcom. 145 | Jimcom is now providing a 20% discount for their broadcast-quality network-connected PTZ cameras and free shipping in the USA. 146 | Visit [Jimcom USA](https://www.jimcom.us/?ref=2) and enter the coupon code **FACETRACK20** when you order. 147 | 148 | ## Acknowledgments 149 | - [dlib](http://dlib.net/) - [git hub repository](https://github.com/davisking/dlib) 150 | - [obz-ptz](https://github.com/glikely/obs-ptz) - PTZ camera control goes through this plugin. 151 | - [OBS Project](https://obsproject.com/) 152 | -------------------------------------------------------------------------------- /src/face-tracker-preset.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "plugin-macros.generated.h" 3 | #include "face-tracker.hpp" 4 | #include "face-tracker-preset.h" 5 | 6 | static void copy_data_double(obs_data_t *dst, obs_data_t *src, const char *name) 7 | { 8 | blog(LOG_INFO, "copying %s as double", name); 9 | double v = obs_data_get_double(src, name); 10 | obs_data_set_double(dst, name, v); 11 | } 12 | 13 | #define preset_mask_track 1 14 | #define preset_mask_control 2 15 | static const struct 16 | { 17 | const uint32_t flag; 18 | const char *name; 19 | } preset_mask_flags[] = {{preset_mask_track, "preset_mask_track"}, 20 | {preset_mask_control, "preset_mask_control"}, 21 | {0, NULL}}; 22 | 23 | static uint32_t prop_to_preset_mask(obs_data_t *prop) 24 | { 25 | uint32_t mask = 0; 26 | for (auto *p = preset_mask_flags; p->flag; p++) { 27 | if (obs_data_get_bool(prop, p->name)) 28 | mask |= p->flag; 29 | } 30 | return mask; 31 | } 32 | 33 | static const struct 34 | { 35 | const char *name; 36 | void (*func)(obs_data_t *, obs_data_t *, const char *); 37 | uint32_t mask; 38 | } preset_property_list[] = {{"upsize_l", copy_data_double, preset_mask_track}, 39 | {"upsize_r", copy_data_double, preset_mask_track}, 40 | {"upsize_t", copy_data_double, preset_mask_track}, 41 | {"upsize_b", copy_data_double, preset_mask_track}, 42 | {"scale_max", copy_data_double, preset_mask_track}, 43 | {"track_z", copy_data_double, preset_mask_track}, 44 | {"track_x", copy_data_double, preset_mask_track}, 45 | {"track_y", copy_data_double, preset_mask_track}, 46 | {"Kp", copy_data_double, preset_mask_control}, 47 | {"Ki", copy_data_double, preset_mask_control}, 48 | {"Kd", copy_data_double, preset_mask_control}, 49 | {"Tdlpf", copy_data_double, preset_mask_control}, 50 | {"e_deadband_x", copy_data_double, preset_mask_control}, 51 | {"e_deadband_y", copy_data_double, preset_mask_control}, 52 | {"e_deadband_z", copy_data_double, preset_mask_control}, 53 | {"e_nonlineaeer_x", copy_data_double, preset_mask_control}, 54 | {"e_nonlineaeer_y", copy_data_double, preset_mask_control}, 55 | {"e_nonlineaeer_z", copy_data_double, preset_mask_control}, 56 | // specific to face_tracker_ptz 57 | {"Kp_x_db", copy_data_double, preset_mask_control}, 58 | {"Kp_y_db", copy_data_double, preset_mask_control}, 59 | {"Kp_z_db", copy_data_double, preset_mask_control}, 60 | {"Ki_x", copy_data_double, preset_mask_control}, 61 | {"Ki_y", copy_data_double, preset_mask_control}, 62 | {"Ki_z", copy_data_double, preset_mask_control}, 63 | {"Td_x", copy_data_double, preset_mask_control}, 64 | {"Td_y", copy_data_double, preset_mask_control}, 65 | {"Td_z", copy_data_double, preset_mask_control}, 66 | {"Tdlpf_z", copy_data_double, preset_mask_control}, 67 | {"Tatt_int", copy_data_double, preset_mask_control}, 68 | {NULL, NULL, 0}}; 69 | 70 | static void copy_preset(obs_data_t *dst, obs_data_t *src, uint32_t mask) 71 | { 72 | for (auto *p = preset_property_list; p->name && p->func; p++) { 73 | if (!(p->mask & mask)) 74 | continue; 75 | if (!obs_data_has_user_value(src, p->name) && !obs_data_has_default_value(src, p->name)) 76 | continue; 77 | p->func(dst, src, p->name); 78 | } 79 | } 80 | 81 | void ftf_preset_item_to_list(obs_property_t *p, obs_data_t *settings) 82 | { 83 | obs_data_t *presets = obs_data_get_obj(settings, "presets"); 84 | if (!presets) 85 | return; 86 | 87 | for (obs_data_item_t *item = obs_data_first(presets); item; obs_data_item_next(&item)) { 88 | const char *name = obs_data_item_get_name(item); 89 | obs_property_list_add_string(p, name, name); 90 | } 91 | 92 | obs_data_release(presets); 93 | } 94 | 95 | bool ftf_preset_load(obs_properties_t *, obs_property_t *, void *ctx_data) 96 | { 97 | auto *s = (struct face_tracker_filter *)ctx_data; 98 | 99 | obs_data_t *settings = NULL; 100 | obs_data_t *presets = NULL; 101 | obs_data_t *preset_data = NULL; 102 | const char *preset_name; 103 | 104 | settings = obs_source_get_settings(s->context); 105 | if (!settings) 106 | goto err; 107 | preset_name = obs_data_get_string(settings, "preset_name"); 108 | blog(LOG_INFO, "ftf_preset_load: loading preset %s", preset_name); 109 | presets = obs_data_get_obj(settings, "presets"); 110 | if (!presets) 111 | goto err; 112 | 113 | preset_data = obs_data_get_obj(presets, preset_name); 114 | if (!preset_data) { 115 | blog(LOG_ERROR, "ftf_preset_load: preset %s does not exist", preset_name); 116 | goto err; 117 | } 118 | copy_preset(settings, preset_data, prop_to_preset_mask(settings)); 119 | obs_source_update(s->context, settings); 120 | 121 | err: 122 | obs_data_release(preset_data); 123 | obs_data_release(presets); 124 | obs_data_release(settings); 125 | return true; 126 | } 127 | 128 | static inline void list_insert_string(obs_property_t *p, const char *name) 129 | { 130 | size_t count = obs_property_list_item_count(p); 131 | for (size_t i = 0; i < count; i++) { 132 | const char *s = obs_property_list_item_name(p, i); 133 | if (s && strcmp(s, name) == 0) 134 | return; 135 | if (!s || strcmp(s, name) > 0) { 136 | obs_property_list_insert_string(p, i, name, name); 137 | return; 138 | } 139 | } 140 | obs_property_list_add_string(p, name, name); 141 | } 142 | 143 | static inline void list_delete_string(obs_property_t *p, const char *name) 144 | { 145 | size_t count = obs_property_list_item_count(p); 146 | for (size_t i = 0; i < count; i++) { 147 | const char *s = obs_property_list_item_name(p, i); 148 | if (s && strcmp(s, name) == 0) { 149 | obs_property_list_item_remove(p, i); 150 | return; 151 | } 152 | } 153 | } 154 | 155 | bool ftf_preset_save(obs_properties_t *props, obs_property_t *, void *ctx_data) 156 | { 157 | auto *s = (struct face_tracker_filter *)ctx_data; 158 | 159 | obs_data_t *settings = NULL; 160 | obs_data_t *presets = NULL; 161 | obs_data_t *preset_data = NULL; 162 | const char *preset_name; 163 | 164 | settings = obs_source_get_settings(s->context); 165 | if (!settings) { 166 | blog(LOG_ERROR, "cannot get settings for %p", s->context); 167 | goto err; 168 | } 169 | preset_name = obs_data_get_string(settings, "preset_name"); 170 | blog(LOG_INFO, "ftf_preset_save: saving preset %s", preset_name); 171 | presets = obs_data_get_obj(settings, "presets"); 172 | if (!presets) 173 | presets = obs_data_create(); 174 | 175 | preset_data = obs_data_create(); 176 | copy_preset(preset_data, settings, prop_to_preset_mask(settings)); 177 | obs_data_set_obj(presets, preset_name, preset_data); 178 | 179 | obs_data_set_obj(settings, "presets", presets); 180 | 181 | if (obs_property_t *p = obs_properties_get(props, "preset_name")) 182 | list_insert_string(p, preset_name); 183 | 184 | err: 185 | obs_data_release(preset_data); 186 | obs_data_release(presets); 187 | obs_data_release(settings); 188 | return true; 189 | } 190 | 191 | bool ftf_preset_delete(obs_properties_t *props, obs_property_t *, void *ctx_data) 192 | { 193 | auto *s = (struct face_tracker_filter *)ctx_data; 194 | 195 | obs_data_t *settings = NULL; 196 | obs_data_t *presets = NULL; 197 | const char *preset_name; 198 | 199 | settings = obs_source_get_settings(s->context); 200 | if (!settings) { 201 | blog(LOG_ERROR, "cannot get settings for %p", s->context); 202 | goto err; 203 | } 204 | preset_name = obs_data_get_string(settings, "preset_name"); 205 | blog(LOG_INFO, "ftf_preset_save: deleting preset %s", preset_name); 206 | if (obs_property_t *p = obs_properties_get(props, "preset_name")) 207 | list_delete_string(p, preset_name); 208 | presets = obs_data_get_obj(settings, "presets"); 209 | if (!presets) 210 | goto err; 211 | 212 | obs_data_unset_user_value(presets, preset_name); 213 | 214 | obs_data_set_obj(settings, "presets", presets); 215 | 216 | err: 217 | obs_data_release(presets); 218 | obs_data_release(settings); 219 | return true; 220 | } 221 | -------------------------------------------------------------------------------- /src/libvisca-thread.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "plugin-macros.generated.h" 9 | #include "libvisca-thread.hpp" 10 | #include "libvisca.h" 11 | 12 | #define debug(...) // blog(LOG_INFO, __VA_ARGS__) 13 | 14 | #define TH_FAIL 4 15 | 16 | libvisca_thread::libvisca_thread() 17 | { 18 | debug("libvisca_thread::libvisca_thread"); 19 | iface = NULL; 20 | camera = NULL; 21 | data = NULL; 22 | pan_rsvd = 0; 23 | tilt_rsvd = 0; 24 | zoom_rsvd = 0; 25 | zoom_got = 0; 26 | pthread_mutex_init(&mutex, 0); 27 | 28 | add_ref(); // release inside thread_main 29 | pthread_t thread; 30 | pthread_create(&thread, NULL, libvisca_thread::thread_main, (void *)this); 31 | pthread_detach(thread); 32 | } 33 | 34 | libvisca_thread::~libvisca_thread() 35 | { 36 | if (iface) { 37 | VISCA_close(iface); 38 | bfree(iface); 39 | } 40 | if (camera) 41 | bfree(camera); 42 | if (data) 43 | obs_data_release(data); 44 | pthread_mutex_destroy(&mutex); 45 | } 46 | 47 | void libvisca_thread::thread_connect() 48 | { 49 | pthread_mutex_lock(&mutex); 50 | 51 | const char *address = obs_data_get_string(data, "address"); 52 | int port = (int)obs_data_get_int(data, "port"); 53 | auto *iface_new = (struct _VISCA_interface *)bzalloc(sizeof(struct _VISCA_interface)); 54 | debug("libvisca_thread::thread_connect connecting to address=%s port=%d...", address, port); 55 | if (VISCA_open_tcp(iface_new, address, port) != VISCA_SUCCESS) { 56 | blog(LOG_ERROR, "failed to connect %s:%d", address, port); 57 | bfree(iface_new); 58 | iface_new = NULL; 59 | } 60 | debug("libvisca_thread::thread_connect connected."); 61 | pthread_mutex_unlock(&mutex); 62 | if (!iface_new) 63 | return; 64 | 65 | if (iface) { 66 | VISCA_close(iface); 67 | bfree(iface); 68 | } 69 | iface = iface_new; 70 | if (!camera) 71 | camera = (VISCACamera_t *)bzalloc(sizeof(VISCACamera_t)); 72 | data_changed = false; 73 | 74 | debug("libvisca_thread::thread_connect sending VISCA_clear..."); 75 | camera->address = 1; 76 | VISCA_clear(iface, camera); 77 | debug("libvisca_thread::thread_connect exiting successfully"); 78 | } 79 | 80 | void *libvisca_thread::thread_main(void *data) 81 | { 82 | auto *visca = (libvisca_thread *)data; 83 | 84 | // add_ref() was called just before creating this thread. 85 | 86 | visca->thread_loop(); 87 | 88 | visca->release(); 89 | 90 | return NULL; 91 | } 92 | 93 | static inline bool send_pantilt(struct _VISCA_interface *iface, struct _VISCA_camera *camera, int pan, int tilt, 94 | int retry = 0) 95 | { 96 | debug("send_pantilt moving pan=%d tilt=%d", pan, tilt); 97 | int pan_a = abs(pan); 98 | int tilt_a = abs(tilt); 99 | if (pan_a > 127) 100 | pan_a = 127; 101 | if (tilt_a > 127) 102 | tilt_a = 127; 103 | 104 | uint32_t res = VISCA_SUCCESS; 105 | if (tilt < 0 && pan < 0) // 1=up, 1=left 106 | res = VISCA_set_pantilt_upleft(iface, camera, pan_a, tilt_a); 107 | else if (tilt < 0 && pan == 0) // 1=up 108 | res = VISCA_set_pantilt_up(iface, camera, pan_a, tilt_a); 109 | else if (tilt < 0 && pan > 0) // 1=up, 2=right 110 | res = VISCA_set_pantilt_upright(iface, camera, pan_a, tilt_a); 111 | else if (tilt == 0 && pan < 0) // 1=left 112 | res = VISCA_set_pantilt_left(iface, camera, pan_a, tilt_a); 113 | else if (tilt == 0 && pan == 0) 114 | res = VISCA_set_pantilt_stop(iface, camera, pan_a, tilt_a); 115 | else if (tilt == 0 && pan > 0) // 2=right 116 | res = VISCA_set_pantilt_right(iface, camera, pan_a, tilt_a); 117 | else if (tilt > 0 && pan < 0) // 2=down, 1=left 118 | res = VISCA_set_pantilt_downleft(iface, camera, pan_a, tilt_a); 119 | else if (tilt > 0 && pan == 0) // 2=down 120 | res = VISCA_set_pantilt_down(iface, camera, pan_a, tilt_a); 121 | else if (tilt > 0 && pan > 0) 122 | res = VISCA_set_pantilt_downright(iface, camera, pan_a, tilt_a); 123 | 124 | if (res != VISCA_SUCCESS) 125 | return false; 126 | 127 | if (iface->type == VISCA_RESPONSE_ERROR && retry < 3) { 128 | blog(LOG_INFO, "send_pantilt(%d, %d): retrying (%d)", pan, tilt, retry); 129 | return send_pantilt(iface, camera, pan, tilt, retry + 1); 130 | } 131 | 132 | return true; 133 | } 134 | 135 | static inline bool send_zoom(struct _VISCA_interface *iface, struct _VISCA_camera *camera, int zoom, int retry = 0) 136 | { 137 | debug("send_zoom moving zoom=%d", zoom); 138 | uint32_t res; 139 | int zoom_a = std::abs(zoom); 140 | if (zoom_a > 7) 141 | zoom_a = 7; 142 | // zoom>0 : wide 143 | if (zoom > 0) 144 | res = VISCA_set_zoom_wide_speed(iface, camera, zoom_a); 145 | else if (zoom < 0) 146 | res = VISCA_set_zoom_tele_speed(iface, camera, zoom_a); 147 | else 148 | res = VISCA_set_zoom_stop(iface, camera); 149 | 150 | if (res != VISCA_SUCCESS) 151 | return false; 152 | 153 | if (iface->type == VISCA_RESPONSE_ERROR && retry < 3) { 154 | blog(LOG_INFO, "send_zoom(%d): retrying (%d)", zoom, retry); 155 | return send_zoom(iface, camera, zoom, retry + 1); 156 | } 157 | 158 | return true; 159 | } 160 | 161 | void libvisca_thread::thread_loop() 162 | { 163 | int pan_prev = INT_MIN, tilt_prev = INT_MIN, zoom_prev = INT_MIN; 164 | int n_fail = 0; 165 | 166 | while (get_ref() > 1) { 167 | if (data_changed || n_fail > TH_FAIL) { 168 | thread_connect(); 169 | pan_prev = INT_MIN; 170 | tilt_prev = INT_MIN; 171 | zoom_prev = INT_MIN; 172 | } 173 | if (!iface) { 174 | os_sleep_ms(50); 175 | continue; 176 | } 177 | int pan = os_atomic_load_long(&pan_rsvd); 178 | int tilt = os_atomic_load_long(&tilt_rsvd); 179 | int zoom = os_atomic_load_long(&zoom_rsvd); 180 | bool ptz_changed = false; 181 | if (pan != pan_prev || tilt != tilt_prev) { 182 | if (send_pantilt(iface, camera, pan, tilt)) { 183 | pan_prev = pan; 184 | tilt_prev = tilt; 185 | ptz_changed = true; 186 | n_fail = 0; 187 | } else { 188 | n_fail++; 189 | } 190 | } 191 | if (zoom != zoom_prev) { 192 | if (send_zoom(iface, camera, zoom)) { 193 | zoom_prev = zoom; 194 | ptz_changed = true; 195 | n_fail = 0; 196 | } else { 197 | n_fail++; 198 | } 199 | } 200 | 201 | if (os_atomic_set_bool(&preset_changed, false)) { 202 | os_sleep_ms(48); 203 | debug("libvisca_thread::thread_loop recall preset=%d", (int)preset_rsvd); 204 | VISCA_memory_recall(iface, camera, preset_rsvd); 205 | os_sleep_ms(48); 206 | } 207 | 208 | if (zoom != 0) { 209 | uint16_t zoom_cur = 0; 210 | if (VISCA_get_zoom_value(iface, camera, &zoom_cur) == VISCA_SUCCESS) { 211 | if (zoom_cur != zoom_got) { 212 | debug("libvisca_thread::thread_loop got zoom=%d", (int)zoom_cur); 213 | } 214 | os_atomic_set_long(&zoom_got, (long)zoom_cur); 215 | } 216 | } 217 | 218 | if (!ptz_changed) 219 | os_sleep_ms(50); 220 | } 221 | } 222 | 223 | void libvisca_thread::set_config(struct obs_data *data_) 224 | { 225 | pthread_mutex_lock(&mutex); 226 | 227 | obs_data_addref(data_); 228 | if (data) { 229 | obs_data_release(data); 230 | const char *address_old = obs_data_get_string(data, "address"); 231 | int port_old = (int)obs_data_get_int(data, "port"); 232 | const char *address_new = obs_data_get_string(data_, "address"); 233 | int port_new = (int)obs_data_get_int(data_, "port"); 234 | if (strcmp(address_old, address_new)) 235 | data_changed = true; 236 | if (port_old != port_new) 237 | data_changed = true; 238 | } else 239 | data_changed = true; 240 | data = data_; 241 | 242 | pthread_mutex_unlock(&mutex); 243 | } 244 | 245 | float libvisca_thread::raw2zoomfactor(int zoom) 246 | { 247 | // TODO: configurable 248 | return expf((float)zoom * (logf(20.0f) / 16384.f)); 249 | } 250 | 251 | bool libvisca_thread::ptz_type_modified(obs_properties_t *pp, obs_data_t *settings) 252 | { 253 | (void)settings; 254 | if (obs_properties_get(pp, "ptz.visca-over-tcp.address")) 255 | return false; 256 | 257 | obs_properties_add_text(pp, "ptz.visca-over-tcp.address", obs_module_text("IP address"), OBS_TEXT_DEFAULT); 258 | obs_properties_add_int(pp, "ptz.visca-over-tcp.port", obs_module_text("Port"), 1, 65535, 1); 259 | return true; 260 | } 261 | -------------------------------------------------------------------------------- /src/face-tracker-monitor.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "plugin-macros.generated.h" 4 | #include "helper.hpp" 5 | 6 | #define MAX_ERROR 2 7 | 8 | struct face_tracker_monitor 9 | { 10 | obs_source_t *context; 11 | 12 | // properties 13 | char *source_name; 14 | char *filter_name; 15 | bool notrack; 16 | bool nosource; 17 | bool landmark_only; 18 | 19 | obs_weak_source_t *source_ref; 20 | obs_weak_source_t *filter_ref; 21 | 22 | int n_error; 23 | }; 24 | 25 | static const char *ftmon_get_name(void *unused) 26 | { 27 | UNUSED_PARAMETER(unused); 28 | return obs_module_text("Face Tracker Monitor"); 29 | } 30 | 31 | static void *ftmon_create(obs_data_t *settings, obs_source_t *context) 32 | { 33 | auto *s = (struct face_tracker_monitor *)bzalloc(sizeof(struct face_tracker_monitor)); 34 | s->context = context; 35 | 36 | obs_source_update(context, settings); 37 | 38 | return s; 39 | } 40 | 41 | static void ftmon_destroy(void *data) 42 | { 43 | auto *s = (struct face_tracker_monitor *)data; 44 | 45 | bfree(s->source_name); 46 | bfree(s->filter_name); 47 | obs_weak_source_release(s->filter_ref); 48 | obs_weak_source_release(s->source_ref); 49 | 50 | bfree(s); 51 | } 52 | 53 | static void ftmon_update(void *data, obs_data_t *settings) 54 | { 55 | auto *s = (struct face_tracker_monitor *)data; 56 | 57 | const char *source_name = obs_data_get_string(settings, "source_name"); 58 | const char *filter_name = obs_data_get_string(settings, "filter_name"); 59 | if (source_name && (!s->source_name || strcmp(source_name, s->source_name))) { 60 | bfree(s->source_name); 61 | s->source_name = bstrdup(source_name); 62 | s->n_error = 0; 63 | } 64 | 65 | if (!filter_name || !*filter_name) { 66 | bfree(s->filter_name); 67 | s->filter_name = NULL; 68 | s->n_error = 0; 69 | } else if (!s->filter_name || strcmp(filter_name, s->filter_name)) { 70 | bfree(s->filter_name); 71 | s->filter_name = bstrdup(filter_name); 72 | s->n_error = 0; 73 | } 74 | 75 | s->notrack = obs_data_get_bool(settings, "notrack"); 76 | 77 | s->nosource = obs_data_get_bool(settings, "nosource"); 78 | 79 | s->landmark_only = obs_data_get_bool(settings, "landmark_only"); 80 | } 81 | 82 | static obs_properties_t *ftmon_properties(void *) 83 | { 84 | obs_properties_t *props; 85 | props = obs_properties_create(); 86 | 87 | // TODO: use obs_properties_add_list 88 | obs_properties_add_text(props, "source_name", obs_module_text("Source name"), OBS_TEXT_DEFAULT); 89 | obs_properties_add_text(props, "filter_name", obs_module_text("Filter name"), OBS_TEXT_DEFAULT); 90 | 91 | obs_properties_add_bool(props, "notrack", "Display original source"); 92 | 93 | obs_properties_add_bool(props, "nosource", "Overlay only"); 94 | 95 | obs_properties_add_bool(props, "landmark_only", "Display landmark information only"); 96 | 97 | return props; 98 | } 99 | 100 | static void ftmon_get_defaults(obs_data_t *) {} 101 | 102 | static obs_source_t *get_source(struct face_tracker_monitor *s) 103 | { 104 | if (!s->source_ref) 105 | return NULL; 106 | return obs_weak_source_get_source(s->source_ref); 107 | } 108 | 109 | static obs_source_t *get_filter(struct face_tracker_monitor *s) 110 | { 111 | if (!s->filter_ref) 112 | return NULL; 113 | return obs_weak_source_get_source(s->filter_ref); 114 | } 115 | 116 | static obs_source_t *get_target(struct face_tracker_monitor *s) 117 | { 118 | if (!s->filter_name || !*s->filter_name) 119 | return get_source(s); 120 | return get_filter(s); 121 | } 122 | 123 | static inline bool test_weak_source_name(obs_weak_source_t *ref, const char *name) 124 | { 125 | obs_source_t *src = obs_weak_source_get_source(ref); 126 | if (!src) 127 | return false; 128 | 129 | const char *n = obs_source_get_name(src); 130 | if (!n) 131 | return false; 132 | bool ret = strcmp(n, name) == 0; 133 | obs_source_release(src); 134 | return ret; 135 | } 136 | 137 | static inline void tick_source(struct face_tracker_monitor *s, obs_weak_source_t *&ref, const char *name, 138 | obs_source_t *(*get_by_name)(struct face_tracker_monitor *)) 139 | { 140 | if (!name || !*name) 141 | return; 142 | 143 | if (!test_weak_source_name(ref, name)) { 144 | obs_weak_source_release(ref); 145 | ref = NULL; 146 | obs_source_t *src = get_by_name(s); 147 | ref = obs_source_get_weak_source(src); 148 | obs_source_release(src); 149 | } 150 | } 151 | 152 | obs_source_t *get_source_by_name(struct face_tracker_monitor *s) 153 | { 154 | return obs_get_source_by_name(s->source_name); 155 | } 156 | 157 | obs_source_t *get_filter_by_name(struct face_tracker_monitor *s) 158 | { 159 | obs_source_t *src = get_source(s); 160 | obs_source_t *ret = obs_source_get_filter_by_name(src, s->filter_name); 161 | obs_source_release(src); 162 | return ret; 163 | } 164 | 165 | static void ftmon_tick(void *data, float) 166 | { 167 | auto *s = (struct face_tracker_monitor *)data; 168 | 169 | bool source_specified = s->source_name && *s->source_name; 170 | bool filter_specified = s->filter_name && *s->filter_name; 171 | 172 | if (source_specified) 173 | tick_source(s, s->source_ref, s->source_name, get_source_by_name); 174 | if (filter_specified) 175 | tick_source(s, s->filter_ref, s->filter_name, get_filter_by_name); 176 | 177 | if (source_specified && !s->source_ref) { 178 | if (s->n_error < MAX_ERROR) { 179 | blog(LOG_INFO, "failed to get source \"%s\"", s->source_name); 180 | s->n_error++; 181 | } 182 | } else if (filter_specified && !s->filter_ref) { 183 | if (s->n_error < MAX_ERROR) { 184 | blog(LOG_INFO, "failed to get filter \"%s\"", s->filter_name); 185 | s->n_error++; 186 | } 187 | } else { 188 | s->n_error = 0; 189 | } 190 | } 191 | 192 | static uint32_t ftmon_get_width(void *data) 193 | { 194 | auto *s = (struct face_tracker_monitor *)data; 195 | 196 | if (s->notrack) { 197 | OBSSource target(get_target(s)); 198 | obs_source_release(target); 199 | 200 | proc_handler_t *ph = obs_source_get_proc_handler(target); 201 | if (!ph) 202 | return 0; 203 | 204 | CALLDATA_FIXED_DECL(cd, 128); 205 | if (proc_handler_call(ph, "get_target_size", &cd)) { 206 | long long ret; 207 | if (calldata_get_int(&cd, "width", &ret)) 208 | return (uint32_t)ret; 209 | } 210 | } 211 | 212 | OBSSource source(get_source(s)); 213 | obs_source_release(source); 214 | if (!source) 215 | return 0; 216 | return obs_source_get_width(source); 217 | } 218 | 219 | static uint32_t ftmon_get_height(void *data) 220 | { 221 | auto *s = (struct face_tracker_monitor *)data; 222 | 223 | if (s->notrack) { 224 | OBSSource target(get_target(s)); 225 | obs_source_release(target); 226 | 227 | proc_handler_t *ph = obs_source_get_proc_handler(target); 228 | if (!ph) 229 | return 0; 230 | 231 | CALLDATA_FIXED_DECL(cd, 128); 232 | if (proc_handler_call(ph, "get_target_size", &cd)) { 233 | long long ret; 234 | if (calldata_get_int(&cd, "height", &ret)) 235 | return (uint32_t)ret; 236 | } 237 | } 238 | 239 | OBSSource source(get_source(s)); 240 | obs_source_release(source); 241 | if (!source) 242 | return 0; 243 | return obs_source_get_height(source); 244 | } 245 | 246 | static void ftmon_video_render(void *data, gs_effect_t *) 247 | { 248 | auto *s = (struct face_tracker_monitor *)data; 249 | 250 | OBSSource target(get_target(s)); 251 | obs_source_release(target); 252 | if (!target) 253 | return; 254 | 255 | proc_handler_t *ph = obs_source_get_proc_handler(target); 256 | if (!ph) 257 | return; 258 | 259 | CALLDATA_FIXED_DECL(cd, 128); 260 | calldata_set_bool(&cd, "notrack", s->notrack); 261 | 262 | if (!s->nosource) { 263 | if (!proc_handler_call(ph, "render_frame", &cd)) { 264 | OBSSource src(get_source(s)); 265 | obs_source_release(src); 266 | obs_source_video_render(src); 267 | } 268 | } 269 | 270 | if (s->landmark_only) 271 | calldata_set_bool(&cd, "landmark_only", true); 272 | 273 | proc_handler_call(ph, "render_info", &cd); 274 | } 275 | 276 | extern "C" void register_face_tracker_monitor(bool hide_monitor) 277 | { 278 | struct obs_source_info info = {}; 279 | info.id = "face_tracker_monitor"; 280 | info.type = OBS_SOURCE_TYPE_INPUT; 281 | info.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW | OBS_SOURCE_DO_NOT_DUPLICATE; 282 | if (hide_monitor) 283 | info.output_flags |= OBS_SOURCE_CAP_DISABLED; 284 | info.get_name = ftmon_get_name; 285 | info.create = ftmon_create; 286 | info.destroy = ftmon_destroy; 287 | info.update = ftmon_update; 288 | info.get_properties = ftmon_properties; 289 | info.get_defaults = ftmon_get_defaults; 290 | info.get_width = ftmon_get_width; 291 | info.get_height = ftmon_get_height; 292 | info.video_tick = ftmon_tick; 293 | info.video_render = ftmon_video_render; 294 | obs_register_source(&info); 295 | } 296 | -------------------------------------------------------------------------------- /doc/properties.md: -------------------------------------------------------------------------------- 1 | # Face Tracker Properties 2 | 3 | ### Reset tracking (button) 4 | When clicked, tracking state is reset to the initial condition; zero crop, reset internal states of the integrators. 5 | (This is not a property.) 6 | 7 | ## Preset 8 | **Deprecated** 9 | Preset will be provided through a dock. The preset group in the property dialog will be removed in the future release. 10 | 11 | ### Preset 12 | This combo-box sets the name of the preset to be loaded or saved. 13 | 14 | ### Load preset 15 | To load the preset, select the preset from `Preset` combo-box and click `Load preset` button. 16 | 17 | ### Save preset 18 | To save current propeties as a preset, type preset name in `Preset` combo-box and click `Save preset` button. 19 | 20 | ### Delete preset 21 | To delete awn existing preset, select the preset from `Preset` combo-box and click `Delete preset` button. 22 | 23 | ### Save and load tracking parameters 24 | If you want to save only tracking parameters (`Upsize recognized face` and `Tracking target location`), enable only this check-box. 25 | 26 | ### Save and load control parameters 27 | If you want to save only contol parameters (`Tracking response`), enable only this check-box. 28 | 29 | ## Face detection options 30 | 31 | ### Left, right, top, bottom 32 | These properties upsize (or downsize) the recognized face by multiple of the width or height. 33 | 34 | The motivation is that the face recognition returns a rectangle that is smaller than the actual face. 35 | 36 | The properties will be saved to and recalled from presets. 37 | 38 | ### Scale image 39 | The frame will be scaled before sending into face detection and tracking algorithm. 40 | Default is `2`. 41 | Larger value will reduce CPU usage but too large value will fail to detect faces. 42 | The face detection engine requires size of the faces at least 80x80. 43 | If you have low resolution image, it is highly recommended to set to `1`. 44 | 45 | If your resolution is much smaller, make a intermediate scene and apply face tracker filter to the scene. 46 | 1. Make a blank scene. 47 | 1. Put your source to the scene and expand the size of the source. 48 | 1. Apply the filter to the scene. 49 | 1. Put the scene to your desired scene. 50 | 51 | ### Crop left, right, top, and bottom for detector 52 | These properties crop the image before sending to the face detection algorithm. 53 | The unit is pixel before scaling the image. 54 | 55 | The properties won't affect tracking. 56 | If the face is once detected and moved out from the cropped region, 57 | the tracking will still continue. 58 | 59 | ### Landmark detection 60 | Specify dataset for face landmark detection and enable the checkbox 61 | to calculate location and size of the face. 62 | The location is calculated by the average of all the landmark points for each face. 63 | The size is calculated by the area surrounded by the landmark points. 64 | You might need to adjust tracking target location and zoom depending on the landmark datasets. 65 | 66 | A dataset file `shape_predictor_5_face_landmarks.dat` is bundled so that you can try it soon. 67 | Original data is distributed at [dlib-models](https://github.com/davisking/dlib-models). 68 | Another model `shape_predictor_68_face_landmarks.dat` is ready but not bundled due to a license incompatibility. 69 | 70 | ### Tracking threshold 71 | This property sets the threshold when to stop tracking after the face is lost. 72 | During correlation tracking, scores are accumulated. 73 | When the score drops lower than the specified threshold, 74 | the tracking will be stopped. 75 | 76 | ## Tracking target location 77 | 78 | ### Zoom 79 | This property set the target zoom amount in multiple of the screen size. 80 | If set to `1.0`, face size and screen size is same. 81 | Smaller value results smaller face, i.e. less zoom. 82 | 83 | The property will be saved to and recalled from presets. 84 | 85 | ### X, Y 86 | This property set the location where the center of the face is placed. 87 | `0` indicates the center, `+/-0.5` will result the center of the face is located at the edge. 88 | 89 | The properties will be saved to and recalled from presets. 90 | 91 | ### Scale max 92 | This property set the maximum of the zoom. 93 | 94 | The property will be saved to and recalled from presets. 95 | 96 | ## Tracking response 97 | 98 | The tracking system has a PID control element + integrator. 99 | 100 | ### Kp 101 | This is a proportional constant. The dimension is inverse time and the unit is s-1. 102 | Larger value will result faster response. 103 | 104 | The property will be saved to and recalled from presets. 105 | 106 | ### Ki 107 | This is a integral constant. The dimension is inverse time and the unit is s-1. 108 | Larger value results more tracking of slow move. 109 | 110 | The property will be saved to and recalled from presets. 111 | 112 | ### Td 113 | This is a derivative constant. The dimension is time and the unit is s. 114 | 0 will result no derivative term. 115 | Larger value will make faster tracking when the subject start to move. 116 | 117 | The property will be saved to and recalled from presets. 118 | 119 | ### LPF for Td 120 | This is an inverse of the cut-off frequency for the low-pass filter (LPF), which affects the derivative term of PID control element. The dimension is time and the unit is s. 121 | The LPF will reduce noise of face detection and small move of the subject. 122 | 123 | The property will be saved to and recalled from presets. 124 | 125 | ### Attenuation (Z) 126 | This is another proportional constant that affects only the zoom factor. 127 | Smaller value will result in slower response of the zoom. 128 | 129 | ### Dead band nonlinear band (X, Y, Z) 130 | These parameters make dead bands and nonlinear bands for the error signal that goes to PID control element. 131 | The unit is a percentage of the average of source width and height. 132 | If the error signal is within the dead band, error signal is forced to zero to avoid small move to be tracked. 133 | The nonlinear band makes smooth connection from the dead band to the linear range. 134 | 135 | The properties will be saved to and recalled from presets. 136 | 137 | ## Source and output 138 | 139 | ### Source 140 | **Face Tracker Source only** 141 | This property specifies the source to take the frame texture. 142 | 143 | ### Aspect 144 | This parameter sets output aspect ratio. The default is same as the source. 145 | You can choose or type any aspect ratio. 146 | If the aspect is set narrower than the source, the height will be taken from the source and the width will be calculated. 147 | If the aspect is set wider than the source, the width will be taken from the source and the height will be calculated. 148 | 149 | Known issue: The bottom or right pixels might show flicker. The workaround is to set `1` for the crop properties in transform dialog. 150 | 151 | ## Automation 152 | 153 | ### Reset while inactive 154 | If you enable this, the tracking will be reset and paused when the source got inactive, ie. not displayed on the program. 155 | Disabled by default. 156 | 157 | ## Debug 158 | These properties enables how the face detection and tracking works. 159 | Note that these features are automatically turned off when the source is displayed on the program of OBS Studio. 160 | You can keep enable the checkboxes and keep monitoring the detection accuracy before the scene goes to the program. 161 | 162 | ### Show face detection results 163 | If enabled, face detection and tracking results are shown. 164 | The face detection results are displayed in blue boxes. 165 | The Tracking results are displayed in green boxes. 166 | 167 | ### Stop tracking faces 168 | If enabled, whole image will be displayed and yellow box shows how cropped. 169 | This is useful to check how much margins are there around the cropped area. 170 | 171 | ### Always show information 172 | If enabled, debugging properties listed above are effective even if the source is displayed on the program. 173 | This will be useful to make a demonstration of face-tracker itself. 174 | 175 | ### Save correlation tracker, calculated error, control data to file 176 | **Not available for released version** 177 | Save internal calculation into the specified file for each. 178 | This option is not available without building with `ENABLE_DEBUG_DATA` 179 | but still can be set through obs-websocket or manually editing the scene file to add a text property with a file name to be written. 180 | To disable it back, remove the property or set zero-length text. 181 | 182 | #### Correlation tracker 183 | Property name: `debug_data_tracker` 184 | 185 | The data contains time in second, 3 coordinates (X, Y, Z), and score of the correlation tracker. 186 | The X and Y coordinates are the center of the face. 187 | The Z coordinate is a square-root of the area. 188 | Sometimes multiple correlation trackers run at the same time. In that case, multiple lines are written at the same timing. 189 | 190 | #### Calculated error 191 | Property name: `debug_data_error` 192 | 193 | The data contains time in second, 3 coordinates (X, Y, Z). 194 | The calculated error is the adjusted measure with current resolution, the cropped region when the frame was rendered, and user-specified tracking target. 195 | 0-value indicates the face is well aligned and positive or negative value indicates the cropped region need to be moved. 196 | 197 | #### Control 198 | Property name: `debug_data_control` 199 | 200 | The data contains time in second, 3 coordinates (X, Y, Z). 201 | The X and Y coordinates are the center of the cropped region. 202 | The Z coordinate is a square-root of the area of the cropped region. 203 | -------------------------------------------------------------------------------- /doc/properties-ptz.md: -------------------------------------------------------------------------------- 1 | # Face Tracker PTZ Properties 2 | 3 | ### Reset tracking (button) 4 | When clicked, tracking state is reset to the initial condition; reset internal states of the integrators, send reset command to the PTZ device. 5 | (This is not a property.) 6 | 7 | ## Preset 8 | **Deprecated** 9 | Preset will be provided through a dock. The preset group in the property dialog will be removed in the future release. 10 | 11 | ### Preset 12 | This combo-box sets the name of the preset to be loaded or saved. 13 | 14 | ### Load preset 15 | To load the preset, select the preset from `Preset` combo-box and click `Load preset` button. 16 | 17 | ### Save preset 18 | To save current propeties as a preset, type preset name in `Preset` combo-box and click `Save preset` button. 19 | 20 | ### Delete preset 21 | To delete awn existing preset, select the preset from `Preset` combo-box and click `Delete preset` button. 22 | 23 | ### Save and load tracking parameters 24 | If you want to save only tracking parameters (`Upsize recognized face` and `Tracking target location`), enable only this check-box. 25 | 26 | ### Save and load control parameters 27 | If you want to save only contol parameters (`Tracking response`), enable only this check-box. 28 | 29 | ## Face detection options 30 | 31 | ### Left, right, top, bottom 32 | These properties upsize (or downsize) the recognized face by multiple of the width or height. 33 | 34 | The motivation is that the face recognition returns a rectangle that is smaller than the actual face. 35 | 36 | The properties will be saved to and recalled from presets. 37 | 38 | ### Scale image 39 | The frame will be scaled before sending into face detection and tracking algorithm. 40 | Default is `2`. 41 | Larger value will reduce CPU usage but too large value will fail to detect faces. 42 | The face detection engine requires size of the faces at least 80x80. 43 | If you have low resolution image, it is highly recommended to set to `1`. 44 | 45 | ### Crop left, right, top, and bottom for detector 46 | These properties crop the image before sending to the face detection algorithm. 47 | The unit is pixel before scaling the image. 48 | 49 | The properties won't affect tracking. 50 | If the face is once detected and moved out from the cropped region, 51 | the tracking will still continue. 52 | 53 | ### Landmark detection 54 | Specify dataset for face landmark detection and enable the checkbox 55 | to calculate location and size of the face. 56 | The location is calculated by the average of all the landmark points for each face. 57 | The size is calculated by the area surrounded by the landmark points. 58 | You might need to adjust tracking target location and zoom depending on the landmark datasets. 59 | 60 | A dataset file `shape_predictor_5_face_landmarks.dat` is bundled so that you can try it soon. 61 | Original data is distributed at [dlib-models](https://github.com/davisking/dlib-models). 62 | Another model `shape_predictor_68_face_landmarks.dat` is ready but not bundled due to a license incompatibility. 63 | 64 | ### Tracking threshold 65 | This property sets the threshold when to stop tracking after the face is lost. 66 | During correlation tracking, scores are accumulated. 67 | When the score drops lower than the specified threshold, 68 | the tracking will be stopped. 69 | 70 | ## Tracking target location 71 | 72 | ### Zoom 73 | This property set the target zoom amount in multiple of the screen size. 74 | If set to `1.0`, face size and screen size is same. 75 | Smaller value results smaller face, i.e. less zoom. 76 | 77 | The property will be saved to and recalled from presets. 78 | 79 | ### X, Y 80 | This property set the location where the center of the face is placed. 81 | `0` indicates the center, `+/-0.5` will result the center of the face is located at the edge. 82 | 83 | The properties will be saved to and recalled from presets. 84 | 85 | ## Tracking response 86 | 87 | The tracking system has a PID control element + integrator. 88 | 89 | ### Kp (X, Y, Z) 90 | This is a proportional constant in decibel. 91 | Larger value will result faster response. 92 | Since the gain of the PTZ camera depends on the manufactures and models, 93 | you need to adjust Kp for your camera. 94 | 95 | The properties will be saved to and recalled from presets. 96 | 97 | ### Ki (X, Y, Z) 98 | This is an integral constant. The dimension is inverse time and the unit is s-1. 99 | Larger value results in more tracking of slow movement. 100 | 101 | The properties will be saved to and recalled from presets. (version 0.7.3 and later) 102 | 103 | ### Td (X, Y, Z) 104 | This is a derivative constant. The dimension is time and the unit is s. 105 | 0 will result in no derivative term. 106 | Larger value will make tracking faster when the subject starts to move. 107 | 108 | The properties will be saved to and recalled from presets. 109 | 110 | ### LPF for Td (X, Y, Z) 111 | This is an inverse of the cut-off frequency for the low-pass filter (LPF), which affects the derivative term of the PID control element. The dimension is time and the unit is s. 112 | The LPF will reduce noise of face detection and small movement of the subject. 113 | This property is shared for X and Y axises. 114 | 115 | The properties will be saved to and recalled from presets. 116 | 117 | ### Dead band nonlinear band (X, Y, Z) 118 | These parameters make dead bands and nonlinear bands for the error signal that goes to PID control element. 119 | The unit is a percentage of the average of source width and height. 120 | If the error signal is within the dead band, error signal is forced to zero to avoid small move to be tracked. 121 | The nonlinear band makes smooth connection from the dead band to the linear range. 122 | 123 | The properties will be saved to and recalled from presets. 124 | 125 | ### Attenuation time for lost face 126 | After the face is lost, integral term will be attenuated by this time. The dimension is time and the unit is s. 127 | 128 | The property will be saved to and recalled from presets. 129 | 130 | ## Face lost behavior 131 | 132 | Make an action if the face has been lost. 133 | 134 | ### Timeout until recalling memory 135 | This is a timeout in seconds until the action will be triggered. 136 | 137 | ### Recall memory 138 | Recall a memory (or preset) that is configured in your PTZ camera. 139 | Set `-1` to disable. The default is `-1`. 140 | 141 | Please be careful to use this option as the PTZ camera might change not only pan, tilt, and zoom but also other settings such as focus, and white balance shutter speed. 142 | 143 | ### Timeout until zoom-out 144 | Zoom-out if the face is lost and specified time has passed. 145 | Set `0` to disable. 146 | 147 | ## Output 148 | This property group configure how to connect to the PTZ camera. 149 | 150 | ### PTZ Type 151 | Specify the protocol to connect to the camera. 152 | | Type | Description | 153 | | ---- | ----------- | 154 | | `None` | Do not connect to camera. Control message will be logged. | 155 | | `through PTZ Controls` | Send through the PTZ Controls plugin. | 156 | | `VISCA over TCP` | Send using TCP connection to the camera. | 157 | 158 | The option `through PTZ Controls` requires the other plugin [PTZ Controls](https://github.com/glikely/obs-ptz). 159 | The feature could be broken by future release of either plugin. 160 | 161 | ### IP address, port 162 | The address and port of the camera you are connect to. 163 | You can specify IP address or host name if your system can resolve it. 164 | 165 | ### Max control (pan, tilt, zoom) 166 | These sliders can limit the maximum control amount to the camera. 167 | If you want to disable changing zoom, set it to `0`. 168 | 169 | ### Invert control (pan, tilt, zoom) 170 | These checkboxes invert the direction of the control. 171 | It might be useful if you camera is mounted on ceil. 172 | 173 | ### Invert control (zoom) 174 | Just in c ase the zoom control behave opposite directon, check this. 175 | You should not check this in most cases. 176 | This is a deplicated option. 177 | 178 | ## Debug 179 | These properties enables how the face detection and tracking works. 180 | Note that these features are automatically turned off when the source is displayed on the program of OBS Studio. 181 | You can keep enable the checkboxes and keep monitoring the detection accuracy before the scene goes to the program. 182 | 183 | ### Show face detection results 184 | **Deprecated** 185 | If enabled, face detection and tracking results are shown. 186 | The face detection results are displayed in blue boxes. 187 | The Tracking results are displayed in green boxes. 188 | 189 | ### Always show information 190 | **Deprecated** 191 | If enabled, debugging properties listed above are effective even if the source is displayed on the program. 192 | This will be useful to make a demonstration of face-tracker itself. 193 | 194 | ### Save correlation tracker, calculated error, control data to file 195 | **Not available for released version** 196 | Save internal calculation into the specified file for each. 197 | This option is not available without building with `ENABLE_DEBUG_DATA` 198 | but still can be set through obs-websocket or manually editing the scene file to add a text property with a file name to be written. 199 | To disable it back, remove the property or set zero-length text. 200 | 201 | #### Correlation tracker 202 | Property name: `debug_data_tracker` 203 | 204 | The data contains time in second, 3 coordinates (X, Y, Z), and score of the correlation tracker. 205 | The X and Y coordinates are the center of the face. 206 | The Z coordinate is a square-root of the area. 207 | Sometimes multiple correlation trackers run at the same time. In that case, multiple lines are written at the same timing. 208 | 209 | #### Calculated error 210 | Property name: `debug_data_error` 211 | 212 | The data contains time in second, 3 coordinates (X, Y, Z). 213 | The calculated error is the adjusted measure with current resolution, the cropped region when the frame was rendered, and user-specified tracking target. 214 | 0-value indicates the face is well aligned and positive or negative value indicates the cropped region need to be moved. 215 | 216 | #### Control 217 | Property name: `debug_data_control` 218 | 219 | The data contains time in second, 3 coordinates (X, Y, Z), and another set of 3 coordinates. 220 | The first set of the coordinates is a linear floating-point value of the control signal. 221 | The second set of the coordinates is an integer value that should go to the PTZ device. 222 | -------------------------------------------------------------------------------- /cmake/ObsPluginHelpers.cmake: -------------------------------------------------------------------------------- 1 | if(POLICY CMP0087) 2 | cmake_policy(SET CMP0087 NEW) 3 | endif() 4 | 5 | set(OBS_STANDALONE_PLUGIN_DIR ${CMAKE_SOURCE_DIR}/release) 6 | set(INCLUDED_LIBOBS_CMAKE_MODULES ON) 7 | 8 | include(GNUInstallDirs) 9 | if(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") 10 | set(OS_MACOS ON) 11 | set(OS_POSIX ON) 12 | elseif(${CMAKE_SYSTEM_NAME} MATCHES "Linux|FreeBSD|OpenBSD") 13 | set(OS_POSIX ON) 14 | string(TOUPPER "${CMAKE_SYSTEM_NAME}" _SYSTEM_NAME_U) 15 | set(OS_${_SYSTEM_NAME_U} ON) 16 | elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") 17 | set(OS_WINDOWS ON) 18 | set(OS_POSIX OFF) 19 | endif() 20 | 21 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT AND (OS_WINDOWS OR OS_MACOS)) 22 | set(CMAKE_INSTALL_PREFIX 23 | ${OBS_STANDALONE_PLUGIN_DIR} 24 | CACHE STRING "Directory to install OBS plugin after building" FORCE) 25 | endif() 26 | 27 | if(NOT CMAKE_BUILD_TYPE) 28 | set(CMAKE_BUILD_TYPE 29 | "RelWithDebInfo" 30 | CACHE STRING 31 | "OBS build type [Release, RelWithDebInfo, Debug, MinSizeRel]" FORCE) 32 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Release RelWithDebInfo 33 | Debug MinSizeRel) 34 | endif() 35 | 36 | if(NOT QT_VERSION) 37 | set(QT_VERSION 38 | "5" 39 | CACHE STRING "OBS Qt version [5, 6]" FORCE) 40 | set_property(CACHE QT_VERSION PROPERTY STRINGS 5 6) 41 | endif() 42 | 43 | macro(find_qt) 44 | set(oneValueArgs VERSION) 45 | set(multiValueArgs COMPONENTS COMPONENTS_WIN COMPONENTS_MAC COMPONENTS_LINUX) 46 | cmake_parse_arguments(FIND_QT "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) 47 | 48 | if(OS_WINDOWS) 49 | find_package( 50 | Qt${FIND_QT_VERSION} 51 | COMPONENTS ${FIND_QT_COMPONENTS} ${FIND_QT_COMPONENTS_WIN} 52 | REQUIRED) 53 | elseif(OS_MACOS) 54 | find_package( 55 | Qt${FIND_QT_VERSION} 56 | COMPONENTS ${FIND_QT_COMPONENTS} ${FIND_QT_COMPONENTS_MAC} 57 | REQUIRED) 58 | else() 59 | find_package( 60 | Qt${FIND_QT_VERSION} 61 | COMPONENTS ${FIND_QT_COMPONENTS} ${FIND_QT_COMPONENTS_LINUX} 62 | REQUIRED) 63 | endif() 64 | 65 | if("Gui" IN_LIST FIND_QT_COMPONENTS) 66 | list(APPEND FIND_QT_COMPONENTS "GuiPrivate") 67 | endif() 68 | 69 | foreach(_COMPONENT IN LISTS FIND_QT_COMPONENTS FIND_QT_COMPONENTS_WIN 70 | FIND_QT_COMPONENTS_MAC FIND_QT_COMPONENTS_LINUX) 71 | if(NOT TARGET Qt::${_COMPONENT} AND TARGET 72 | Qt${FIND_QT_VERSION}::${_COMPONENT}) 73 | 74 | add_library(Qt::${_COMPONENT} INTERFACE IMPORTED) 75 | set_target_properties( 76 | Qt::${_COMPONENT} PROPERTIES INTERFACE_LINK_LIBRARIES 77 | "Qt${FIND_QT_VERSION}::${_COMPONENT}") 78 | endif() 79 | endforeach() 80 | endmacro() 81 | 82 | file(RELATIVE_PATH RELATIVE_INSTALL_PATH ${CMAKE_SOURCE_DIR} ${CMAKE_INSTALL_PREFIX}) 83 | file(RELATIVE_PATH RELATIVE_BUILD_PATH ${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR}) 84 | 85 | if(OS_POSIX) 86 | if(NOT CCACHE_SET) 87 | find_program(CCACHE_PROGRAM "ccache") 88 | set(CCACHE_SUPPORT 89 | ON 90 | CACHE BOOL "Enable ccache support") 91 | mark_as_advanced(CCACHE_PROGRAM) 92 | if(CCACHE_PROGRAM AND CCACHE_SUPPORT) 93 | set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_PROGRAM} CACHE INTERNAL "") 94 | set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE_PROGRAM} CACHE INTERNAL "") 95 | set(CMAKE_OBJC_COMPILER_LAUNCHER ${CCACHE_PROGRAM} CACHE INTERNAL "") 96 | set(CMAKE_OBJCXX_COMPILER_LAUNCHER ${CCACHE_PROGRAM} CACHE INTERNAL "") 97 | set(CMAKE_CUDA_COMPILER_LAUNCHER ${CCACHE_PROGRAM} CACHE INTERNAL "") # CMake 3.9+ 98 | set(CCACHE_SET ON CACHE INTERNAL "") 99 | endif() 100 | endif() 101 | endif() 102 | 103 | if(OS_MACOS) 104 | set(CMAKE_OSX_ARCHITECTURES "x86_64" CACHE STRING "OBS build architecture for macOS - x86_64 required at least") 105 | set_property(CACHE CMAKE_OSX_ARCHITECTURES PROPERTY STRINGS x86_64 arm64 "x86_64;arm64") 106 | 107 | set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "OBS deployment target for macOS - 10.13+ required") 108 | set_property(CACHE CMAKE_OSX_DEPLOYMENT_TARGET PROPERTY STRINGS 10.15 11 12) 109 | 110 | set(OBS_BUNDLE_CODESIGN_IDENTITY "-" CACHE STRING "OBS code signing identity for macOS") 111 | set(OBS_CODESIGN_LINKER ON 112 | CACHE BOOL "Enable linker code-signing on macOS (macOS 11+ required)") 113 | 114 | if(OBS_CODESIGN_LINKER) 115 | set(CMAKE_XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS "-o linker-signed") 116 | endif() 117 | 118 | if(XCODE) 119 | # Tell Xcode to pretend the linker signed binaries so that editing with 120 | # install_name_tool preserves ad-hoc signatures. This option is supported by 121 | # codesign on macOS 11 or higher. See CMake Issue 21854: 122 | # https://gitlab.kitware.com/cmake/cmake/-/issues/21854 123 | 124 | set(CMAKE_XCODE_GENERATE_SCHEME ON) 125 | endif() 126 | 127 | # Set default options for bundling on macOS 128 | set(CMAKE_MACOSX_RPATH ON) 129 | set(CMAKE_SKIP_BUILD_RPATH OFF) 130 | set(CMAKE_BUILD_WITH_INSTALL_RPATH OFF) 131 | set(CMAKE_INSTALL_RPATH "@executable_path/../Frameworks/") 132 | set(CMAKE_INSTALL_RPATH_USE_LINK_PATH OFF) 133 | 134 | function(setup_plugin_target target) 135 | if(NOT DEFINED MACOSX_PLUGIN_GUI_IDENTIFIER) 136 | message( 137 | FATAL_ERROR 138 | "No 'MACOSX_PLUGIN_GUI_IDENTIFIER' set, but is required to build plugin bundles on macOS - example: 'com.yourname.pluginname'" 139 | ) 140 | endif() 141 | 142 | if(NOT DEFINED MACOSX_PLUGIN_BUNDLE_VERSION) 143 | message( 144 | FATAL_ERROR 145 | "No 'MACOSX_PLUGIN_BUNDLE_VERSION' set, but is required to build plugin bundles on macOS - example: '25'" 146 | ) 147 | endif() 148 | 149 | if(NOT DEFINED MACOSX_PLUGIN_SHORT_VERSION_STRING) 150 | message( 151 | FATAL_ERROR 152 | "No 'MACOSX_PLUGIN_SHORT_VERSION_STRING' set, but is required to build plugin bundles on macOS - example: '1.0.2'" 153 | ) 154 | endif() 155 | 156 | set(MACOSX_PLUGIN_BUNDLE_NAME "${target}" PARENT_SCOPE) 157 | set(MACOSX_PLUGIN_BUNDLE_VERSION "${MACOSX_BUNDLE_BUNDLE_VERSION}" PARENT_SCOPE) 158 | set(MACOSX_PLUGIN_SHORT_VERSION_STRING "${MACOSX_BUNDLE_SHORT_VERSION_STRING}" PARENT_SCOPE) 159 | set(MACOSX_PLUGIN_EXECUTABLE_NAME "${target}" PARENT_SCOPE) 160 | 161 | if("${MACOSX_PLUGIN_BUNDLE_TYPE}" STREQUAL "BNDL") 162 | message(STATUS "Bundle type plugin") 163 | 164 | install( 165 | TARGETS ${target} 166 | LIBRARY DESTINATION "." 167 | COMPONENT obs_plugins 168 | NAMELINK_COMPONENT ${target}_Development) 169 | 170 | set_target_properties( 171 | ${target} 172 | PROPERTIES 173 | BUNDLE ON 174 | BUNDLE_EXTENSION "plugin" 175 | OUTPUT_NAME ${target} 176 | MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/bundle/macOS/Plugin-Info.plist.in" 177 | XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${MACOSX_PLUGIN_GUI_IDENTIFIER}" 178 | XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "${OBS_BUNDLE_CODESIGN_IDENTITY}" 179 | XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/bundle/macOS/entitlements.plist") 180 | 181 | install_bundle_resources(${target}) 182 | 183 | set(FIRST_DIR_SUFFIX ".plugin" PARENT_SCOPE) 184 | else() 185 | message(STATUS "Old type plugin") 186 | 187 | install( 188 | TARGETS ${target} 189 | LIBRARY DESTINATION "${target}/bin/" 190 | COMPONENT obs_plugins 191 | NAMELINK_COMPONENT ${target}_Development) 192 | 193 | if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/data) 194 | install( 195 | DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/ 196 | DESTINATION "${target}/data/" 197 | USE_SOURCE_PERMISSIONS 198 | COMPONENT obs_plugins) 199 | endif() 200 | set(FIRST_DIR_SUFFIX "" PARENT_SCOPE) 201 | endif() 202 | 203 | endfunction() 204 | 205 | function(install_bundle_resources target) 206 | if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/data) 207 | file(GLOB_RECURSE _DATA_FILES "${CMAKE_CURRENT_SOURCE_DIR}/data/*") 208 | foreach(_DATA_FILE IN LISTS _DATA_FILES) 209 | file(RELATIVE_PATH _RELATIVE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/data/ 210 | ${_DATA_FILE}) 211 | get_filename_component(_RELATIVE_PATH ${_RELATIVE_PATH} PATH) 212 | target_sources(${target} PRIVATE ${_DATA_FILE}) 213 | set_source_files_properties( 214 | ${_DATA_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION 215 | Resources/${_RELATIVE_PATH}) 216 | string(REPLACE "\\" "\\\\" _GROUP_NAME "${_RELATIVE_PATH}") 217 | source_group("Resources\\${_GROUP_NAME}" FILES ${_DATA_FILE}) 218 | endforeach() 219 | endif() 220 | endfunction() 221 | 222 | else() 223 | if(CMAKE_SIZEOF_VOID_P EQUAL 8) 224 | set(_ARCH_SUFFIX 64) 225 | else() 226 | set(_ARCH_SUFFIX 32) 227 | endif() 228 | set(OBS_OUTPUT_DIR ${CMAKE_BINARY_DIR}/rundir) 229 | 230 | if(OS_POSIX) 231 | option(LINUX_PORTABLE "Build portable version (Linux)" ON) 232 | option(LINUX_RPATH "Set runpath (Linux)" ON) 233 | if(NOT LINUX_PORTABLE) 234 | set(OBS_LIBRARY_DESTINATION ${CMAKE_INSTALL_LIBDIR}) 235 | set(OBS_PLUGIN_DESTINATION ${OBS_LIBRARY_DESTINATION}/obs-plugins) 236 | if (LINUX_RPATH) 237 | set(CMAKE_INSTALL_RPATH ${CMAKE_INSTALL_PREFIX}/lib) 238 | endif() 239 | set(OBS_DATA_DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/obs) 240 | else() 241 | set(OBS_LIBRARY_DESTINATION bin/${_ARCH_SUFFIX}bit) 242 | set(OBS_PLUGIN_DESTINATION obs-plugins/${_ARCH_SUFFIX}bit) 243 | if (LINUX_RPATH) 244 | set(CMAKE_INSTALL_RPATH "$ORIGIN/" "${CMAKE_INSTALL_PREFIX}/${OBS_LIBRARY_DESTINATION}") 245 | endif() 246 | set(OBS_DATA_DESTINATION "data") 247 | endif() 248 | 249 | if(OS_LINUX) 250 | set(CPACK_PACKAGE_NAME "${CMAKE_PROJECT_NAME}") 251 | set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${LINUX_MAINTAINER_EMAIL}") 252 | set(CPACK_PACKAGE_VERSION "${CMAKE_PROJECT_VERSION}") 253 | set(PKG_SUFFIX "-linux-x86_64" CACHE STRING "Suffix of package name") 254 | set(CPACK_PACKAGE_FILE_NAME 255 | "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}${PKG_SUFFIX}") 256 | 257 | set(CPACK_GENERATOR "DEB") 258 | 259 | if(NOT LINUX_PORTABLE) 260 | set(CPACK_SET_DESTDIR ON) 261 | endif() 262 | include(CPack) 263 | endif() 264 | else() 265 | set(OBS_LIBRARY_DESTINATION "bin/${_ARCH_SUFFIX}bit") 266 | set(OBS_LIBRARY32_DESTINATION "bin/32bit") 267 | set(OBS_LIBRARY64_DESTINATION "bin/64bit") 268 | set(OBS_PLUGIN_DESTINATION "obs-plugins/${_ARCH_SUFFIX}bit") 269 | set(OBS_PLUGIN32_DESTINATION "obs-plugins/32bit") 270 | set(OBS_PLUGIN64_DESTINATION "obs-plugins/64bit") 271 | 272 | set(OBS_DATA_DESTINATION "data") 273 | endif() 274 | 275 | function(setup_plugin_target target) 276 | set_target_properties(${target} PROPERTIES PREFIX "") 277 | 278 | install( 279 | TARGETS ${target} 280 | RUNTIME DESTINATION "${OBS_PLUGIN_DESTINATION}" 281 | COMPONENT ${target}_Runtime 282 | LIBRARY DESTINATION "${OBS_PLUGIN_DESTINATION}" 283 | COMPONENT ${target}_Runtime 284 | NAMELINK_COMPONENT ${target}_Development) 285 | 286 | install( 287 | FILES $ 288 | DESTINATION $/${OBS_PLUGIN_DESTINATION} 289 | COMPONENT obs_rundir 290 | EXCLUDE_FROM_ALL) 291 | 292 | if(OS_WINDOWS) 293 | install( 294 | FILES $ 295 | CONFIGURATIONS "RelWithDebInfo" "Debug" 296 | DESTINATION ${OBS_PLUGIN_DESTINATION} 297 | COMPONENT ${target}_Runtime 298 | OPTIONAL) 299 | 300 | install( 301 | FILES $ 302 | CONFIGURATIONS "RelWithDebInfo" "Debug" 303 | DESTINATION $/${OBS_PLUGIN_DESTINATION} 304 | COMPONENT obs_rundir 305 | OPTIONAL EXCLUDE_FROM_ALL) 306 | endif() 307 | 308 | if(MSVC) 309 | target_link_options( 310 | ${target} 311 | PRIVATE 312 | "LINKER:/OPT:REF" 313 | "$<$>:LINKER\:/SAFESEH\:NO>" 314 | "$<$:LINKER\:/INCREMENTAL:NO>" 315 | "$<$:LINKER\:/INCREMENTAL:NO>") 316 | endif() 317 | 318 | setup_target_resources(${target} obs-plugins/${target}) 319 | 320 | if(OS_WINDOWS) 321 | add_custom_command( 322 | TARGET ${target} 323 | POST_BUILD 324 | COMMAND 325 | "${CMAKE_COMMAND}" -DCMAKE_INSTALL_PREFIX=${OBS_OUTPUT_DIR} 326 | -DCMAKE_INSTALL_COMPONENT=obs_rundir 327 | -DCMAKE_INSTALL_CONFIG_NAME=$ -P 328 | ${CMAKE_CURRENT_BINARY_DIR}/cmake_install.cmake 329 | COMMENT "Installing to plugin rundir" 330 | VERBATIM) 331 | endif() 332 | endfunction() 333 | 334 | function(setup_target_resources target destination) 335 | if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/data) 336 | install( 337 | DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/ 338 | DESTINATION ${OBS_DATA_DESTINATION}/${destination} 339 | USE_SOURCE_PERMISSIONS 340 | COMPONENT obs_plugins) 341 | 342 | install( 343 | DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data 344 | DESTINATION $/${OBS_DATA_DESTINATION}/${destination} 345 | USE_SOURCE_PERMISSIONS 346 | COMPONENT obs_rundir 347 | EXCLUDE_FROM_ALL) 348 | endif() 349 | endfunction() 350 | endif() 351 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Plugin Build 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | branches: 8 | - main 9 | tags: 10 | - '*' 11 | pull_request: 12 | paths-ignore: 13 | - '**.md' 14 | branches: 15 | - main 16 | 17 | env: 18 | artifactName: ${{ contains(github.ref_name, '/') && 'artifact' || github.ref_name }} 19 | 20 | jobs: 21 | linux_build: 22 | runs-on: ${{ matrix.ubuntu }} 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | obs: [30] 27 | ubuntu: ['ubuntu-22.04'] 28 | defaults: 29 | run: 30 | shell: bash 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | with: 35 | submodules: recursive 36 | 37 | - name: Restore ccache 38 | id: ccache-cache 39 | uses: actions/cache@v4 40 | with: 41 | path: ${{ github.workspace }}/.ccache 42 | key: linux-obs${{ matrix.obs }}-${{ matrix.ubuntu }}-ccache 43 | 44 | - name: Setup ccache 45 | run: | 46 | sudo apt install ccache 47 | ccache --set-config=cache_dir="${{ github.workspace }}/.ccache" 48 | ccache --set-config=compression=true 49 | 50 | - name: Download obs-studio development environment 51 | id: obsdeps 52 | uses: norihiro/obs-studio-devel-action@v2 53 | with: 54 | obs: ${{ matrix.obs }} 55 | verbose: true 56 | 57 | - name: Download dlib-models 58 | run: | 59 | set -ex 60 | DESTDIR='./' ci/download-dlib-models.sh 61 | 62 | - name: Build plugin 63 | run: | 64 | set -ex 65 | sudo apt install -y \ 66 | libopenblas-dev libopenblas0 \ 67 | || true 68 | export OPENBLAS_HOME=/lib/x86_64-linux-gnu/ 69 | cmake -S . -B build \ 70 | -D CMAKE_BUILD_TYPE=RelWithDebInfo \ 71 | -D CPACK_DEBIAN_PACKAGE_SHLIBDEPS=ON \ 72 | -D PKG_SUFFIX=-obs${{ matrix.obs }}-${{ matrix.ubuntu }}-x86_64 \ 73 | ${{ steps.obsdeps.outputs.PLUGIN_CMAKE_OPTIONS }} 74 | cd build 75 | make -j4 76 | make package 77 | echo "FILE_NAME=$(find $PWD -name '*.deb' | head -n 1)" >> $GITHUB_ENV 78 | - name: Upload build artifact 79 | uses: actions/upload-artifact@v4 80 | with: 81 | name: ${{ env.artifactName }}-linux-obs${{ matrix.obs }}-${{ matrix.ubuntu }} 82 | path: '${{ env.FILE_NAME }}' 83 | - name: Check package 84 | run: | 85 | . build/ci/ci_includes.generated.sh 86 | set -ex 87 | sudo apt install -y '${{ env.FILE_NAME }}' 88 | ldd /usr/lib/x86_64-linux-gnu/obs-plugins/${PLUGIN_NAME}.so > ldd.out 89 | if grep not.found ldd.out ; then 90 | echo "Error: unresolved shared object." >&2 91 | exit 1 92 | fi 93 | ls /usr/share/obs/obs-plugins/${PLUGIN_NAME}/ 94 | 95 | macos_build: 96 | runs-on: ${{ matrix.macos }} 97 | strategy: 98 | fail-fast: false 99 | matrix: 100 | include: 101 | - obs: 30 102 | arch: x86_64 103 | macos: macos-13 # Intel x86_64 104 | - obs: 30 105 | arch: arm64 106 | macos: macos-14 # Apple arm64 107 | defaults: 108 | run: 109 | shell: bash 110 | steps: 111 | - name: Checkout 112 | uses: actions/checkout@v4 113 | with: 114 | submodules: recursive 115 | 116 | - name: Setup Environment 117 | id: setup 118 | run: | 119 | set -e 120 | echo '::group::Set up code signing' 121 | if [[ '${{ secrets.MACOS_SIGNING_APPLICATION_IDENTITY }}' != '' && \ 122 | '${{ secrets.MACOS_SIGNING_INSTALLER_IDENTITY }}' != '' && \ 123 | '${{ secrets.MACOS_SIGNING_CERT }}' != '' ]]; then 124 | echo "haveCodesignIdent=true" >> $GITHUB_OUTPUT 125 | else 126 | echo "haveCodesignIdent=false" >> $GITHUB_OUTPUT 127 | fi 128 | if [[ '${{ secrets.MACOS_NOTARIZATION_USERNAME }}' != '' && \ 129 | '${{ secrets.MACOS_NOTARIZATION_PASSWORD }}' != '' ]]; then 130 | echo "haveNotarizationUser=true" >> $GITHUB_OUTPUT 131 | else 132 | echo "haveNotarizationUser=false" >> $GITHUB_OUTPUT 133 | fi 134 | echo '::endgroup::' 135 | echo '::group::Set up ccache' 136 | brew install ccache 137 | ccache --set-config=cache_dir="${{ github.workspace }}/.ccache" 138 | ccache --set-config=compression=true 139 | echo '::endgroup::' 140 | 141 | - name: Restore ccache 142 | id: ccache-cache 143 | uses: actions/cache@v4 144 | with: 145 | path: ${{ github.workspace }}/.ccache 146 | key: macos-obs${{ matrix.obs }}-${{ matrix.arch }}-ccache 147 | 148 | - name: Install Apple Developer Certificate 149 | if: ${{ github.event_name != 'pull_request' && steps.setup.outputs.haveCodesignIdent == 'true' }} 150 | uses: apple-actions/import-codesign-certs@v2 151 | with: 152 | keychain-password: ${{ github.run_id }} 153 | p12-file-base64: ${{ secrets.MACOS_SIGNING_CERT }} 154 | p12-password: ${{ secrets.MACOS_SIGNING_CERT_PASSWORD }} 155 | 156 | - name: Set Signing Identity 157 | if: ${{ startsWith(github.ref, 'refs/tags/') && github.event_name != 'pull_request' && steps.setup.outputs.haveCodesignIdent == 'true' && steps.setup.outputs.haveNotarizationUser == 'true' }} 158 | run: | 159 | set -e 160 | TEAM_ID=$(echo "${{ secrets.MACOS_SIGNING_APPLICATION_IDENTITY }}" | sed 's/.*(\([A-Za-z0-9]*\))$/\1/') 161 | xcrun notarytool store-credentials AC_PASSWORD \ 162 | --apple-id "${{ secrets.MACOS_NOTARIZATION_USERNAME }}" \ 163 | --team-id "$TEAM_ID" \ 164 | --password "${{ secrets.MACOS_NOTARIZATION_PASSWORD }}" 165 | 166 | - name: Download obs-studio development environment 167 | id: obsdeps 168 | uses: norihiro/obs-studio-devel-action@v2 169 | with: 170 | path: /tmp/deps-${{ matrix.obs }}-${{ matrix.arch }} 171 | arch: ${{ matrix.arch }} 172 | obs: ${{ matrix.obs }} 173 | verbose: true 174 | 175 | - name: Prepare dlib dependency 176 | run: | 177 | brew install openblas 178 | for d in '/usr/local/opt/openblas/' '/opt/homebrew/opt/openblas/'; do 179 | if test -d "$d"; then 180 | OPENBLAS_HOME="$d" 181 | break 182 | fi 183 | done 184 | echo "OPENBLAS_HOME=$OPENBLAS_HOME" >> $GITHUB_ENV 185 | cp $OPENBLAS_HOME/LICENSE data/LICENSE-openblas 186 | 187 | - name: Download dlib-models 188 | run: | 189 | set -ex 190 | mkdir data/dlib_hog_model 191 | curl -LO https://github.com/norihiro/obs-face-tracker/releases/download/0.7.0-hogdata/frontal_face_detector.dat.bz2 192 | bunzip2 < frontal_face_detector.dat.bz2 > data/dlib_hog_model/frontal_face_detector.dat 193 | git clone --depth 1 https://github.com/davisking/dlib-models 194 | mkdir data/{dlib_cnn_model,dlib_face_landmark_model} 195 | bunzip2 < dlib-models/mmod_human_face_detector.dat.bz2 > data/dlib_cnn_model/mmod_human_face_detector.dat 196 | bunzip2 < dlib-models/shape_predictor_5_face_landmarks.dat.bz2 > data/dlib_face_landmark_model/shape_predictor_5_face_landmarks.dat 197 | cp dlib/LICENSE.txt data/LICENSE-dlib 198 | cp dlib-models/LICENSE data/LICENSE-dlib-models 199 | 200 | - name: Build plugin 201 | run: | 202 | arch=${{ matrix.arch }} 203 | deps=/tmp/deps-${{ matrix.obs }}-${{ matrix.arch }} 204 | MACOSX_DEPLOYMENT_TARGET=${{ steps.obsdeps.outputs.MACOSX_DEPLOYMENT_TARGET }} 205 | GIT_TAG=$(git describe --tags --always) 206 | PKG_SUFFIX=-${GIT_TAG}-obs${{ matrix.obs }}-macos-${{ matrix.arch }} 207 | export OPENBLAS_HOME=${{ env.OPENBLAS_HOME }} 208 | set -e 209 | cmake -S . -B build -G Ninja \ 210 | -DCMAKE_BUILD_TYPE=RelWithDebInfo \ 211 | -DCMAKE_PREFIX_PATH="$PWD/release/" \ 212 | -DCMAKE_OSX_ARCHITECTURES=$arch \ 213 | -D OBS_BUNDLE_CODESIGN_IDENTITY='${{ secrets.MACOS_SIGNING_APPLICATION_IDENTITY }}' \ 214 | -D PKG_SUFFIX=$PKG_SUFFIX \ 215 | ${{ steps.obsdeps.outputs.PLUGIN_CMAKE_OPTIONS }} 216 | cmake --build build --config RelWithDebInfo 217 | 218 | - name: Prepare package 219 | run: | 220 | set -ex 221 | . build/ci/ci_includes.generated.sh 222 | cmake --install build --config RelWithDebInfo --prefix=release 223 | ci/macos/change-rpath.py \ 224 | --exclude-regex='(/usr/lib/|/System/Library/)' \ 225 | --exclude-regex='.*(obs-frontend-api|libobs|/Qt)' \ 226 | --libdir release/${PLUGIN_NAME}.plugin/Contents/lib \ 227 | --verbose \ 228 | release/${PLUGIN_NAME}.plugin/Contents/MacOS/obs-face-tracker 229 | ci/macos/change-rpath.py \ 230 | --exclude-regex='(/usr/lib/|/System/Library/)' \ 231 | --exclude-regex='.*(obs-frontend-api|libobs|/Qt)' \ 232 | --check-invalid-regex='(/usr/local|/opt)' \ 233 | --check \ 234 | release/${PLUGIN_NAME}.plugin/Contents/MacOS/obs-face-tracker \ 235 | release/${PLUGIN_NAME}.plugin/Contents/lib/* 236 | cp LICENSE release/${PLUGIN_NAME}.plugin/Contents/Resources/LICENSE-$PLUGIN_NAME 237 | 238 | - name: Codesign 239 | if: ${{ github.event_name != 'pull_request' && steps.setup.outputs.haveCodesignIdent == 'true' }} 240 | run: | 241 | . build/ci/ci_includes.generated.sh 242 | set -ex 243 | files=( 244 | $(find release/${PLUGIN_NAME}.plugin/ -name '*.dylib') 245 | release/${PLUGIN_NAME}.plugin/Contents/MacOS/${PLUGIN_NAME} 246 | ) 247 | for dylib in "${files[@]}"; do 248 | codesign --force --sign "${{ secrets.MACOS_SIGNING_APPLICATION_IDENTITY }}" "$dylib" 249 | done 250 | for dylib in "${files[@]}"; do 251 | codesign -vvv --deep --strict "$dylib" 252 | done 253 | 254 | - name: Package 255 | run: | 256 | . build/ci/ci_includes.generated.sh 257 | set -ex 258 | zipfile=$PWD/package/${PLUGIN_NAME}${PKG_SUFFIX}.zip 259 | mkdir package 260 | (cd release/ && zip -r $zipfile ${PLUGIN_NAME}.plugin) 261 | ci/macos/install-packagesbuild.sh 262 | packagesbuild \ 263 | --build-folder $PWD/package/ \ 264 | build/installer-macOS.generated.pkgproj 265 | 266 | - name: Productsign 267 | if: ${{ github.event_name != 'pull_request' && steps.setup.outputs.haveCodesignIdent == 'true' }} 268 | run: | 269 | . build/ci/ci_includes.generated.sh 270 | pkgfile=package/${PLUGIN_NAME}${PKG_SUFFIX}.pkg 271 | set -e 272 | . build/ci/ci_includes.generated.sh 273 | productsign --sign "${{ secrets.MACOS_SIGNING_INSTALLER_IDENTITY }}" $pkgfile package/${PLUGIN_NAME}-signed.pkg 274 | mv package/${PLUGIN_NAME}-signed.pkg $pkgfile 275 | 276 | - name: Notarize 277 | if: ${{ startsWith(github.ref, 'refs/tags/') && github.event_name != 'pull_request' && steps.setup.outputs.haveCodesignIdent == 'true' }} 278 | uses: norihiro/macos-notarize-action@v1 279 | with: 280 | path: package/* 281 | keychainProfile: AC_PASSWORD 282 | verbose: true 283 | 284 | - name: Upload build artifact 285 | uses: actions/upload-artifact@v4 286 | with: 287 | name: ${{ env.artifactName }}-macos-obs${{ matrix.obs }}-${{ matrix.arch }} 288 | path: package/* 289 | 290 | windows_build: 291 | runs-on: windows-2022 292 | strategy: 293 | fail-fast: false 294 | matrix: 295 | obs: [30] 296 | arch: [x64] 297 | env: 298 | visualStudio: 'Visual Studio 17 2022' 299 | Configuration: 'RelWithDebInfo' 300 | defaults: 301 | run: 302 | shell: pwsh 303 | steps: 304 | - name: Checkout 305 | uses: actions/checkout@v4 306 | with: 307 | submodules: recursive 308 | - name: Download obs-studio 309 | id: obsdeps 310 | uses: norihiro/obs-studio-devel-action@v2 311 | with: 312 | obs: ${{ matrix.obs }} 313 | 314 | - name: Download dlib-models 315 | shell: bash 316 | run: | 317 | set -ex 318 | mkdir data/dlib_hog_model 319 | curl -LO https://github.com/norihiro/obs-face-tracker/releases/download/0.7.0-hogdata/frontal_face_detector.dat.bz2 320 | bunzip2 < frontal_face_detector.dat.bz2 > data/dlib_hog_model/frontal_face_detector.dat 321 | git clone --depth 1 https://github.com/davisking/dlib-models 322 | mkdir data/{dlib_cnn_model,dlib_face_landmark_model} 323 | 7z x dlib-models/mmod_human_face_detector.dat.bz2 -so > data/dlib_cnn_model/mmod_human_face_detector.dat 324 | 7z x dlib-models/shape_predictor_5_face_landmarks.dat.bz2 -so > data/dlib_face_landmark_model/shape_predictor_5_face_landmarks.dat 325 | cp dlib/LICENSE.txt data/LICENSE-dlib 326 | cp dlib-models/LICENSE data/LICENSE-dlib-models 327 | 328 | - name: Build plugin 329 | run: | 330 | $CmakeArgs = @( 331 | '-G', "${{ env.visualStudio }}" 332 | '-DCMAKE_SYSTEM_VERSION=10.0.18363.657' 333 | ) 334 | cmake -S . -B build @CmakeArgs ${{ steps.obsdeps.outputs.PLUGIN_CMAKE_OPTIONS_PS }} 335 | cmake --build build --config RelWithDebInfo -j 4 336 | cmake --install build --config RelWithDebInfo --prefix "$(Resolve-Path -Path .)/release" 337 | - name: Package plugin 338 | run: ci/windows/package-windows.cmd ${{ matrix.obs }} 339 | - name: Upload build artifact 340 | uses: actions/upload-artifact@v4 341 | with: 342 | name: ${{ env.artifactName }}-windows-obs${{ matrix.obs }}-${{ matrix.arch }} 343 | path: package/* 344 | -------------------------------------------------------------------------------- /src/face-tracker-manager.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "plugin-macros.generated.h" 3 | #include "face-tracker-manager.hpp" 4 | #include "face-detector-dlib-hog.h" 5 | #include "face-detector-dlib-cnn.h" 6 | #include "face-tracker-dlib.h" 7 | #include "texture-object.h" 8 | #include "helper.hpp" 9 | 10 | // #define debug_track(fmt, ...) blog(LOG_INFO, fmt, __VA_ARGS__) 11 | // #define debug_detect(fmt, ...) blog(LOG_INFO, fmt, __VA_ARGS__) 12 | #define debug_track(fmt, ...) 13 | #define debug_detect(fmt, ...) 14 | #define debug_track_thread(fmt, ...) // blog(LOG_INFO, fmt, __VA_ARGS__) 15 | 16 | #define DIR_DLIB_HOG "dlib_hog_model" 17 | #define DIR_DLIB_CNN "dlib_cnn_model" 18 | #define DIR_DLIB_LANDMARK "dlib_face_landmark_model" 19 | 20 | face_tracker_manager::face_tracker_manager() 21 | { 22 | upsize_l = upsize_r = upsize_t = upsize_b = 0.0f; 23 | scale = 0.0f; 24 | tracking_threshold = 1e-2f; 25 | landmark_detection_data = NULL; 26 | crop_cur.x0 = crop_cur.x1 = crop_cur.y0 = crop_cur.y1 = 0.0f; 27 | tick_cnt = detect_tick = next_tick_stage_to_detector = 0; 28 | detector_in_progress = false; 29 | detect = NULL; 30 | } 31 | 32 | face_tracker_manager::~face_tracker_manager() 33 | { 34 | for (auto &t : trackers_idlepool) { 35 | if (t.tracker) { 36 | t.tracker->stop(); 37 | delete t.tracker; 38 | t.tracker = NULL; 39 | } 40 | } 41 | for (auto &t : trackers) { 42 | if (t.tracker) { 43 | t.tracker->stop(); 44 | delete t.tracker; 45 | t.tracker = NULL; 46 | } 47 | } 48 | if (detect) { 49 | detect->stop(); 50 | delete detect; 51 | } 52 | bfree(landmark_detection_data); 53 | } 54 | 55 | inline void face_tracker_manager::retire_tracker(int ix) 56 | { 57 | debug_track_thread("%p retire_tracker(%d %p)", this, ix, trackers[ix].tracker); 58 | trackers_idlepool.push_back(trackers[ix]); 59 | trackers[ix].tracker->request_suspend(); 60 | trackers.erase(trackers.begin() + ix); 61 | } 62 | 63 | inline bool face_tracker_manager::is_low_confident(const tracker_inst_s &t, float th1) 64 | { 65 | if (t.att * t.rect.score <= th1) 66 | return true; 67 | 68 | if (t.att * t.rect.score <= tracking_threshold * t.score_first) 69 | return true; 70 | 71 | return false; 72 | } 73 | 74 | void face_tracker_manager::remove_duplicated_tracker() 75 | { 76 | for (size_t i = 0; i < trackers.size(); i++) { 77 | if (trackers[i].state != tracker_inst_s::tracker_state_available) 78 | continue; 79 | 80 | rect_s r = trackers[i].rect; 81 | int a0 = (r.x1 - r.x0) * (r.y1 - r.y0); 82 | int a_overlap_sum = 0; 83 | bool to_remove = false; 84 | for (size_t j = i + 1; j < trackers.size() && !to_remove; j++) { 85 | if (trackers[j].state != tracker_inst_s::tracker_state_available) 86 | continue; 87 | int a = common_area(r, trackers[j].rect); 88 | a_overlap_sum += a; 89 | if (a * 10 > a0 && a_overlap_sum * 2 > a0) 90 | to_remove = true; 91 | } 92 | 93 | if (to_remove) { 94 | retire_tracker(i); 95 | i--; 96 | } 97 | } 98 | } 99 | 100 | inline void face_tracker_manager::attenuate_tracker() 101 | { 102 | for (size_t i = 0; i < trackers.size(); i++) { 103 | if (trackers[i].state != tracker_inst_s::tracker_state_available) 104 | continue; 105 | struct tracker_inst_s &t = trackers[i]; 106 | 107 | int a1 = (t.rect.x1 - t.rect.x0) * (t.rect.y1 - t.rect.y0); 108 | float amax = (float)a1 * 0.1f; 109 | for (size_t j = 0; j < detect_rects.size(); j++) { 110 | rect_s r = detect_rects[j]; 111 | float a = (float)common_area(r, t.rect); 112 | if (a > amax) 113 | amax = a; 114 | } 115 | 116 | t.att *= powf(amax / a1, 0.1f); // if no faces, remove the tracker 117 | } 118 | 119 | float score_max = 1e-17f; 120 | for (size_t i = 0; i < trackers.size(); i++) { 121 | if (trackers[i].state == tracker_inst_s::tracker_state_available) { 122 | float s = trackers[i].att * trackers[i].rect.score; 123 | if (s > score_max) 124 | score_max = s; 125 | } 126 | } 127 | 128 | for (size_t i = 0; i < trackers.size(); i++) { 129 | if (trackers[i].state != tracker_inst_s::tracker_state_available) 130 | continue; 131 | if (!is_low_confident(trackers[i], 1e-2f * score_max)) 132 | continue; 133 | 134 | retire_tracker(i); 135 | i--; 136 | } 137 | } 138 | 139 | inline void face_tracker_manager::copy_detector_to_tracker() 140 | { 141 | size_t i_tracker; 142 | for (i_tracker = 0; i_tracker < trackers.size(); i_tracker++) 143 | if (trackers[i_tracker].tick_cnt == detect_tick && 144 | trackers[i_tracker].state == tracker_inst_s::tracker_state_e::tracker_state_reset_texture) 145 | break; 146 | if (i_tracker >= trackers.size()) 147 | return; 148 | 149 | if (detect_rects.size() <= 0) { 150 | retire_tracker(i_tracker); 151 | return; 152 | } 153 | 154 | struct tracker_inst_s &t = trackers[i_tracker]; 155 | 156 | struct rect_s r = detect_rects[0]; 157 | int w = r.x1 - r.x0; 158 | int h = r.y1 - r.y0; 159 | r.x0 -= w * upsize_l; 160 | r.x1 += w * upsize_r; 161 | r.y0 -= h * upsize_t; 162 | r.y1 += h * upsize_b; 163 | t.tracker->set_position(r); // TODO: consider how to track two or more faces. 164 | t.tracker->set_upsize_info(rectf_s{upsize_l, upsize_t, upsize_r, upsize_b}); 165 | t.tracker->start(); 166 | t.state = tracker_inst_s::tracker_state_constructing; 167 | } 168 | 169 | inline void face_tracker_manager::stage_to_detector() 170 | { 171 | if (!detect || detect->trylock()) 172 | return; 173 | 174 | // get previous results 175 | if (detector_in_progress) { 176 | detect->get_faces(detect_rects); 177 | for (size_t i = 0; i < detect_rects.size(); i++) 178 | debug_detect("stage_to_detector: detect_rects %d %d %d %d %d %f", i, detect_rects[i].x0, 179 | detect_rects[i].y0, detect_rects[i].x1, detect_rects[i].y1, detect_rects[i].score); 180 | attenuate_tracker(); 181 | copy_detector_to_tracker(); 182 | detector_in_progress = false; 183 | } 184 | 185 | if ((next_tick_stage_to_detector - tick_cnt) > 0) { 186 | detect->unlock(); 187 | return; 188 | } 189 | 190 | if (auto cvtex = get_cvtex()) { 191 | detect->set_texture(cvtex, detector_crop_l, detector_crop_r, detector_crop_t, detector_crop_b); 192 | if (detector_engine == engine_dlib_hog) { 193 | if (auto *d = dynamic_cast(detect)) 194 | d->set_model(detector_dlib_hog_model.c_str()); 195 | } else if (detector_engine == engine_dlib_cnn) { 196 | if (auto *d = dynamic_cast(detect)) 197 | d->set_model(detector_dlib_cnn_model.c_str()); 198 | } 199 | detect->signal(); 200 | detector_in_progress = true; 201 | detect_tick = tick_cnt; 202 | 203 | struct tracker_inst_s t; 204 | t.rect = rect_s{0, 0, 0, 0, 0.0f}; 205 | t.crop_rect = rectf_s{0.0f, 0.0f, 0.0f, 0.0f}; 206 | t.att = 0.0f; 207 | t.score_first = 0.0f; 208 | if (trackers_idlepool.size() > 0) { 209 | t.tracker = trackers_idlepool[0].tracker; 210 | trackers_idlepool[0].tracker = NULL; 211 | trackers_idlepool.pop_front(); 212 | } else { 213 | debug_track_thread( 214 | "%p No available idle tracker, creating new tracker thread. There are %d existing thread.", 215 | this, trackers.size()); 216 | t.tracker = new face_tracker_dlib(); 217 | for (size_t i = 0; i < trackers.size(); i++) { 218 | debug_track_thread("%p existing tracker[%d]: state=%d", this, i, 219 | (int)trackers[i].state); 220 | } 221 | } 222 | t.crop_tracker = crop_cur; 223 | t.state = tracker_inst_s::tracker_state_e::tracker_state_reset_texture; 224 | t.tick_cnt = tick_cnt; 225 | t.tracker->set_texture(cvtex); 226 | t.tracker->set_landmark_detection(landmark_detection_data); 227 | if (!landmark_detection_data) 228 | t.landmark.clear(); 229 | trackers.push_back(t); 230 | } 231 | 232 | detect->unlock(); 233 | } 234 | 235 | inline int face_tracker_manager::stage_surface_to_tracker(struct tracker_inst_s &t) 236 | { 237 | if (auto cvtex = get_cvtex()) { 238 | t.tracker->set_texture(cvtex); 239 | t.crop_tracker = crop_cur; 240 | t.tracker->signal(); 241 | } else 242 | return 1; 243 | return 0; 244 | } 245 | 246 | inline void face_tracker_manager::stage_to_trackers() 247 | { 248 | bool have_new_tracker = false; 249 | for (size_t i = 0; i < trackers.size(); i++) { 250 | struct tracker_inst_s &t = trackers[i]; 251 | if (t.state == tracker_inst_s::tracker_state_constructing) { 252 | if (!t.tracker->trylock()) { 253 | if (!stage_surface_to_tracker(t)) { 254 | t.crop_tracker = crop_cur; 255 | t.state = tracker_inst_s::tracker_state_first_track; 256 | } 257 | t.tracker->unlock(); 258 | t.state = tracker_inst_s::tracker_state_first_track; 259 | } 260 | } else if (t.state == tracker_inst_s::tracker_state_first_track) { 261 | if (!t.tracker->trylock()) { 262 | bool ret = t.tracker->get_face(t.rect); 263 | t.crop_rect = t.crop_tracker; 264 | debug_track("tracker_state_first_track %p %d %d %d %d %f", t.tracker, t.rect.x0, 265 | t.rect.y0, t.rect.x1, t.rect.y1, t.rect.score); 266 | t.att = 1.0f; 267 | t.score_first = t.rect.score; 268 | if (!ret || !landmark_detection_data || !t.tracker->get_landmark(t.landmark)) 269 | t.landmark.resize(0); 270 | stage_surface_to_tracker(t); 271 | t.tracker->signal(); 272 | t.tracker->unlock(); 273 | if (ret) { 274 | t.state = tracker_inst_s::tracker_state_available; 275 | have_new_tracker = true; 276 | } 277 | } 278 | } else if (t.state == tracker_inst_s::tracker_state_available) { 279 | if (!t.tracker->trylock()) { 280 | bool ret = t.tracker->get_face(t.rect); 281 | t.crop_rect = t.crop_tracker; 282 | debug_track("tracker_state_available %p %d %d %d %d %f landmark=%d", t.tracker, 283 | t.rect.x0, t.rect.y0, t.rect.x1, t.rect.y1, t.rect.score, 284 | t.landmark.size()); 285 | if (!ret || !landmark_detection_data || !t.tracker->get_landmark(t.landmark)) 286 | t.landmark.resize(0); 287 | stage_surface_to_tracker(t); 288 | t.tracker->signal(); 289 | t.tracker->unlock(); 290 | } 291 | } 292 | } 293 | 294 | if (have_new_tracker) 295 | remove_duplicated_tracker(); 296 | } 297 | 298 | static inline void make_tracker_rects(std::vector &tracker_rects, 299 | const std::deque &trackers) 300 | { 301 | size_t n = 0; 302 | for (size_t i = 0; i < trackers.size(); i++) { 303 | if (trackers[i].state != face_tracker_manager::tracker_inst_s::tracker_state_available) 304 | continue; 305 | 306 | float score = trackers[i].rect.score * trackers[i].att; 307 | 308 | if (score <= 0.0f || isnan(score)) 309 | continue; 310 | 311 | if (tracker_rects.size() <= n) 312 | tracker_rects.resize(n + 1); 313 | auto &r = tracker_rects[n++]; 314 | 315 | r.rect = trackers[i].rect; 316 | r.rect.score = score; 317 | r.crop_rect = trackers[i].crop_rect; 318 | r.landmark = trackers[i].landmark; 319 | } 320 | 321 | if (tracker_rects.size() > n) 322 | tracker_rects.resize(n); 323 | } 324 | 325 | void face_tracker_manager::tick(float second) 326 | { 327 | if (reset_requested) { 328 | for (int i = trackers.size() - 1; i >= 0; i--) 329 | trackers[i].att = 0.0f; 330 | detect_rects.clear(); 331 | reset_requested = false; 332 | } 333 | 334 | if (detect_tick == tick_cnt) 335 | next_tick_stage_to_detector = tick_cnt + (int)(2.0f / second); // detect for each _ second(s). 336 | 337 | tick_cnt += 1; 338 | 339 | make_tracker_rects(tracker_rects, trackers); 340 | } 341 | 342 | void face_tracker_manager::post_render() 343 | { 344 | stage_to_detector(); 345 | stage_to_trackers(); 346 | } 347 | 348 | static void update_detector(face_tracker_manager *ftm, enum face_tracker_manager::detector_engine_e detector_engine) 349 | { 350 | if (ftm->detect) { 351 | ftm->detect->stop(); 352 | delete ftm->detect; 353 | ftm->detect = NULL; 354 | } 355 | 356 | switch (detector_engine) { 357 | case face_tracker_manager::engine_dlib_hog: 358 | ftm->detect = new face_detector_dlib_hog(); 359 | break; 360 | case face_tracker_manager::engine_dlib_cnn: 361 | ftm->detect = new face_detector_dlib_cnn(); 362 | break; 363 | default: 364 | blog(LOG_ERROR, "unknown detector_engine %d", (int)detector_engine); 365 | } 366 | 367 | ftm->detector_engine = detector_engine; 368 | 369 | if (ftm->detect) 370 | ftm->detect->start(); 371 | } 372 | 373 | void face_tracker_manager::update(obs_data_t *settings) 374 | { 375 | upsize_l = obs_data_get_double(settings, "upsize_l"); 376 | upsize_r = obs_data_get_double(settings, "upsize_r"); 377 | upsize_t = obs_data_get_double(settings, "upsize_t"); 378 | upsize_b = obs_data_get_double(settings, "upsize_b"); 379 | scale = obs_data_get_double(settings, "scale"); 380 | auto _detector_engine = (enum detector_engine_e)obs_data_get_int(settings, "detector_engine"); 381 | if (_detector_engine != detector_engine) 382 | update_detector(this, _detector_engine); 383 | detector_dlib_hog_model = obs_data_get_string(settings, "detector_dlib_hog_model"); 384 | detector_dlib_cnn_model = obs_data_get_string(settings, "detector_dlib_cnn_model"); 385 | detector_crop_l = obs_data_get_int(settings, "detector_crop_l"); 386 | detector_crop_r = obs_data_get_int(settings, "detector_crop_r"); 387 | detector_crop_t = obs_data_get_int(settings, "detector_crop_t"); 388 | detector_crop_b = obs_data_get_int(settings, "detector_crop_b"); 389 | bool landmark_detection = obs_data_get_bool(settings, "landmark_detection"); 390 | bfree(landmark_detection_data); 391 | landmark_detection_data = NULL; 392 | if (landmark_detection) 393 | landmark_detection_data = bstrdup(obs_data_get_string(settings, "landmark_detection_data")); 394 | if (obs_data_get_bool(settings, "tracking_th_en")) 395 | tracking_threshold = from_dB(obs_data_get_double(settings, "tracking_th_dB")); 396 | else 397 | tracking_threshold = 0.0; 398 | } 399 | 400 | static bool tracking_th_en_modified(obs_properties_t *props, obs_property_t *, obs_data_t *settings) 401 | { 402 | bool tracking_th_en = obs_data_get_bool(settings, "tracking_th_en"); 403 | obs_property_t *tracking_th_dB = obs_properties_get(props, "tracking_th_dB"); 404 | obs_property_set_visible(tracking_th_dB, tracking_th_en); 405 | return true; 406 | } 407 | 408 | void face_tracker_manager::get_properties(obs_properties_t *pp) 409 | { 410 | obs_property_t *p; 411 | std::string data_path = obs_get_module_data_path(obs_current_module()); 412 | 413 | obs_properties_add_float(pp, "upsize_l", obs_module_text("Left"), -0.4, 4.0, 0.2); 414 | obs_properties_add_float(pp, "upsize_r", obs_module_text("Right"), -0.4, 4.0, 0.2); 415 | obs_properties_add_float(pp, "upsize_t", obs_module_text("Top"), -0.4, 4.0, 0.2); 416 | obs_properties_add_float(pp, "upsize_b", obs_module_text("Bottom"), -0.4, 4.0, 0.2); 417 | obs_properties_add_float(pp, "scale", obs_module_text("Scale image"), 1.0, 16.0, 1.0); 418 | p = obs_properties_add_list(pp, "detector_engine", obs_module_text("Detector"), OBS_COMBO_TYPE_LIST, 419 | OBS_COMBO_FORMAT_INT); 420 | obs_property_list_add_int(p, obs_module_text("Detector.dlib.hog"), (int)engine_dlib_hog); 421 | obs_property_list_add_int(p, obs_module_text("Detector.dlib.cnn"), (int)engine_dlib_cnn); 422 | obs_properties_add_path(pp, "detector_dlib_hog_model", obs_module_text("Dlib HOG model"), OBS_PATH_FILE, 423 | "Data Files (*.dat);;" 424 | "All Files (*.*)", 425 | (data_path + "/" DIR_DLIB_CNN).c_str()); 426 | obs_properties_add_path(pp, "detector_dlib_cnn_model", obs_module_text("Dlib CNN model"), OBS_PATH_FILE, 427 | "Data Files (*.dat);;" 428 | "All Files (*.*)", 429 | (data_path + "/" DIR_DLIB_CNN).c_str()); 430 | obs_properties_add_int(pp, "detector_crop_l", obs_module_text("Crop left for detector"), 0, 1920, 1); 431 | obs_properties_add_int(pp, "detector_crop_r", obs_module_text("Crop right for detector"), 0, 1920, 1); 432 | obs_properties_add_int(pp, "detector_crop_t", obs_module_text("Crop top for detector"), 0, 1080, 1); 433 | obs_properties_add_int(pp, "detector_crop_b", obs_module_text("Crop bottom for detector"), 0, 1080, 1); 434 | obs_properties_add_bool(pp, "landmark_detection", obs_module_text("Enable landmark detection")); 435 | p = obs_properties_add_path(pp, "landmark_detection_data", obs_module_text("Landmark detection data"), 436 | OBS_PATH_FILE, 437 | "Data Files (*.dat);;" 438 | "All Files (*.*)", 439 | (data_path + "/" DIR_DLIB_LANDMARK).c_str()); 440 | obs_property_set_long_description( 441 | p, obs_module_text("You can get the shape_predictor_68_face_landmarks.dat file from: " 442 | "http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2")); 443 | p = obs_properties_add_bool(pp, "tracking_th_en", obs_module_text("Set tracking threshold")); 444 | obs_property_set_modified_callback(p, tracking_th_en_modified); 445 | p = obs_properties_add_float(pp, "tracking_th_dB", obs_module_text("Tracking threshold"), -120.0, -20.0, 5.0); 446 | obs_property_float_set_suffix(p, " dB"); 447 | } 448 | 449 | void face_tracker_manager::get_defaults(obs_data_t *settings) 450 | { 451 | obs_data_set_default_double(settings, "upsize_l", 0.2); 452 | obs_data_set_default_double(settings, "upsize_r", 0.2); 453 | obs_data_set_default_double(settings, "upsize_t", 0.3); 454 | obs_data_set_default_double(settings, "upsize_b", 0.1); 455 | obs_data_set_default_double(settings, "scale", 2.0); 456 | obs_data_set_default_bool(settings, "tracking_th_en", true); 457 | obs_data_set_default_double(settings, "tracking_th_dB", -80.0); 458 | 459 | if (char *f = obs_module_file(DIR_DLIB_HOG "/frontal_face_detector.dat")) { 460 | obs_data_set_default_string(settings, "detector_dlib_hog_model", f); 461 | bfree(f); 462 | } else { 463 | blog(LOG_ERROR, "frontal_face_detector.dat is not found in the data directory."); 464 | } 465 | 466 | if (char *f = obs_module_file(DIR_DLIB_CNN "/mmod_human_face_detector.dat")) { 467 | obs_data_set_default_string(settings, "detector_dlib_cnn_model", f); 468 | bfree(f); 469 | } else { 470 | blog(LOG_ERROR, "mmod_human_face_detector.dat is not found in the data directory."); 471 | } 472 | 473 | if (char *f = obs_module_file(DIR_DLIB_LANDMARK "/shape_predictor_5_face_landmarks.dat")) { 474 | obs_data_set_default_string(settings, "landmark_detection_data", f); 475 | bfree(f); 476 | } else { 477 | blog(LOG_ERROR, "shape_predictor_5_face_landmarks.dat is not found in the data directory."); 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. --------------------------------------------------------------------------------