第4章:B样条曲线基础

各位同学,今天我们来聊聊B样条曲线。说实话,我刚入行那会儿,对B样条也是一头雾水。那时候做轨迹规划,用贝塞尔曲线总觉得不够灵活——改一个点整条曲线都跟着变,太痛苦了。后来接触到B样条,才感觉找到了趁手的工具。

4.1 B样条的定义

B样条曲线,说白了就是贝塞尔曲线的升级版。它把一条曲线分成若干段,每段用一组基函数来控制。这样做的最大好处是——局部修改不影响全局。

数学上,一条k阶B样条曲线定义为:

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

其中Pᵢ是控制点,Nᵢₖ(u)是k阶B样条基函数。这个公式看起来和贝塞尔很像,但基函数完全不同。

基函数Nᵢₖ(u)是通过递推定义的:

Nᵢ₁(u) = 1, 如果 uᵢ ≤ u < uᵢ₊₁
         0, 其他

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

嗯,这个递推公式看着有点吓人。我建议你先记住它的核心思想:每个基函数只在局部区间内非零,其他地方都是0。这就是B样条局部性的根源。

4.2 控制点与节点向量

B样条有两个核心要素:控制点和节点向量。

控制点就是那些你拖来拖去的点,决定了曲线的大致形状。但和贝塞尔不同,B样条的控制点并不一定在曲线上。

节点向量是B样条独有的东西。它是一个非递减的实数序列:

U = {u₀, u₁, u₂, ..., uₘ}

节点向量把参数区间分成若干段。每段对应一个基函数的作用区间。节点向量的分布方式直接影响曲线的性质。

常见的节点向量有两种:

类型 特点 应用场景
均匀节点向量 节点等距分布 简单曲线,计算快
非均匀节点向量 节点不等距 需要精确控制曲线形状时

我个人习惯用非均匀节点向量。为什么?因为实际项目中,轨迹的某些段需要更精细的控制,比如机器人经过狭窄通道时。均匀节点向量做不到这种局部加密。

小技巧:如果你想让曲线通过首尾控制点,可以在节点向量两端重复k次相同的值。这叫「夹持」B样条,我经常用这个技巧来保证轨迹的起点和终点位置精确。

4.3 B样条的性质

B样条之所以在轨迹规划中这么受欢迎,离不开它几个关键性质。

4.3.1 局部性

这是B样条最牛的地方。移动一个控制点,只影响它附近的一段曲线,其他部分纹丝不动。

为什么会这样?因为每个基函数只在局部区间非零。你想想看,如果移动第i个控制点Pᵢ,只有那些包含Nᵢₖ(u)的曲线段会受影响。具体来说,影响范围是[uᵢ, uᵢ₊ₖ]这个区间。

我在项目中遇到过这样的情况:调试机器人轨迹时,发现中间某段路径需要微调。如果用贝塞尔曲线,改一个点整条线都得重新算。但用B样条,我只动了3个控制点,前后段完全没变。调试效率提升了好几倍。

4.3.2 凸包性

B样条曲线始终位于其控制点构成的凸包内。这个性质保证了曲线的「安全性」。

举个例子:如果你用B样条规划机械臂的轨迹,只要控制点都在安全区域内,曲线就一定不会跑出去。这比用多项式插值靠谱多了——多项式经常在控制点之间乱窜,撞到障碍物都不知道。

重要:凸包性对轨迹规划的意义在于——你可以通过约束控制点的位置来间接约束整条曲线。这是B样条在避障规划中广泛应用的原因之一。

4.3.3 连续性

k阶B样条曲线在节点处具有Cᵏ⁻²连续性。什么意思?就是曲线足够光滑。

对于轨迹规划来说,我们通常要求至少C²连续——位置、速度、加速度都连续。这样机器人运动起来才平稳,不会出现抖动。

我记得有一次做AGV的路径规划,用了C¹连续的B样条。结果小车在转弯时加速度突变,货物差点翻倒。后来改成C²连续,问题就解决了。所以,我建议你在做轨迹规划时,至少保证C²连续。

4.3.4 强凸包性

这个性质比凸包性更强。它说:曲线上任意一点,只受附近k个控制点的影响,并且这些控制点的凸包包含了该点。

说白了,就是局部性和凸包性的结合。这个性质在实时轨迹调整中特别有用——你只需要考虑局部控制点,不用管全局。

避坑指南:我曾经在项目中忽略了节点向量的选择,用了默认的均匀节点向量。结果曲线在首尾段出现了不期望的振荡。后来才发现,对于非周期性的轨迹,应该用夹持B样条(首尾节点重复)。这个坑我踩过,你们别踩了。

4.4 一个简单的例子

说了这么多理论,我们来写个简单的B样条生成代码。这里用Python演示:

import numpy as np

def bspline_basis(i, k, u, knots):
    """计算B样条基函数"""
    if k == 1:
        return 1.0 if knots[i] <= u < knots[i+1] else 0.0
    
    # 计算两个系数
    denom1 = knots[i+k-1] - knots[i]
    denom2 = knots[i+k] - knots[i+1]
    
    coeff1 = (u - knots[i]) / denom1 if denom1 != 0 else 0
    coeff2 = (knots[i+k] - u) / denom2 if denom2 != 0 else 0
    
    return coeff1 * bspline_basis(i, k-1, u, knots) + \
           coeff2 * bspline_basis(i+1, k-1, u, knots)

def generate_bspline(control_points, k=3, num_points=100):
    """生成B样条曲线点"""
    n = len(control_points) - 1
    m = n + k + 1
    
    # 生成夹持节点向量
    knots = np.zeros(m+1)
    knots[:k] = 0
    knots[-k:] = 1
    for i in range(k, m-k+1):
        knots[i] = (i - k + 1) / (m - 2*k + 1)
    
    # 采样曲线点
    curve_points = []
    for u in np.linspace(0, 1, num_points):
        point = np.zeros(2)
        for i in range(n+1):
            basis = bspline_basis(i, k, u, knots)
            point += basis * control_points[i]
        curve_points.append(point)
    
    return np.array(curve_points)

这段代码虽然简单,但包含了B样条的核心逻辑。你可以试着改改控制点位置,看看局部性效果——移动一个点,只有附近几段曲线会变化。

好了,这一章就到这里。B样条是轨迹规划的基石,后面的课程会反复用到。下一章我们聊聊B样条曲线的求导和插值,那才是真正实用的东西。