数学基础(三):插值与拟合

插值与拟合,说白了就是「猜中间的值」。你有一堆离散点,想连成一条平滑的线,或者想预测两个点之间会发生什么。这在轨迹规划里太常见了——机器人走过的路径点,不可能每个都存下来,得靠插值把中间补上。

我刚开始做轨迹规划时,觉得插值不就是画条线嘛。后来发现,选错方法,机器人要么抖得像帕金森,要么直接撞墙。嗯,这里面的门道不少。

多项式插值:简单但危险

多项式插值是最直觉的方法。给你 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 个等距点,结果两端直接飞到天上去,机器人路径完全不能用。

避坑指南:多项式插值只适合点数少(一般不超过 5 个)且节点分布均匀的场景。点数多了,老老实实用分段插值。

样条插值:分段的艺术

既然高次多项式不靠谱,那就分段呗。样条插值的思路是:把整个区间切成小段,每段用低次多项式拟合,段与段之间保证连续且光滑。

最常用的是三次样条。每段用三次多项式,保证整条曲线二阶连续可导。你想想看,这意味着位置、速度、加速度都连续——对机器人轨迹来说,这就是基本要求。

样条类型 连续阶 适用场景
线性样条 C⁰(位置连续) 简单路径,不关心速度
二次样条 C¹(速度连续) 需要平滑速度变化
三次样条 C²(加速度连续) 轨迹规划标准选择

三次样条的数学形式:

// 每段三次多项式:Sᵢ(x) = aᵢ + bᵢ(x-xᵢ) + cᵢ(x-xᵢ)² + dᵢ(x-xᵢ)³
// 约束条件:
// 1. 穿过所有节点
// 2. 一阶导数连续
// 3. 二阶导数连续
// 4. 边界条件(自然样条或固定端点条件)

我个人习惯用自然样条(两端二阶导数为 0),因为计算简单,而且大多数场景下效果已经够好。如果你需要精确控制端点速度,那就用固定端点条件。

经验之谈:三次样条是轨迹规划里的「万金油」。我在做 AGV 路径平滑时,几乎都用它。计算量不大,效果稳定,唯一要注意的是——别让路径点太密,否则曲线会「拧麻花」。

贝塞尔曲线:直观的控制

贝塞尔曲线是另一种思路。它不要求曲线穿过所有点,而是通过「控制点」来塑造形状。你拖动控制点,曲线就会跟着变形——特别适合交互式设计。

数学上,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];
}

贝塞尔曲线有几个特点:

  • 凸包性质:曲线始终在控制点构成的凸多边形内
  • 端点性质:曲线经过首尾控制点,但不经过中间点
  • 全局性:移动一个控制点,整条曲线都会变

这个「全局性」既是优点也是缺点。优点是调整直观,缺点是局部修改会牵连全局。我记得有一次做机械臂避障,想微调中间一段路径,结果整个轨迹都变了,不得不重新规划。

注意:贝塞尔曲线的次数 = 控制点数 - 1。控制点一多,次数就高,计算量和数值稳定性都会出问题。实际工程中,一般用三次贝塞尔曲线(4 个控制点),再多就分段。

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⁰ 连续(位置连续但速度不连续)。

工程建议:如果你做的是实时轨迹规划,B样条是首选。它的局部性意味着你只需要重新计算受影响的那几段,计算量可控。我在做无人机航迹规划时,就用 B样条做路径平滑,效果比贝塞尔好得多。

四种方法对比

方法 穿过节点 局部控制 连续阶 计算量 推荐场景
多项式插值 是 否 Cⁿ 低 点数少(≤5)
三次样条 是 是 C² 中 路径平滑、轨迹规划
贝塞尔曲线 否 否 Cⁿ 低 交互设计、形状编辑
B样条曲线 否 是 Cᵏ⁻¹ 中高 实时规划、复杂路径

知识体系总览

下面这张图把四种方法的关系和适用场景串起来了:

插值与拟合方法体系 插值与拟合 插值(穿过节点) 拟合(逼近节点) 多项式插值 样条插值 贝塞尔曲线 B样条曲线 龙格现象 点数≤5 C²连续 路径平滑首选 全局控制 交互设计 局部控制 实时规划 选型原则:场景决定方法

看到这张图,你应该能理解:没有「最好」的方法,只有「最合适」的方法。多项式插值简单但脆弱,样条插值稳定但需要解方程组,贝塞尔直观但全局影响,B样条灵活但实现稍复杂。

我个人建议,刚入门时先把三次样条和三次贝塞尔吃透。这两个是基础,也是工程中最常用的。等你遇到更复杂的场景——比如非均匀节点、带权重的拟合、实时动态调整——再深入 B样条也不迟。

一句话总结:插值是「必须穿过已知点」,拟合是「尽量靠近已知点」。轨迹规划里,样条和 B样条是主力,多项式插值只适合小规模数据,贝塞尔更适合设计阶段。