数学基础(三):插值与拟合
插值与拟合,说白了就是「猜中间的值」。你有一堆离散点,想连成一条平滑的线,或者想预测两个点之间会发生什么。这在轨迹规划里太常见了——机器人走过的路径点,不可能每个都存下来,得靠插值把中间补上。
我刚开始做轨迹规划时,觉得插值不就是画条线嘛。后来发现,选错方法,机器人要么抖得像帕金森,要么直接撞墙。嗯,这里面的门道不少。
多项式插值:简单但危险
多项式插值是最直觉的方法。给你 n+1 个点,找一个 n 次多项式,让曲线刚好穿过所有点。数学上这叫「拉格朗日插值」或「牛顿插值」。
举个例子,三个点 (0,0)、(1,2)、(2,1),用二次多项式就能搞定。公式长这样:
// 拉格朗日插值基函数
function lagrangeInterpolation(x, points) {
let result = 0;
for (let i = 0; i < points.length; i++) {
let term = points[i].y;
for (let j = 0; j < points.length; j++) {
if (i !== j) {
term *= (x - points[j].x) / (points[i].x - points[j].x);
}
}
result += term;
}
return result;
}
看着挺简单对吧?但有个大坑——龙格现象。当点数变多,多项式次数升高,曲线两端会剧烈震荡。我在项目中遇到过,用 10 阶多项式插值 11 个等距点,结果两端直接飞到天上去,机器人路径完全不能用。
样条插值:分段的艺术
既然高次多项式不靠谱,那就分段呗。样条插值的思路是:把整个区间切成小段,每段用低次多项式拟合,段与段之间保证连续且光滑。
最常用的是三次样条。每段用三次多项式,保证整条曲线二阶连续可导。你想想看,这意味着位置、速度、加速度都连续——对机器人轨迹来说,这就是基本要求。
| 样条类型 | 连续阶 | 适用场景 |
|---|---|---|
| 线性样条 | C⁰(位置连续) | 简单路径,不关心速度 |
| 二次样条 | C¹(速度连续) | 需要平滑速度变化 |
| 三次样条 | C²(加速度连续) | 轨迹规划标准选择 |
三次样条的数学形式:
// 每段三次多项式:Sᵢ(x) = aᵢ + bᵢ(x-xᵢ) + cᵢ(x-xᵢ)² + dᵢ(x-xᵢ)³
// 约束条件:
// 1. 穿过所有节点
// 2. 一阶导数连续
// 3. 二阶导数连续
// 4. 边界条件(自然样条或固定端点条件)
我个人习惯用自然样条(两端二阶导数为 0),因为计算简单,而且大多数场景下效果已经够好。如果你需要精确控制端点速度,那就用固定端点条件。
贝塞尔曲线:直观的控制
贝塞尔曲线是另一种思路。它不要求曲线穿过所有点,而是通过「控制点」来塑造形状。你拖动控制点,曲线就会跟着变形——特别适合交互式设计。
数学上,n 次贝塞尔曲线由 n+1 个控制点定义:
// 德卡斯特里奥算法(De Casteljau's Algorithm)
function bezierPoint(t, controlPoints) {
let points = controlPoints.slice();
while (points.length > 1) {
let next = [];
for (let i = 0; i < points.length - 1; i++) {
next.push({
x: (1 - t) * points[i].x + t * points[i + 1].x,
y: (1 - t) * points[i].y + t * points[i + 1].y
});
}
points = next;
}
return points[0];
}
贝塞尔曲线有几个特点:
- 凸包性质:曲线始终在控制点构成的凸多边形内
- 端点性质:曲线经过首尾控制点,但不经过中间点
- 全局性:移动一个控制点,整条曲线都会变
这个「全局性」既是优点也是缺点。优点是调整直观,缺点是局部修改会牵连全局。我记得有一次做机械臂避障,想微调中间一段路径,结果整个轨迹都变了,不得不重新规划。
B样条曲线:贝塞尔的升级版
B样条曲线解决了贝塞尔的两个痛点:局部控制和次数限制。它把曲线分成多段,每段由几个控制点决定,但每个控制点只影响局部区域。
B样条的核心是「节点向量」和「基函数」。听着复杂,但效果很实用:
- 局部性:移动一个控制点,只影响附近几段曲线
- 低次光滑:可以用低次多项式实现复杂形状
- 连续性可控:通过重复节点,可以控制曲线在特定点的连续阶
// 均匀三次B样条基函数(简化版)
function uniformBSplineBasis(i, k, t) {
// i: 控制点索引, k: 次数(3), t: 参数[0,1)
if (k === 0) {
return (t >= i && t < i + 1) ? 1 : 0;
}
let left = (t - i) / k * uniformBSplineBasis(i, k - 1, t);
let right = (i + k + 1 - t) / k * uniformBSplineBasis(i + 1, k - 1, t);
return left + right;
}
我个人最喜欢 B样条的一点是——你可以通过调整节点向量,精确控制曲线在某个点的「光滑程度」。比如想让机器人急停,就在那个位置重复节点,让曲线变成 C⁰ 连续(位置连续但速度不连续)。
四种方法对比
| 方法 | 穿过节点 | 局部控制 | 连续阶 | 计算量 | 推荐场景 |
|---|---|---|---|---|---|
| 多项式插值 | 是 | 否 | Cⁿ | 低 | 点数少(≤5) |
| 三次样条 | 是 | 是 | C² | 中 | 路径平滑、轨迹规划 |
| 贝塞尔曲线 | 否 | 否 | Cⁿ | 低 | 交互设计、形状编辑 |
| B样条曲线 | 否 | 是 | Cᵏ⁻¹ | 中高 | 实时规划、复杂路径 |
知识体系总览
下面这张图把四种方法的关系和适用场景串起来了:
看到这张图,你应该能理解:没有「最好」的方法,只有「最合适」的方法。多项式插值简单但脆弱,样条插值稳定但需要解方程组,贝塞尔直观但全局影响,B样条灵活但实现稍复杂。
我个人建议,刚入门时先把三次样条和三次贝塞尔吃透。这两个是基础,也是工程中最常用的。等你遇到更复杂的场景——比如非均匀节点、带权重的拟合、实时动态调整——再深入 B样条也不迟。