Add ExpASFilter

It uses ML techniques to process tablet input and achieve three objectives depending on configuration: smoothing, latency compensation, and noise reduction.
This commit is contained in:
X9VoiD 2020-07-23 06:27:17 +08:00
parent 0543da0a9f
commit ec881bd0c5
No known key found for this signature in database
GPG key ID: 3AC5DBB8A2717CFF
3 changed files with 346 additions and 0 deletions

View file

@ -0,0 +1,322 @@
using ExpASFilter;
using MathNet.Numerics;
using MathNet.Numerics.LinearRegression;
using System;
using System.Collections.Generic;
using TabletDriverPlugin;
using TabletDriverPlugin.Attributes;
using TabletDriverPlugin.Tablet;
namespace OpenTabletDriverPlugins
{
[PluginName("Experimental AS Filter")]
public class ExperimentalASFilter : Notifier, IFilter
{
public virtual Point Filter(Point point)
{
DateTime date = DateTime.Now;
CalcReportRate(date);
var reportRateAvg = CalcReportRateAvg();
if (AddTimeSeriesPoint(point, date))
{
var predicted = new Point();
var timeMatrix = ConstructTimeDesignMatrix();
double[] x, y;
if (Normalize)
{
x = ConstructNormalizedTargetMatrix(Axis.X);
y = ConstructNormalizedTargetMatrix(Axis.Y);
}
else
{
x = ConstructTargetMatrix(Axis.X);
y = ConstructTargetMatrix(Axis.Y);
}
Polynomial xCoeff, yCoeff;
if (Weighted)
{
var weights = CalcWeight();
xCoeff = new Polynomial(Fit.PolynomialWeighted(timeMatrix, x, weights, Degree));
yCoeff = new Polynomial(Fit.PolynomialWeighted(timeMatrix, y, weights, Degree));
}
else
{
xCoeff = new Polynomial(Fit.Polynomial(timeMatrix, x, Degree, DirectRegressionMethod.Svd));
yCoeff = new Polynomial(Fit.Polynomial(timeMatrix, y, Degree, DirectRegressionMethod.Svd));
}
double predictAhead;
if (Sync)
predictAhead = 1000.0 / reportRateAvg * Ahead;
else
predictAhead = (date - _timeSeriesPoints.First.Value.Date).TotalMilliseconds + Compensation;
predicted.X = (float)xCoeff.Evaluate(predictAhead);
predicted.Y = (float)yCoeff.Evaluate(predictAhead);
if (Normalize)
{
predicted.X *= ScreenWidth;
predicted.Y *= ScreenHeight;
}
var now = DateTime.Now;
if ((now - date).TotalMilliseconds > CalcReportRateAvg())
Log.Write("ExpASFilter", now + ": CPU choking hard. Latency higher than normal. We missed a hz.");
_lastTime = date;
return predicted;
}
_lastTime = date;
return point;
}
#region Private Functions
private bool AddTimeSeriesPoint(Point point, DateTime time)
{
_timeSeriesPoints.AddLast(new TimeSeriesPoint(point, time));
if (_timeSeriesPoints.Count > Samples)
_timeSeriesPoints.RemoveFirst();
if (_timeSeriesPoints.Count == Samples)
return true;
return false;
}
private double[] ConstructTimeDesignMatrix()
{
DateTime baseTime = _timeSeriesPoints.First.Value.Date;
var data = new double[Samples];
var index = -1;
foreach (var timePoint in _timeSeriesPoints)
{
++index;
data[index] = (timePoint.Date - baseTime).TotalMilliseconds;
}
return data;
}
private double[] ConstructTargetMatrix(Axis axis)
{
var points = new double[Samples];
var index = -1;
if (axis == Axis.X)
foreach (var timePoint in _timeSeriesPoints)
{
points[++index] = timePoint.Point.X;
}
else if (axis == Axis.Y)
foreach (var timePoint in _timeSeriesPoints)
{
points[++index] = timePoint.Point.Y;
}
return points;
}
private double[] ConstructNormalizedTargetMatrix(Axis axis)
{
var points = new double[Samples];
var index = -1;
if (axis == Axis.X)
foreach (var timePoint in _timeSeriesPoints)
{
points[++index] = timePoint.Point.X / ScreenWidth;
}
else if (axis == Axis.Y)
foreach (var timePoint in _timeSeriesPoints)
{
points[++index] = timePoint.Point.Y / ScreenHeight;
}
return points;
}
private void CalcReportRate(DateTime now)
{
_reportRate = 1000.0 / (now - _lastTime).TotalMilliseconds;
_reportRateAvg.AddLast(_reportRate);
if (_reportRateAvg.Count > 10)
_reportRateAvg.RemoveFirst();
}
private double CalcReportRateAvg()
{
double avg = 0;
foreach (var sample in _reportRateAvg)
avg += sample;
return avg / _reportRateAvg.Count;
}
private double[] CalcWeight()
{
var weights = new List<double>();
var weightsNormalized = new List<double>();
double weight = 1;
foreach (var point in _timeSeriesPoints)
weights.Add(weight *= 2);
foreach (var _weight in weights)
weightsNormalized.Add(_weight / weights[^1]);
return weightsNormalized.ToArray();
}
#endregion Private Functions
private int _samples = 4, _degree = 1, _ahead = 6;
private LinkedList<TimeSeriesPoint> _timeSeriesPoints = new LinkedList<TimeSeriesPoint>();
private LinkedList<double> _reportRateAvg = new LinkedList<double>();
private double _reportRate;
private DateTime _lastTime = DateTime.Now;
private double _compensation;
private bool _normalize, _sync, _weighted;
private int _screenWidth, _screenHeight;
#region Controls
[UnitProperty("Compensation", "ms")]
public double Compensation
{
set => CompensationFunc(ref _compensation, value);
get => _compensation;
}
[Property("Samples")]
public int Samples
{
set => SamplesFunc(ref _samples, value);
get => _samples;
}
[Property("Degree")]
public int Degree
{
set => DegreeFunc(ref _degree, value);
get => _degree;
}
[BooleanProperty("Normalize", "Preprocess the input before performing regression. (may improve accuracy) Set Screen Dimensions below when enabling Normalization.")]
public bool Normalize
{
set => RaiseAndSetIfChanged(ref _normalize, value);
get => _normalize;
}
[UnitProperty("Screen Width", "px")]
public int ScreenWidth
{
set => RaiseAndSetIfChanged(ref _screenWidth, value);
get => _screenWidth;
}
[UnitProperty("Screen Height", "px")]
public int ScreenHeight
{
set => RaiseAndSetIfChanged(ref _screenHeight, value);
get => _screenHeight;
}
[BooleanProperty("Synchronize to Report Rate", "Compensation will be ignored in respect for tablet's report rate. Set below how many tablet reports to predict ahead.")]
public bool Sync
{
set => RaiseAndSetIfChanged(ref _sync, value);
get => _sync;
}
[Property("Reports Ahead")]
public int Ahead
{
set => RaiseAndSetIfChanged(ref _ahead, value);
get => _ahead;
}
[BooleanProperty("Exponential Weighted Polynomial Regression", "Well...")]
public bool Weighted
{
set => RaiseAndSetIfChanged(ref _weighted, value);
get => _weighted;
}
#endregion Controls
#region Control Utility Functions
private void CompensationFunc(ref double a, double value)
{
if (value > 30)
Log.Write("ExpASFilter", "Unrealistic latency compensation. [Compensation: " + value + "ms]", true);
RaiseAndSetIfChanged(ref a, value);
}
private void SamplesFunc(ref int a, int value)
{
int minimum = Degree + 1;
var suggested = Math.Pow(Degree, 2);
if (value > 12)
Log.Write("ExpASFilter",
"Samples too high. Expect higher latency." +
"[Samples: " + value + "]", true);
else if (value <= minimum)
{
Log.Write("ExpASFilter",
"Samples too low for selected degree." +
"[Samples: " + value + ", Required Minimum: " + minimum + "]", true);
RaiseAndSetIfChanged(ref a, minimum);
return;
}
if (value <= suggested)
Log.Write("ExpASFilter",
"Samples might not be enough for an accurate prediction." +
"[Samples: " + value + ", Suggestion: " + suggested + "]");
RaiseAndSetIfChanged(ref a, value);
}
private void DegreeFunc(ref int a, int value)
{
if (value == 0)
{
Log.Write("ExpASFilter", "Degree cannot be zero", true);
}
else if (value > 5)
Log.Write("ExpASFilter", "Degree too high, might cause instability and inaccuracy issues" +
"[Suggestion: Degree <= 5]");
RaiseAndSetIfChanged(ref a, value);
}
#endregion Control Utility Functions
public FilterStage FilterStage => FilterStage.PostTranspose;
}
}
namespace ExpASFilter
{
enum Axis {
X,
Y
}
public class TimeSeriesPoint
{
public TimeSeriesPoint(Point point, DateTime date)
{
Point = point;
Date = date;
}
public Point Point { get; }
public DateTime Date { get; }
}
}

View file

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.1; net5.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<CopyOutputSymbolsToOutputDirectory>false</CopyOutputSymbolsToOutputDirectory>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MathNet.Numerics" Version="4.11.0" />
<PackageReference Include="TabletDriverPlugin" Version="0.3.0" />
</ItemGroup>
</Project>

View file

@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.30309.148
MinimumVisualStudioVersion = 15.0.26124.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdvancedAntiSmoothingFilter", "AdvancedAntiSmoothingFilter\AdvancedAntiSmoothingFilter.csproj", "{57B25DD4-4DBC-4A63-84D0-7C50E33D3185}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExperimentalASFilter", "ExperimentalASFilter\ExperimentalASFilter.csproj", "{6421C523-5A5A-4F52-BB6B-A35F2381AA6E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -15,6 +17,10 @@ Global
{57B25DD4-4DBC-4A63-84D0-7C50E33D3185}.Debug|Any CPU.Build.0 = Debug|Any CPU
{57B25DD4-4DBC-4A63-84D0-7C50E33D3185}.Release|Any CPU.ActiveCfg = Release|Any CPU
{57B25DD4-4DBC-4A63-84D0-7C50E33D3185}.Release|Any CPU.Build.0 = Release|Any CPU
{6421C523-5A5A-4F52-BB6B-A35F2381AA6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6421C523-5A5A-4F52-BB6B-A35F2381AA6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6421C523-5A5A-4F52-BB6B-A35F2381AA6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6421C523-5A5A-4F52-BB6B-A35F2381AA6E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE