From ec881bd0c5fd6d2766ff69d552d7a883b5df5432 Mon Sep 17 00:00:00 2001 From: X9VoiD Date: Thu, 23 Jul 2020 06:27:17 +0800 Subject: [PATCH] Add ExpASFilter It uses ML techniques to process tablet input and achieve three objectives depending on configuration: smoothing, latency compensation, and noise reduction. --- ExperimentalASFilter/ExperimentalASFilter.cs | 322 ++++++++++++++++++ .../ExperimentalASFilter.csproj | 18 + OTDPlugins.sln | 6 + 3 files changed, 346 insertions(+) create mode 100644 ExperimentalASFilter/ExperimentalASFilter.cs create mode 100644 ExperimentalASFilter/ExperimentalASFilter.csproj diff --git a/ExperimentalASFilter/ExperimentalASFilter.cs b/ExperimentalASFilter/ExperimentalASFilter.cs new file mode 100644 index 0000000..0519c8e --- /dev/null +++ b/ExperimentalASFilter/ExperimentalASFilter.cs @@ -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(); + var weightsNormalized = new List(); + 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 _timeSeriesPoints = new LinkedList(); + private LinkedList _reportRateAvg = new LinkedList(); + 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; } + + } +} \ No newline at end of file diff --git a/ExperimentalASFilter/ExperimentalASFilter.csproj b/ExperimentalASFilter/ExperimentalASFilter.csproj new file mode 100644 index 0000000..53032de --- /dev/null +++ b/ExperimentalASFilter/ExperimentalASFilter.csproj @@ -0,0 +1,18 @@ + + + + netcoreapp3.1; net5.0 + + + + none + false + false + + + + + + + + diff --git a/OTDPlugins.sln b/OTDPlugins.sln index 29e0be2..97f4d83 100644 --- a/OTDPlugins.sln +++ b/OTDPlugins.sln @@ -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