diff --git a/src/Baballonia/Models/SliderBindableSetting.cs b/src/Baballonia/Models/SliderBindableSetting.cs index ad4d8ff6..1eb948b4 100644 --- a/src/Baballonia/Models/SliderBindableSetting.cs +++ b/src/Baballonia/Models/SliderBindableSetting.cs @@ -1,4 +1,10 @@ using CommunityToolkit.Mvvm.ComponentModel; +using Baballonia.Contracts; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Threading.Tasks; +using Avalonia.Threading; namespace Baballonia.Models; @@ -18,7 +24,82 @@ public SliderBindableSetting(string name, float lower = 0f, float upper = 1f, fl Name = name; Lower = lower; Upper = upper; - Min = max; - Max = min; + Min = min; + Max = max; } } + +public partial class ParameterGroupCollection : ObservableCollection +{ + public string GroupName { get; } + public IFilterSettings FilterSettings { get; } + + public ParameterGroupCollection(string groupName, IFilterSettings filterSettings, IEnumerable items) + : base(items) + { + GroupName = groupName; + FilterSettings = filterSettings; + } +} + +public interface IFilterSettings : INotifyPropertyChanged +{ + bool Enabled { get; set; } + float MinFreqCutoff { get; set; } + float SpeedCutoff { get; set; } +} + +public partial class GroupFilterSettings : ObservableObject, IFilterSettings +{ + private readonly ILocalSettingsService _localSettingsService; + private readonly string _prefix; + + [ObservableProperty] + private bool _enabled; + + [ObservableProperty] + private float _minFreqCutoff; + + [ObservableProperty] + private float _speedCutoff; + + public GroupFilterSettings(ILocalSettingsService localSettingsService, string settingPrefix, + bool defaultEnabled, float defaultMinFreqCutoff, float defaultSpeedCutoff) + { + _localSettingsService = localSettingsService; + _prefix = settingPrefix; + + Enabled = defaultEnabled; + MinFreqCutoff = defaultMinFreqCutoff; + SpeedCutoff = defaultSpeedCutoff; + + Task.Run(async () => + { + var enabled = await _localSettingsService.ReadSettingAsync($"{_prefix}_Enabled", defaultEnabled); + var min = await _localSettingsService.ReadSettingAsync($"{_prefix}_MinFreq", defaultMinFreqCutoff); + var speed = await _localSettingsService.ReadSettingAsync($"{_prefix}_Speed", defaultSpeedCutoff); + Dispatcher.UIThread.Post(() => + { + Enabled = enabled; + MinFreqCutoff = min; + SpeedCutoff = speed; + }); + }); + + PropertyChanged += async (_, e) => + { + switch (e.PropertyName) + { + case nameof(Enabled): + await _localSettingsService.SaveSettingAsync($"{_prefix}_Enabled", Enabled); + break; + case nameof(MinFreqCutoff): + await _localSettingsService.SaveSettingAsync($"{_prefix}_MinFreq", MinFreqCutoff); + break; + case nameof(SpeedCutoff): + await _localSettingsService.SaveSettingAsync($"{_prefix}_Speed", SpeedCutoff); + break; + } + }; + } +} \ No newline at end of file diff --git a/src/Baballonia/Services/Inference/Filters/OneEuroFilter.cs b/src/Baballonia/Services/Inference/Filters/OneEuroFilter.cs index c661a5ce..5cf433af 100644 --- a/src/Baballonia/Services/Inference/Filters/OneEuroFilter.cs +++ b/src/Baballonia/Services/Inference/Filters/OneEuroFilter.cs @@ -3,101 +3,109 @@ namespace Baballonia.Services.Inference.Filters; -public class OneEuroFilter : IFilter +public class GroupedOneEuroFilter : IFilter { - private float[] minCutoff; - private float[] beta; - private float[] dCutoff; - private float[] xPrev; - private float[] dxPrev; - private DateTime tPrev; - public OneEuroFilter(float[] x0, float minCutoff = 1.0f, float beta = 0.0f) + private sealed class GroupState { - float dx0 = 0.0f; - float dCutoff = 1.0f; - int length = x0.Length; - this.minCutoff = CreateFilledArray(length, minCutoff); - this.beta = CreateFilledArray(length, beta); - this.dCutoff = CreateFilledArray(length, dCutoff); - // Previous values. - this.xPrev = (float[])x0.Clone(); - this.dxPrev = CreateFilledArray(length, dx0); - this.tPrev = DateTime.UtcNow; - + public int[] Indices = Array.Empty(); + public float[] XPrev = Array.Empty(); + public float[] DxPrev = Array.Empty(); + public float MinCutoff; + public float Beta; + public float DCutoff = 1.0f; + public DateTime TPrev; + public bool Initialized; } - public float[] Filter(float[] x) - { - if (x.Length != xPrev.Length) - throw new ArgumentException($"Input shape does not match initial shape. Expected: {xPrev.Length}, got: {x.Length}"); - - DateTime now = DateTime.UtcNow; - float elapsedTime = (float)(now - tPrev).TotalSeconds; - - if (elapsedTime == 0.0f) - { - xPrev = (float[])x.Clone(); - return x; - } - - float[] t_e = CreateFilledArray(x.Length, elapsedTime); - - // Derivative - float[] dx = new float[x.Length]; - for (int i = 0; i < x.Length; i++) - { - dx[i] = (x[i] - xPrev[i]) / t_e[i]; - } + private readonly Dictionary _groups = new(); - float[] a_d = SmoothingFactor(t_e, dCutoff); - float[] dxHat = ExponentialSmoothing(a_d, dx, dxPrev); + public void ConfigureGroup(string groupName, int[] parameterIndices, float minCutoff, float beta) + { + if (parameterIndices.Length == 0) + return; - // Adjusted cutoff - float[] cutoff = new float[x.Length]; - for (int i = 0; i < x.Length; i++) + var state = new GroupState { - cutoff[i] = minCutoff[i] + beta[i] * Math.Abs(dxHat[i]); - } - - float[] a = SmoothingFactor(t_e, cutoff); - float[] xHat = ExponentialSmoothing(a, x, xPrev); - - // Store previous values - xPrev = xHat; - dxPrev = dxHat; - tPrev = now; - - return xHat; + Indices = (int[])parameterIndices.Clone(), + XPrev = new float[parameterIndices.Length], + DxPrev = new float[parameterIndices.Length], + MinCutoff = Math.Max(0.001f, minCutoff), + Beta = Math.Max(0f, beta), + TPrev = DateTime.UtcNow, + Initialized = false + }; + + _groups[groupName] = state; } - private float[] CreateFilledArray(int length, float value) + public void DisableGroup(string groupName) { - float[] arr = new float[length]; - for (int i = 0; i < length; i++) arr[i] = value; - return arr; + _groups.Remove(groupName); } - private float[] SmoothingFactor(float[] t_e, float[] cutoff) + public float[] Filter(float[] input) { - int length = t_e.Length; - float[] result = new float[length]; - for (int i = 0; i < length; i++) + if (_groups.Count == 0) + return input; + + var now = DateTime.UtcNow; + float[] result = (float[])input.Clone(); + + foreach (var kvp in _groups) { - float r = 2 * (float)Math.PI * cutoff[i] * t_e[i]; - result[i] = r / (r + 1); + var state = kvp.Value; + if (state.Indices.Length == 0) + continue; + + int n = state.Indices.Length; + float[] x = new float[n]; + var indices = state.Indices; + for (int i = 0; i < n; i++) + { + x[i] = input[indices[i]]; + } + + float dt = (float)(now - state.TPrev).TotalSeconds; + if (!state.Initialized || dt <= 0f) + { + for (int i = 0; i < n; i++) + state.XPrev[i] = x[i]; + state.TPrev = now; + state.Initialized = true; + continue; + } + + // dx = (x - xPrev) / dt + for (int i = 0; i < n; i++) + { + state.DxPrev[i] = OneEuroSmooth(state.DCutoff, dt, (x[i] - state.XPrev[i]) / dt, state.DxPrev[i]); + } + + // cutoff = minCutoff + beta * |dxHat| + for (int i = 0; i < n; i++) + { + float cutoff = state.MinCutoff + state.Beta * MathF.Abs(state.DxPrev[i]); + float a = SmoothingFactor(cutoff, dt); + float xHat = a * x[i] + (1f - a) * state.XPrev[i]; + state.XPrev[i] = xHat; + result[indices[i]] = xHat; + } + + state.TPrev = now; } + return result; } - private float[] ExponentialSmoothing(float[] a, float[] x, float[] xPrev) + private static float OneEuroSmooth(float cutoff, float dt, float value, float prev) { - int length = a.Length; - float[] result = new float[length]; - for (int i = 0; i < length; i++) - { - result[i] = a[i] * x[i] + (1 - a[i]) * xPrev[i]; - } - return result; + float a = SmoothingFactor(cutoff, dt); + return a * value + (1f - a) * prev; } + private static float SmoothingFactor(float cutoff, float dt) + { + float r = 2f * MathF.PI * cutoff * dt; + return r / (r + 1f); + } } diff --git a/src/Baballonia/Services/Inference/Platforms/PlatformSettings.cs b/src/Baballonia/Services/Inference/Platforms/PlatformSettings.cs index 5575b77e..610a4956 100644 --- a/src/Baballonia/Services/Inference/Platforms/PlatformSettings.cs +++ b/src/Baballonia/Services/Inference/Platforms/PlatformSettings.cs @@ -1,4 +1,5 @@ using System; +using Baballonia.Services.Inference; using Baballonia.Services.Inference.Filters; using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; @@ -10,7 +11,7 @@ public class PlatformSettings( Size inputSize, InferenceSession session, DenseTensor tensor, - OneEuroFilter oneEuroFilter, + IFilter oneEuroFilter, float lastTime, string inputName, string modelName) @@ -19,7 +20,7 @@ public class PlatformSettings( public InferenceSession Session { get; } = session; public DenseTensor Tensor { get; } = tensor; - public OneEuroFilter Filter { get; } = oneEuroFilter; + public IFilter Filter { get; } = oneEuroFilter; public float LastTime { get; set; } = lastTime; public string InputName { get; } = inputName; public string ModelName { get; } = modelName; diff --git a/src/Baballonia/Services/ProcessingLoopService.cs b/src/Baballonia/Services/ProcessingLoopService.cs index f7f5e7fa..7688387a 100644 --- a/src/Baballonia/Services/ProcessingLoopService.cs +++ b/src/Baballonia/Services/ProcessingLoopService.cs @@ -52,66 +52,14 @@ public ProcessingLoopService( dualTransformer.RightTransformer.TargetSize = new Size(128, 128); EyesProcessingPipeline.ImageTransformer = dualTransformer; - var face = LoadFaceInference(); - var eyes = LoadEyeInference(); - - FaceProcessingPipeline.InferenceService = face; - EyesProcessingPipeline.InferenceService = eyes; - - var faceFilter = LoadFaceFilter(); - var eyeFilter = LoadEyeFilter(); - FaceProcessingPipeline.Filter = faceFilter; - EyesProcessingPipeline.Filter = eyeFilter; - LoadEyeStabilizationSetting(); + _ = SetupFaceInference(); + _ = SetupEyeInference(); _drawTimer.Tick += TimerEvent; _drawTimer.Start(); } - private IFilter? LoadFaceFilter() - { - var enabled = _localSettingsService.ReadSetting("AppSettings_OneEuroEnabled"); - var cutoff = _localSettingsService.ReadSetting("AppSettings_OneEuroMinFreqCutoff"); - var speedCutoff = _localSettingsService.ReadSetting("AppSettings_OneEuroSpeedCutoff"); - - if (!enabled) - return null; - - float[] faceArray = new float[Utils.FaceRawExpressions]; - var faceFilter = new OneEuroFilter( - faceArray, - minCutoff: cutoff, - beta: speedCutoff - ); - - return faceFilter; - } - - private IFilter? LoadEyeFilter() - { - var enabled = _localSettingsService.ReadSetting("AppSettings_OneEuroEnabled"); - var cutoff = _localSettingsService.ReadSetting("AppSettings_OneEuroMinFreqCutoff"); - var speedCutoff = _localSettingsService.ReadSetting("AppSettings_OneEuroSpeedCutoff"); - - if (!enabled) - return null; - - float[] eyeArray = new float[Utils.EyeRawExpressions]; - var eyeFilter = new OneEuroFilter( - eyeArray, - minCutoff: cutoff, - beta: speedCutoff - ); - return eyeFilter; - } - - - public Task LoadEyeInferenceAsync() - { - return Task.Run(LoadEyeInference); - } - - public DefaultInferenceRunner LoadEyeInference() + public async Task SetupEyeInference() { const string defaultEyeModel = "eyeModel.onnx"; var eyeModel = _localSettingsService.ReadSetting("EyeHome_EyeModel", defaultEyeModel); diff --git a/src/Baballonia/ViewModels/SplitViewPane/AppSettingsViewModel.cs b/src/Baballonia/ViewModels/SplitViewPane/AppSettingsViewModel.cs index 94506ef0..38d6332e 100644 --- a/src/Baballonia/ViewModels/SplitViewPane/AppSettingsViewModel.cs +++ b/src/Baballonia/ViewModels/SplitViewPane/AppSettingsViewModel.cs @@ -35,18 +35,6 @@ public partial class AppSettingsViewModel : ViewModelBase [property: SavedSetting("AppSettings_OSCPrefix", "")] private string _oscPrefix; - [ObservableProperty] - [property: SavedSetting("AppSettings_OneEuroEnabled", true)] - private bool _oneEuroMinEnabled; - - [ObservableProperty] - [property: SavedSetting("AppSettings_OneEuroMinFreqCutoff", 1f)] - private float _oneEuroMinFreqCutoff; - - [ObservableProperty] - [property: SavedSetting("AppSettings_OneEuroSpeedCutoff", 1f)] - private float _oneEuroSpeedCutoff; - [ObservableProperty] [property: SavedSetting("AppSettings_UseGPU", true)] private bool _useGPU; @@ -70,18 +58,15 @@ public partial class AppSettingsViewModel : ViewModelBase [ObservableProperty] private bool _onboardingEnabled; private ProcessingLoopService _processingLoopService; - private ILogger _logger; + public AppSettingsViewModel() { - // General/Calibration Settings OscTarget = Ioc.Default.GetService()!; GithubService = Ioc.Default.GetService()!; SettingsService = Ioc.Default.GetService()!; _processingLoopService = Ioc.Default.GetService()!; _logger = Ioc.Default.GetService>()!; SettingsService.Load(this); - - // Handle edge case where OSC port is used and the system freaks out if (OscTarget.OutPort == 0) { const int Port = 8888; @@ -89,36 +74,12 @@ public AppSettingsViewModel() SettingsService.SaveSetting("OSCOutPort", Port); } - // Risky Settings ParameterSenderService = Ioc.Default.GetService()!; OnboardingEnabled = Utils.IsSupportedDesktopOS; PropertyChanged += (_, _) => { - if (!_oneEuroMinEnabled) - { - _processingLoopService.FaceProcessingPipeline.Filter = null; - _processingLoopService.EyesProcessingPipeline.Filter = null; - } - else - { - float[] faceArray = new float[Utils.FaceRawExpressions]; - var faceFilter = new OneEuroFilter( - faceArray, - minCutoff: _oneEuroMinFreqCutoff, - beta: _oneEuroSpeedCutoff - ); - float[] eyeArray = new float[Utils.EyeRawExpressions]; - var eyeFilter = new OneEuroFilter( - eyeArray, - minCutoff: _oneEuroMinFreqCutoff, - beta: _oneEuroSpeedCutoff - ); - _processingLoopService.FaceProcessingPipeline.Filter = faceFilter; - _processingLoopService.EyesProcessingPipeline.Filter = eyeFilter; - } - SettingsService.Save(this); }; } diff --git a/src/Baballonia/ViewModels/SplitViewPane/CalibrationViewModel.cs b/src/Baballonia/ViewModels/SplitViewPane/CalibrationViewModel.cs index 884af50d..2df85d6a 100644 --- a/src/Baballonia/ViewModels/SplitViewPane/CalibrationViewModel.cs +++ b/src/Baballonia/ViewModels/SplitViewPane/CalibrationViewModel.cs @@ -7,21 +7,24 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; +using System.Threading.Tasks; using Avalonia.Threading; using Baballonia.Helpers; using Baballonia.Services; using CommunityToolkit.Mvvm.Input; +using Baballonia.Services.Inference.Filters; namespace Baballonia.ViewModels.SplitViewPane; public partial class CalibrationViewModel : ViewModelBase, IDisposable { - public ObservableCollection EyeSettings { get; set; } - public ObservableCollection JawSettings { get; set; } - public ObservableCollection MouthSettings { get; set; } - public ObservableCollection TongueSettings { get; set; } - public ObservableCollection NoseSettings { get; set; } - public ObservableCollection CheekSettings { get; set; } + public ParameterGroupCollection EyeMovementSettings { get; set; } + public ParameterGroupCollection EyeBlinkingSettings { get; set; } + public ParameterGroupCollection JawSettings { get; set; } + public ParameterGroupCollection MouthSettings { get; set; } + public ParameterGroupCollection TongueSettings { get; set; } + public ParameterGroupCollection NoseSettings { get; set; } + public ParameterGroupCollection CheekSettings { get; set; } [ObservableProperty] [property: SavedSetting("AppSettings_StabilizeEyes", false)] @@ -34,6 +37,7 @@ public partial class CalibrationViewModel : ViewModelBase, IDisposable private readonly Dictionary _eyeKeyIndexMap; private readonly Dictionary _faceKeyIndexMap; + public CalibrationViewModel() { _settingsService = Ioc.Default.GetService()!; @@ -41,35 +45,43 @@ public CalibrationViewModel() _parameterSenderService = Ioc.Default.GetService()!; _processingLoopService = Ioc.Default.GetService()!; - EyeSettings = + EyeMovementSettings = new ParameterGroupCollection("EyeMovement", new GroupFilterSettings(_settingsService, "Filter_EyeMovement", false, 0.1f, 0.1f), + [ + new("LeftEyeX", -1f, 1f, -1f, 1f), + new("LeftEyeY", -1f, 1f, -1f, 1f), + new("RightEyeX", -1f, 1f, -1f, 1f), + new("RightEyeY", -1f, 1f, -1f, 1f) + ]); + + EyeBlinkingSettings = new ParameterGroupCollection("EyeBlinking", new GroupFilterSettings(_settingsService, "Filter_EyeBlinking", false, 0.5f, 0.5f), [ new("LeftEyeLid"), new("RightEyeLid") - ]; + ]); - JawSettings = + JawSettings = new ParameterGroupCollection("Jaw", new GroupFilterSettings(_settingsService, "Filter_Jaw", false, 1.0f, 1.0f), [ new("JawOpen"), new("JawForward"), new("JawLeft"), new("JawRight") - ]; + ]); - CheekSettings = + CheekSettings = new ParameterGroupCollection("Cheek", new GroupFilterSettings(_settingsService, "Filter_Cheek", false, 1.0f, 1.0f), [ new("CheekPuffLeft"), new("CheekPuffRight"), new("CheekSuckLeft"), new("CheekSuckRight") - ]; + ]); - NoseSettings = + NoseSettings = new ParameterGroupCollection("Nose", new GroupFilterSettings(_settingsService, "Filter_Nose", false, 1.0f, 1.0f), [ new("NoseSneerLeft"), new("NoseSneerRight") - ]; + ]); - MouthSettings = + MouthSettings = new ParameterGroupCollection("Mouth", new GroupFilterSettings(_settingsService, "Filter_Mouth", false, 1.0f, 1.0f), [ new("MouthFunnel"), new("MouthPucker"), @@ -94,9 +106,9 @@ public CalibrationViewModel() new("MouthPressRight"), new("MouthStretchLeft"), new("MouthStretchRight") - ]; + ]); - TongueSettings = + TongueSettings = new ParameterGroupCollection("Tongue", new GroupFilterSettings(_settingsService, "Filter_Tongue", false, 1.0f, 1.0f), [ new("TongueOut"), new("TongueUp"), @@ -110,24 +122,24 @@ public CalibrationViewModel() new("TongueFlat"), new("TongueTwistLeft"), new("TongueTwistRight") - ]; + ]); - foreach (var setting in EyeSettings.Concat(JawSettings).Concat(CheekSettings) + foreach (var setting in EyeMovementSettings.Concat(EyeBlinkingSettings).Concat(JawSettings).Concat(CheekSettings) .Concat(NoseSettings).Concat(MouthSettings).Concat(TongueSettings)) { setting.PropertyChanged += OnSettingChanged; } - // Convert dictionary order into index mapping - _eyeKeyIndexMap = new Dictionary() + var allParameterGroups = new[] { EyeMovementSettings, EyeBlinkingSettings, JawSettings, + MouthSettings, TongueSettings, NoseSettings, CheekSettings }; + foreach (var group in allParameterGroups) { - { "LeftEyeX", 0 }, - { "LeftEyeY", 1 }, - { "RightEyeX", 3 }, - { "RightEyeY", 4 }, - { "LeftEyeLid", 2 }, - { "RightEyeLid", 5 } - }; + group.FilterSettings.PropertyChanged += OnFilterSettingChanged; + } + + _eyeKeyIndexMap = _parameterSenderService.EyeExpressionMap.Keys + .Select((key, index) => new { key, index }) + .ToDictionary(x => x.key, x => x.index); _faceKeyIndexMap = _parameterSenderService.FaceExpressionMap.Keys .Select((key, index) => new { key, index }) @@ -147,6 +159,7 @@ public CalibrationViewModel() _processingLoopService.ExpressionChangeEvent += ExpressionUpdateHandler; LoadInitialSettings(); + UpdateProcessingFilters(); _settingsService.Load(this); PropertyChanged += (o, p) => @@ -157,6 +170,7 @@ public CalibrationViewModel() _processingLoopService.EyesProcessingPipeline.StabilizeEyes = StabilizeEyes; } }; + } private void ExpressionUpdateHandler(ProcessingLoopService.Expressions expressions) @@ -173,7 +187,8 @@ private void ExpressionUpdateHandler(ProcessingLoopService.Expressions expressio if(expressions.EyeExpression != null) Dispatcher.UIThread.Post(() => { - ApplyCurrentEyeExpressionValues(expressions.EyeExpression, EyeSettings); + ApplyCurrentEyeExpressionValues(expressions.EyeExpression, EyeMovementSettings); + ApplyCurrentEyeExpressionValues(expressions.EyeExpression, EyeBlinkingSettings); }); } private void OnSettingChanged(object? sender, PropertyChangedEventArgs e) @@ -191,7 +206,7 @@ private void OnSettingChanged(object? sender, PropertyChangedEventArgs e) } } - private void ApplyCurrentEyeExpressionValues(float[] values, IEnumerable settings) + private void ApplyCurrentEyeExpressionValues(float[] values, ParameterGroupCollection settings) { foreach (var setting in settings) { @@ -208,7 +223,7 @@ private void ApplyCurrentEyeExpressionValues(float[] values, IEnumerable settings) + private void ApplyCurrentFaceExpressionValues(float[] values, ParameterGroupCollection settings) { foreach (var setting in settings) { @@ -241,7 +256,8 @@ public void ResetMaximums() private void LoadInitialSettings() { - LoadInitialSettings(EyeSettings); + LoadInitialSettings(EyeMovementSettings); + LoadInitialSettings(EyeBlinkingSettings); LoadInitialSettings(CheekSettings); LoadInitialSettings(JawSettings); LoadInitialSettings(MouthSettings); @@ -261,8 +277,79 @@ private void LoadInitialSettings(IEnumerable settings) } } + private void OnFilterSettingChanged(object? sender, PropertyChangedEventArgs e) + { + UpdateProcessingFilters(); + } + + private void UpdateProcessingFilters() + { + var faceGroupFilter = new GroupedOneEuroFilter(); + var eyeGroupFilter = new GroupedOneEuroFilter(); + + // Configure eye tracking filters + if (EyeMovementSettings.FilterSettings.Enabled) + { + var eyeMovementIndices = GetEyeParameterIndices(EyeMovementSettings); + if (eyeMovementIndices.Length > 0) + { + eyeGroupFilter.ConfigureGroup("EyeMovement", eyeMovementIndices, + EyeMovementSettings.FilterSettings.MinFreqCutoff, EyeMovementSettings.FilterSettings.SpeedCutoff); + } + } + + if (EyeBlinkingSettings.FilterSettings.Enabled) + { + var eyeBlinkingIndices = GetEyeParameterIndices(EyeBlinkingSettings); + if (eyeBlinkingIndices.Length > 0) + { + eyeGroupFilter.ConfigureGroup("EyeBlinking", eyeBlinkingIndices, + EyeBlinkingSettings.FilterSettings.MinFreqCutoff, EyeBlinkingSettings.FilterSettings.SpeedCutoff); + } + } + + // Configure face tracking filters + var faceParameterGroups = new[] { JawSettings, MouthSettings, TongueSettings, NoseSettings, CheekSettings }; + foreach (var group in faceParameterGroups) + { + if (group.FilterSettings.Enabled) + { + var indices = GetFaceParameterIndices(group); + if (indices.Length > 0) + { + faceGroupFilter.ConfigureGroup(group.GroupName, indices, + group.FilterSettings.MinFreqCutoff, group.FilterSettings.SpeedCutoff); + } + } + } + + _processingLoopService.FaceProcessingPipeline.Filter = faceGroupFilter; + _processingLoopService.EyesProcessingPipeline.Filter = eyeGroupFilter; + } + + private int[] GetFaceParameterIndices(ParameterGroupCollection group) + { + return group.Select(setting => _faceKeyIndexMap.TryGetValue(setting.Name, out var index) ? index : -1) + .Where(index => index >= 0) + .ToArray(); + } + + private int[] GetEyeParameterIndices(ParameterGroupCollection group) + { + return group.Select(setting => _eyeKeyIndexMap.TryGetValue(setting.Name, out var index) ? index : -1) + .Where(index => index >= 0) + .ToArray(); + } + public void Dispose() { - // _processingLoopService.ExpressionUpdateEvent -= ExpressionUpdateHandler; + _processingLoopService.ExpressionChangeEvent -= ExpressionUpdateHandler; + + var allParameterGroups = new[] { EyeMovementSettings, EyeBlinkingSettings, JawSettings, + MouthSettings, TongueSettings, NoseSettings, CheekSettings }; + foreach (var group in allParameterGroups) + { + group.FilterSettings.PropertyChanged -= OnFilterSettingChanged; + } } } diff --git a/src/Baballonia/Views/AppSettingsView.axaml b/src/Baballonia/Views/AppSettingsView.axaml index 121814d4..4859afe0 100644 --- a/src/Baballonia/Views/AppSettingsView.axaml +++ b/src/Baballonia/Views/AppSettingsView.axaml @@ -176,120 +176,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + ("LangCombo")!; _langComboBox.SelectionChanged += LangComboBox_SelectionChanged; - _selectedMinFreqCutoffUpDown = this.Find("SelectedMinFreqCutoffUpDown")!; - _selectedSpeedCutoffUpDown = this.Find("SelectedSpeedCutoffUpDown")!; - UpdateThemes(); if (_themeSelectorService.Theme is null) @@ -122,7 +117,6 @@ private void LangComboBox_SelectionChanged(object? sender, SelectionChangedEvent _languageSelectorService.SetLanguage(item!.Tag!.ToString()!); } - // Workaround for https://github.com/AvaloniaUI/Avalonia/issues/4460 private void UpdateThemes() { var selectedIndex = _themeComboBox.SelectedIndex; @@ -145,30 +139,5 @@ private void LaunchFirstTimeSetUp(object? sender, RoutedEventArgs e) break; } } - - private void SelectedSpeedCutoffComboBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - if (sender is not ComboBox comboBox) return; - - _selectedSpeedCutoffUpDown.Value = comboBox.SelectedIndex switch - { - 0 => 0.5m, - 1 => 1, - 2 => 2, - _ => _selectedSpeedCutoffUpDown.Value - }; - } - - private void SelectedMinFreqCutoffComboBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - if (sender is not ComboBox comboBox) return; - - _selectedMinFreqCutoffUpDown.Value = comboBox.SelectedIndex switch - { - 0 => 0.5m, - 1 => 1, - 2 => 2, - _ => _selectedMinFreqCutoffUpDown.Value - }; - } } + diff --git a/src/Baballonia/Views/CalibrationView.axaml b/src/Baballonia/Views/CalibrationView.axaml index 9afb6977..666a66df 100644 --- a/src/Baballonia/Views/CalibrationView.axaml +++ b/src/Baballonia/Views/CalibrationView.axaml @@ -13,6 +13,15 @@ x:DataType="splitViewPane:CalibrationViewModel" mc:Ignorable="d"> + + + + + @@ -56,17 +65,116 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -76,6 +184,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -153,6 +289,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -192,6 +355,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -231,6 +421,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -270,6 +487,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + +