☰
机器人运动规划中的曲线生成:BSpline与ReedsShepp核心原理与实现
2026/10/18 8:42:52 网站建设 项目流程

做机器人运动规划的人,迟早会和“曲线生成”这四个字正面相遇。路径规划算法给你一串离散的路径点,可机器人真正要跟着走的时候,需要的是连续、平滑、满足运动学约束的轨迹。这个“从路径点到连续轨迹”的转换过程,就是曲线生成要解决的问题,它直接决定了机器人是丝滑地转过拐角,还是在每个路径点处停下来急转弯。

这节教程里,我挑了两种极具代表性的曲线来拆解:BSpline和ReedsShepp。BSpline是参数曲线的代表,擅长把离散点拟合成光顺曲线,常用于路径平滑;ReedsShepp则是非完整约束下的最优路径求解器,专门解决“车不能横着走”这类约束下的最短路径问题。两者一个重“形”,一个重“法”,组合在一起基本覆盖了运动规划库中最核心的曲线生成场景。这篇内容适合正在写运动规划库、或者想搞懂常见曲线算法底层的读者,我会把原理、代码和踩坑经验一起聊透。

1. 曲线生成问题的总体判断与方案拆解

1.1 为什么需要“曲线生成”这一层?

先想清楚一个问题:规划算法输出的路径到底是什么?咱们常见的地图搜索类规划器,输出是一串离散的路径点,比如[(0,0), (1,0), (2,1), ...]。这些点只包含空间位置信息,没有时间信息,也没有方向信息——它们只是一条折线的顶点。

但机器人要的是能真正执行的东西。以差速小车为例,它需要知道每一时刻的线速度和角速度,这等价于要知道轨迹上每个点处的方向(朝向角)、曲率,以及曲率的变化率是否连续。如果直接把折线路径丢给底层控制,机器人到每个拐点都得停一下再转向,否则瞬时角速度冲击会令电机过载、车身打滑。

曲线生成层要做的,就是把离散路径点转化为一条参数化的连续曲线,并在此基础上做插值、平滑和重采样,最终输出带朝向信息和曲率信息的轨迹点序列。这一步做好了,下游的轨迹跟踪控制器才能有稳定的输入。这就是BSpline的主要应用场景:它用一个较低维度的控制点集合,去描述一条具有全局连续性的曲线。

1.2 BSpline和ReedsShepp在解决什么问题上有本质区别

要理解这两种算法为什么会被放在同一节里讲,得先看清楚它们解决的问题落脚点完全不同。

BSpline解决的是“如何让曲线更光顺”的问题。它关注的是曲线的数学形态,给定一组离散点,通过最小二乘拟合或者插值的方式,生成一条在节点处具有连续性(通常到C2甚至更高)的平滑曲线。它不关心机器人是否走得出来,只关心曲线本身长得好不好看、光顺不顺。从数学上说,C2连续意味着曲率连续,机器人执行起来加速度不会突变,这对乘坐舒适性和电机寿命都非常友好。

ReedsShepp解决的是“在非完整约束下怎么从A点到B点距离最短”的问题。它针对的是像汽车这样的非完整系统,特点是转弯半径有下限、不能横向移动。ReedsShepp曲线是在给定最小转弯半径的前提下,只由直线段(Straight)和圆弧段(Curve)拼接而成的,在几何上证明了这些组合中可以找到两状态之间的最短路径。它输出的是一条满足运动学约束的可行路径,但不保证曲率连续——圆弧和直线交界处,曲率直接跳变。

所以,BSpline追求光顺度,ReedsShepp追求可行性与最短路程。在运动规划库的设计里,这两者通常不是替代关系,而是管道关系:先有ReedsShepp这样的几何路径,再用BSpline做平滑后处理。这个组合拳打好了,路径既可行又丝滑。

2. BSpline曲线生成内核拆解:控制点、节点向量与递推求值

2.1 基础概念:为什么是“B样条”而不是“贝塞尔曲线”

很多初学者问过我一个问题:明明贝塞尔曲线用的人那么多,我们为什么还要用BSpline?核心原因是贝塞尔曲线的控制点对曲线形状影响是全局的——你拖任何一个控制点,整条曲线都会受影响。这个特性在运动规划里很尴尬,因为路径点往往包含局部障碍信息,我们希望局部修形不影响远处。

BSpline引入了一个“节点向量”的概念,把参数域切分成若干区间,每个控制点只在一定参数区间内影响曲线形状,这就是它所说的“局部支撑性”。通俗地讲,BSpline像是一段一段拼接的曲线,每一段只由附近的几个控制点决定,控制点的修改影响范围是局部的。实际工程里,我不可能因为一个路径点有障碍就重新优化整条轨迹,所以局部支撑这个性质非常关键。

BSpline曲线的数学定义长这样:

C(u) = Σ P_i * N_{i,p}(u)

其中 P_i 是控制点,N_{i,p}(u) 是p阶B样条基函数,定义在节点向量 U=[u_0, u_1, ..., u_m] 上。基函数由Cox-de Boor递推关系给出,这是所有BSpline实现的核心。

2.2 节点向量分类:均匀、clamped与三种常见设定

节点向量直接决定了曲线起点终点是否穿过控制点,这是新手极易踩坑的地方。

  • 均匀节点向量:节点在参数轴上均匀分布,但曲线不穿过首尾控制点。路径规划里很少直接用,因为轨迹起点终点不受控。

  • Clamped节点向量(也叫非均匀、开),这是运动规划中最常用的类型:首尾节点分别重复 p+1 次(p为曲线阶数)。这样曲线严格经过第一个和最后一个控制点,并且起点切向由前两个控制点决定,终点切向由最后两个控制点决定。这个特性对轨迹执行非常重要——机器人必须在起点出发,也必须准确到达终点。

  • 非均匀节点向量:根据控制点间距调整节点分布,可以让曲线在控制点密集的区域有更大的自由度形状逼近。常见做法是用向心参数化,节点间距与控制点间距的平方根成正比,避免曲线在控制点密集处出现尖角。

实际使用时的默认选择,我建议直接上clamped的3次(p=3)BSpline。3次意味着在内部节点处达到C2连续,也就是曲率连续,这是轨迹跟踪控制最舒服的输入信号。

2.3 Python实现:Cox-de Boor递推与de Boor求值

说到代码实现,最常用的BSpline曲线求值算法是de Boor递推法,它比直接算基函数再求和更稳定、计算量更小。核心思路是:给定参数u,找到它落在哪个节点区间 [u_k, u_{k+1}) 内,然后从那一层的控制点开始反复做线性插值。

def bspline_de_boor(ctrl_pts, knots, degree, u): # ctrl_pts: list of [x, y] control points # knots: clamped knot vector # degree: curve degree (usually 3) # u: parameter value in [0, 1] n = len(ctrl_pts) - 1 # find knot span index k such that knots[k] <= u < knots[k+1] if u >= knots[-1]: k = n else: k = 0 while k < len(knots) - 2 and u >= knots[k + 1]: k += 1 # copy control points in the local span d = [ctrl_pts[j][:] for j in range(max(k - degree, 0), min(k, n) + 1)] for r in range(1, degree + 1): for j in range(degree, r - 1, -1): denom = knots[j + k - degree + 1] - knots[j + k - degree + r] alpha = (u - knots[j + k - degree]) / denom if denom != 0 else 0.0 d[j] = [ (1 - alpha) * d[j - 1][0] + alpha * d[j][0], (1 - alpha) * d[j - 1][1] + alpha * d[j][1] ] return d[degree]

这段代码虽然看起来只有二十多行,但我建议你有条件的话在本地跑一跑,观察节点向量、阶数对曲线形状的影响。有一个实操经验:当节点向量首尾重复度不够 p+1 时,曲线不会穿过首尾控制点,这会导致轨迹起点和终点偏离路径规划的起终点。这个bug在视觉上不太明显,但机器人启动时会突然有一个小的位置偏差,这种问题调试起来极其消耗时间。

2.4 参数选择:阶数、控制点数量与节点向量的权衡

曲线阶数越高,曲线越光滑,但计算量也越大,而且单点控制的影响范围越宽,局部修形能力越差。3次是一个工程上的黄金选择:C2连续够用,计算也轻。

控制点数量则直接决定拟合精度和曲线形态复杂度。控制点太少,曲线会“削”掉路径的拐角,偏离原始路径太多;控制点太多,曲线会出现不自然的波浪状抖动,而且优化的自由度过多,实时计算压力也大。我自己的经验是,先用Ramer-Douglas-Peucker算法做一次路径抽稀,把明显冗余的共线点去掉,再以剩余点作为控制点做BSpline拟合。这样做出来的轨迹既平滑又不丢失关键转角信息。

3. ReedsShepp曲线:非完整约束下的最短路径求解

3.1 非完整约束到底是什么意思

要理解ReedsShepp,先得理解“非完整约束”这个听着高大上的词。完整约束是指系统位置变量之间存在硬性的几何限制关系,比如两轮车只能在平面运动,z坐标恒为0,这是完整约束。

非完整约束则是对速度的约束,而不是对位置的约束。车的典型特征是:不能横向移动,原地转弯半径不大于某个最小值。车的朝向角 θ 和速度 v 之间的关系是

x_dot = v * cos(θ) y_dot = v * sin(θ)

这个约束无法直接积分成位置层面的几何限制,所以叫非完整约束。生活中很好理解:车不能像人一样横着平移,它有最小转弯半径,倒车入库的时候要打好多把方向,这就是非完整约束在起作用。

ReedsShepp曲线的核心贡献,是证明了在这个约束下,任意两个位姿(x, y, θ)之间的最短路径,一定可以由固定模式的 圆弧(Curve)和 直线(Straight)拼接而成。圆弧的半径是最小转弯半径,前进或后退。这个结论极具工程价值,因为它把无穷多可能的路径压缩成了有限个模式的组合,每个模式独立计算长度,取最小值即可。

3.2 路径模式分类:CSC、CCC与那12种基本组合

ReedsShepp路径模式的分类,工程上可以简化描述为几大族:

  • CSC(弯-直-弯):直线段连接两个圆弧,适用于大多数常规场景,例如从某个车位倒出来再正向开走,都是这个模式。

  • CCC(弯-弯-弯):三段圆弧拼接,适用于极狭窄的空间,比如两车之间的平行泊车操作,需要来回揉库时会出现这种模式。

  • 其他复合模式,如 CCSC、CCCC 等,通常是在CS和CCC的基础上增加额外的圆弧段来处理更特殊的边界条件。

完整证明需要计算机辅助枚举,但工程实现上只需要知道一件事:对每种模式,基于起点与终点的相对位姿,用几何关系求解圆弧的弧长和直线长度,然后取总长最小者。大部分开源运动规划库里实现的是12种左右的模式,覆盖所有可行状态组合。

3.3 一个可用的简化实现思路

我不打算贴一个几百行的完整ReedsShepp实现,那样篇幅过长且大部分是模式枚举,容易劝退。我这里给一个核心的几何计算片段,以CSC模式中 左转-直线-右转 为例,展示如何根据起终点位姿计算路径参数。

import math def cs_path_length(x, y, phi, rho): # x, y: relative target position # phi: target heading relative to start # rho: minimum turning radius # transform to vehicle frame at origin, heading 0 # start circle center: (0, rho) for left turn c1 = (0, rho) # target circle center: solve for (cx, cy) with heading phi c2 = (x - rho * math.sin(phi), y + rho * math.cos(phi)) # straight line is common tangent between the two circles dx = c2[0] - c1[0] dy = c2[1] - c1[1] d = math.hypot(dx, dy) # distance between centers must be >= 2*rho for solution to exist if d < 2 * rho: return None alpha = math.atan2(dy, dx) beta = math.acos(d / (2 * rho)) # tangent point on start circle p1 = (c1[0] + rho * math.cos(alpha - beta), c1[1] + rho * math.sin(alpha - beta)) # tangent point on target circle p2 = (c2[0] - rho * math.cos(alpha - beta), c2[1] - rho * math.sin(alpha - beta)) arc1 = rho * (alpha - beta) # length of initial left turn arc straight = math.hypot(p2[0] - p1[0], p2[1] - p1[1]) arc2 = rho * ((phi - (alpha - beta)) if phi > (alpha - beta) else 2*math.pi - (alpha - beta) + phi) return arc1 + straight + arc2

这段代码只展示了模式里最核心的几何求解:求两个同半径圆的公切线切点。真实库中你需要对每种 C 的符号(左转右转)、每个圆弧的走向(前进后退)枚举情况并规范化角度范围,然后统一取最小。

我要强调一个新手容易忽略的细节:ReedsShepp路径是无曲率连续性的。在圆弧与直线的连接点上,曲率从 1/ρ 直接跳变到0。这意味着路径“可通行”,但履带式或阿克曼式的车辆在实际跟踪时,需要在那个点附近慢下来或者允许一定的路径跟踪误差,否则转向执行机构会吃不消。

3.4 ReedsShepp能做轨迹优化吗

很多同学以为ReedsShepp本身就是轨迹优化的结果——它确实在“最短路径”这个指标下是最优的,但它优化的维度有限。你可以通过调整 ρ 的大小来观察路径形态的变化:ρ 越小,路径越“刁钻”,转弯越急,长度越短;ρ 越大,路径越舒展,但里程更长。

因此工程上常用做法是拿ReedsShepp结果做参考线,然后再做二次优化。比如以ReedsShepp路径为初值,用数值优化方法在保持非完整约束(曲率上限)的前提下,把路径打磨到曲率连续。这就是“轨迹优化”这4个字在标题里的含义——不是选一种曲线就完事了,而是选型、生成、平滑、校验的全流程。

4. 轨迹优化管道:如何把两种曲线串联出实用价值

4.1 一条完整的曲线生成管线应该长什么样

在运动规划库里,典型的曲线生成管线是这样的:

第一步,输入一条无碰撞的离散路径点,路径可能来自混合A*或者栅格地图搜索,也可能是全局规划器的输出。

第二步,提取路径中的关键转折点,做抽稀处理,去掉冗余点。

第三步,调用ReedsShepp或者类似几何路径生成器,计算出满足车辆运动学约束的粗路径。这一步保证了路径“走得了”,把最小转弯半径的因素考虑进来。

第四步,对粗路径采样,得到密集轨迹点,再将这些点作为BSpline拟合的目标点,生成一条平滑、曲率连续的最终轨迹。

第五步,对平滑后的轨迹做可行性校验:曲率是否超过车辆极限、是否偏离原始路径太远、是否碰撞。

这个管道里,每种曲线都只做自己最擅长的事。ReedsShepp管“能不能走”,BSpline管“走得顺不顺”。如果在第四步直接对原始路径点做BSpline拟合,忽略了最小转弯半径约束,得到的轨迹很可能“看着平滑但实际转不过去”。

4.2 结合时的约束处理技巧

用BSpline平滑ReedsShepp路径时,最容易犯的错是放任曲线自由拟合,结果起点和终点的朝向角跑偏了。解决方案是采用带端点约束的B样条拟合:把起点位置、起点朝向、终点位置、终点朝向作为硬约束,剩余内部点做最小二乘拟合。

实现上有个小技巧:先把采样点转换到局部坐标系(以起点为原点,航向角为x轴正方向),在这个坐标系下拟合,拟合完再变换回全局坐标。这样做的好处是数值稳定性好,避免了全局坐标下坐标值数量级差异引起的病态矩阵问题。

BSpline平滑前后的曲率检查是个必须的环节。我一般会计算每个采样点处的曲率,再对比最小转弯半径对应的最大曲率。如果超限,就回到ReedsShepp,把 ρ 调大一些重新生成。这个迭代过程,才是真正意义上的轨迹优化闭环。

4.3 参数标定建议:从实际机器人出发

不同底盘的运动学参数差异巨大。我之前做过一个小型四轮差速机器人,最小转弯半径接近零,理论上可以直接原地旋转。但如果把 ρ 设得太小,ReedsShepp生成的路径里圆弧段曲率很大,BSpline平滑后虽然曲率连续了,最大值却没有显著下降,控制器的角速度上限反而成了瓶颈。

