.NET 白板書寫加速-曲線擬合預測

唐宋元明清2188發表於2024-10-10

白板軟體書寫速度是其最核心的功能,註冊StylusPlugin從觸控執行緒拿觸控點資料並在另一UI執行緒繪製渲染是比較穩妥的方案,具體的可以檢視小夥伴德熙的2019-1-28-WPF-高效能筆 - lindexi - 部落格園 (cnblogs.com)

上面StylusPlugin方案能提升在大屏目前如富創通、華欣觸控框的主要產品版本上,1幀16ms左右的書寫效能。除了這個跳過一些流程來減少延時,我們還能繼續最佳化書寫效能麼?答案肯定是可以的

本文我們介紹下書寫加速的一類實現方向,透過預測下一個甚至N個點,提前繪製筆跡來降低書寫延遲。

曲線擬合預測

書寫預測,這裡介紹下曲線擬合的方案:

取N個點擬合成一條曲線,算出它的曲線公式,然後下一個點可以輸入它的X位置得到Y -- 以X方法或者Y方法為基準,擬合出以X為引數的曲線。

這裡採用的是開源元件MathNet.Numerics,也可以使用其它的擬合曲線方案,目標就是先輸出一條曲線公式。先安裝其Nuget包MathNet.Numerics:

<PackageReference Include="MathNet.Numerics" Version="5.0.0" />

引用using MathNet.Numerics;我們以X座標為基準預測Y座標值,已經寫好函式:

 1     private static Point[] PredictPoints(Point[] pointArray, int degree, int predictCount)
 2     {
 3         Debug.WriteLine("輸入:" + string.Join(",", pointArray.Select(i => $"({i.X},{i.Y})")));
 4         var xList = pointArray.Select(i => i.X).ToArray();
 5         var yList = pointArray.Select(i => i.Y).ToArray();
 6         var lastX = xList[xList.Length - 1];
 7         var lastX1 = xList[xList.Length - 2];
 8         var lastPointLength = lastX - lastX1;
 9         double[] parameters = Fit.Polynomial(xList, yList, degree);
10         var predictPoints = new Point[predictCount];
11         for (int i = 0; i < predictCount; i++)
12         {
13             var currentX = lastX + (i + 1) * lastPointLength;
14             var currentY = 0d;
15             for (int j = 0; j < degree + 1; j++)
16             {
17                 var parameterJ = parameters[j];
18                 for (int k = 0; k < j; k++)
19                 {
20                     parameterJ *= currentX;
21                 }
22 
23                 currentY += parameterJ;
24             }
25 
26             var newPoint = new Point(currentX, currentY);
27             predictPoints[i] = newPoint;
28         }
29         Debug.WriteLine("輸出:" + string.Join(",", predictPoints.Select(i => $"({i.X},{i.Y})")));
30         return predictPoints;
31     }

這裡的double[] parameters = Fit.Polynomial(xList, yList, degree),表示透過X以及Y系列資料,以階數degree(如二階曲線)計算出當前多項式引數值parameters。

如果degree是2階,可以計算得到y值:

y = parameters[0] + parameters[1]*x + parameters[2]*x^2

好,曲線公式有了,那下面就是塞x座標得到y值,也就是point。

如果是Y軸向上遞增的二隊曲線,如下圖,從左到右綠色點是已知點列表,黃色為預測的點。這裡我們依次預測4個點:

這類場景,預測結果是比較正常的。

我們再看看拋物線的場景,沿X方向Y座標值依次遞減,即順時針角度值增加,預測點如下:

從上圖可以看出,Y方向值遞減時預測結果是拋物線的另一半,Y軸值另一側反向加速遞減。但我們的期望肯定不是像這類拋物線,我們期望它能延著綠色軌跡向斜上方走。

為何它會快速彈回來呢?原因就是擬合的2階曲線,公式如此。

這類方法,只能做到一維的預測,即遇到X、Y方向值不變或者值變化較少時,曲線變化就會像拋物線一樣到達它的頂端後,就會快速彈回來。

以擬合成曲線公式來計算,會有一定缺陷。這個我們也是能最佳化的,上方的函式里我們是以X軸為基準,得到的公式中X為變化量,下面換一下以Y軸為基準:

 1     public static Point[] PredictPointsY(Point[] pointArray, int degree, int predictCount)
 2     {
 3         Debug.WriteLine("輸入:" + string.Join(",", pointArray.Select(i => $"({i.X},{i.Y})")));
 4         var xList = pointArray.Select(i => i.X).ToArray();
 5         var yList = pointArray.Select(i => i.Y).ToArray();
 6         var lastY = yList[yList.Length - 1];
 7         var lastY1 = yList[yList.Length - 2];
 8         var lastPointLength = lastY - lastY1;
 9         double[] parameters = Fit.Polynomial(yList, xList, degree);
10         var predictPoints = new Point[predictCount];
11         for (int i = 0; i < predictCount; i++)
12         {
13             var currentY = lastY + (i + 1) * lastPointLength;
14             var currentX = 0d;
15             for (int j = 0; j < degree + 1; j++)
16             {
17                 var parameterJ = parameters[j];
18                 for (int k = 0; k < j; k++)
19                 {
20                     parameterJ *= currentY;
21                 }
22 
23                 currentX += parameterJ;
24             }
25 
26             var newPoint = new Point(currentX, currentY);
27             predictPoints[i] = newPoint;
28         }
29         Debug.WriteLine("輸出:" + string.Join(",", predictPoints.Select(i => $"({i.X},{i.Y})")));
30         return predictPoints;
31     }

