1. 路径规划这件事,为什么绕不开B样条
做机器人或者自动驾驶相关项目的朋友,大概率都碰过这样一个场景:前端规划器给出一串离散的路径点,这些点连起来倒是能走,但直接拿去做轨迹跟踪,轮子或者舵机就开始抖,速度稍微提一点整个车体都在颤。这个问题的根源在于,离散折线路径的曲率是不连续的,一阶导数在拐点处直接跳变,控制器解算出来的角速度指令自然也是阶跃信号,执行机构根本跟不上。
解决思路其实很直接——把折线“磨圆”,用一条光滑的连续曲线去逼近或者插值这些路径点。而B样条曲线,就是在这个环节里出镜率最高的数学工具之一。它不像贝塞尔曲线那样牵一发动全身(移动一个控制点整条曲线都变),也不像多项式插值那样在高阶时出现龙格现象,局部支撑性和凸包性质让它在路径规划后处理中非常顺手。
这篇文章面向的是正在做路径规划、轨迹生成、机器人运动控制相关工作的开发者,尤其是那些已经跑通了A星、RRT、Hybrid A星等前端搜索算法,但卡在路径平滑后处理这一步的朋友。我会从B样条的数学原理讲起,把控制点、节点向量、基函数这几个容易绕晕的概念用工程视角拆开,然后给出完整的MATLAB实现思路和代码结构,最后分享几个我在实际项目中踩过的坑和调参经验。代码可以直接拿去用,参数改一改就能适配你的项目。
2. B样条曲线的数学底子,用工程师的方式理解
2.1 从贝塞尔到B样条:为什么要换工具
先说说贝塞尔曲线。它的定义很直观:给定n+1个控制点,用伯恩斯坦多项式做基函数,参数t从0到1扫一遍就得到曲线。二次贝塞尔三个点,三次贝塞尔四个点,公式写出来也简洁。但问题在于,贝塞尔曲线的基函数是全局的——你动任何一个控制点,整条曲线的形状都会跟着变。路径规划里控制点少说十几个,多则上百个,用贝塞尔去拟合,调一个点整条路都得重新检查碰撞,这在实际工程里完全不可接受。
B样条的核心改进就是把基函数从全局变成局部。它引入了一个叫“节点向量”的东西,把参数域切成若干段,每个控制点只在自己对应的参数区间内对曲线有影响。数学上这叫“局部支撑性”。带来的直接好处是:修改第i个控制点,最多只影响相邻的p+1段曲线(p是次数),其余部分纹丝不动。对于路径规划来说,这意味着局部避障调整不会破坏整条路径的可行性,工程上太重要了。
另一个关键性质是凸包性。B样条曲线一定落在其控制点构成的凸包内。这个性质在碰撞检测里价值巨大——你不需要对曲线本身做密集采样去检查碰撞,只需要检查控制点凸包是否与障碍物相交,就能快速判断路径是否安全。虽然凸包检测偏保守(曲线在凸包内部,凸包不碰障碍不代表曲线一定不碰),但作为快速筛选手段,能省掉大量计算。
2.2 节点向量、控制点与基函数:三者的关系
B样条曲线由三个要素完全确定:次数p、控制点序列{P0, P1, ..., Pn}、节点向量U={u0, u1, ..., um}。它们之间满足关系m = n + p + 1。节点向量是一个非递减的实数序列,决定了参数域的分段方式。
节点向量分两类:均匀节点向量和准均匀节点向量。均匀节点向量就是节点等间距分布,比如{0, 0.1, 0.2, ..., 1.0}。这种节点向量生成的B样条曲线在两端不经过控制点,对于路径规划来说,起点和终点不精确落在给定位置上,这通常是不能接受的。准均匀节点向量则把两端的节点重复p+1次,比如三次B样条(p=3)的准均匀节点向量形如{0, 0, 0, 0, 0.25, 0.5, 0.75, 1, 1, 1, 1}。这样曲线在起点和终点处分别与第一条和最后一条控制边相切,且精确通过首末控制点。路径规划里用的基本都是准均匀B样条,原因就在这里。
基函数递推公式是Cox-de Boor公式,这是B样条计算的基石:
N_{i,0}(u) = 1 if u_i <= u < u_{i+1}, else 0 N_{i,p}(u) = (u - u_i)/(u_{i+p} - u_i) * N_{i,p-1}(u) + (u_{i+p+1} - u)/(u_{i+p+1} - u_{i+1}) * N_{i+1,p-1}(u)这个递推公式看着吓人,但实现起来就是一个双层循环。需要注意的是分母可能为零的情况(当节点重复时),工程上通常约定0/0=0。MATLAB里写的时候要加一个判断,否则会出现NaN。
2.3 次数p怎么选:二阶连续是底线
路径规划里B样条次数一般选3次,也就是p=3。原因要从控制器的需求倒推。轨迹跟踪控制器通常需要曲率连续,而曲率对应的是曲线二阶导数。B样条曲线在节点处具有C^{p-k}阶连续性,k是节点重复度。对于准均匀节点向量,内部节点重复度为1,所以三次B样条内部达到C²连续,曲率连续,满足控制器要求。
二次B样条(p=2)内部只有C¹连续,曲率在节点处跳变,虽然比折线好,但对于高速跟踪或者对平顺性要求高的场景还是不够。四次及以上当然更光滑,但计算量增加,而且控制点对曲线的影响范围变大,局部调整的灵活性下降。所以三次是工程上的甜点区,兼顾了光滑性、计算效率和局部控制能力。
3. 从离散路径点到光滑曲线:完整实现流程
3.1 输入数据的预处理
前端规划器给出的路径点通常比较密集,而且可能包含冗余点或者微小抖动。直接把这些点当控制点用,会导致控制点数量过多,曲线虽然光滑但计算量大,而且局部调整困难。我的做法是先做一次降采样和关键点提取。
降采样用曲率自适应的方法:计算每个路径点处的离散曲率(用相邻三点拟合圆),曲率大的地方保留密一些,曲率小的地方稀疏一些。这样既保留了路径的形状特征,又减少了控制点数量。一般把控制点数量控制在20到50个之间比较合适,具体看路径长度和复杂度。
另一个预处理步骤是去除共线点。如果连续三个点近似共线,中间那个点对路径形状贡献很小,可以去掉。判断标准用向量叉积的模除以模长,得到点到直线的距离,小于阈值(比如0.01米)就认为是共线。这一步能进一步压缩控制点数量。
注意:预处理阶段不要过度简化,否则拟合出来的曲线和原始路径偏差太大,可能撞上障碍物。建议保留一个偏差检查环节,计算拟合曲线到原始路径点的最大距离,超过安全阈值就减少简化力度。
3.2 控制点反算:让曲线通过给定路径点
这里有一个关键选择:是把路径点直接当控制点,还是把路径点当曲线上的型值点,反算控制点?两种做法各有适用场景。
直接当控制点:曲线不经过中间路径点,只是被控制点“拉扯”着走。优点是计算简单,控制点就是路径点,不需要反算。缺点是曲线和原始路径有偏差,在狭窄通道里可能出问题。
反算控制点:把路径点作为曲线上的点(型值点),通过解线性方程组反算出控制点,这样曲线精确通过每个路径点。优点是路径保真度高,缺点是计算量稍大,而且当路径点密集时方程组可能病态。
我的经验是:如果路径点本身就是稀疏的关键点(比如RRT出来的路径经过简化后只剩十几个点),用反算控制点,保证路径精确;如果路径点很密集(比如A星出来的栅格路径有上百个点),先降采样再直接当控制点,用B样条的逼近性质去“磨光”路径。
反算控制点的数学过程:给定型值点Q0到Qn,要找控制点P0到Pn+2(三次B样条,控制点数=型值点数+2),使得曲线在参数u_i处通过Q_i。这形成一个三对角线性方程组,用追赶法(Thomas算法)求解,效率很高。MATLAB里可以直接用\运算符解,但自己写追赶法更可控。
3.3 节点向量的生成
准均匀节点向量的生成规则:对于n+1个控制点、次数p,节点向量长度为n+p+2。前p+1个节点为0,后p+1个节点为1,中间n-p个节点均匀分布在(0,1)区间。
以7个控制点、三次B样条为例:n=6, p=3,节点向量长度=6+3+2=11。前4个为0,后4个为1,中间3个节点均匀分布:{0, 0, 0, 0, 0.25, 0.5, 0.75, 1, 1, 1, 1}。这样生成的曲线起点在P0,终点在P6,且起点切向沿P0P1方向,终点切向沿P5P6方向。
如果希望曲线在起点和终点处的切向也精确可控(比如和车辆初始航向对齐),可以调整首末控制点的位置。具体做法是:在P0前面虚拟一个点P_{-1},使得P0P1方向等于期望的起始切向,然后把这个虚拟点纳入控制点序列。终点同理。这样曲线在起点的切向就由P_{-1}到P0的方向决定,可以精确匹配航向要求。
3.4 曲线离散化与输出
B样条曲线是参数曲线,实际控制器需要的是离散的路径点序列。离散化的密度要兼顾精度和计算量。太稀,曲率信息丢失;太密,数据量大且相邻点差异小,控制器可能因为数值精度问题产生抖动。
我的做法是按弧长均匀离散,目标点间距根据控制器周期和最大速度确定。比如控制器周期20ms,最大速度2m/s,那么一个周期内车辆前进0.04m,离散点间距取0.02m左右比较合适,保证每个控制周期至少有一个路径点可用。离散化时用二分法或者牛顿法从参数u反求弧长s,再均匀取s对应的u值,这样得到的点列在弧长上均匀分布。
输出格式一般包含:x坐标、y坐标、航向角、曲率、弧长。航向角由一阶导数算出,曲率由一阶和二阶导数算出。这些信息直接喂给纯跟踪或者模型预测控制器都很方便。
4. MATLAB代码实现与关键函数解析
4.1 代码整体结构
整个实现分成五个模块:数据预处理、控制点生成、B样条基函数计算、曲线离散化、可视化与验证。主函数接收原始路径点序列,输出平滑后的路径点序列及其导数信息。
function [path_smooth, info] = bspline_path_smooth(path_raw, p, num_samples) % 输入: % path_raw - Nx2矩阵,原始路径点[x, y] % p - B样条次数,推荐3 % num_samples - 输出离散点数量 % 输出: % path_smooth - num_samples x 4矩阵,[x, y, theta, kappa] % info - 结构体,包含控制点、节点向量等信息 % 1. 预处理:去除重复点和共线点 path_clean = preprocess_path(path_raw); % 2. 生成控制点(反算或直接使用) [ctrl_pts, knots] = generate_control_points(path_clean, p); % 3. 离散化B样条曲线 [path_smooth, info] = discretize_bspline(ctrl_pts, knots, p, num_samples); % 4. 可视化 plot_result(path_raw, ctrl_pts, path_smooth); end这个结构清晰,每个模块可以单独测试和替换。比如你想换一种预处理方式,只改preprocess_path就行,不影响其他部分。
4.2 基函数计算:Cox-de Boor递推的实现细节
基函数计算是整个代码的核心,也是最容易出bug的地方。MATLAB实现如下:
function N = basis_function(i, p, u, knots) % 计算第i个p次B样条基函数在参数u处的值 % 使用Cox-de Boor递推公式 if p == 0 if u >= knots(i) && u < knots(i+1) N = 1; else N = 0; end else % 第一项 denom1 = knots(i+p) - knots(i); if denom1 == 0 term1 = 0; else term1 = (u - knots(i)) / denom1 * basis_function(i, p-1, u, knots); end % 第二项 denom2 = knots(i+p+1) - knots(i+1); if denom2 == 0 term2 = 0; else term2 = (knots(i+p+1) - u) / denom2 * basis_function(i+1, p-1, u, knots); end N = term1 + term2; end end递归实现直观但效率低,每次计算都要递归展开。实际项目中我会预计算所有基函数在离散参数点上的值,存成矩阵,后面直接查表。对于n+1个控制点、m个采样点,基函数矩阵是(m+1) x (n+1)的,计算一次后面反复用。
注意:当u恰好等于最后一个节点值时,按定义应该取到最后一个非零区间。上面的代码在u=1时可能返回0,因为判断条件是
u < knots(i+1)。解决办法是在调用前把u钳制到[0, 1-eps],或者单独处理右端点。
4.3 曲线求值与导数计算
有了基函数,曲线求值就是加权求和:
function [C, dC, ddC] = evaluate_bspline(ctrl_pts, knots, p, u) % 计算B样条曲线在参数u处的位置、一阶导、二阶导 n = size(ctrl_pts, 1) - 1; C = zeros(1, 2); dC = zeros(1, 2); ddC = zeros(1, 2); for i = 0:n N = basis_function(i, p, u, knots); C = C + N * ctrl_pts(i+1, :); % 一阶导 if p > 0 dN = p * (basis_function(i, p-1, u, knots) / (knots(i+p)-knots(i)) ... - basis_function(i+1, p-1, u, knots) / (knots(i+p+1)-knots(i+1))); dC = dC + dN * ctrl_pts(i+1, :); end % 二阶导类似,略 end end导数计算容易在节点重复处出现除零,需要加保护。实际工程中更稳妥的做法是用差分近似导数,虽然精度略低但数值稳定性好得多。对于路径规划来说,差分精度完全够用。
4.4 离散化与弧长参数化
按弧长均匀离散的代码如下:
function [path_out, info] = discretize_bspline(ctrl_pts, knots, p, num_samples) % 按弧长均匀离散B样条曲线 % 先粗采样计算弧长 u_coarse = linspace(0, 1, 1000); pts_coarse = zeros(1000, 2); for i = 1:1000 pts_coarse(i, :) = evaluate_bspline(ctrl_pts, knots, p, u_coarse(i)); end % 累积弧长 ds = sqrt(sum(diff(pts_coarse).^2, 2)); s_cum = [0; cumsum(ds)]; s_total = s_cum(end); % 目标弧长序列 s_target = linspace(0, s_total, num_samples); % 反求参数u u_target = interp1(s_cum, u_coarse, s_target, 'linear'); % 计算最终路径点 path_out = zeros(num_samples, 4); for i = 1:num_samples [C, dC, ddC] = evaluate_bspline(ctrl_pts, knots, p, u_target(i)); theta = atan2(dC(2), dC(1)); kappa = (dC(1)*ddC(2) - dC(2)*ddC(1)) / (sum(dC.^2))^1.5; path_out(i, :) = [C, theta, kappa]; end info.u_target = u_target; info.s_total = s_total; end这里用粗采样加线性插值反求参数,精度足够且实现简单。如果对精度要求极高,可以用牛顿迭代精修,但一般路径规划场景没必要。
5. 实操中踩过的坑与调参经验
5.1 控制点数量与曲线保真度的权衡
控制点太少,曲线过于“僵硬”,无法贴合原始路径的细节;控制点太多,曲线虽然贴合但局部调整困难,而且计算量上升。我的一般原则是:控制点数量取原始路径点数量的1/3到1/2,且不少于8个,不多于50个。
具体操作时,先按这个范围生成控制点,然后计算拟合曲线到原始路径的最大偏差。如果偏差超过阈值(比如0.05米),就增加控制点数量;如果曲线出现不必要的振荡,就减少控制点。这个迭代过程一般两三次就能收敛。
还有一个经验:在路径曲率变化大的地方(比如急转弯),控制点要密一些;直线段可以稀疏。可以用曲率作为权重来非均匀地选择控制点位置,效果比均匀采样好很多。
5.2 节点向量的重复度控制
准均匀节点向量两端重复p+1次,保证曲线通过首末控制点。但有时候我希望曲线在中间某个特定点也精确通过,比如一个必经的航点。这时候可以把该点对应的节点重复度提高到p,这样曲线在该点处被“拉”到控制点上。
具体做法:假设希望曲线在参数u=0.5处通过控制点Pk,就把0.5这个节点重复p次。这样基函数在u=0.5处退化为只在Pk上有非零值,曲线自然通过Pk。这个技巧在需要精确经过特定航点的场景里非常有用。
注意:提高节点重复度会降低该点处的连续性。重复p次后,曲线在该点只有C⁰连续,也就是位置连续但切向可能跳变。如果对平顺性要求高,慎用这个技巧,或者只在低速场景使用。
5.3 数值稳定性问题与解决方案
B样条计算在节点重复度高的时候容易出现数值问题。主要表现是基函数计算时分母接近零,导致结果出现NaN或者Inf。解决办法有两个:一是加一个极小值eps保护,分母小于eps时直接置零;二是用有理B样条(NURBS)的稳定算法,但那个复杂度高很多,路径规划里一般用不上。
另一个稳定性问题是反算控制点时的线性方程组可能病态。当型值点非常密集时,三对角矩阵的条件数会变大,解出来的控制点可能出现剧烈振荡。解决办法是对型值点做一次低通滤波,或者用正则化方法(在方程组里加一个小的对角项)来改善条件数。我一般用后者,加一个1e-6量级的正则项,效果立竿见影。
5.4 与前端规划器的接口设计
B样条平滑是路径规划的后处理步骤,它的输入来自前端规划器,输出送给控制器。接口设计要注意几点:
输入方面,前端规划器给出的路径点可能包含重复点、急转弯、甚至自交。预处理阶段要能处理这些异常情况。我的做法是:先去重(距离小于1e-4的点合并),再去共线,最后检查自交(如果有自交,说明前端规划有问题,直接报错返回)。
输出方面,控制器需要的不仅是位置,还有航向和曲率。航向角要处理角度跳变(从π到-π的跳变),曲率要限幅(超过车辆最大曲率能力的部分要截断)。这些细节不做,控制器那边就会出问题。
还有一个实际问题是实时性。如果路径规划是在线运行的,B样条平滑的计算时间必须控制在规划周期内。我的实测数据:50个控制点、1000个采样点,MATLAB下大约需要5到10毫秒,C++实现能降到1毫秒以内。如果不够快,可以预计算基函数矩阵,或者降低采样密度。
6. 常见问题速查与排查思路
6.1 曲线不经过起点或终点
这是最常见的问题,原因通常是节点向量生成错误。检查节点向量前p+1个元素是否全为0,后p+1个是否全为1。如果用了均匀节点向量,曲线确实不经过首末控制点,换成准均匀即可。
另一个可能原因是控制点序列的第一个和最后一个没有正确设置。如果做了反算,检查反算结果的首末控制点是否等于原始路径的首末点。反算时首末控制点应该等于首末型值点,这是三对角方程组的边界条件。
6.2 曲线出现异常振荡
振荡通常来自控制点分布不合理或者节点向量重复度设置不当。排查步骤:先可视化控制点和曲线,看振荡区域对应的控制点是否密集且位置跳跃。如果是,对控制点做一次平滑滤波。如果控制点看起来正常但曲线还是振荡,检查节点向量的重复度,特别是中间节点是否意外重复了。
还有一种振荡来自数值精度问题。当控制点坐标值很大(比如UTM坐标几十万米量级)时,浮点数的有效位数不够,导致计算误差被放大。解决办法是把坐标原点平移到路径附近,用相对坐标计算,最后再平移回去。
6.3 曲率计算出现尖峰
曲率尖峰一般出现在节点处,原因是二阶导数在节点处不连续(虽然理论上C²连续,但数值计算误差会导致微小跳变)。如果尖峰幅度不大(小于最大曲率的10%),可以忽略;如果幅度大,检查节点重复度是否过高,或者离散化密度是否不够。
另一个原因是离散化时参数u的采样不均匀。按弧长均匀采样能缓解这个问题,但如果弧长计算本身有误差,曲率还是会受影响。可以用更高精度的弧长计算方法(比如高斯求积)来改善。
6.4 计算速度慢
MATLAB下B样条计算慢的主要原因是基函数的递归调用。优化方法:预计算基函数矩阵,把所有采样点的基函数值一次性算出来存成矩阵,后面用矩阵乘法代替循环。对于1000个采样点、50个控制点,基函数矩阵是1000x50,内存占用很小,计算速度能提升一个数量级。
另一个优化点是向量化。MATLAB的循环很慢,能用矩阵运算的地方尽量用。比如曲线求值可以写成C = N_matrix * ctrl_pts,其中N_matrix是基函数矩阵。这样一行代码代替双层循环,速度快很多。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 曲线不经过起点 | 节点向量非准均匀 | 检查首末节点重复度 | 改用准均匀节点向量 |
| 曲线振荡 | 控制点分布不合理 | 可视化控制点 | 平滑控制点或调整数量 |
| 曲率尖峰 | 节点处数值误差 | 检查节点重复度 | 降低重复度或加密采样 |
| 计算速度慢 | 递归基函数调用 | 计时各模块 | 预计算基函数矩阵 |
| 反算控制点失败 | 方程组病态 | 检查条件数 | 加正则项或滤波型值点 |
6.5 与障碍物的碰撞检查
B样条的凸包性质可以加速碰撞检查,但凸包偏保守。我的做法是两级检查:先用控制点凸包做快速筛选,如果凸包与障碍物不相交,直接通过;如果相交,再对曲线做密集采样做精确检查。这样大部分路径段都能快速通过,只有少数可疑段需要精细检查。
采样密度根据障碍物大小和路径曲率确定。障碍物小、曲率大,采样密一些;反之稀疏。一般取弧长间距为障碍物最小尺寸的1/3左右比较稳妥。
7. 进阶玩法:从平滑路径到动态可行轨迹
7.1 加入时间参数:从路径到轨迹
B样条平滑得到的是几何路径,不含时间信息。实际控制器需要的是轨迹,也就是每个时刻的位置、速度、加速度。把路径变成轨迹需要做时间参数化,常用方法是梯形速度规划或者S型速度规划。
我的做法是在弧长参数化的基础上,对弧长s(t)做规划。给定最大速度v_max、最大加速度a_max、最大加加速度j_max,用S型规划生成s(t)曲线,然后代入B样条曲线得到x(t)、y(t)。这样生成的轨迹在几何上光滑,在时间上也可行。
S型规划的MATLAB实现有很多开源代码,核心是分段计算加速、匀速、减速三个阶段的时间节点。需要注意的是,B样条曲线的曲率限制了最大速度——在曲率大的地方,向心加速度v²κ不能超过侧向加速度限制,所以v_max是随曲率变化的。实际实现时要做曲率限速,取v_max = min(v_max_global, sqrt(a_lat_max / κ))。
7.2 考虑车辆运动学约束
如果路径是给阿克曼转向车辆用的,还需要考虑最小转弯半径约束。B样条曲线在曲率大的地方可能超过车辆的最小转弯能力。解决办法有两种:一是调整控制点,把曲率压下来;二是在曲率超限的地方做速度规划,降低速度通过。
调整控制点的方法更根本。具体操作:计算曲线各点曲率,找到超限区域,把该区域对应的控制点向外侧移动(增大转弯半径),然后重新生成曲线。迭代几次直到曲率满足约束。这个迭代过程可以自动化,写一个循环判断即可。
7.3 多段B样条拼接
对于长距离路径,用一条B样条拟合可能导致控制点过多、计算量大。更好的做法是分段拟合,每段用一条B样条,段与段之间保证C¹或C²连续。
拼接的关键是边界条件。假设第一段终点控制点为P_end,第二段起点控制点为Q_start,要保证C¹连续,需要P_end = Q_start,且P_{end-1}、P_end、Q_1三点共线。C²连续则需要更复杂的条件,一般C¹就够用了。
分段的好处是每段可以独立调整,局部修改不影响其他段。而且每段的控制点数量少,计算快。缺点是段与段的连接处需要额外处理,代码复杂度上升。
7.4 从MATLAB到C++的移植要点
MATLAB验证完算法后,通常要移植到C++做实时运行。移植时要注意几点:
第一,MATLAB的矩阵运算在C++里要用Eigen或者自己写循环。基函数矩阵预计算在C++里同样有效,而且更快。
第二,MATLAB的interp1在C++里需要自己实现。线性插值很简单,但要注意边界处理。
第三,浮点数精度问题在C++里更明显,因为MATLAB默认double,C++里可能用float。建议路径规划相关计算统一用double,避免精度损失。
第四,内存管理。MATLAB自动管理内存,C++里要手动管理。基函数矩阵如果很大,要考虑用稀疏矩阵或者分块计算。
移植完成后,用相同的输入数据对比MATLAB和C++的输出,确保结果一致。一般会有微小差异(1e-10量级),这是正常的浮点误差。如果差异大,检查算法实现是否有遗漏。
8. 一些个人体会
B样条路径平滑这个技术,原理不复杂,但工程落地时细节很多。我刚开始做的时候,觉得公式推导清楚了代码写出来就行,结果实际跑起来各种问题:曲线不经过起点、曲率尖峰、计算太慢。后来慢慢摸索,发现关键是把每个环节的边界条件处理好,数值稳定性做扎实。
控制点数量的选择上,我现在的习惯是先按路径长度的1/10取,然后根据拟合偏差调整。比如100米的路径,先取10个控制点,看偏差,大了就加到15个,小了就减到8个。这个经验值不一定最优,但省事,大部分场景够用。
节点向量的生成,我强烈建议用准均匀,不要用均匀。均匀节点向量虽然数学上更“干净”,但工程上不实用,因为曲线不经过首末点这个特性在路径规划里基本不可接受。
最后说一个调试技巧:把控制点、节点向量、基函数矩阵、曲线采样点全部可视化出来。很多时候问题一眼就能看出来,比如控制点分布不均、节点向量重复度不对、采样太稀导致曲率计算不准。可视化花的时间,比盯着代码找bug少得多。
代码方面,上面给的MATLAB实现是精简版,实际项目中我会加更多的错误检查和日志输出。比如检查输入路径点是否为空、是否包含NaN、控制点数量是否足够等。这些检查在调试阶段能省很多时间。另外,参数都做成可配置的,方便不同场景快速切换。