第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样条曲线的求导和插值,那才是真正实用的东西。