├── .gitignore ├── .gitattributes ├── script-templates └── object │ └── structured.gd ├── project.godot ├── README.md ├── icon.svg.import ├── LICENSE ├── config.gd ├── icon.svg ├── main.tscn └── main.gd /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /script-templates/object/structured.gd: -------------------------------------------------------------------------------- 1 | extends _BASE_ 2 | 3 | #-----------------------------------------------------------------------------# 4 | # Builtin functions 5 | #-----------------------------------------------------------------------------# 6 | 7 | func _init() -> void: 8 | pass 9 | 10 | #-----------------------------------------------------------------------------# 11 | # Private functions 12 | #-----------------------------------------------------------------------------# 13 | 14 | #-----------------------------------------------------------------------------# 15 | # Public functions 16 | #-----------------------------------------------------------------------------# 17 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="OpenSeeFaceGD" 14 | run/main_scene="res://main.tscn" 15 | config/features=PackedStringArray("4.0", "Mobile") 16 | config/icon="res://icon.svg" 17 | 18 | [display] 19 | 20 | window/size/viewport_width=1600 21 | window/size/viewport_height=900 22 | 23 | [editor] 24 | 25 | script/templates_search_path="res://script-templates" 26 | export/convert_text_resources_to_binary=true 27 | 28 | [gui] 29 | 30 | theme/default_theme_scale=1.5 31 | 32 | [rendering] 33 | 34 | renderer/rendering_method="mobile" 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openseeface-gd 2 | A Godot 4 project for running [OpenSeeFace](https://github.com/emilianavt/OpenSeeFace) via a GUI. 3 | 4 | STILL WIP 5 | 6 | ## IMPORTANT If you are looking for the VTuber application that was previously named `openseeface-gd`, that has been moved to [puppeteer](https://github.com/virtual-puppet-project/puppeteer) as to not conflict with [OpenSeeFace](https://github.com/emilianavt/OpenSeeFace). 7 | 8 | ## FAQ 9 | 10 | ### Why did you move the VTuber application to [puppeteer](https://github.com/virtual-puppet-project/puppeteer)? 11 | The initial release was more of a curiosity for me, as I wanted to see how hard it would be to track anything at all from within Godot. One thing led to another and the little test project eventually grew into an actual application. 12 | 13 | I regret delaying the name change for as long as I did, and I apologize for the confusion this might have caused since the projects are only related by face tracker dependency. 14 | 15 | 16 | -------------------------------------------------------------------------------- /icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://ctfmq5kbj3wna" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/lossy_quality=0.7 20 | compress/hdr_compression=1 21 | compress/bptc_ldr=0 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2023 Tim Yuen 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 19 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 25 | POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /config.gd: -------------------------------------------------------------------------------- 1 | class_name Config 2 | extends Resource 3 | 4 | # TODO (Tim Yuen) Must be in a seperate file otherwise Godot cannot load the resource 5 | 6 | const SAVE_PATH := "user://osfgd_config.tres" 7 | 8 | const RunTypes := { 9 | "BINARY": "Binary", 10 | "PYTHON": "Python" 11 | } 12 | 13 | @export 14 | var run_type := RunTypes.BINARY 15 | 16 | @export 17 | var binary_path := "" 18 | 19 | @export 20 | var python_path := "" 21 | @export 22 | var osf_path := "" 23 | 24 | @export 25 | var ip := "127.0.0.1" 26 | @export 27 | var port := "11573" 28 | @export 29 | var list_cameras := "0" 30 | @export 31 | var list_dcaps := "" 32 | @export 33 | var width := "640" 34 | @export 35 | var height := "360" 36 | @export 37 | var dcap := "" 38 | @export 39 | var blackmagic := "0" 40 | @export 41 | var fps := "24" 42 | @export 43 | var capture := "0" 44 | @export 45 | var mirror_input := false 46 | @export 47 | var max_threads := "1" 48 | @export 49 | var threshold := "" 50 | @export 51 | var detection_threshold := "0.6" 52 | @export 53 | var visualize := "0" 54 | @export 55 | var pnp_points := "0" 56 | @export 57 | var silent := "0" 58 | @export 59 | var faces := "1" 60 | @export 61 | var scan_retinaface := "0" 62 | @export 63 | var scan_every := "3" 64 | @export 65 | var discard_after := "10" 66 | @export 67 | var max_feature_updates := "900" 68 | @export 69 | var no_3d_adapt := "1" 70 | @export 71 | var try_hard := "0" 72 | @export 73 | var video_out := "" 74 | @export 75 | var video_scale := "1" 76 | @export 77 | var video_fps := "24" 78 | @export 79 | var raw_rgb := "0" 80 | @export 81 | var log_data := "" 82 | @export 83 | var log_output := "" 84 | @export 85 | var model := "3" 86 | @export 87 | var model_dir := "" 88 | @export 89 | var gaze_tracking := "1" 90 | @export 91 | var face_id_offset := "0" 92 | @export 93 | var repeat_video := "0" 94 | @export 95 | var dump_points := "" 96 | @export 97 | var benchmark := "0" 98 | @export 99 | var use_dshowcapture := "1" 100 | @export 101 | var blackmagic_options := "" 102 | @export 103 | var priority := "" 104 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /main.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://dr0v00f6koeie"] 2 | 3 | [ext_resource type="Script" path="res://main.gd" id="1_hemfs"] 4 | 5 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2pm8h"] 6 | content_margin_left = 10.0 7 | content_margin_top = 10.0 8 | content_margin_right = 10.0 9 | content_margin_bottom = 10.0 10 | bg_color = Color(0.176471, 0.176471, 0.176471, 1) 11 | 12 | [node name="Main" type="CanvasLayer"] 13 | script = ExtResource("1_hemfs") 14 | 15 | [node name="PanelContainer" type="PanelContainer" parent="."] 16 | anchors_preset = 15 17 | anchor_right = 1.0 18 | anchor_bottom = 1.0 19 | grow_horizontal = 2 20 | grow_vertical = 2 21 | theme_override_styles/panel = SubResource("StyleBoxFlat_2pm8h") 22 | 23 | [node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer"] 24 | layout_mode = 2 25 | 26 | [node name="HFlowContainer" type="HFlowContainer" parent="PanelContainer/VBoxContainer"] 27 | layout_mode = 2 28 | 29 | [node name="Run" type="Button" parent="PanelContainer/VBoxContainer/HFlowContainer"] 30 | unique_name_in_owner = true 31 | layout_mode = 2 32 | text = "Run" 33 | 34 | [node name="RunType" type="OptionButton" parent="PanelContainer/VBoxContainer/HFlowContainer"] 35 | unique_name_in_owner = true 36 | layout_mode = 2 37 | 38 | [node name="Status" type="RichTextLabel" parent="PanelContainer/VBoxContainer/HFlowContainer"] 39 | unique_name_in_owner = true 40 | layout_mode = 2 41 | size_flags_horizontal = 3 42 | focus_mode = 2 43 | bbcode_enabled = true 44 | selection_enabled = true 45 | 46 | [node name="BinaryOptions" type="VBoxContainer" parent="PanelContainer/VBoxContainer"] 47 | unique_name_in_owner = true 48 | layout_mode = 2 49 | size_flags_horizontal = 3 50 | 51 | [node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/VBoxContainer/BinaryOptions"] 52 | layout_mode = 2 53 | 54 | [node name="Label" type="Label" parent="PanelContainer/VBoxContainer/BinaryOptions/HBoxContainer"] 55 | layout_mode = 2 56 | size_flags_horizontal = 3 57 | text = "OpenSeeFace Binary" 58 | 59 | [node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/VBoxContainer/BinaryOptions/HBoxContainer"] 60 | layout_mode = 2 61 | size_flags_horizontal = 3 62 | 63 | [node name="BinaryPath" type="LineEdit" parent="PanelContainer/VBoxContainer/BinaryOptions/HBoxContainer/HBoxContainer"] 64 | unique_name_in_owner = true 65 | layout_mode = 2 66 | size_flags_horizontal = 3 67 | placeholder_text = "Absolute path to the OpenSeeFace binary" 68 | 69 | [node name="ChooseBinary" type="Button" parent="PanelContainer/VBoxContainer/BinaryOptions/HBoxContainer/HBoxContainer"] 70 | unique_name_in_owner = true 71 | layout_mode = 2 72 | text = "Select" 73 | 74 | [node name="PythonOptions" type="VBoxContainer" parent="PanelContainer/VBoxContainer"] 75 | unique_name_in_owner = true 76 | visible = false 77 | layout_mode = 2 78 | size_flags_horizontal = 3 79 | 80 | [node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/VBoxContainer/PythonOptions"] 81 | layout_mode = 2 82 | 83 | [node name="Label" type="Label" parent="PanelContainer/VBoxContainer/PythonOptions/HBoxContainer"] 84 | layout_mode = 2 85 | size_flags_horizontal = 3 86 | text = "Python Binary" 87 | 88 | [node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/VBoxContainer/PythonOptions/HBoxContainer"] 89 | layout_mode = 2 90 | size_flags_horizontal = 3 91 | 92 | [node name="PythonPath" type="LineEdit" parent="PanelContainer/VBoxContainer/PythonOptions/HBoxContainer/HBoxContainer"] 93 | unique_name_in_owner = true 94 | layout_mode = 2 95 | size_flags_horizontal = 3 96 | placeholder_text = "Absolute path or PATH name" 97 | 98 | [node name="ChoosePython" type="Button" parent="PanelContainer/VBoxContainer/PythonOptions/HBoxContainer/HBoxContainer"] 99 | unique_name_in_owner = true 100 | layout_mode = 2 101 | text = "Select" 102 | 103 | [node name="HBoxContainer2" type="HBoxContainer" parent="PanelContainer/VBoxContainer/PythonOptions"] 104 | layout_mode = 2 105 | 106 | [node name="Label" type="Label" parent="PanelContainer/VBoxContainer/PythonOptions/HBoxContainer2"] 107 | layout_mode = 2 108 | size_flags_horizontal = 3 109 | text = "OpenSeeFace Folder" 110 | 111 | [node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/VBoxContainer/PythonOptions/HBoxContainer2"] 112 | layout_mode = 2 113 | size_flags_horizontal = 3 114 | 115 | [node name="OsfPath" type="LineEdit" parent="PanelContainer/VBoxContainer/PythonOptions/HBoxContainer2/HBoxContainer"] 116 | unique_name_in_owner = true 117 | layout_mode = 2 118 | size_flags_horizontal = 3 119 | placeholder_text = "Absolute path to the OpenSeeFace folder" 120 | 121 | [node name="ChooseOsf" type="Button" parent="PanelContainer/VBoxContainer/PythonOptions/HBoxContainer2/HBoxContainer"] 122 | unique_name_in_owner = true 123 | layout_mode = 2 124 | text = "Select" 125 | 126 | [node name="GeneralOptions" type="ScrollContainer" parent="PanelContainer/VBoxContainer"] 127 | layout_mode = 2 128 | size_flags_vertical = 3 129 | 130 | [node name="OsfOptions" type="VBoxContainer" parent="PanelContainer/VBoxContainer/GeneralOptions"] 131 | unique_name_in_owner = true 132 | layout_mode = 2 133 | size_flags_horizontal = 3 134 | -------------------------------------------------------------------------------- /main.gd: -------------------------------------------------------------------------------- 1 | extends CanvasLayer 2 | 3 | # TODO (Tim Yuen) Godot still can't infer types correctly, so they have to be casted 4 | 5 | ## Redundant keys for quick access 6 | const OSF_OPTIONS := { 7 | "IP": { 8 | "display_name": "IP", 9 | "flag": "--ip", 10 | "config_mapping": "ip", 11 | "hint": "The IP address for sending tracking data." 12 | }, 13 | "Port": { 14 | "display_name": "Port", 15 | "flag": "--port", 16 | "config_mapping": "port", 17 | "hint": "The port for sending tracking data." 18 | }, 19 | "List Cameras": { 20 | "display_name": "List Cameras", 21 | "flag": "--list-cameras", 22 | "config_mapping": "list_cameras", 23 | "hint": "Set to 1 to list the available cameras and quit. Set to 2 or higher to output only the camera names.", 24 | "windows_only": true 25 | }, 26 | "List DCaps": { 27 | "display_name": "List DCaps", 28 | "flag": "--list-dcaps", 29 | "config_mapping": "list_dcaps", 30 | "hint": "Set to -1 to list all cameras and their available capabilities. Set this to a camera ID to list that camera's capabilities.", 31 | "windows_only": true 32 | }, 33 | "DCap": { 34 | "display_name": "DCap", 35 | "flag": "--dcap", 36 | "config_mapping": "dcap", 37 | "hint": "Set which device capability line to use or -1 to use the default camera settings. FPS will still need to be set separately.", 38 | "windows_only": true 39 | }, 40 | "Blackmagic": { 41 | "display_name": "Blackmagic", 42 | "flag": "--blackmagic", 43 | "config_mapping": "blackmagic", 44 | "hint": "Set to 1 to enable Blackmagic device support, if applicable.", 45 | "windows_only": true 46 | }, 47 | "Width": { 48 | "display_name": "Width", 49 | "flag": "--width", 50 | "config_mapping": "width", 51 | "hint": "The RGB width." 52 | }, 53 | "Height": { 54 | "display_name": "Height", 55 | "flag": "--height", 56 | "config_mapping": "height", 57 | "hint": "The raw RGB height." 58 | }, 59 | "FPS": { 60 | "display_name": "FPS", 61 | "flag": "--fps", 62 | "config_mapping": "fps", 63 | "hint": "The capture frames per second." 64 | }, 65 | "Capture": { 66 | "display_name": "Capture", 67 | "flag": "--capture", 68 | "config_mapping": "capture", 69 | "hint": "The camera to use (e.g. Windows: 0, 1 Linux: /dev/video0)." 70 | }, 71 | "Mirror Input": { 72 | "display_name": "Mirror Input", 73 | "flag": "--mirror-input", 74 | "config_mapping": "mirror_input", 75 | "hint": "If the input should be mirrored before processing.", 76 | "type": TYPE_BOOL 77 | }, 78 | "Max Threads": { 79 | "display_name": "Max Threads", 80 | "flag": "--max-threads", 81 | "config_mapping": "max_threads", 82 | "hint": "The maximum number of threads to use. MORE THREADS DOES _NOT_ MEAN MORE PERFORMANCE." 83 | }, 84 | "Threshold": { 85 | "display_name": "Threshold", 86 | "flag": "--threshold", 87 | "config_mapping": "threshold", 88 | "hint": "The minimum confidence threshold for face tracking." 89 | }, 90 | "Detection Threshold": { 91 | "display_name": "Detection Threshold", 92 | "flag": "--detection-threshold", 93 | "config_mapping": "detection_threshold", 94 | "hint": "The minimum confidence interval for face detection." 95 | }, 96 | "Visualize": { 97 | "display_name": "Visualize", 98 | "flag": "--visualize", 99 | "config_mapping": "visualize", 100 | "hint": "Set to 1 to visualize the tracking results, set to 2 to also show face ids, set to 3 to add confidence values, and set to 4 to add numbers to the point display." 101 | }, 102 | "PnP Points": { 103 | "display_name": "PnP Points", 104 | "flag": "--pnp-points", 105 | "config_mapping": "pnp_points", 106 | "hint": "Set to 1 to add the 3D fitting points to the visualization." 107 | }, 108 | "Silent": { 109 | "display_name": "Silent", 110 | "flag": "--silent", 111 | "config_mapping": "silent", 112 | "hint": "Set to 1 to prevent console text output." 113 | }, 114 | "Faces": { 115 | "display_name": "Faces", 116 | "flag": "--faces", 117 | "config_mapping": "faces", 118 | "hint": "Set the maximum number of faces (slow)." 119 | }, 120 | "Scan RetinaFace": { 121 | "display_name": "Scan RetinaFace", 122 | "flag": "--scan-retinaface", 123 | "config_mapping": "scan_retinaface", 124 | "hint": "Set to 1 to enable scanning for additional faces using RetinaFace in a background thread. Otherwise, a simpler, faster face detection mechanism is used. Does nothing when the maximum number of faces is 1." 125 | }, 126 | "Scan Every": { 127 | "display_name": "Scan Every", 128 | "flag": "--scan-every", 129 | "config_mapping": "scan_every", 130 | "hint": "The amount of frames to wait before scanning for new faces." 131 | }, 132 | "Discard After": { 133 | "display_name": "Discard After", 134 | "flag": "--discard-after", 135 | "config_mapping": "discard_after", 136 | "hint": "How long the tracker should keep looking for lost faces." 137 | }, 138 | "Max Feature Updates": { 139 | "display_name": "Max Feature Updates", 140 | "flag": "--max-feature-updates", 141 | "config_mapping": "max_feature_updates", 142 | "hint": "The number of seconds after which feature min/max/medium values will no longer be updated once a face has been detected." 143 | }, 144 | "No 3D Adapt": { 145 | "display_name": "No 3D Adapt", 146 | "flag": "--no-3d-adapt", 147 | "config_mapping": "no_3d_adapt", 148 | "hint": "When set to 1, the 3D face model will not be adapted to increase the fit." 149 | }, 150 | "Try Hard": { 151 | "display_name": "Try Hard", 152 | "flag": "--try-hard", 153 | "config_mapping": "try_hard", 154 | "hint": "When set to 1, the tracker will try harder to find a face." 155 | }, 156 | "Video Out": { 157 | "display_name": "Video Out", 158 | "flag": "--video-out", 159 | "config_mapping": "video_out", 160 | "hint": "The filename of an AVI file to save the tracking visualization as a video." 161 | }, 162 | "Video Scale": { 163 | "display_name": "Video Scale", 164 | "flag": "--video-scale", 165 | "config_mapping": "video_scale", 166 | "hint": "The resolution scale factor applied to the saved AVI file. Choices: 1, 2, 3, 4." 167 | }, 168 | "Video FPS": { 169 | "display_name": "Video FPS", 170 | "flag": "--video-fps", 171 | "config_mapping": "video_fps", 172 | "hint": "The frame rate of the output AVI file." 173 | }, 174 | "Raw RGB": { 175 | "display_name": "Raw RGB", 176 | "flag": "--raw-rgb", 177 | "config_mapping": "raw_rgb", 178 | "hint": "When set, raw RGB frames of the size given from Width and Height are read from standard input instead of reading a video." 179 | }, 180 | "Log Data": { 181 | "display_name": "Log Data", 182 | "flag": "--log-data", 183 | "config_mapping": "log_data", 184 | "hint": "A filename where tracking data will be logged." 185 | }, 186 | "Log Output": { 187 | "display_name": "Log Output", 188 | "flag": "--log-output", 189 | "config_mapping": "log_output", 190 | "hint": "A filename where console data will be logged." 191 | }, 192 | "Model": { 193 | "display_name": "Model", 194 | "flag": "--model", 195 | "config_mapping": "model", 196 | "hint": "The tracking model to use. Higher numbers are models with better tracking quality but slower speed except for model 4 which is wink optimized. Models 1 and 0 tend to be too rigid for expression and blink detection. Model -2 is roughly equivalent to model 1 but faster. Model -3 is between models 0 and -1." 197 | }, 198 | "Model Directory": { 199 | "display_name": "Model Directory", 200 | "flag": "--model-dir", 201 | "config_mapping": "model_dir", 202 | "hint": "The path to the directory containing the .onnx model files." 203 | }, 204 | "Gaze Tracking": { 205 | "display_name": "Gaze Tracking", 206 | "flag": "--gaze-tracking", 207 | "config_mapping": "gaze_tracking", 208 | "hint": "Set to 1 to enable gaze tracking which makes things slightly slower." 209 | }, 210 | "Face ID Offset": { 211 | "display_name": "Face ID Offset", 212 | "flag": "--face-id-offset", 213 | "config_mapping": "face_id_offset", 214 | "hint": "An offset added to all face IDs, which can be useful for mixing tracking data from multiple network sources." 215 | }, 216 | "Repeat Video": { 217 | "display_name": "Repeat Video", 218 | "flag": "--repeat-video", 219 | "config_mapping": "repeat_video", 220 | "hint": "When set to 1 and a video file was specified with Capture, the tracker will loop until interrupted." 221 | }, 222 | "Dump Points": { 223 | "display_name": "Dump Points", 224 | "flag": "--dump-points", 225 | "config_mapping": "dump_points", 226 | "hint": "When set to a filename, the current face 3D points are made symmetric and dumped to the given file when quitting the visualization with the `q` key." 227 | }, 228 | "Benchmark": { 229 | "display_name": "Benchmark", 230 | "flag": "--benchmark", 231 | "config_mapping": "benchmark", 232 | "hint": "Set to 1 to benchmark the various tracking models starting with the best and ending with the fastest. Gaze tracking is disabled for models with negative IDs." 233 | }, 234 | "Use DShowCapture": { 235 | "display_name": "Use DShowCapture", 236 | "flag": "--use-dshowcapture", 237 | "config_mapping": "use_dshowcapture", 238 | "hint": "Set to 1 to use libdshowcapture instead of OpenCV", 239 | "windows_only": true 240 | }, 241 | "Blackmagic Options": { 242 | "display_name": "Blackmagic Options", 243 | "flag": "--blackmagic-options", 244 | "config_mapping": "blackmagic_options", 245 | "hint": "An additional option string to be passed to the Blackmagic capture library", 246 | "windows_only": true 247 | }, 248 | "Priority": { 249 | "display_name": "Priority", 250 | "flag": "--priority", 251 | "config_mapping": "priority", 252 | "hint": "The process priority. Options: 0, 1, 2, 3, 4, 5", 253 | "windows_only": true 254 | } 255 | } 256 | class OsfOption extends HBoxContainer: 257 | var _label: Label = null 258 | var _line_edit: LineEdit = null 259 | var _element: Control = null 260 | 261 | func _init( 262 | option_name: String, 263 | option_value: Variant, 264 | callback: Callable, 265 | hint: String, 266 | element_type: int 267 | ) -> void: 268 | _label = Label.new() 269 | _label.size_flags_horizontal = Control.SIZE_EXPAND_FILL 270 | _label.text = option_name 271 | 272 | if element_type == TYPE_STRING: 273 | _element = LineEdit.new() 274 | _element.text = option_value 275 | _element.text_changed.connect(callback) 276 | else: 277 | _element = CheckBox.new() 278 | _element.button_pressed = option_value 279 | _element.toggled.connect(callback) 280 | 281 | _element.size_flags_horizontal = Control.SIZE_EXPAND_FILL 282 | 283 | add_child(_label) 284 | add_child(_element) 285 | 286 | if not hint.is_empty(): 287 | self.tooltip_text = hint 288 | _label.tooltip_text = hint 289 | _element.tooltip_text = hint 290 | 291 | func option_name() -> String: 292 | return _label.text 293 | 294 | func option_value() -> Variant: 295 | return _element.text if _element is LineEdit else _element.button_pressed 296 | 297 | var _config: Config = null 298 | var _is_windows := false 299 | var _use_binary := false 300 | var _tracker_pid := -1 301 | 302 | @onready 303 | var _status := %Status as RichTextLabel 304 | 305 | @onready 306 | var _binary_path := %BinaryPath as LineEdit 307 | 308 | @onready 309 | var _python_path := %PythonPath as LineEdit 310 | @onready 311 | var _osf_path := %OsfPath as LineEdit 312 | 313 | @onready 314 | var _osf_options := %OsfOptions as VBoxContainer 315 | 316 | #-----------------------------------------------------------------------------# 317 | # Builtin functions 318 | #-----------------------------------------------------------------------------# 319 | 320 | func _ready() -> void: 321 | get_viewport().gui_embed_subwindows = false 322 | 323 | _config = _read_config() 324 | 325 | _is_windows = OS.get_name().to_lower() == "windows" 326 | 327 | var run_button := %Run as Button 328 | run_button.pressed.connect(func() -> void: 329 | var exe_path := "" 330 | var run_options := [] 331 | 332 | if _use_binary: 333 | if _config.binary_path.is_empty(): 334 | self._update_status("No OpenSeeFace binary configured") 335 | return 336 | 337 | exe_path = _config.binary_path 338 | else: 339 | if _config.python_path.is_empty(): 340 | self._update_status("No Python binary configured") 341 | return 342 | elif _config.osf_path.is_empty(): 343 | self._update_status("No OpenSeeFace folder configured") 344 | return 345 | 346 | exe_path = _config.python_path 347 | run_options.push_back("%s/facetracker.py" % _config.osf_path) 348 | 349 | if not FileAccess.file_exists(exe_path): 350 | self._update_status("%s does not exist" % exe_path) 351 | return 352 | if run_options.front() != null and not FileAccess.file_exists(run_options.front()): 353 | self._update_status("%s does not exist" % run_options.front()) 354 | return 355 | 356 | for child in _osf_options.get_children(): 357 | var val = child.option_value() 358 | match typeof(val): 359 | TYPE_STRING: 360 | if val.is_empty(): 361 | continue 362 | TYPE_BOOL: 363 | if not val: 364 | continue 365 | 366 | run_options.push_back(OSF_OPTIONS[child.option_name()].flag) 367 | run_options.push_back(val) 368 | 369 | var pid := OS.create_process(exe_path, run_options, true) 370 | if pid < 0: 371 | self._update_status("Unable to start tracker") 372 | else: 373 | _tracker_pid = pid 374 | ) 375 | 376 | var binary_options := %BinaryOptions as VBoxContainer 377 | var python_options := %PythonOptions as VBoxContainer 378 | 379 | var run_type := %RunType as OptionButton 380 | for val in Config.RunTypes.values(): 381 | run_type.add_item(val) 382 | run_type.item_selected.connect(func(idx: int) -> void: 383 | match run_type.get_item_text(idx): 384 | Config.RunTypes.BINARY: 385 | python_options.hide() 386 | binary_options.show() 387 | _use_binary = true 388 | Config.RunTypes.PYTHON: 389 | binary_options.hide() 390 | python_options.show() 391 | _use_binary = false 392 | ) 393 | run_type.select(0) 394 | run_type.item_selected.emit(0) 395 | 396 | %ChooseBinary.pressed.connect(func() -> void: 397 | var popup := _show_file_dialog() 398 | popup.file_selected.connect(func(path: String) -> void: 399 | _binary_path.text = path 400 | _config.binary_path = path 401 | ) 402 | ) 403 | _binary_path.text = _config.binary_path 404 | 405 | %ChoosePython.pressed.connect(func() -> void: 406 | var popup := _show_file_dialog() 407 | popup.file_selected.connect(func(path: String) -> void: 408 | _python_path.text = path 409 | _config.python_path = path 410 | ) 411 | ) 412 | _python_path.text = _config.python_path 413 | %ChooseOsf.pressed.connect(func() -> void: 414 | var popup := _show_file_dialog(FileDialog.FILE_MODE_OPEN_DIR) 415 | popup.dir_selected.connect(func(path: String) -> void: 416 | _osf_path.text = path 417 | _config.osf_path = path 418 | ) 419 | ) 420 | _osf_path.text = _config.osf_path 421 | 422 | for data in OSF_OPTIONS.values(): 423 | if data.get("windows_only", false) and not _is_windows: 424 | continue 425 | var option := OsfOption.new( 426 | data.display_name, 427 | _config.get(data.config_mapping), 428 | func(value: Variant) -> void: 429 | _config.set(data.config_mapping, value), 430 | data.get("hint", ""), 431 | data.get("type", TYPE_STRING) 432 | ) 433 | 434 | _osf_options.add_child(option) 435 | 436 | _update_status("Ready!") 437 | 438 | func _exit_tree() -> void: 439 | _write_config(_config) 440 | 441 | # Technically this should be -1, but PID 0 is usually the system root process 442 | # On Linux, I'm pretty sure all PIDs must be greater than 1000 443 | if _tracker_pid > 0: 444 | OS.kill(_tracker_pid) 445 | 446 | #-----------------------------------------------------------------------------# 447 | # Private functions 448 | #-----------------------------------------------------------------------------# 449 | 450 | func _update_status(text: String) -> void: 451 | _status.text = "[right][rainbow][wave]%s[/wave][/rainbow][/right]" % text 452 | 453 | func _show_file_dialog(file_mode: int = FileDialog.FILE_MODE_OPEN_FILE) -> FileDialog: 454 | var popup := FileDialog.new() 455 | popup.file_mode = file_mode 456 | popup.access = FileDialog.ACCESS_FILESYSTEM 457 | 458 | add_child(popup) 459 | popup.popup_centered_ratio(0.5) 460 | 461 | popup.close_requested.connect(func() -> void: 462 | popup.queue_free() 463 | ) 464 | popup.visibility_changed.connect(func() -> void: 465 | popup.close_requested.emit() 466 | ) 467 | 468 | return popup 469 | 470 | static func _read_config() -> Config: 471 | var config: Resource = ResourceLoader.load(Config.SAVE_PATH, "tres") 472 | if config == null or not config is Config: 473 | print("Unable to load config, using new config") 474 | config = Config.new() 475 | 476 | return config as Config 477 | 478 | static func _write_config(config: Config) -> void: 479 | if ResourceSaver.save(config, Config.SAVE_PATH, ResourceSaver.FLAG_CHANGE_PATH) != OK: 480 | printerr("Unable to write config") 481 | 482 | #-----------------------------------------------------------------------------# 483 | # Public functions 484 | #-----------------------------------------------------------------------------# 485 | 486 | --------------------------------------------------------------------------------