第四节:B样条曲线与路径平滑

做AGV运动控制这些年,我越来越觉得路径平滑是个被低估的活。很多人觉得能走就行,结果车一拐弯就抖,一加速就偏。说白了,路径不光要“对”,还得“顺”。B样条曲线,就是解决这个问题的利器。

4.1 为什么是B样条?

先说说我踩过的坑。最早做路径规划时,我用的是多项式插值。结果呢?控制点一多,曲线就开始乱抖——这就是所谓的“龙格现象”。后来换Bezier曲线,好一些,但改一个控制点整条曲线都变,调参调到崩溃。

B样条不一样。它最大的特点是局部性——你动一个控制点,只影响附近一小段曲线。这在AGV路径调整时太实用了。比如你发现某个拐弯太急,只需要微调那附近的几个点,其他路段纹丝不动。

核心优势总结:

  • 局部可控:修改一个点不影响全局
  • 连续性好:天然满足C²连续(加速度连续)
  • 计算稳定:数值上比多项式插值靠谱得多

4.2 B样条的基本原理

B样条的数学定义其实不复杂。给定n+1个控制点P₀, P₁, ..., Pₙ,以及一个节点向量U = {u₀, u₁, ..., uₘ},k阶B样条曲线定义为:

C(u) = Σᵢ₌₀ⁿ Nᵢₖ(u) · Pᵢ

其中Nᵢₖ(u)是基函数,通过Cox-de Boor递推公式计算:

Nᵢ₀(u) = 1  if uᵢ ≤ u < uᵢ₊₁, else 0

Nᵢₖ(u) = (u - uᵢ)/(uᵢ₊ₖ - uᵢ) · Nᵢₖ₋₁(u) 
         + (uᵢ₊ₖ₊₁ - u)/(uᵢ₊ₖ₊₁ - uᵢ₊₁) · Nᵢ₊₁ₖ₋₁(u)

嗯,看着有点吓人。但实际用起来,你只需要记住几个关键点:

  • 阶数k:一般用3阶(三次B样条),保证C²连续
  • 节点向量:决定了曲线在控制点间的“过渡方式”
  • 控制点:你真正要调的东西

我的经验:刚开始别纠结数学推导。先跑通一个三次均匀B样条的例子,感受一下“调控制点→曲线变化”的直观效果。数学是后补的。

4.3 控制点选取策略

控制点怎么选?这问题我当年琢磨了很久。后来总结出一套实用方法:

4.3.1 从路径点反算控制点

大多数时候,我们已经有了一串路径点(比如A*规划出来的)。需要反算出B样条的控制点,让曲线通过这些点。

假设有m个路径点Q₀, Q₁, ..., Qₘ₋₁,想用三次B样条插值。需要解一个线性方程组:

对于每个路径点Qⱼ:
Qⱼ = (1/6)·Pᵢ₋₁ + (2/3)·Pᵢ + (1/6)·Pᵢ₊₁

写成矩阵形式:

[Q₀]   [2/3  1/6   0   ...  0  ] [P₀]
[Q₁] = [1/6  2/3  1/6  ...  0  ] [P₁]
[Q₂]   [ 0   1/6  2/3  ...  0  ] [P₂]
...     ...                       ...
[Qₘ₋₁] [ 0    0    0   ... 2/3 ] [Pₘ₋₁]

解这个三对角矩阵,就能得到控制点。我在项目里直接用Eigen库的稀疏矩阵求解,几毫秒就搞定。

注意:路径点数量不能太少。少于4个点,三次B样条插值会出问题。我建议至少6-8个点起步。

4.3.2 控制点的手动微调

自动反算出来的控制点,往往还需要手动调一调。我一般遵循几个原则:

  • 拐弯处加密:曲率大的地方多放几个控制点
  • 直道处稀疏:直线段两个点就够了
  • 端点处理:起点和终点用重复节点,让曲线严格经过

举个例子。有一次做仓储AGV,货架间的通道很窄。自动生成的路径在拐弯处离货架太近。我手动把拐弯处的控制点往通道中心拉了拉,曲线就顺过去了。前后花了不到10分钟。

4.4 路径插值与平滑

有了控制点,接下来就是生成平滑路径。说白了,就是在控制点之间“插值”出足够密的点,让AGV能跟着走。

4.4.1 均匀采样 vs 弧长参数化

最简单的做法是均匀采样u参数。但问题来了——曲线上的点分布不均匀。u变化均匀,不代表弧长变化均匀。结果就是:AGV在直道上跑得快,拐弯处突然减速,不平滑。

我建议用弧长参数化。大致思路:

  1. 先均匀采样一批点,计算累计弧长
  2. 建立u到弧长的映射关系
  3. 按目标弧长间隔反查u值
// 伪代码示例
function resampleByArcLength(controlPoints, numSamples):
    // 1. 粗采样
    rawPoints = sampleUniform(controlPoints, numSamples * 10)
    
    // 2. 计算累计弧长
    arcLengths = [0]
    for i = 1 to len(rawPoints)-1:
        dist = distance(rawPoints[i], rawPoints[i-1])
        arcLengths.append(arcLengths[-1] + dist)
    
    // 3. 按目标弧长插值
    totalLength = arcLengths[-1]
    step = totalLength / (numSamples - 1)
    
    result = []
    for i = 0 to numSamples-1:
        targetLength = i * step
        // 二分查找对应区间,线性插值
        idx = binarySearch(arcLengths, targetLength)
        t = (targetLength - arcLengths[idx]) / (arcLengths[idx+1] - arcLengths[idx])
        point = lerp(rawPoints[idx], rawPoints[idx+1], t)
        result.append(point)
    
    return result

避坑指南:我曾经直接用均匀采样给AGV下路径,结果车在弯道里一卡一卡的。换成弧长参数化后,问题立刻解决。这个坑我替你们踩过了。

4.4.2 曲率约束检查

路径平滑不光要“看着顺”,还得“跑得顺”。AGV有最小转弯半径限制。所以插值完后,一定要检查曲率:

// 计算三点确定的曲率
function computeCurvature(p0, p1, p2):
    // 向量
    v1 = p1 - p0
    v2 = p2 - p1
    
    // 叉积的模 / 向量模的乘积
    cross = v1.x * v2.y - v1.y * v2.x
    len1 = length(v1)
    len2 = length(v2)
    
    if len1 * len2 < 1e-6:
        return 0  // 近似直线
    
    curvature = 2 * abs(cross) / (len1 * len2 * (len1 + len2))
    return curvature

如果某段曲率超过AGV的极限,就需要调整控制点。我一般设一个安全系数,比如极限曲率的80%作为报警阈值。

4.5 知识体系总览

下面这张图是我自己整理的B样条路径平滑流程,你可以照着这个思路搭系统:

B样条路径平滑知识体系 原始路径点 控制点反算(解三对角矩阵) 控制点手动微调(拐弯加密) 路径插值(弧长参数化 → 均匀采样) 平滑路径(含曲率检查) 关键:三对角矩阵求解 经验:拐弯处加密控制点 技巧:弧长参数化防抖动 检查:曲率不超极限80%

4.6 实战中的几个要点

最后分享几个我在项目里总结的要点:

  • 节点向量的选择:均匀节点向量最简单,但非均匀节点向量在拐弯处效果更好。我一般用弦长参数化法生成节点向量。
  • 实时性考虑:如果路径需要在线更新(比如动态避障),控制点数量控制在20个以内,计算量可以接受。
  • 与轨迹规划的衔接:B样条输出的是几何路径,还需要加上速度规划(梯形/S形速度曲线)才能变成可执行的轨迹。

一句话总结:B样条路径平滑,核心就三步——反算控制点、手动微调、弧长参数化插值。每一步都有坑,但踩过去之后,你会发现AGV跑起来顺滑得像在冰面上滑行。


公众号:蓝海资料掘金营,微信deep3321