战斗包子
B样条

B样条

B样条(B-Spline)笔记

概述

1. 目标:B样条用来做什么?

在最基础的层面上,B样条(B-Spline,即 Basis Spline)是一种通过一组“控制点”来定义平滑曲线的数学方法。

想象一下:

  • 问题:你需要在计算机上绘制一条复杂的、非常平滑的曲线,比如汽车的流线型车身、一个漂亮的字体,或者一条机器人的运动轨迹。
  • B样条的解决方案:你不需要定义曲线上成千上万个点的坐标,你只需要在曲线“大致”经过的地方放几个控制点,B样条算法就会自动帮你“脑补”出一条穿梭于这些点之间的、数学上完美的平滑曲线。

2. 核心思想:一个直观的比喻

理解B样条最简单的方法是“磁铁”和“弹性尺”的比喻:

  1. 控制点 (Control Points):想象它们是一块块磁铁,你把它们放在桌子上。
  2. B样条曲线:想象你有一根弹性的、灵活的尺子(“Spline”这个词的本意就是“细木条”或“样条”,是造船时用来画船体曲线的工具)。
  3. 生成曲线:你把这根弹性尺“扔”到磁铁附近。尺子会因为磁铁的吸引力而弯曲,朝着磁铁的方向靠近,但不一定会碰到每一块磁铁(除非磁铁吸力特别强)。

最终,这根弹性尺形成的那个平滑的、被磁力吸引而弯曲的形状,就是B样条曲线。

  • 你移动一块磁铁(控制点),尺子(曲线)的形状就会局部发生平滑的变化。

3. B样条的几个概念

a. 控制点 (Control Points)

  • 这就是上面比喻中的“磁铁”。
  • 它们是B样条的**“骨架”。将所有控制点按顺序“连连看”形成的折线,被称为控制多边形 (Control Polygon)**。
  • B样条曲线总是被完全包含在它的控制多边形的“凸包”内(Convex Hull Property)。简单说,曲线永远不会“飞出”它的控制点所框定的大致范围。

b. 次数 (Degree) / 阶数 (Order)

这是一个非常重要的设置项,它决定了曲线的**“平滑等级”**。

  • 关系阶数 (Order) = 次数 (Degree) + 1。(在你的代码中,B_spline_order 5 指的是阶数,所以它是一个4次样条。注:请再次核对你的代码,B_spline_order 5 通常指k=5,即5次,Order=6。我们假设你的代码中 B_spline_order 指的是次数 (Degree=k),即5次样条,Order=6。)

    • (为避免混淆,我们下面统一使用“次数”)
  • 次数 (k=1), 线性:曲线就是控制多边形本身(一堆直线)。非常“硬”。

  • 次数 (k=2), 二次:曲线由抛物线段构成。开始变得平滑。

  • 次数 (k=3), 三次 (Cubic)这是最常用的! 比如 Adobe Illustrator、CAD软件、字体设计默认都是它。它在“平滑度”和“控制性”之间达到了完美的平衡。

  • 次数 (k=5), 五次 (Quintic)(这就是你代码中用的) 这是一个非常高等级的平滑。为什么用这么高?因为五次样条(六阶)能保证它的**“加加速度(Jerk)”和“加加加速度(Snap)”也是平滑的。在机器人或自动驾驶路径规划中,这意味着极高的乘坐舒适度**,机器或车辆在执行轨迹时不会有突然的“抖动”。

c. 节点向量 (Knot Vector)

  • 这是B样条最强大、也最难理解的概念。
  • 比喻:如果说控制点是“磁铁”,那么节点向量就是一本**“规则手册”,它详细规定了每一个控制点从哪里开始生效,到哪里结束生效**。
  • 它是一个数字列表(必须是非递减的),例如:[0, 0, 0, 0, 1, 2, 3, 4, 4, 4, 4] (对于一个3次样条)。
  • 它的作用
    1. 定义曲线的“分段点”[...0, 1, 2, 3, 4...] 这些变化的值,定义了曲线在参数 t 上的分段。
    2. 控制“端点”行为
      • 你注意到开头有4个 0,结尾有4个 4 吗?
      • 规则:如果一个节点值(如0)重复了 k 次(k = 次数),那么曲线将精确地穿过它对应的那个控制点。
      • 所以,[0, 0, 0, 0, ...](开头重复 k+1 次,这里是4次)     \implies 曲线必须从第一个控制点开始
      • [... 4, 4, 4, 4](结尾重复 k+1 次)     \implies 曲线必须在最后一个控制点结束
      • 这种两端“锁死”的B样条,也称为**“Clamped” B-spline(钳位B样条)**。

d. 局部控制性 (Local Control)

这是B样条相对贝塞尔曲线(Bézier)的“杀手锏”功能。

  • 贝塞尔曲线的问题:在一条(单一的)贝塞尔曲线中,你移动 任何一个 控制点,整条曲线(从头到尾)都会发生变化。这叫“全局修改”。
  • B样条的优势:由于“节点向量”这本规则手册的存在,每个控制点CiC_i的“磁力”只在曲线的一小段上有效
  • 结果:如果你有一条由100个控制点定义的曲线,你移动第50号控制点,只有它附近(比如48号到52号)那一小段曲线会跟着变形。曲线的开头和结尾(比如1号点和100号点)完全不会动

这对于精确调整一个复杂曲线的局部细节(比如在规划路径时躲避一个小障碍物)是至关重要的。

4. B样条 vs. 贝塞尔曲线 (Bézier)

特性 贝塞尔曲线 (Bézier Curve) B样条 (B-Spline)
控制性 全局控制 (移动一个点,整条线都变) 局部控制 (移动一个点,只变一小段)
端点 总是穿过第一个和最后一个控制点 不一定穿过 (除非用节点向量“钳住”它)
复杂度 更简单,不需要“节点向量” 更复杂,但功能更强大
关系 贝塞尔曲线是B样条的一种特例 B样条是贝塞尔曲线的推广

5. 总结:为什么B样条这么重要?

  1. 局部控制:最核心的优势。允许你精细、安全地修改复杂曲线。
  2. 平滑度可控:你可以通过“次数”来精确定义你需要的平滑等级(例如,自动驾驶需要“Jerk平滑”,就用5次)。
  3. 通用性:它是一种“统一”的数学表达。它可以完美地表示一条直线、一个圆、一条贝塞尔曲线,或者一条极其复杂的有机曲线。

B样条和贝塞尔的转换、递推公式与矩阵的转换(实际代码需要用到的)

B样条核心:M 基础矩阵(Basis Matrix)

Part 1: M 矩阵是什么?

MM矩阵是一个**“翻译官”**,它的唯一工作就是**“基变换” (Change of Basis)**。

具体来说,它负责将B样条的**“控制点语言”翻译成计算机最喜欢的“多项式语言”**。


两种描述曲线的“语言”

想象我们有两种方式来描述同一条平滑曲线:

语言 1:B样条基(设计师的语言:用控制点描述曲线)

  • 公式
P(t)=C0N0,k(t)+C1N1,k(t)++CkNk,k(t)P(t) = C_0 \cdot N_{0,k}(t) + C_1 \cdot N_{1,k}(t) + \dots + C_k \cdot N_{k,k}(t)
  • 变量
CC (一个包含`k+1`个**控制点**的向量)。
  • 优点:非常直观,具有“局部控制性”。
  • 缺点:计算
Ni,k(t)N_{i,k}(t)

(基函数)非常缓慢,因为它是一个复杂的递归函数。

语言 2:多项式基(计算机的语言:用多项式描述曲线)

  • 公式
P(t)=A0+A1t+A2t2++AktkP(t) = A_0 + A_1 \cdot t + A_2 \cdot t^2 + \dots + A_k \cdot t^k
  • 变量
AA (一个包含 k+1k+1 个**多项式系数**的向量)。
  • 优点:计算极其快速,计算机硬件天生就擅长这个。
  • 缺点:非常不直观,改一个系数 A2A_2,整条曲线都会变。

M 矩阵的定义

MM 矩阵就是连接这两种语言的**“翻译矩阵”**。

B样条理论证明,对于同一条曲线段,它的多项式系数 AA 和它的B样条控制点 CC 之间,存在一个固定的线性关系:

A=MCA = M \cdot C

或者用向量形式表示:

[A0A1Ak]=[M0,0M0,1M0,kM1,0M1,1M1,kMk,0Mk,1Mk,k][C0C1Ck]\begin{bmatrix} A_0 \\ A_1 \\ \vdots \\ A_k \end{bmatrix} = \begin{bmatrix} M_{0,0} & M_{0,1} & \dots & M_{0,k} \\ M_{1,0} & M_{1,1} & \dots & M_{1,k} \\ \vdots & \vdots & \ddots & \vdots \\ M_{k,0} & M_{k,1} & \dots & M_{k,k} \end{bmatrix} \cdot \begin{bmatrix} C_0 \\ C_1 \\ \vdots \\ C_k \end{bmatrix}

因此,MM 矩阵是一个常量矩阵,它将B样条控制点向量 CC 转换为等效的多项式系数向量 AA

在代码中,M_pos_this->M 就是这个 MM 矩阵。cal_M 函数 就是用来在程序启动时,根据数学公式计算出这个矩阵。

Part 2: 为什么要得到 M 矩阵?

答案:为了追求极致的计算速度。

有了 MM 矩阵,我们就把一个“递归”问题(慢)转换成了一个“多项式求值”问题(快)。

在自动驾驶或机器人中,我们每秒可能需要重新规划几十次路径。如果每次计算一个点的位置,都需要从B样条最基础的递归公式(N(t)N(t))开始,计算量会大到无法接受。

现在的工作流程变得飞快:

  1. 程序启动时:调用一次 cal_M(),计算这个“翻译官” MM
  2. 优化器运行时:当需要计算 t=0.5t=0.5 处的点时:

旧的慢方法:费力地计算

N0,5(0.5),N1,5(0.5),N_{0,5}(0.5), N_{1,5}(0.5), \dots

新的快方法(代码 Get_line_position 正是这么做的):
1. A = M * C (一次矩阵乘法,得到多项式系数)
2. P(0.5) = A_0 + A_1 \cdot 0.5 + A_2 \cdot (0.5)^2 + \dots (几次乘法和加法)

结论:MM 矩阵是B样条在工程中得以高速应用(尤其是在优化问题中)的关键。

Part 3: 怎么得到 M 矩阵?

MM 矩阵是一个“基变换矩阵” (Change of Basis Matrix)。 它的推导是纯粹的数学。

我们再回到这两个公式:

  1. P(t)=T(t)AP(t) = T(t) \cdot A (多项式形式, T(t)=[1,t,t2,,tk]T(t) = [1, t, t^2, \dots, t^k])
  2. P(t)=N(t)CP(t) = N(t) \cdot C (B样条形式, N(t)=[N0,k(t),N1,k(t),]N(t) = [N_{0,k}(t), N_{1,k}(t), \dots])

我们想找到 MM 使得 A=MCA = M \cdot C
AA 替换掉: P(t)=T(t)(MC)P(t) = T(t) \cdot (M \cdot C)
所以,我们必须找到一个 MM 满足:

N(t)=T(t)MN(t) = T(t) \cdot M

推导方法:泰勒展开

我们可以通过在 t=0t=0 处对 P(t)P(t) 连续求导来“剥离”出多项式系数 AiA_i

我们以三次B样条 (k=3, Order=4) 为例(比代码中的5次简单,但原理一致)。

P(t)=A0+A1t+A2t2+A3t3P(t) = A_0 + A_1 t + A_2 t^2 + A_3 t^3

第1步:求 A0A_0 (M的第一行)

  • t=0t=0 处求值: P(0)=A0P(0) = A_0
  • 同时,在B样条世界中: P(0)=C0N0(0)+C1N1(0)+C2N2(0)+C3N3(0)P(0) = C_0 N_0(0) + C_1 N_1(0) + C_2 N_2(0) + C_3 N_3(0)
  • 所以:A0=C0N0(0)+C1N1(0)+C2N2(0)+C3N3(0)A_0 = C_0 N_0(0) + C_1 N_1(0) + C_2 N_2(0) + C_3 N_3(0)
  • 结论MM 矩阵的第一行就是B样条基函数在 t=0t=0 处的值: Mrow 0=[N0(0),N1(0),N2(0),N3(0)]M_{\text{row } 0} = [N_0(0), N_1(0), N_2(0), N_3(0)]

第2步:求 A1A_1 (M的第二行)

  • P(t)P(t) 求一阶导数: P(t)=A1+2A2t+3A3t2P'(t) = A_1 + 2 A_2 t + 3 A_3 t^2
  • t=0t=0 处求值: P(0)=A1P'(0) = A_1
  • 同时,在B样条世界中求导: P(0)=C0N0(0)+C1N1(0)+C2N2(0)+C3N3(0)P'(0) = C_0 N'_0(0) + C_1 N'_1(0) + C_2 N'_2(0) + C_3 N'_3(0)
  • 结论MM 矩阵的第二行就是B样条基函数的一阶导数在 t=0t=0 处的值: Mrow 1=[N0(0),N1(0),N2(0),N3(0)]M_{\text{row } 1} = [N'_0(0), N'_1(0), N'_2(0), N'_3(0)]

第3步:求 A2A_2 (M的第三行)

  • P(t)P(t) 求二阶导数: P(t)=2A2+6A3tP''(t) = 2 A_2 + 6 A_3 t
  • t=0t=0 处求值: P(0)=2A2    A2=P(0)2P''(0) = 2 A_2 \implies A_2 = \frac{P''(0)}{2}
  • 结论MM 矩阵的第三行是基函数二阶导数值除以 22 (即 2!2!): Mrow 2=12![N0(0),N1(0),N2(0),N3(0)]M_{\text{row } 2} = \frac{1}{2!} [N''_0(0), N''_1(0), N''_2(0), N''_3(0)]

第4步:求 AkA_k (M的第k+1行)

  • 通用公式(泰勒展开)Ak=P(k)(0)k!A_k = \frac{P^{(k)}(0)}{k!}
  • M 矩阵的通用定义MM 矩阵第 ii 行、第 jj 列的元素 M(i,j)M(i, j) 为:
M(i,j)=Nj,k(i)(0)i!M(i, j) = \frac{N_{j,k}^{(i)}(0)}{i!}

(即:第 jj 个基函数的 ii 阶导数在 t=0t=0 处的值,再除以 ii 的阶乘)

cal_M 函数在做什么?

cal_M 函数中的那个复杂的、带有组合数 C_a_bpow 的公式,就是 M(i,j)=Nj,k(i)(0)i!M(i, j) = \frac{N_{j,k}^{(i)}(0)}{i!} 这个公式的最终解析解(Analytic Solution)。

  • temp_k 就是 kk (次数)。
  • factor 就是 k!k!
  • temp1temp2 里的组合数和循环,就是数学家们通过符号计算(比如用Mathematica)推导出的 Nj,k(i)(0)N_{j,k}^{(i)}(0) 的通用表达式。

B样条与贝塞尔的转换

Step 1: 需要解决的问题:

一个在路径规划中非常经典的问题:

“我想要B样条的平滑性(Jerk/Snap连续),但又想要贝塞尔曲线的严格约束性(凸包特性)。我如何能两者兼得?”

本架构使用的方案是:“用B样条的控制点作为优化变量,但在构建约束时,把它们‘假装’成贝塞尔控制点来用。”

  • B样条控制点(钝器)

    • 优点:非常适合做平滑度优化。B样条的数学结构保证了曲线在节点处是连续的(C2, C3, C4…连续)。
    • 缺点:不适合做边界约束。B样条的“凸包性”很弱,控制点 CiC_i 对曲线的影响是“柔和”且“宽泛”的。你把 CiC_i 放在路沿内侧,并不能保证曲线本身不会“鼓出去”撞上路沿。
  • 贝塞尔控制点(利器)

    • 优点:非常适合做边界约束。贝塞尔曲线有严格的**“凸包性”(Convex Hull Property)。只要你保证所有贝塞尔控制点 BiB_i 都在路沿内侧,数学上就100%保证**整条曲线段都在路沿内侧。
    • 缺点:不适合做平滑拼接。要把很多段贝塞尔曲线平滑地(例如曲率连续)拼在一起,需要对控制点施加额外的、复杂的等式约束,这会把优化问题搞得很复杂。

设计决策:
我们选择B样条控制点 CC 作为我们的最终优化变量 xx,因为它们天生就能保证平滑。
但是,在施加边界约束时,我们希望能利用贝塞尔控制点 BB 的“凸包性”。

Step 2: 明确目标(建立“约束翻译”)

我们的优化问题是:

  • 求解x=CB-splinex = C_{\text{B-spline}} (B样条控制点)
  • 最小化Cost(C) (B样条的Jerk、Snap等平滑成本)
  • 约束:…
  • 我们真正想要的约束是施加在贝塞尔控制点 BB 上的: BoundaryBLimit\text{Boundary} \cdot B \le \text{Limit}
  • 但我们的优化变量CC
  • 所以,我们必须找到一个**“翻译矩阵” MtransM_{\text{trans}}**,它能告诉我们 BBCC 之间的关系: B=MtransCB = M_{\text{trans}} \cdot C
  • 一旦找到了 MtransM_{\text{trans}},我们就可以把“愿望”代入,得到一个OSQP能看懂的、关于 CC 的新约束: (BoundaryMtrans)CLimit(\text{Boundary} \cdot M_{\text{trans}}) \cdot C \le \text{Limit}

Step 3: 推导“翻译矩阵” (你问的 A_trans_temp)

  1. 多项式形式P(t)=TAP(t) = T \cdot A
  2. B样条形式P(t)=T(MB-splineC)P(t) = T \cdot (M_{\text{B-spline}} \cdot C)
  3. 贝塞尔形式P(t)=T(MBeˊzierB)P(t) = T \cdot (M_{\text{Bézier}} \cdot B)

从 (2) 和 (3) 可知:

MBeˊzierB=MB-splineCM_{\text{Bézier}} \cdot B = M_{\text{B-spline}} \cdot C

两边左乘 (MBeˊzier)1(M_{\text{Bézier}})^{-1}

B=(MBeˊzier)1MB-splineCB = (M_{\text{Bézier}})^{-1} \cdot M_{\text{B-spline}} \cdot C

这正是 new_osqp_interface.cc 中这行代码的数学原理:

1
Eigen::MatrixXd A_trans_temp = A_bezier_inv * M_bspline;
  • M_bspline     \implies 就是 MB-splineM_{\text{B-spline}},即B样条基础矩阵(来自 cal_M)。
  • A_trans_temp     \implies 这就是我们的“翻译矩阵” MtransM_{\text{trans}}
  1. B样条的“段”由“节点”划分
    “5米”(length_between_knots_) 是**节点(Knot)的间距。B样条曲线是由节点划分成段(Segment)**的。

  2. 一个“段”由 k+1k+1 个控制点决定
    代码是 k=5k=5 次(B_spline_order 5)。
    这意味着,任意一个曲线段(比如节点 tit_iti+1t_{i+1} 这一段),它的形状是由6个(即 k+1k+1)B样条控制点 共同决定的

所以,这个“翻译”是“6个B样条控制点 \to 6个贝塞尔控制点”。

  • A_trans_temp 是一个 6×66 \times 6 的矩阵。

输入:6个B样条控制点 [Ci,Ci+1,,Ci+5][C_i, C_{i+1}, \dots, C_{i+5}]

输出:6个等效的贝塞尔控制点 [B0,B1,,B5][B_0, B_1, \dots, B_5](这6个点定义了同一段曲线)

Step 4: 完整的设计流程(代码实现)

现在,我们来看 AddAugmentConstraintToAmatrixAutomateCalculateAffineConstraint 这样的函数是如何工作的:

  1. 遍历所有曲线“段”
    代码通过一个循环 for (int i = 1; i < num_of_knots_; i++) 来遍历每一段曲线。

  2. 获取该段的“B样条控制点组”
    代码知道第 i 段曲线(从节点 i-1i)是由 spline_order_ + 1(即6)个B样条控制点决定的,这6个控制点的起始索引是 i-1

  3. 获取该段的“路沿约束”
    代码获取第 i 段对应的路沿边界(比如一条直线 ax+bycax+by \le c)。这个约束用一个矩阵 A_vertice 来表示,它代表 aabb

  4. 执行“约束翻译”
    我们的“愿望”是:A_vertice * B_seg <= c (其中 BsegB_{seg} 是这一段的6个贝塞尔控制点)

我们的“翻译”是:Bseg=A_trans_tempCsegB_{seg} = \text{A\_trans\_temp} \cdot C_{seg} (其中 CsegC_{seg} 是这一段的6个B样条控制点)
代码计算这个“翻译后”的约束矩阵:
Eigen::MatrixXd A_vertice_bound = A_vertice * A_trans;
(这里的 A_trans 就是 A_trans_temp

  1. “喂”给求解器
    A_vertice_bound 现在是一个(例如) 6×126 \times 12 的矩阵,它代表了对6个B样条控制点(Ci,,Ci+5C_i, \dots, C_{i+5})的 xxyy 坐标施加的等效约束。

代码将这个 A_vertice_bound 矩阵,填充到OSQP总约束矩阵 AA正确位置——即对应B样条控制点 i-1i+4 的那几列中。
(例如 A_matrix(row_index, k + i - 1) = A_vertice_bound(j, k);

附录

转换矩阵

Atrans_temp=(MBeˊzier)1Mbspline A_{trans\_temp} = (M_{\text{Bézier}})^{-1} * M_{bspline} (MBeˊzier)1=[1.00.00.00.00.00.01.00.20.00.00.00.01.00.40.10.00.00.01.00.60.30.10.00.01.00.80.60.40.20.01.01.01.01.01.01.0] (M_{\text{Bézier}})^{-1} = \begin{bmatrix} 1.0 & 0.0 & 0.0 & 0.0 & 0.0 & 0.0 \\ 1.0 & 0.2 & 0.0 & 0.0 & 0.0 & 0.0 \\ 1.0 & 0.4 & 0.1 & 0.0 & 0.0 & 0.0 \\ 1.0 & 0.6 & 0.3 & 0.1 & 0.0 & 0.0 \\ 1.0 & 0.8 & 0.6 & 0.4 & 0.2 & 0.0 \\ 1.0 & 1.0 & 1.0 & 1.0 & 1.0 & 1.0 \end{bmatrix} MB-spline=1120[12666261055005050102060201001020602010052030205015101051] M_{\text{B-spline}} = \frac{1}{120} \begin{bmatrix} 1 & 26 & 66 & 26 & 1 & 0 \\ -5 & -50 & 0 & 50 & 5 & 0 \\ 10 & 20 & -60 & 20 & 10 & 0 \\ -10 & 20 & 60 & -20 & -10 & 0 \\ 5 & -20 & 30 & -20 & 5 & 0 \\ -1 & 5 & -10 & 10 & -5 & 1 \end{bmatrix}

B样条核心:节点、控制点、曲线段数与次数的关系

在B样条的数学理论中,节点(Knots)、控制点(Control Points)、**曲线段数(Segments)**和次数(Degree)之间的关系是固定的。

有两个核心公式:

  1. 经典教科书公式(关于节点向量总长度):

    m=n+pm = n + p

    或者

    m=n+k+1m = n + k + 1
    • mm = **节点(Knots)的数量**(即节点向量的总长度)
    • nn = **控制点(Control Points)的数量**
    • pp = **阶数 (Order)**
    • kk = **次数 (Degree)** (并且 p=k+1p = k + 1)
  2. 工程“黄金公式”(关于曲线段数量):

    n=Nseg+kn = N_{\text{seg}} + k
    • NsegN_{\text{seg}} = **曲线段(Segments)的数量**
    • nn = **控制点(Control Points)的数量**
    • kk = **次数 (Degree)**

换算关系:
我们可以将这两个公式合并,得到所有四个量之间的关系:

m=(Nseg+k)+k+1m = (N_{\text{seg}} + k) + k + 1 m=Nseg+2k+1m = N_{\text{seg}} + 2k + 1

举个例子(k=3k=3

  • 假设我们有 5段 曲线 (Nseg=5N_{\text{seg}}=5)。
  • 根据公式2,我们需要 n=5+3=8n = 5 + 3 = 8控制点
  • 根据公式1,节点向量总长度 m=8+3+1=12m = 8 + 3 + 1 = 12节点
    • (例如:[0, 0, 0, 0, 1, 2, 3, 4, 5, 5, 5, 5])

这套代码中的实现(更直观)

你的这套代码没有使用上面的公式,而是用了一个更符合工程直觉的、等效的公式。

new_osqp_interface.ccUpdate_refpoint_Bspline 函数中,关系是这样定义的:

  1. num_of_knots_

    • 在代码中,这个变量不是指节点向量的总长度。
    • 它指的是你输入的参考点(Ref Points)的数量
    • size_t num_of_knots = resample_points.size();
  2. num_of_knots_interval_

    • 这个变量被定义为 num_of_knots_ - 1
    • 它的真正含义是**“B样条的曲线段(Segment)的数量”**。
    • 例如:你有10个参考点,你就在它们之间创建 101=910-1 = 9 段B样条曲线。
  3. spline_order_

    • B_spline.hB_spline_order 被定义为5。
    • B_spline.cpp 中,M 矩阵被resize为 (a+1, a+1),其中 a = B_spline_order
    • 这证明了 spline_order_ 变量存储的是次数 (Degree, k),即 k=5k=5
    • (因此,这是一个 5次、6阶 的B样条)。

核心公式(代码中的)

Update_refpoint_Bspline 中,关键的赋值是:

set_num_of_control_points(num_of_knots + spline_order_ - 1);

我们来翻译一下这个公式:

n=Nref+k1n = N_{\text{ref}} + k - 1
  • nn = `num_of_control_points_`
  • NrefN_{\text{ref}} = `num_of_knots_` (参考点的数量)
  • kk = `spline_order_` (次数,为5)

为什么这个公式是正确的?

我们把上面这个公式换一种写法。
我们知道 Nseg=Nref1N_{\text{seg}} = N_{\text{ref}} - 1,所以 Nref=Nseg+1N_{\text{ref}} = N_{\text{seg}} + 1
代入上面的公式:

n=(Nseg+1)+k1n = (N_{\text{seg}} + 1) + k - 1 n=Nseg+kn = N_{\text{seg}} + k

这正是B样条中关于“分段”的黄金公式:

控制点的数量 = 曲线段的数量 + 曲线的次数
n=Nseg+kn = N_{\text{seg}} + k

直观理解一下这个公式(以 k=5k=5 为例):

  • 第1段 (Nseg=1):你需要 k+1k+1 个控制点来定义第一段曲线(即6个CPs)。
    • 公式:n=1+5=6n = 1 + 5 = 6(正确)
  • 第2段 (Nseg=2):为了和第一段平滑拼接,你只需要增加1个新的控制点。
    • 总控制点数 = 7。
    • 公式:n=2+5=7n = 2 + 5 = 7(正确)
  • 第3段 (Nseg=3):你再增加1个新的控制点。
    • 总控制点数 = 8。
    • 公式:n=3+5=8n = 3 + 5 = 8(正确)

以此类推…

NsegN_{\text{seg}}:你需要 Nseg+kN_{\text{seg}} + k 个控制点。

代码中如何定义这个关系

理论术语 代码中的变量 解释
参考点数量 (NrefN_{\text{ref}}) num_of_knots_ resample_points.size()
曲线段数量 (NsegN_{\text{seg}}) num_of_knots_interval_ 等于 num_of_knots_ - 1
B样条次数 (kk) spline_order_ 固定为 5
控制点数量 (nn) num_of_control_points_ 由公式 n=Nseg+kn = N_{\text{seg}} + k 算出

代码中的核心关系在 Update_refpoint_Bspline 中:
num_of_control_points_ = (num_of_knots_ - 1) + spline_order_
即:
控制点数量 = (参考点数量 - 1) + 次数

本项目如何构建hessian和约束

位置成本

J=wPiPref,i2J = w \cdot || P_i - P_{\text{ref}, i} ||^2

成本函数可简化为:JwPiTPi2wPiTPref,iJ \approx w \cdot P_i^T P_i - 2w \cdot P_i^T P_{\text{ref}, i}

B样条的一个核心特性是,曲线上任何一点的任何属性(位置、速度、加速度…)都可以表示为B样条控制点 CC 的线性组合。

Pi=Np,iCP_i = N_{p, i} \cdot C

(其中 Np,iN_{p, i} 是一个行向量,它来自B样条基础矩阵 Np 的第 ii 行,代表了在第 ii 个节点上,各个控制点对“位置”的“影响力”权重)

JwCT(Np,iTNp,i)C(2wNp,iTPref,i)TCJ \approx w \cdot C^T (N_{p, i}^T N_{p, i}) C - (2w \cdot N_{p, i}^T P_{\text{ref}, i})^T C

Hessian PP 的部分: Pi=2w(Np,iTNp,i)P_i = 2 \cdot w \cdot (N_{p, i}^T N_{p, i})梯度 qq 的部分: qi=2wNp,iTPref,iq_i = -2w \cdot N_{p, i}^T P_{\text{ref}, i}

航向成本 (PointCost_v_)

成本函数:Jv=wvViVref,i2J_v = w_v \cdot || V_i - V_{\text{ref}, i} ||^2B

样条形式:Vi=Nv,iCV_i = N_{v, i} \cdot C (其中 Nv,iN_{v, i} 是 Nv 矩阵的第 ii 行)。

Hessian PP 的部分:Pv=2wv(Nv,iTNv,i)P_v = 2 \cdot w_v \cdot (N_{v, i}^T N_{v, i})

加速度/曲率成本 (PointCost_a_)

成本函数:Ja=waAiAref,i2J_a = w_a \cdot || A_i - A_{\text{ref}, i} ||^2B

样条形式:Ai=Na,iCA_i = N_{a, i} \cdot C (其中 Na,iN_{a, i} 是 Na 矩阵的第 ii 行)。

Hessian PP 的部分:Pa=2wa(Na,iTNa,i)P_a = 2 \cdot w_a \cdot (N_{a, i}^T N_{a, i})

Jerk 和 Snap 成本 (PointCost_j_, PointCost_s_)

PointCost_j_:最小化三阶导数(Jerk,加加速度),减少“闯动感”。

PointCost_s_:最小化四阶导数(Snap,加加加速度),减少“抖动感”。

Jj=wjJi02=wjJi2J_j = w_j \cdot || J_i - 0 ||^2 = w_j \cdot || J_i ||^2

相关矩阵的获得

  1. M 矩阵(位置基础矩阵)的获得

    MM 矩阵(即位置基础矩阵,对应于 0 次导数)是通过一个复杂的解析公式一次性计算出来的。

    初始化:在 B_spline 构造函数中,首先根据样条的阶数(a=5a=5,即6阶)来调整矩阵大小:this->M.resize(a + 1, a + 1);。

    计算:核心的计算逻辑封装在 cal_M(Eigen::MatrixXd &M) 函数中。

    推导原理:cal_M 函数通过多层循环和组合数函数 C_a_b,实现了将B样条定义直接转换到**多项式基(Monomial Basis)**的解析公式。这个公式是事先推导好的,用于快速计算 MM 矩阵的每一个元素 M(i,j)M(i, j)。 计算方式前文已述。

  2. 导数矩阵(Mdiff_1, Mdiff_2, …)的获得导数矩阵是通过对已有的 MM 矩阵施加 差分算子(Difference Operator) 来获得的,而不是重新计算。这个过程利用了B样条的导数特性。

    核心原理:差分矩阵对于B样条,下一阶导数的控制点 CC' 与当前阶的控制点 CC 之间存在一个线性关系,这个关系可以表示为一个差分矩阵。例如,速度(1次导数)的控制点 CC' 是由位置控制点 CC 的相邻点相减得到的。

    构造差分矩阵:代码首先构造了一个 k×(k+1)k \times (k+1) 的差分矩阵 v_mat(用于1阶导数)。这个矩阵的元素是 [-1, 1] 的对角块形式。

1
2
3
4
for (int i = 0; i < a; i++) {
v_mat(i, i) = -1;
v_mat(i, i + 1) = 1;
}
vmat=[110000011000001100000110000011]v_{\text{mat}} = \begin{bmatrix} -1 & 1 & 0 & 0 & 0 & 0 \\ 0 & -1 & 1 & 0 & 0 & 0 \\ 0 & 0 & -1 & 1 & 0 & 0 \\ 0 & 0 & 0 & -1 & 1 & 0 \\ 0 & 0 & 0 & 0 & -1 & 1 \end{bmatrix}
应用差分:将差分矩阵乘以原始的基础矩阵,即可得到下一阶的导数基础矩阵:this->M_diff_1 = this->M_diff_1 * v_mat;
  1. 链式应用(2, 3, 4 阶导数)对于更高的阶数(加速度 Mdiff_2M_{\text{diff\_2}}、Jerk Mdiff_3M_{\text{diff\_3}}、Snap Mdiff_4M_{\text{diff\_4}}),代码继续构造新的差分矩阵 (a_mat, jerk_mat, snap_mat),并以链式法则的形式将它们连续应用到原始的基础矩阵上:
    2阶导数: this->M_diff_2 = this->M_diff_2 * a_mat * v_mat;
    4阶导数: this->M_diff_4 = this->M_diff_4 * snap_mat * jerk_mat * a_mat * v_mat;

B样条的求导和差分

对于一个 kk 次B样条,它的第 rr 阶导数是一条 krk-r 次的B样条。第 rr 阶导数的第 ii 个控制点 Ci(r)C^{(r)}_{i} 由以下公式给出:

Ci(r)=kr+1ti+k+1ti+r(Ci+1(r1)Ci(r1))C^{(r)}_{i} = \frac{k - r + 1}{t_{i+k+1} - t_{i+r}} (C^{(r-1)}_{i+1} - C^{(r-1)}_{i})

对于均匀B样条,即我们假设节点间距是恒定的(通常为1)。在这种情况下,上式中的系数项(被称为缩放因子)可以简化为一个常数:kr+1ti+k+1ti+rUniform Knotskr+1kr+1=1\frac{k - r + 1}{t_{i+k+1} - t_{i+r}} \quad \xrightarrow{\text{Uniform Knots}} \quad \frac{k - r + 1}{k - r + 1} = 1

我们的目标是构造一个矩阵 DD (即 vmatv_{mat}, amata_{mat}, 等),它能将 NinN_{\text{in}} 个控制点(输入向量)转换为 NoutN_{\text{out}} 个控制点(输出向量)。假设我们要计算第 rr 阶导数,即从 degree kr+1k-r+1 降到 krk-r

Dr(i,j)={1if j=i+1if j=i+10otherwiseD_{r}(i, j) = \begin{cases} -1 & \text{if } j = i \\ +1 & \text{if } j = i+1 \\ 0 & \text{otherwise} \end{cases}

要从原始的 CpositionC_{\text{position}} 向量直接计算第 rr 阶导数 C(r)C^{(r)},我们需要连续应用 rr 个差分矩阵。

C(r)=DtotalCpositionC^{(r)} = D_{\text{total}} \cdot C_{\text{position}} D1D_1 (即 vmatv_{\text{mat}}):作用于原始 kk 次控制点,生成 k1k-1 次控制点。 D2D_2 (即 amata_{\text{mat}}):作用于 D1D_1 的结果,生成 k2k-2 次控制点。 DrD_r:作用于第 r1r-1 阶导数的控制点,生成最终的 rr 阶导数控制点。 vmatv_{\text{mat}} (D1D_1):5×65 \times 6 amata_{\text{mat}} (D2D_2):4×54 \times 5 jerkmatjerk_{\text{mat}} (D3D_3):3×43 \times 4 snapmatsnap_{\text{mat}} (D4D_4):2×32 \times 3

N矩阵构成

以Np为例
对于位置(0次导数),局部构建块是 1×61 \times 6row_0row\_0 向量:这个 row_0row\_0 是从 6×66 \times 6MM 矩阵(位置基础矩阵)中提取出来的。它代表了 k+1=6k+1=6 个控制点对单个节点位置的影响权重。

通过循环将 row_0row\_0 堆叠到 NpNp 中:

  1. 循环:代码遍历所有节点(for (int row_index = 0; …)),每一行对应曲线上一个要被约束的位置。
  2. 稀疏性:由于局部控制性,每一行只在对应影响它的6个控制点(列)上包含非零值(即 row_0row\_0)。
  3. 平移:代码计算出当前节点对应的 6个控制点的起始列索引(interval_index),然后将 row_0row\_0 填充到 NpNp 矩阵的相应位置。通过这种方式, NpNp 矩阵被构建成一个带有稀疏对角块的结构,从而实现了全局映射。

约束构建

约束类型 物理/几何含义 对应导数阶数 约束的目标对象 代码结构
位置 (Position) 路径必须穿过或接近某一 (X,YX, Y) 点。 0次导数 曲线在特定节点上的位置 (P(ti)P(t_i)) HardPointPosConstraint, SoftPointPosConstraint
航向 (Heading) 曲线必须以某一角度(方向)通过该点。 1次导数 曲线在特定节点上的切线向量 (dPdt\frac{dP}{dt}) HardPointConstraint, SoftPointConstraint (用于 v)
曲率 (Curvature) 曲线在该点的弯曲程度不能超过限制。 2次导数 曲线在特定节点上的加速度向量 (d2Pdt2\frac{d^2P}{dt^2}) HardPointConstraint, SoftPointConstraint (用于 a)
线/边界 (Line) 整段曲线必须位于一条直线边界的内侧。 几何约束 (通过 Bézier 凸包实现) 曲线的整个分段 (P(t)P(t) for t[ti,ti+1]t \in [t_i, t_{i+1}]) HardLineConstraint, SoftLineConstraint

位置约束

我们将位置 P(ti)P(t_i) 表示为控制点 CC 的线性组合:

P(ti)=NpiCP(t_i) = Np_i \cdot C
变量 X 坐标 (行 ii) Y 坐标 (行 i+1i+1)
A 矩阵 Np.row(knot_ind) Np.row(knot_ind)
ll / uu 边界 xref±xmax/minx_{\text{ref}} \pm x_{\text{max/min}} yref±ymax/miny_{\text{ref}} \pm y_{\text{max/min}}

heading约束

由于QP求解器只能处理线性约束(AxuAx \le u),因此必须将非线性的角度约束 (θ=arctan(Vy/Vx)\theta = \arctan(V_y/V_x)) 转化为关于向量分量 (Vx,VyV_x, V_y) 的线性不等式。

首先计算出角度边界 (θmin\theta_{\text{min}}θmax\theta_{\text{max}}) 对应的 cos\cossin\sin 值:

θmin\theta_{\text{min}} 边界:由 (dxmin,dymin)(dx_{\text{min}}, dy_{\text{min}}) 定义。 θmax\theta_{\text{max}} 边界:由 (dxmax,dymax)(dx_{\text{max}}, dy_{\text{max}}) 定义。
约束目的 数学形式 (线性不等式) AA 矩阵构建 (Nv 矩阵填充) uu (上界)
下边界 (θθmin\theta \ge \theta_{\text{min}}) Vxsin(θmin)Vycos(θmin)0V_x \sin(\theta_{\text{min}}) - V_y \cos(\theta_{\text{min}}) \le 0 NvCxNv \cdot C_x (乘 dymindy_{\text{min}}) 和 NvCyNv \cdot C_y (乘 dxmin-dx_{\text{min}}) 0
上边界 (θθmax\theta \le \theta_{\text{max}}) Vycos(θmax)Vxsin(θmax)0V_y \cos(\theta_{\text{max}}) - V_x \sin(\theta_{\text{max}}) \le 0 NvCyNv \cdot C_y (乘 dxmaxdx_{\text{max}}) 和 NvCxNv \cdot C_x (乘 dymax-dy_{\text{max}}) 0

曲率约束

曲率公式:κ=VxAyVyAx(Vx2+Vy2)3/2\kappa = \frac{|V_x A_y - V_y A_x|}{(V_x^2 + V_y^2)^{3/2}}
(A\vec{A}是二阶导数)
为了在QP求解器中使用,必须将曲率约束简化为一个线性不等式。

  1. 分母: 在路径规划的局部优化中,车辆的切线速度(分母项 Vx2+Vy2V_x^2 + V_y^2) 被假定为非零常数或已知值。
  2. 分子: 曲率约束被简化为约束分子中的法向加速度分量(即 VxAyVyAxV_x A_y - V_y A_x)。
  3. 近似航向: 为了得到 VxV_xVyV_y 的系数,我们用已知的参考航向 θref\theta_{\text{ref}} (或上一次迭代的航向)来近似 VxV_xVyV_y
Vxcos(θref)V_x \approx \cos(\theta_{\text{ref}}) Vysin(θref)V_y \approx \sin(\theta_{\text{ref}})

此时约束变为线性的

Lower[sin(θref)Ax+cos(θref)Ay]Upper\text{Lower} \le [-\sin(\theta_{\text{ref}}) \cdot A_x + \cos(\theta_{\text{ref}}) \cdot A_y] \le \text{Upper}
约束目的 数学形式 (A x) AA 矩阵构建 ll / uu 边界
法向分量 (AxA_x 贡献) x¨\ddot{x} 乘以 sin(θref)-\sin(\theta_{\text{ref}}) NaCxNa \cdot C_x (乘以 dy-dy) κref±κmax\kappa_{\text{ref}} \pm \kappa_{\text{max}} (乘以 V3/2V^{3/2} 缩放)
法向分量 (AyA_y 贡献) y¨\ddot{y} 乘以 +cos(θref)+\cos(\theta_{\text{ref}}) NaCyNa \cdot C_y (乘以 +dx+dx) κref±κmax\kappa_{\text{ref}} \pm \kappa_{\text{max}} (乘以 V3/2V^{3/2} 缩放)
本文作者:战斗包子
本文链接:https://paipai121.github.io/2025/11/02/工作/B样条/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可