简介:面向图形图像处理与数字艺术开发者,这份C#源码项目聚焦毛笔书写效果的计算机模拟,完整涵盖毛笔字算法、笔锋算法与平滑算法三大模块,并提供可直接编译运行的工程。资源包共47个文件、约809KB,以.cs源码为核心,配合test.sln解决方案、Windows窗体界面文件、示例图片(png/jpg)以及编译后的exe与配置文件,便于边阅读边运行调试,快速理解算法设计与实现思路。算法部分覆盖笔触随压力与速度的粗细变化、笔尖形态计算、边缘去锯齿和轮廓平滑等关键环节,描述中具体解析了高斯滤波、双线性插值等平滑方法在笔迹上的应用,通过Form1等界面代码还可实时调整参数观察不同效果。包内源码按功能拆分为多个类,结构清晰,方便二次扩展。目前已有715人学习浏览,适合数字书法研究者、游戏UI特效开发者及对笔触模拟感兴趣的C#程序员,有助于掌握将物理模型与图像处理融入实际程序的做法。
1. 从坐标点到墨迹:C#毛笔字算法到底在算什么
把鼠标随手画的一个“永”字放大看,笔画边缘是圆的、粗细从头到尾都一样,这就是没做毛笔字算法的效果。毛笔字算法拆开是三件事:平滑算法去掉采样抖动,笔锋算法还原起笔收笔的形态,最后沿中心线生成轮廓填充成墨迹。这份C# 源码可编译,意味着不靠图形学黑匣子也能在.NET里跑通。
这类算法最常落在手写签名、书法练习App、字帖工具上。图形库自带的Pen只能做等宽描边,永远画不出尖头;想实现起笔藏锋、收笔出锋,只能把每个采样点的宽度算出来,再生成闭合轮廓。下面沿着数据结构、笔锋、平滑、轮廓、避坑、验证六步展开。
适合三类人:会用C#画线但不知道怎么生成笔宽的入门者,给手写板做笔迹还原的开发者,想把轨迹存成SVG而不是截图的工具作者。
2. 把笔锋拆成可计算的东西:压力、速度与端点形态
2.1 笔锋的本质是“接触面积变化”,不是贴一张尖角贴图
笔锋不是一个固定的三角形状。观察一支毛笔在纸面落下去,笔尖先接触纸,面积很小;下压后笔肚摊开,面积变大;收笔时逐渐提笔,接触面积又缩小。这一过程的数学投影就是“笔宽随笔画进度变化”:起笔处宽度从0涨到正常值,收笔处从正常值落到0,中间段宽度由压感和速度共同决定。如果算法直接按固定宽度描边,出来的就是圆头记号笔,这不是渲染问题,是宽度模型问题。
开发上最常见做法是:每个采样点只保留“横坐标、纵坐标、时间、压感”四个值,然后根据压感或速度算出一个瞬时宽度。最后用这段宽度序列生成包络线。这样做的好处是算法本身不依赖任何第三方渲染,宽度和轮廓都是纯数学,后面想输出SVG、位图还是GDI路径全都一样。这是理解后面所有代码的前提:笔锋不是“画一个三角形”,而是“让笔宽函数沿路径变化”。
实际调笔锋时,你会在屏幕上看到两种极端:一种是把起笔收笔做成两个机械三角形,转折处有明显的折线;另一种则是完全没变化,整笔粗细一致。前者问题在于宽度从0突变到正常值,后者问题在于宽度模型根本没有参与计算。真正接近毛笔的写法,宽度变化应当是连续的,变化率由运笔速度决定。
2.2 用C#定义一笔的最小数据结构
手写轨迹在C#里最常见的表示是一个struct数组。不要用PointF数组,因为PointF只有坐标,装不下计算笔锋必需的压感、时间戳和宽度。我一般会定一个StrokePoint,把一笔的采样信息全放进去:
// 一笔上的采样点,坐标、时刻、压感、最终笔宽各占一个字段 public struct StrokePoint { public float X; public float Y; public long Ticks; // 采样时刻,用于算速度 public float Pressure; // 0~1;设备没有压感时可写0.5 public float Width; // 由压感/速度映射出的瞬时参考宽度,0表示悬空 }再用一个StrokeBuilder把它串起来:
public class StrokeBuilder { private readonly List<StrokePoint> _points = new(); public void AddPoint(float x, float y, long ticks, float pressure = 0.5f) { _points.Add(new StrokePoint { X = x, Y = y, Ticks = ticks, Pressure = pressure, Width = 0f }); } public StrokePoint[] Build() => _points.ToArray(); }这段代码本身没有图形逻辑,但它决定了后面所有算法能拿到什么信息。关键在Ticks字段必须带上,因为鼠标、触摸屏、手写板采样不是均匀的,只有用时间差才能算出真实速度。Pressure默认0.5是给没有压感的鼠标一个居中值,后面用速度去修正它。Width先留0,放到笔锋阶段统一赋值。
很多C#入门者在第一步直接用List ,到调笔锋算法时发现还缺速度、缺宽度,只能反过来改全部代码,这是最常见的返工点。数据结构多两个字段,后面能省一天调试时间。如果你的输入源是触摸屏且系统已经给了Pressure,直接写入;如果没有,就按0.5占位,不要在这个阶段做归一化,留到宽度映射统一处理。
2.3 平滑算法为什么必须在计算笔锋之前跑
一笔轨迹从输入设备出来是带抖动的。鼠标有微颤,手写板有电磁噪声,触摸屏更夸张,坐标每次都可能偏几个像素。如果直接拿原始点算相邻方向,每个点的切线方向会跳来跳去;笔锋算法又要根据方向生成法线,方向一跳,笔画边缘就像被狗啃过。
平滑算法要解决的是两类抖:坐标抖和宽度抖。坐标抖用插值或滤波处理,宽度抖用滑动平均处理。顺序不能反:先平滑坐标,再算宽度,然后再平滑宽度。凡是先算宽度再平滑坐标的,经常出现起笔收笔的位置对不上,后面渲染出缺口。
这里的“先平滑坐标”也不是无条件全局平滑。判断是否需要平滑,先看相邻三点夹角:夹角越小,说明轨迹越顺滑;频繁出现大于45度的跳变,才是需要处理的抖动。我一般在进入笔锋算法前保留一份原始点数组,平滑后的点数组单独存放,这样一旦发现平滑把转折点削掉,还能回过头来比对。这一步做好了,第3章的宽度计算才有意义。
3. 用C#实现笔锋算法:核心代码与3个必须调的参数
3.1 用速度映射笔宽:没有压感时的第一选择
在鼠标设备上,Pressure字段没有真实值,只能靠速度近似。逻辑是:运笔越快,笔与纸的接触面积越小;运笔越慢,下压越重,笔画越粗。这个近似不算精确,但对签名、批注类应用足够。把前一步得到的点数组过一遍,对相邻两个点算距离,除以时间差得到速度。
// 把采样点数组里的每个 Width 按速度或压感算出来 static void MapWidthFromSpeed(StrokePoint[] pts, float minW, float maxW, float speedNorm) { for (int i = 0; i < pts.Length; i++) { if (pts[i].Pressure > 0.01f) { // 有压感直接用压感,速度只做微弱干扰 pts[i].Width = minW + (maxW - minW) * pts[i].Pressure; continue; } int prev = Math.Max(i - 1, 0); int next = Math.Min(i + 1, pts.Length - 1); float dist = Distance(pts[prev], pts[next]); float dtMs = Math.Max(1, pts[next].Ticks - pts[prev].Ticks); float speed = dist / dtMs; // 像素/毫秒 float k = Math.Clamp(speed / speedNorm, 0f, 1f); pts[i].Width = maxW - (maxW - minW) * k; // 速度快 -> 窄 } }参数上最重要的就是speedNorm。这里用“移动距离除以时间差”,单位可以理解为像素/毫秒。speedNorm越小,速度对笔宽越敏感;我一般取0.5到2.0之间。鼠标快速甩过去时速度能到3以上,慢写时只有0.1左右。如果你发现整支笔画都变成同样粗细,就把speedNorm调小;如果笔画忽粗忽细像心电图,就调大。minW和maxW分别代表最细和最粗的绝对像素值,建议minW不低于1.5像素,否则抗锯齿后容易断线。
注意,这里用的是“前后两点之间的距离除以总时间”,而不是当前点到下一点。这样能避免单个抖动点造成的速度毛刺。压感设备和鼠标混用场景里,压感分支要保留,否则同一支笔在数位板上写,宽度会被速度二次干扰,出现“写得快反而断水”的假象。
3.2 起笔收笔的锥度包络:让笔画两端“尖”起来
速度映射已经让笔画粗细有了变化,但两端仍然是宽度落在最大值或最小值上,形不成尖头。典型表现为横折处粗,起笔处却是圆头。解决办法是加一个“锥度包络”,把整个笔画进度t从0到1映射成0~1的缩放系数,起笔从0长到1,收笔从1跌到0。
// t 在 [0,1],headLen 是起笔锥度占比,tailLen 是收笔锥度占比 static float Envelope(float t, float headLen, float tailLen) { if (t < headLen) return t / headLen; // 起笔:从0线性升到1 if (t > 1f - tailLen) return (1f - t) / tailLen; // 收笔:从1线性降到0 return 1f; }调用时机放在MapWidth之后:
float realW = pts[i].Width * Envelope(t, headLen, tailLen);t怎么算?遍历整个点数组,把当前点下标除以总点数得到一个近似进度。更精确的做法是累计相邻点距离,用走过的弧长除以总弧长,因为光标不会匀速移动,按点数算出来的进度会失真。弧长计算在拐弯多的笔画上尤其重要,否则收笔锥度可能落在笔画中段而不是真正收笔处。
headLen和tailLen是算法里最直观的两个参数,按笔画总长度的比例传。想写“藏锋”效果,headLen给0.08到0.12,起笔有一个短促的渐宽;想写“露锋”效果,headLen给0.02,起笔几乎瞬间到正常宽度。tailLen要配合收笔速度,写悬针竖时tailLen取0.15以上,收笔尖能拉出来;写圆转笔画时tailLen设0.05以下,更像是垂露。参数不要超过0.25,超过后整个笔画一大半是锥度,字会像纺锤。
锥度包络也不只用线性映射,常见做法是中间加一个缓动:起笔用EaseOut,收笔用EaseIn。好处是起笔宽度变化自然,不会出现一个明显的“钉子头”。但入门先线性就够了,把线性跑通,再去替换曲线函数。
3.3 出锋:在收笔处延长轨迹制造“锋尖”
锥度包络只能把收笔变细,但锋尖是顺着笔画方向往前多“拖”出来的一小段。古人说出锋是“提笔运行”,指尖离开纸面时笔毫还要滑一段。纯靠包络做不到,因为原始点已经结束了。常见做法是:取最后两个点的方向,沿该方向额外补几个宽度递减的点。
// 在收笔处追加4个点,宽度从 tailWidth 线性降到 0 static List<StrokePoint> ExtendTail(List<StrokePoint> pts, float tailLenPx, float tailWidth) { if (pts.Count < 3) return pts; var p1 = pts[pts.Count - 2]; var p2 = pts[pts.Count - 1]; float dx = p2.X - p1.X, dy = p2.Y - p1.Y; float len = MathF.Sqrt(dx * dx + dy * dy); if (len < 1e-6f) return pts; float ux = dx / len, uy = dy / len; var result = new List<StrokePoint>(pts); for (int i = 1; i <= 4; i++) { var p = p2; p.X += ux * tailLenPx * i / 4f; p.Y += uy * tailLenPx * i / 4f; p.Width = MathF.Max(0f, tailWidth * (1f - i / 4f)); result.Add(p); } return result; }参数tailLenPx是出锋的物理长度,单位像素。我写字大小在256像素时一般给8到16像素,太短看不到尖,太长收笔像拖尾巴。tailWidth是原子收笔宽度,建议给它所在位置已经过包络缩小的宽度,而不是整笔的最大宽度;否则尖锋起点突然变粗,轮廓会外凸。补4个点的理由是:4点够形成平滑的尖锋折线,点再多也不会更好看,只会增加后续轮廓点数。
出锋还有一个容易被忽略的前提:收笔处最后一段方向必须稳定。如果最后几个点本身在抖动,延长出去的方向也会乱。所以我在ExtendTail前面会先判断最后5个点的平均方向夹角是否小于15度,不满足就先做局部平滑再取方向。这一步能避免出现钩子一样的歪锋尖。
4. 平滑算法:去抖而不糊边
4.1 先判断抖动是否值得处理,避免把有效笔锋也抹掉
不是所有轨迹都必须平滑。鼠标在普通屏幕上的坐标噪声通常只有1到2像素,肉眼几乎看不出;但触控笔在快速起笔时会有几个离群点,方向突变很大。我一般会先做一个低成本判断:遍历相邻点夹角变化率,超过45度的点标记为可疑抖动,统计可疑比例超过10%才启用平滑。否则强行平滑,会把原来故意的折笔(比如写“永”字的钩)当成抖动处理掉。
判断方向变化可以用点积:
// 返回三点之间的夹角,单位角度 static float AngleChange(StrokePoint a, StrokePoint b, StrokePoint c) { float v1x = b.X - a.X, v1y = b.Y - a.Y; float v2x = c.X - b.X, v2y = c.Y - b.Y; float d = (v1x * v2x + v1y * v2y) / (MathF.Sqrt(v1x*v1x + v1y*v1y) * MathF.Sqrt(v2x*v2x + v2y*v2y)); return MathF.Acos(Math.Clamp(d, -1f, 1f)) * 180f / MathF.PI; }角度的阈值不要设太紧,45度以上才算一次明显折转。写楷书时一个笔画内部很少超过30度,折笔都在转折处。如果设成15度,正常运笔的微小弯曲全部被判成抖动,平滑算法就会把笔锋削圆。实际接入新的手写板时,我会先用这个函数把整条轨迹的角度变化画出来,看看是集中在固定方向还是随机散布。随机散布才是需要平滑的,集中在某个位置的是书写者真实意图,这个区分比任何参数都重要。
还有一种常见误用:对原始点做多次平滑。有人为了“更丝滑”把整条轨迹跑三遍移动平均,结果笔画整体缩短一大截,起笔收笔位置全被吸到中间。正确思路是“一次到位”:要么插值细分,要么滤波,不要把多个平滑策略叠加在同一个阶段。
4.2 Catmull-Rom插值:坐标平滑的常用手段
坐标平滑有两个思路:一是移动平均,把当前点与前几个点做加权平均;二是插值,用Catmull-Rom样条在相邻点之间生成更密的点。移动平均实现快,但它会把笔画整体缩短并损失拐角;Catmull-Rom不会缩短笔迹,反而能细分出更流畅的路径,所以笔锋项目里我优先选后者。
Catmull-Rom每段需要前后各两个点:p1和p2之间的点生成中间段,p0和p3是控制切线方向的辅助点。公式带张力系数0.5,代入四个点的坐标得到插值结果:
// 对 p1~p2 段生成 steps 个插值点,p0/p3 只参与切线方向计算 static void CatmullRom(PointF p0, PointF p1, PointF p2, PointF p3, List<PointF> output, int steps) { for (int i = 0; i < steps; i++) { float t = i / (float)steps; float t2 = t * t, t3 = t2 * t; float x = 0.5f * ( 2f * p1.X + (-p0.X + p2.X) * t + (2f * p0.X - 5f * p1.X + 4f * p2.X - p3.X) * t2 + (-p0.X + 3f * p1.X - 3f * p2.X + p3.X) * t3); float y = 0.5f * ( 2f * p1.Y + (-p0.Y + p2.Y) * t + (2f * p0.Y - 5f * p1.Y + 4f * p2.Y - p3.Y) * t2 + (-p0.Y + 3f * p1.Y - 3f * p2.Y + p3.Y) * t3); output.Add(new PointF(x, y)); } }这里的steps是每两个原始点之间插出多少个点。点数不是越多越好:原始点间距5像素时,steps给2就够;间距15像素时给4到6。插得太密会让轮廓点数爆炸,一个“永”字四五个笔画动不动几千个点,最后路径填充复杂,渲染会卡。我一般在插值后再做一遍轮廓简化,把直线上的冗余点吃掉。ops。
Catmull-Rom的一个隐藏特性是端点不会经过p0和p3,只经过p1和p2,所以首尾两个端点不要用虚拟点外推。我常用做法是端点重复一次:p0用p1,p3用p2,让首尾保持原位置。这样能避免起笔收笔被插值改位置,也就避开了第5章要讲的“笔锋被抹平”问题。
另一个细节:插值最好在原始点构成的每段上独立执行,而不是把整条轨迹一次性丢给样条。因为整条轨迹的端点条件很难控制,独立分段还能在写完一笔后立刻追加下一笔,不用重新计算全局。
4.3 宽度平滑与轮廓平滑:两条独立的流水线
坐标平滑之后,下一步是平滑宽度。原因很简单:压感设备经常出现单点压力跳变,比如落笔瞬间的压力从0.2跳到0.8再回落到0.3,这个跳变会直接反映到Width上,导致笔画边缘鼓一个包。宽度平滑用最朴素的滑动平均就可以,窗口3到5个点:
// 对宽度序列做窗口平滑,window 取点数而不是距离 static StrokePoint[] SmoothWidth(StrokePoint[] pts, int window) { var output = (StrokePoint[])pts.Clone(); for (int i = 0; i < pts.Length; i++) { int start = Math.Max(0, i - window); int end = Math.Min(pts.Length - 1, i + window); int count = end - start + 1; float sum = 0f; for (int j = start; j <= end; j++) sum += pts[j].Width; output[i].Width = sum / count; } return output; }window取3还是5取决于采样率。60Hz的轨迹,3点窗口大约覆盖50毫秒,能压住单点跳变;120Hz就需要5点窗口。窗口太宽会把起笔收笔的锥度压平,因为起笔只有几个点的渐宽序列,窗口一长大,这段平均后就不再是0开头。遇到这种情况,可以把起点和终点排除在平滑范围外,只处理中间80%的点。
轮廓平滑单独做最后一道。生成左右轮廓线后,如果还有小锯齿,用Douglas-Peucker或直接隔点采样简化。注意轮廓线简化阈值放在0.3像素以下,超过1像素时笔画边缘会出现多边形感,低分辨率下尤其明显。笔锋附近的点不要参与简化,通过保留轮廓中宽度小于某个阈值的点作为锚点来实现;否则尖锋会被简化成圆角。
宽度平滑和轮廓平滑顺序不要颠倒。如果先平滑轮廓再改宽度,轮廓已经定型,再改宽度只能整体缩放,起不到修边作用。正确流水线是:坐标插值、宽度映射、宽度平滑、生成轮廓、轮廓简化。每一步处理的对象不同,混在一起就会互相破坏结果。
5. 避坑与排查:C#编译和毛笔字渲染最常见的5个问题
5.1 编译期坑与跨平台选型
现象:把代码放到.NET Core控制台项目,写着System.Drawing的一句using就报CS0246,或者运行时提示找不到System.Drawing.Common。
原因:System.Drawing在.NET Framework时代是内置组件,到了.NET Core和.NET 5+被拆成了独立包,而且底层调用GDI+,官方只建议在Windows上使用。有些源码标注“可编译”,你用跨平台SDK一编就挂,不是算法问题,是环境问题。
解决:在项目文件里加包引用,命令行执行dotnet add package System.Drawing.Common,回到代码里using System.Drawing;就能编过。如果要把这套毛笔字算法跑在Linux服务器或者Mac上做转图服务,直接改用SkiaSharp,它跨平台且自带抗锯齿,替换成本大约是把像素绘制改成SKCanvas。我一般在本地验证窗口用System.Drawing,服务端统一走SkiaSharp。
5.2 坐标翻转导致笔锋反向和镜像
现象:用鼠标从左往右写横,起笔尖跑到右端,收笔尖跑到左端,整个字像是镜子里的字。
原因:不同设备坐标系的Y轴方向不一致。屏幕和SVG的Y轴向下,而某些手写板或者数学工具返回的坐标Y轴向上。如果设备原始坐标直接交给笔锋方向计算,法线会整体翻转,左右轮廓互换,最终呈现镜像。这个问题在普通线条渲染中常被忽略,但带方向性的笔锋会立刻暴露。
解决:在入口统一做一次坐标归一化,把Y轴改成屏幕向下,同时记录画布高度。在触摸屏和鼠标上通常不需要翻转,但在接入数位板、以及从PDF批注数据流读轨迹时,一定要先确认设备原点在左下还是左上。我一般会在接入新设备时打开一个测试模式,把轨迹原样输出为散点图,确认“向右写横时X增大且Y基本不变”之后,再进入算法。
5.3 过度平滑把起笔和收笔压成圆头
现象:起笔和收笔都是圆弧,整个笔画像用蜡笔画出来的,尖锋完全消失。
原因:平滑算法是全局处理的,Catmull-Rom插值和宽度滑动平均会把端点附近的渐宽序列与抖动一起抹掉。起笔锥度在数学上就是宽度从0到某个值的几个点,这个序列本身短,平滑窗口稍微覆盖它,0值就被平掉了,尖锋自然消失。
解决:把“端点保护”加进流水线。做法是从Catmull-Rom插值中把首尾两个端点排除,让首尾作为锚点原样保留;宽度平滑的区间在整笔的0.1到0.9段内执行,两端的锥度段不参与平均。如果发现收笔还是圆头,检查ExtendTail生成的几个补充点是否也进入了Catmull-Rom插值,如果是,把它们标记成“尾部点”,禁止再插值。
5.4 轮廓自交导致填充出现黑色穿帮
现象:在笔画转折处,SVG填充区域出现一个楔形黑斑,原本应该是纸面的部分被填成墨色。
原因:左右轮廓线是按中心线上的法线分别偏移生成的。当中心线出现锐角弯折时,内侧轮廓线会交叉缠绕,用路径填充时交叉区域会被算成面积,表现就是黑块。这一条在连续的折笔处特别常见。
解决:计算左右轮廓时,把相邻法线的夹角变化限制在60度以内;超过时在两者之间补一个转向插值点,让内侧轮廓平滑转向而不是突然跳变。另外,SVG路径在收尾闭合时要用左轮廓顺序加右轮廓反向顺序,别把左右轮廓打乱拼接。如果轮廓自交已经发生,最直接的排查办法是把生成的轮廓点导出成文本,沿路径走一遍,找到第一个与自身相交的线段再局部重算。
5.5 轮廓点数爆炸导致渲染和序列化变慢
现象:一笔轨迹只有200个采样点,但生成的SVG数据有几十KB,程序回放笔迹时延迟明显。
原因:插值步数设置过大,或者宽度平滑后每个采样点仍然参与轮廓生成。200个采样点乘以插值步数4,就是800个中心点,再乘左右两侧,轮廓点超过1600个。如果一套代码写出几千字的字帖,数据量直接不可收拾。
解决:给一次输入设定上限。插值后的中心点总数超过500时,先对中心线做一次Douglas-Peucker简化,再生成轮廓。笔锋段保留全部分辨率,中间段允许最大偏差0.5像素。输出SVG时,小数点只保留1位,很多40KB的路径能压到15KB以内。轮廓自交与点数爆炸经常同时出现,简化后交叉点也会减少,属于性价比很高的兜底手段。
6. 验证效果:用同一份“永”字跑参数回归
6.1 建立固定样本与参数基线
把验证从“感觉好看”变成可复现,第一件事是固定输入。我保留一份永字坐标点集,大约60个点,默认参数minW=2、maxW=14、headLen=0.1、tailLen=0.12、tailLenPx=12、speedNorm=1.2。每次改参数,把结果输出为SVG并保存成一张PNG,文件名带参数串。之后改任何代码,先跑这组基线对比。
| 参数 | 默认值 | 作用 | 调参方向 |
|---|---|---|---|
| minW | 2 | 最细笔宽 | 太小断线,太粗没有细锋 |
| maxW | 14 | 最粗笔宽 | 字大或笔肚饱满时加大 |
| headLen | 0.10 | 起笔锥度占比 | 藏锋加大,露锋减小 |
| tailLen | 0.12 | 收笔锥度占比 | 悬针加大,垂露减小 |
| speedNorm | 1.2 | 速度-宽度灵敏度 | 忽粗忽细就调大 |
6.2 用像素覆盖率做自动回归
人工看SVG太慢,我会把生成的路径栅格化到400x400的Bitmap,与基准图做逐像素比较,计算差异比例:
int diff = 0; for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) if (MathF.Abs(bmp1.GetPixel(x, y).A - bmp2.GetPixel(x, y).A) > 16) diff++;diff超过总数1%,就说明这次改动影响了笔锋,再回头确认是否预期内。这套方法帮我抓住过坐标翻转、轮廓自交这些“单看一两个样本发现不了”的问题。
我很早就发现,笔锋算法最难的并不是把公式写对,而是换个设备后所有参数都失灵。现在的习惯是任何新设备接入,先画散点图看原始坐标的采样间距和抖动频率,再决定开不开始插值。这个习惯省掉了我大量反复调参的时间,希望帮到你。
本文还有配套的精品资源,点击获取