Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/Baballonia/Contracts/ICalibrationService.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
using Baballonia.Services.Calibration;
using System;
using System.Threading.Tasks;
using Baballonia.Services.Calibration;

namespace Baballonia.Contracts;

public interface ICalibrationService
{
bool AutoCalibrationEnabled { get; set; }

event Action? AutoCalibrationReset;

void SetExpression(string expression, float value);

CalibrationParameter GetExpressionSettings(string parameterName);
Expand All @@ -12,5 +18,5 @@ public interface ICalibrationService
void ResetValues();
void ResetMinimums();
void ResetMaximums();

void ResetAutoCalibration();
}
92 changes: 90 additions & 2 deletions src/Baballonia/Services/CalibrationService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
using Baballonia.Contracts;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Baballonia.Contracts;
using Baballonia.Services.Calibration;
using System.Collections.Concurrent;
using System.Collections.Generic;
Expand All @@ -7,6 +11,8 @@ namespace Baballonia.Services;

public class CalibrationService : ICalibrationService
{
private const float AutoCalSeed = 0.5f;

// Expression parameter names
private readonly Dictionary<string, string> _eyeExpressionMap = new()
{
Expand Down Expand Up @@ -75,17 +81,74 @@ public class CalibrationService : ICalibrationService
{ "TongueTwistRight", "/tongueTwistRight" }
};

private static readonly string[] FaceExpressionNames = ParameterSenderService.FaceExpressionMap.Keys.ToArray();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to pass the ParameterSenderService in through DI, so you don't have to access it statically here


// Eye lid expression names and their indices in the eye output array
private static readonly (string Name, int Index)[] EyeLidExpressions =
[
("LeftEyeLid", 2),
("RightEyeLid", 5)
];

private readonly ConcurrentDictionary<string, CalibrationParameter> _expressionSettings = new();

private readonly ILocalSettingsService _localSettingsService;
private readonly ProcessingLoopService _processingLoopService;

public CalibrationService(ILocalSettingsService localSettingsService)
public bool AutoCalibrationEnabled { get; set; }

public event Action? AutoCalibrationReset;

public CalibrationService(ILocalSettingsService localSettingsService, ProcessingLoopService processingLoopService)
{
_localSettingsService = localSettingsService;
_processingLoopService = processingLoopService;

_processingLoopService.ExpressionChangeEvent += OnExpressionChanged;

Load();
}

private void OnExpressionChanged(ProcessingLoopService.Expressions expressions)
{
if (!AutoCalibrationEnabled)
return;

// Auto-calibrate face expressions (indexed same as ParameterSenderService.FaceExpressionMap)
if (expressions.FaceExpression != null)
{
for (var i = 0; i < FaceExpressionNames.Length && i < expressions.FaceExpression.Length; i++)
{
var name = FaceExpressionNames[i];
var rawValue = expressions.FaceExpression[i];
if (_expressionSettings.TryGetValue(name, out var param))
{
if (rawValue > param.Upper) param.Upper = rawValue;
if (rawValue < param.Lower) param.Lower = rawValue;
}
}
}

// Auto-calibrate eye lid expressions
if (expressions.EyeExpression != null)
{
foreach (var (name, index) in EyeLidExpressions)
{
if (index >= expressions.EyeExpression.Length)
continue;

var rawValue = expressions.EyeExpression[index];
if (_expressionSettings.TryGetValue(name, out var param))
{
if (rawValue > param.Upper) param.Upper = rawValue;
if (rawValue < param.Lower) param.Lower = rawValue;
}
}
}

SaveAsync();
}

public void SetExpression(string expression, float value)
{
if (string.IsNullOrEmpty(expression))
Expand Down Expand Up @@ -195,4 +258,29 @@ public void ResetMaximums()
}
SaveAsync();
}

public void ResetAutoCalibration()
{
// Reset Lower/Upper to seed values for all face expressions and eye lids
foreach (var name in FaceExpressionNames)
{
if (_expressionSettings.TryGetValue(name, out var param))
{
param.Lower = AutoCalSeed;
param.Upper = AutoCalSeed;
}
}

foreach (var (name, _) in EyeLidExpressions)
{
if (_expressionSettings.TryGetValue(name, out var param))
{
param.Lower = AutoCalSeed;
param.Upper = AutoCalSeed;
}
}

SaveAsync();
AutoCalibrationReset?.Invoke();
}
}
10 changes: 5 additions & 5 deletions src/Baballonia/Services/ParameterSenderService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class ParameterSenderService : BackgroundService
private readonly ConcurrentQueue<OscMessage> _dfrQueue = new();