這裡的二階擬合曲線就變成了:

x = parameters[0] + parameters[1]*y + parameters[2]*y^2

同樣預測4個點,看下結果:

這個預測趨勢就符合我們的期望了。

所以上方X方向以及Y方向分別為基準,擬合二隊曲線,然後輸出預測點,我們綜合取一個比較適合的點即可。

如何取呢?可以看到我們期望的點是變化趨勢變化小的一個。即以最後倆個資料點的角度A為基準,預測點與最後資料點的向量角度B1與B2,順時針角度變化較小的點是我們期望輸出的。

曲線擬合其它問題-預測失準

上面我們解決了座標點場景,單方向擬合時輸出點快速彈回的問題。在驗證書寫時,我們還發現一類預測失準問題:

如上圖,黃色點往上偏了(黃色點是直線,並且角度不符合原有趨勢),真實期望我們是想要沿著原有角度減少的趨勢,預測點為角度略偏下的方向:

這裡預測失準的原因是,X方向值無法正常預測。因為按我們上面曲線擬合的方案,這類拋物線場景是以Y軸為基準,輸入Y得到X方向值,但按曲線變化的方向輸入一個最後倆點之間Y軸變化量,預測點的X值應該是接近無限大的,超出了曲線範圍。

這裡可以根據曲線點角度變化量來處理,看上圖點與點之間角度是按順時針依次增加的,所以預測出來的點也應該要繼續順時針增加角度。所以可以將輸出的點按最後倆點Point(n)、Point(n-1)之間向量角度值,或者再增加最後三點之間角度的差值angleChange。

我使用的是增加角度變化量,如下圖輸出效果:

曲線擬合其它問題-預測位置超出

我們做的畢竟是預測,預測點肯定不能完美代替真實書寫觸控點。尤其是當我們按照設定下一個預測點太遠,很大機率是會偏離原有曲線的

在上面PredictPoints預測函式內,使用了var lastPointLength = lastX - lastX1;來作為下一個預測點X方向的位置。但這其實是不符合實際情況的,因為你並不清楚下一個預測點也變化了 lastX - lastX1的X方向距離,如果強行用此X變化量確定預測點,預測點偏離曲線的機率會很大。那如何解決呢?

我的解決方法是,透過上面PredictPoints計算出下一預測點的角度變化量changedAnge即可,然後再以lastPoint-lastPoint1之間的距離作為半徑圍繞lastPoint進行旋轉180+changedAnge至一個新的點。這個新點作為最後預測點

為何以lastPoint-lastPoint1之間的距離作為半徑?因為在快速書寫過程中,觸控框幀率是固定的,書寫速度不怎麼變化的情況下,點與點之間的距離很接近,所以我們只要預測出下一點的changedAnge就行了。

1     var angleChanged = Math.Min(lastChangedAngle, Math.Max(angle1Changed, angle2Changed));
2     var rotateAngle = (180 + angleChanged).GetPositiveAngle();
3     var predictPoint = lastPoint1.Rotate(lastPoint, rotateAngle);

最後,我們按上面的方案驗證下真實書寫預測的效果。輸出書寫痕跡,我們換個顏色,藍色為真實點、紅色為預測點。

預測1個點,紅色與真實書寫曲線重合

預測2個點,紅色是第一個預測點,粉色是第二個預測點

這裡預測效果是在我的開發觸控式螢幕裝置(Dell P2418HT,1080p)上驗證的,看上面效果粉色點偏移的較多。在這臺觸控式螢幕上,不建議預測2個點

需要注意的是,書寫預測與螢幕報點率(幀率)強相關。一般情況下,輸出倆點之間間隔時間越長,倆點之間間距越大,預測點的誤差也會變大,但相應的預測距離變遠了即書寫延遲會降低很大。

我這Dell觸控式螢幕觸控移動時,輸入平均間隔33.3ms,一次輸入包含2-3個點,點平均間隔在16.6ms。倆點平均間隔16.6ms,說明觸控框是60fps報點。另外,詳細的觸控框報點率資料以及開啟WM_POINTER訊息提升應用層的觸控輸入幀率可以看下:白板書寫延遲-觸控式螢幕報點率 - 唐宋元明清2188 - 部落格園 (cnblogs.com)

根據上面觸控報點資料,上面書寫預測方案預測1個點,在這臺Dell觸控式螢幕上書寫延遲可以降低16.67ms。

也在140幀高報點率的大屏觸控框上試了,可以穩定預測2個點,即可降低延遲2*7=14ms左右:

關鍵詞:白板書寫加速、高精度書寫預測、觸控式螢幕書寫效能

相關文章