有个土办法很好用:把底盘的最大转向角换算成最小转弯半径,然后乘上1.2~1.5的安全系数,用这个放大后的 ρ 来做ReedsShepp。虽然路径会稍微长一点,但控制系统跟得上,实际执行效果反而更好。这里的本质是:理论最优不等于工程最优,要给执行留余量。

5. 常见问题与排查技巧实录

5.1 BSpline的节点向量和拟合震荡问题

有人在实际拟合时发现曲线在某个区间里剧烈波动,明明输入的控制点都很规整,曲线却出现“龙蛇”般的抖动。我排查这类问题通常分两步。

第一步,检查节点向量是否首尾clamped。如果没有clamped,曲线不仅不过控制点,而且奇偶幂次造成的Runge现象会更明显。第二步,检查控制点间距是否极端不均匀。如果两个控制点间距特别近而相邻间距特别远,曲线会在密集区出现不必要的弯曲。处理办法是用向心参数化重算节点向量,并且在拟合前做一次控制点去重和均匀化处理。

还有一种常见状况:BSpline拟合时用原始路径点直接作为控制点,而不是做最小二乘逼近。这么做会让曲线严格过每个路径点,噪声被完完整整保留下来,反而放大不光滑性。正确的做法是把原始点作为“目标点”,通过最小二乘求控制点,让曲线逼近而不是穿过这些点。这里其实是“插值”和“逼近”的区别,运动规划里绝大多数情况要的是逼近。

5.2 ReedsShepp无解与高耗时

ReedsShepp求解失败的典型原因:目标点位姿与起始位姿距离小于2倍最小转弯半径,而且朝向又比较刁钻。比如车位于狭窄巷道里,目标点在正侧方很近的地方,这种状态下任何CSC模式都找不到公交切线。此时要检查是不是 ρ 设得过大,或者是不是应该切换到CCC模式。

另一个性能坑是模式枚举的冗余计算。完整ReedsShepp实现要遍历几十种模式,每种模式内部又有弧度规范化、符号分支,如果不做模式对称性裁剪,计算量很容易翻好几倍。优化手段很简单:利用路径的对称性。比如如果起点终点互换时路径长度不变,那么倒着算一遍的结果就能复用。

5.3 采样的均匀性与时间分配问题

曲线生成完之后还有一个隐藏环节:采样。均匀地按弧长采样,而不是按参数u均匀采样。BSpline的参数u均匀采样,会在弧长较长的区间产生稀疏点,在弧长较短的区间产生密集点,这个不均匀会造成下游速度规划的不顺滑。

我在工程里通常先把曲线离散到极密(比如每0.01米一个点),再用累计弧长做线性插值重采样到目标密度。这一小步对后端的速度规划帮助极大,值得写进库里。

5.4 坐标系变换与数值稳定性

最后也是最重要的常见错题集源头:坐标系变换。ReedsShepp求解时,起点位姿必须变换到 (0, 0, 0),终点位姿变换为相对坐标。很多人忘了对终点朝向角做角度归一化,导致后续圆弧长度计算错误。角度差在 π 附近的时候,atan2和acos的组合很容易出现数值抖动,最直接的规避办法就是在每个模式计算前,把朝向角差值 normalize 到 [-π, π]。

数值稳定性还有个容易被忽略的点:当两个圆心距离刚好等于2ρ时,acos的参数是1.0,d/2ρ 在浮点上可能略大于1,导致acos返回NaN。好习惯是代码里做 clamp 判断,这在曲线生成库里算是一个防御性编程的典型示例。

曲线生成这条线,看起来就是调个库的事,但真正动手实现一遍、调试一遍,才能理解数学描述和工程可执行之间的距离。我个人在实际操作中的体会是:先想清楚“我要这条曲线的什么性质”,再去选算法,而不是什么流行用什么。BSpline和ReedsShepp的组合,基本上是一个平衡了运动学可行性与轨迹质量的标准答案。后续如果想继续深入,可以在这个管道的下游加上速度规划和时间最优求解,那就是另一个精彩的话题了。先把手头的曲线生成做稳,是运动规划库地基中的地基。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询