// Expression parameter names
private readonly Dictionary<string, string> _eyeExpressionMap = new()
public static readonly Dictionary<string, string> EyeExpressionMap = new()
{
{ "LeftEyeX", "/LeftEyeX" },
{ "LeftEyeY", "/LeftEyeY" },
Expand All @@ -43,7 +43,7 @@ public class ParameterSenderService : BackgroundService
//{ "RightEyeBrow", "/RightEyeBrow" },
};

public readonly Dictionary<string, string> FaceExpressionMap = new()
public static readonly Dictionary<string, string> FaceExpressionMap = new()
{
{ "CheekPuffLeft", "/cheekPuffLeft" },
{ "CheekPuffRight", "/cheekPuffRight" },
Expand Down Expand Up @@ -113,7 +113,7 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("Starting Parameter Sender Service...");
_logger.LogDebug("OSC parameter mapping initialized with {EyeCount} eye expressions and {FaceCount} face expressions",
_eyeExpressionMap.Count, FaceExpressionMap.Count);
EyeExpressionMap.Count, FaceExpressionMap.Count);

while (!cancellationToken.IsCancellationRequested)
{
Expand Down Expand Up @@ -145,10 +145,10 @@ private void ProcessEyeExpressionData(float[] expressions)
if (expressions is null) return;
if (expressions.Length == 0) return;

for (var i = 0; i < Math.Min(expressions.Length, _eyeExpressionMap.Count); i++)
for (var i = 0; i < Math.Min(expressions.Length, EyeExpressionMap.Count); i++)
{
var weight = expressions[i];
var eyeElement = _eyeExpressionMap.ElementAt(i);
var eyeElement = EyeExpressionMap.ElementAt(i);
var settings = _calibrationService.GetExpressionSettings(eyeElement.Key);

var msg = new OscMessage(_prefix + eyeElement.Value,
Expand Down
24 changes: 24 additions & 0 deletions src/Baballonia/ViewModels/SplitViewPane/AppSettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ public partial class AppSettingsViewModel : ViewModelBase
[property: SavedSetting("AppSettings_StabilizeEyes", true)]
private bool _stabilizeEyes;

[ObservableProperty]
[property: SavedSetting("AppSettings_AutoCalibrationEnabled", false)]
private bool _autoCalibrationEnabled;

public List<string> LowestLogLevel { get; } =
[
"Debug",
Expand All @@ -98,6 +102,7 @@ public partial class AppSettingsViewModel : ViewModelBase
private readonly FacePipelineManager _facePipelineManager;
private readonly EyePipelineManager _eyePipelineManager;
private readonly IIdentityService _identityService;
private readonly ICalibrationService _calibrationService;

public AppSettingsViewModel(
FacePipelineManager facePipelineManager,
Expand All @@ -114,8 +119,12 @@ public AppSettingsViewModel(
GithubService = Ioc.Default.GetService<GithubService>()!;
SettingsService = Ioc.Default.GetService<ILocalSettingsService>()!;
_logger = Ioc.Default.GetService<ILogger<AppSettingsViewModel>>()!;
_calibrationService = Ioc.Default.GetService<ICalibrationService>()!;
SettingsService.Load(this);

// Sync persisted auto-calibration state to service
_calibrationService.AutoCalibrationEnabled = AutoCalibrationEnabled;

// Handle edge case where OSC port is used and the system freaks out
if (OscTarget.OutPort == 0)
{
Expand Down Expand Up @@ -195,6 +204,21 @@ partial void OnSteamvrAutoStartChanged(bool value)
}
}

partial void OnAutoCalibrationEnabledChanged(bool value)
{
if (value)
{
// Reset seeds BEFORE enabling tracking, so the UI can
// refresh to show 0.5 seed values before they get pushed
_calibrationService.ResetAutoCalibration();
_calibrationService.AutoCalibrationEnabled = true;
}
else
{
_calibrationService.AutoCalibrationEnabled = false;
}
}

async partial void OnUseGPUChanged(bool value)
{
var prev = SettingsService.ReadSetting("AppSettings_UseGPU", value);
Expand Down
39 changes: 36 additions & 3 deletions src/Baballonia/ViewModels/SplitViewPane/CalibrationViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ public partial class CalibrationViewModel : ViewModelBase, IDisposable

private ILocalSettingsService _settingsService { get; }
private readonly ICalibrationService _calibrationService;
private readonly ParameterSenderService _parameterSenderService;
private readonly ProcessingLoopService _processingLoopService;
private readonly EyePipelineManager _eyePipelineManager;

Expand All @@ -35,7 +34,6 @@ public CalibrationViewModel(EyePipelineManager eyePipelineManager)
_eyePipelineManager = eyePipelineManager;
_settingsService = Ioc.Default.GetService<ILocalSettingsService>()!;
_calibrationService = Ioc.Default.GetService<ICalibrationService>()!;
_parameterSenderService = Ioc.Default.GetService<ParameterSenderService>()!;
_processingLoopService = Ioc.Default.GetService<ProcessingLoopService>()!;

EyeSettings =
Expand Down Expand Up @@ -138,7 +136,7 @@ public CalibrationViewModel(EyePipelineManager eyePipelineManager)
//{ "RightEyeBrow", },
};

_faceKeyIndexMap = _parameterSenderService.FaceExpressionMap.Keys
_faceKeyIndexMap = ParameterSenderService.FaceExpressionMap.Keys
.Select((key, index) => new { key, index })
.ToDictionary(x => x.key, x => x.index);

Expand All @@ -154,11 +152,23 @@ public CalibrationViewModel(EyePipelineManager eyePipelineManager)
};

_processingLoopService.ExpressionChangeEvent += ExpressionUpdateHandler;
_calibrationService.AutoCalibrationReset += OnAutoCalibrationReset;

LoadInitialSettings();
_settingsService.Load(this);
}

private void OnAutoCalibrationReset()
{
// Called on the UI thread from the toggle handler.
// Must run synchronously so sliders show the 0.5 seed values
// BEFORE auto-cal tracking is enabled and pushes them.
if (Dispatcher.UIThread.CheckAccess())
LoadInitialSettings();
else
Dispatcher.UIThread.Post(LoadInitialSettings);
}

private void ExpressionUpdateHandler(ProcessingLoopService.Expressions expressions)
{
if(expressions.FaceExpression != null)
Expand Down Expand Up @@ -198,6 +208,8 @@ private void ApplyCurrentEyeExpressionValues(float[] values, IEnumerable<SliderB
if (_eyeKeyIndexMap.TryGetValue(setting.Name, out var index)
&& index < values.Length)
{
RefreshAutoCalibrationUpper(setting);

var weight = values[index];
var val = Math.Clamp(
weight.Remap(setting.Lower, setting.Upper, setting.Min, setting.Max),
Expand All @@ -215,6 +227,8 @@ private void ApplyCurrentFaceExpressionValues(float[] values, IEnumerable<Slider
if (_faceKeyIndexMap.TryGetValue(setting.Name, out var index)
&& index < values.Length)
{
RefreshAutoCalibrationUpper(setting);

var weight = values[index];
var val = Math.Clamp(
weight.Remap(setting.Lower, setting.Upper, setting.Min, setting.Max),
Expand All @@ -225,6 +239,25 @@ private void ApplyCurrentFaceExpressionValues(float[] values, IEnumerable<Slider
}
}

private void RefreshAutoCalibrationUpper(SliderBindableSetting setting)
{
if (!_calibrationService.AutoCalibrationEnabled)
return;

var calParam = _calibrationService.GetExpressionSettings(setting.Name);
var upperChanged = Math.Abs(setting.Upper - calParam.Upper) > 0.0001f;
var lowerChanged = Math.Abs(setting.Lower - calParam.Lower) > 0.0001f;

if (!upperChanged && !lowerChanged)
return;

// Temporarily unhook to avoid circular writes back to CalibrationService
setting.PropertyChanged -= OnSettingChanged;
if (upperChanged) setting.Upper = calParam.Upper;
if (lowerChanged) setting.Lower = calParam.Lower;
setting.PropertyChanged += OnSettingChanged;
}

[RelayCommand]
public void ResetMinimums()
{
Expand Down
6 changes: 6 additions & 0 deletions src/Baballonia/Views/AppSettingsView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,12 @@
</Grid>
</Expander>

<controls:SettingsToggle
Title="Auto Calibration"
Description="Automatically adjust calibration ranges based on your expressions"
IconPath="DataBarVerticalRegular"
SettingToggled="{Binding AutoCalibrationEnabled, Mode=TwoWay}" />

<controls:SettingsToggle
Title="{x:Static assets:Resources.Settings_GPU_Header}"
Description="{x:Static assets:Resources.Settings_GPU_Description}"
Expand Down
2 changes: 1 addition & 1 deletion src/HyperText.Avalonia
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happened here btw