白板軟體書寫速度是其最核心的功能,註冊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左右:
關鍵詞:白板書寫加速、高精度書寫預測、觸控式螢幕書寫效能