会议白板之书写加速-曲线拟合预测
本文经原作者授权以原创方式二次分享,欢迎转载、分享。
原文作者:唐宋元明清
原文地址:https://www.cnblogs.com/kybs0/p/18442316
白板软件书写速度是其最核心的功能,注册StylusPlugin
从触摸线程拿触摸点数据并在另一UI
线程绘制渲染是比较稳妥的方案,具体的可以查看小伙伴德熙的2019-1-28-WPF-高性能笔 - lindexi - 博客园 (cnblogs.com)[1]
上面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坐标值,已经写好函数:
private static Point[] PredictPoints(Point[] pointArray, int degree, int predictCount)
{
Debug.WriteLine("输入:" + string.Join(",", pointArray.Select(i => $"({i.X},{i.Y})")));
var xList = pointArray.Select(i => i.X).ToArray();
var yList = pointArray.Select(i => i.Y).ToArray();
var lastX = xList[xList.Length - 1];
var lastX1 = xList[xList.Length - 2];
var lastPointLength = lastX - lastX1;
double[] parameters = Fit.Polynomial(xList, yList, degree);
var predictPoints = new Point[predictCount];
for (int i = 0; i < predictCount; i++)
{
var currentX = lastX + (i + 1) * lastPointLength;
var currentY = 0d;
for (int j = 0; j < degree + 1; j++)
{
var parameterJ = parameters[j];
for (int k = 0; k < j; k++)
{
parameterJ *= currentX;
}
currentY += parameterJ;
}
var newPoint = new Point(currentX, currentY);
predictPoints[i] = newPoint;
}
Debug.WriteLine("输出:" + string.Join(",", predictPoints.Select(i => $"({i.X},{i.Y})")));
return predictPoints;
}
这里的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
轴为基准:
public static Point[] PredictPointsY(Point[] pointArray, int degree, int predictCount)
{
Debug.WriteLine("输入:" + string.Join(",", pointArray.Select(i => $"({i.X},{i.Y})")));
var xList = pointArray.Select(i => i.X).ToArray();
var yList = pointArray.Select(i => i.Y).ToArray();
var lastY = yList[yList.Length - 1];
var lastY1 = yList[yList.Length - 2];
var lastPointLength = lastY - lastY1;
double[] parameters = Fit.Polynomial(yList, xList, degree);
var predictPoints = new Point[predictCount];
for (int i = 0; i < predictCount; i++)
{
var currentY = lastY + (i + 1) * lastPointLength;
var currentX = 0d;
for (int j = 0; j < degree + 1; j++)
{
var parameterJ = parameters[j];
for (int k = 0; k < j; k++)
{
parameterJ *= currentY;
}
currentX += parameterJ;
}
var newPoint = new Point(currentX, currentY);
predictPoints[i] = newPoint;
}
Debug.WriteLine("输出:" + string.Join(",", predictPoints.Select(i => $"({i.X},{i.Y})")));
return predictPoints;
}
这里的二阶拟合曲线就变成了:
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
就行了。
var angleChanged = Math.Min(lastChangedAngle, Math.Max(angle1Changed, angle2Changed));
var rotateAngle = (180 + angleChanged).GetPositiveAngle();
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)[2]
根据上面触摸报点数据,上面书写预测方案预测1
个点,在这台Dell
触摸屏上书写延迟可以降低16.67ms
。
也在140
帧高报点率的大屏触摸框上试了,可以稳定预测2
个点,即可降低延迟2*7=14ms
左右:
2019-1-28-WPF-高性能笔 - lindexi - 博客园 (cnblogs.com): https://www.cnblogs.com/lindexi/p/12085552.html
[2]白板书写延迟-触摸屏报点率 - 唐宋元明清2188 - 博客园 (cnblogs.com): https://www.cnblogs.com/kybs0/p/18453947