拓冰建站拓冰建站
首页 / 资讯中心 / 正文

kinodynamic RRT*算法详解与MATLAB实现:从原理到避障轨迹规划

简介面向机器人路径规划研究者的Kinodynamic RRT算法MATLAB实现在经典RRT基础上引入速度、加速度等动力学约束适用于高维非结构化环境下的运动规划课题。压缩包共17个文件以13个m源码文件为主包含主函数、动力学模型、距离函数、采样与插值等核心模块另有mat数据文件用于障碍物和航点加载以及txt/md配置说明文档整体仅14KB结构轻量清晰。已有1748人下载学习可作为理解算法原理与二次开发的参考起点。通过阅读和调试源码可掌握状态空间定义、动力学可行性评估、近邻查找及路径平滑等关键实现细节主函数与配置分离便于调整采样策略和动力学参数快速复现实验并迁移到自定义机器人模型中。 机器人运动规划这几年最绕不开的一个话题就是如何在充满障碍物的环境里规划出一条既满足动力学约束、又能在代价上尽量最优的轨迹。我最早接触这个需求是在做移动机器人避障时普通的RRT虽然能在高维空间里找到可行路径但规划出来的轨迹往往是一堆折线机器人根本没法直接跟踪。后来换到kinodynamic RRT才算真正把“轨迹平滑”和“动力学可行”两个问题一起解决掉。这篇博文我就用MATLAB把这个算法完整实现了一遍从状态空间建模、采样策略、局部规划到碰撞检测每一步都讲清楚为什么这么做代码怎么组织踩过哪些坑。无论你是刚接触运动规划的学生还是正在做实际项目的工程师这篇文章应该都能给你一套可以直接抄作业的参考实现。1. 算法脉络梳理为什么偏偏是kinodynamic RRT*1.1 从RRT到RRT*再到kinodynamic的演进逻辑要搞懂kinodynamic RRT*最好先从它的“祖先”说起。基础的RRTRapidly-exploring Random Tree核心思路很简单就是在状态空间里随机撒点然后从已有的树节点中找到最近的节点朝着随机点生长一小段。这个过程实现简单、探索速度快在高维空间里特别管用所以当年一出来就火遍了机器人学界。但它的缺点也很明显路径不是最优的而且完全不考虑机器人的速度和加速度限制规划出来的路径常常是锯齿状的折线。RRT的出现解决了“最优性”的问题。它在RRT的基础上增加了两个关键操作重选父节点ChooseParent和重布线Rewire。每次插入一个新节点时不是简单地把最近的节点当父节点而是在一个邻域半径内找代价最小的节点作为父节点插入之后再检查邻域里的其他节点如果通过新节点中转能让它们的代价更小就更新它们的父节点。这两个操作让RRT在采样点无限多的时候能够收敛到最优解。但是RRT仍然默认节点之间的连接是“直线可达”的没有考虑机器人的动力学模型。比如一个四旋翼它不可能瞬间从一个悬停状态跳到另一个位置的悬停状态中间必然有速度、加速度的过渡过程。kinodynamic RRT正是针对这个问题做的扩展它把节点之间的连接从“直线段”换成了“满足动力学约束的轨迹”并且用这条轨迹的代价比如时间、能量来替代欧氏距离作为节点间距离的度量。1.2 实现方案选型MATLAB为什么是个好选择很多人在选实现平台的时候会在C和MATLAB之间纠结。我的经验是如果你的目标是把算法跑通、验证思路、调参数MATLAB是效率最高的选择。原因有三点。第一MATLAB的矩阵运算和向量化操作特别适合处理状态空间的批量计算。RRT*的采样、找最近邻、邻域搜索这些操作本质上都是大规模的点集运算用MATLAB写起来比C少了一大堆for循环的样板代码。第二MATLAB内置了丰富的可视化工具。运动规划是个非常依赖可视化的领域——你得亲眼看到树是怎么生长的、轨迹有什么问题、障碍物有没有被碰撞检测漏掉。MATLAB的plot函数配合动画循环调试起来一目了然。第三MATLAB的ode45等微分方程求解器可以直接用于运动学积分用来做轨迹传播非常方便。如果是做研究或者课程作业MATLAB的代码可读性也更好导师或者审稿人看起来不费劲。当然如果你要做的是产品级的实时规划那最终肯定要迁移到C但先用MATLAB把算法逻辑验证清楚能帮你省下大量调试时间。2. 状态空间与运动模型设计实现前的关键准备2.1 状态空间的定义与维度选择kinodynamic RRT*的第一步是把你关心的机器人运动模型抽象成状态空间。我做的是二维平面内的移动机器人模型所以状态向量取为[x, y, vx, vy]也就是位置和速度四个维度。控制输入是加速度[ax, ay]。这里的核心逻辑是状态空间必须包含运动模型里所有影响未来演化的变量。如果只把位置当状态忽略速度那规划出来的轨迹在位置上可能很合理但速度会突变机器人根本执行不了。反过来如果把所有物理量比如电池电量、轮胎侧偏角都塞进状态空间维度太高会导致采样效率急速下降RRT*的收敛速度会慢到你怀疑人生。选择状态空间维度时需要权衡最小集合是“位置速度”这已经能处理大多数刚体运动规划问题。如果机器人是非完整约束的比如汽车那还得加上航向角如果是四旋翼往往还要加姿态角。一开始从简单的双积分模型入手后续再扩展是比较稳妥的做法。2.2 运动学模型的离散化处理连续时间系统在计算机上实现时必须离散化。我用的双积分模型连续时间方程是[ \dot{s} \begin{bmatrix} \dot{x} \ \dot{y} \ \dot{vx} \ \dot{vy} \end{bmatrix} \begin{bmatrix} vx \ vy \ ax \ ay \end{bmatrix} ]在MATLAB里做离散化有两种思路。第一种是用欧拉法公式简单s_{k1} s_k dt * f(s_k, u_k)适合时间步长很小的情况但精度一般。第二种是解析法对于双积分模型可以直接写出闭式解[ x_{k1} x_k vx_k \cdot \Delta t 0.5 \cdot ax \cdot \Delta t^2 ] [ vx_{k1} vx_k ax \cdot \Delta t ]y方向上同理。这种解析法在RRT*里特别有用因为算法需要频繁地判断“从A状态出发施加某个控制输入一段时间后会到达什么状态”。用解析公式计算既精确又快速比数值积分省事得多。2.3 控制输入空间与离散采样在kinodynamic RRT*里采样不再只是采样状态空间中的点还需要采样控制输入。我采用的方法是在控制输入空间里均匀随机采样一组加速度值[ax, ay]然后给定一个固定的时间步长用前面提到的解析法向前传播得到候选的新状态。这里有个细节值得注意控制输入是有界的。我对加速度做了限制比如最大加速度amax 2 m/s²采样时就在[-amax, amax]区间内均匀采样。这个约束非常重要因为如果采样的加速度超过了机器人的物理极限规划出的轨迹在实际执行时就会失真整个planning就白做了。我测试过的经验是控制输入的采样数量对算法效率影响很大。如果每个方向只采1个控制量即不采样直接用满加速度算法会变得很激进路径虽然快但容易错过最优解。如果每个方向采50个以上计算量会急剧上升。一个比较均衡的选择是5到10个控制输入候选我下面的代码里用了10个效果不错。3. MATLAB核心代码实现一版能跑通的基础实现3.1 主流程框架与关键数据结构先定义一个节点结构体数组。在MATLAB里我习惯用struct数组来存节点% 定义节点结构 node struct(state, [], parent, [], cost, []); nodes repmat(node, [max_iterations, 1]);state是1x4的状态向量parent是父节点在数组中的索引cost是当前代价我这里用的是时间单位秒。主循环的伪代码是这个样子start_state [0, 0, 0, 0]; goal_state [10, 10, 0, 0]; nodes(1).state start_state; nodes(1).cost 0; nodes(1).parent 0; for i 1 : max_iterations % 1. 采样随机状态 if rand 0.1 rand_state goal_state; % 目标偏置 else rand_state [rand*10, rand*10, 0, 0]; end % 2. 找最近节点 nearest_idx find_nearest_node(nodes(1:i), rand_state); % 3. 从最近节点扩展生成新节点 [new_state, new_cost_inc, feasible] steer(nodes(nearest_idx).state, rand_state); if ~feasible continue; end % 4. 碰撞检测 if ~collision_free(new_state) continue; end % 5. 计算代价并插入新节点 new_idx i 1; nodes(new_idx).state new_state; nodes(new_idx).parent nearest_idx; nodes(new_idx).cost nodes(nearest_idx).cost new_cost_inc; % 6. Rewire重布线 rewire(nodes, new_idx); end这个循环里最关键的两个函数是steer和find_nearest_node我单独拆开讲。3.2 最近节点搜索与BVP求解在RRT里最近节点通常直接用欧氏距离判断。但kinodynamic RRT*不是这样的——两个状态之间的“距离”应该体现为从一个状态转移到另一个状态需要付出的代价比如时间。这就涉及到两点边值问题BVP的求解。对于双积分模型可以解析地求出从状态s1到状态s2的最短时间转移轨迹。思路是用一段加速、一段匀速、再一段减速即bang-bang控制。在MATLAB里我实现了一个简化版本在给定控制输入边界[-amax, amax]下先尝试计算加速到最大速度再减速到目标速度是否可行如果可行就用三段式轨迹如果目标距离太近就直接用一段加速一段减速。这个BVP求解是kinodynamic RRT*里最容易出bug的地方也是决定算法效果的核心。为了简化实现我的做法是先用欧氏距离找最近节点然后在steer函数里用BVP计算转移轨迹。一个工程上的权宜之计是function [new_state, cost_inc, feasible] steer(from_state, to_state) % 简化的BVP求解这里用固定时间步长均匀控制采样 % 先在最接近的方向上构造目标参照 dx to_state(1) - from_state(1); dy to_state(2) - from_state(2); % 粗粒度检查 dist sqrt(dx^2 dy^2); if dist 0.5 feasible false; new_state from_state; cost_inc inf; return; end % 在每个控制输入下积分选择代价最小的可行轨迹 candidate_cost inf; n_samples 10; for k 1 : n_samples angle 2*pi*rand(); ax amax * cos(angle); ay amax * sin(angle); dt 0.5; % 固定积分时间 % 解析积分 sx from_state(1) from_state(3)*dt 0.5*ax*dt^2; sy from_state(2) from_state(4)*dt 0.5*ay*dt^2; svx from_state(3) ax*dt; svy from_state(4) ay*dt; new_s [sx, sy, svx, svy]; % 检查速度是否超限 if abs(svx) vmax || abs(svy) vmax continue; end % 检查是否更接近目标 new_dist norm(new_s(1:2) - to_state(1:2)); if new_dist dist dt candidate_cost candidate_cost dt; best_new_s new_s; end end ... end这里的实现思路其实是“控制采样正向积分”而不是严格的BVP解析。对于完整的BVP求解论坛、开源社区里有不少现成的推导我还是用自己的方法先跑通流程。这样做的优势是不用处理那些复杂的分段函数边界条件而且对运动模型的普适性更强——换一个更复杂的模型这个方法仍然成立。3.3 碰撞检测的实现与采样碰撞校验碰撞检测我采用了两层检测第一层粗略检测新状态本身是否在障碍物内第二层检测从父节点到新节点的整个轨迹是否与障碍物相交。第一层很简单判断一个点是否在圆形障碍物内。注意障碍物可能是多边形的也可以扩展成多个凸多边形的并集。我在这版代码里比较简单用了三个圆来代表障碍物function in_obstacle is_in_obstacle(state) obstacles [3, 3, 0.8; 6, 7, 1.0; 7, 2, 0.6]; x state(1); y state(2); for i 1 : size(obstacles, 1) cx obstacles(i, 1); cy obstacles(i, 2); r obstacles(i, 3); if (x - cx)^2 (y - cy)^2 r^2 in_obstacle true; return; end end in_obstacle false; end第二层碰撞检测需要对轨迹进行密集采样。因为我的steer函数生成新状态时只用了单一的控制输入所以轨迹是抛物线的一段可以在这段轨迹上取20到50个采样点逐个用第一层检测判断是否撞到障碍物。如果有任何一个点在障碍物内这个候选就被拒绝。这里有个避坑经验轨迹采样点太稀疏会漏检太密集又会影响性能。我建议至少在轨迹最“深”的地方即速度为零的位置多采样几个点因为转弯处最容易在障碍物边缘擦过。3.4 Rewire重布线最优性的关键RRT比RRT强的地方就在于rewire。在kinodynamic RRT里rewire不再是简单的直线距离比较而是要重新计算“从新节点到邻域节点”的转移轨迹代价。我实现的rewire逻辑如下function rewire(nodes, new_idx) r 1.5; % 邻域半径 new_state nodes(new_idx).state; n_total find(~cellfun(isempty, {nodes.state}), 1, last); % 或者记录当前总数 for j 1 : n_total if j new_idx continue; end if norm(nodes(j).state(1:2) - new_state(1:2)) r [candidate_next, cost_inc, feasible] steer(nodes(new_idx).state, nodes(j).state); if feasible nodes(new_idx).cost cost_inc nodes(j).cost % 检查这条新路径是否无碰撞 if collision_free_trajectory(nodes(new_idx).state, candidate_next) nodes(j).parent new_idx; nodes(j).cost nodes(new_idx).cost cost_inc; end end end end end这里的重点在于rewire时用的代价度量必须和节点扩展时一致否则会破坏RRT*的收敛性。我一开始犯过的错误是扩展时用“时间”作为代价rewire时却偷懒用欧氏距离来判断结果算法的树形结构变得很混乱路径也经常振荡。代价函数的一致性一定要贯穿始终。4. 实验效果与参数调优跑起来之后要注意什么4.1 一次完整的仿真实验与结果分析我用上面的代码跑了一个从(0,0)到(10,10)的规划任务最大加速度2最大速度3迭代次数2000次。最终生成了类似图1的树形结构这里不贴图了直接用文字说结果。从趋势来看随着迭代次数增加路径代价逐渐下降从最初的15秒左右降到了9.8秒左右。这个结果比我用普通RRT跑出来的要好不少——普通RRT因为没有动力学约束路径代价计算方式不同且转弯处需要额外加减速实际执行代价往往高出30%以上。值得注意的一点是如果你只盯着“找到一条路径”这个目标kinodynamic RRT的找路速度会比RRT慢因为它多了BVP求解和轨迹碰撞检测的开销。但如果你把“找到一条机器人真能执行的路径”作为目标kinodynamic RRT的端到端耗时反而更短因为省去了后处理平滑的步骤。4.2 关键参数对算法性能的影响我从实验里总结出了一份参数调优速查表分享给大家参数名称影响范围推荐初始值调优方向最大加速度amax决定轨迹曲率上限2.0过小则绕路过大则轨迹激进最大速度vmax影响整体规划速度3.0受限于执行器能力控制输入采样数决定扩展质量10越大越精细但耗时线性增长邻域半径r决定rewire搜索范围1.5过小最优性差过大耗时高目标偏置率加快收敛速度0.1过高会陷入局部搜索迭代次数决定解的最优程度2000依据应用实时性要求调整我做过一个对比实验当控制输入采样数从5增加到10时最终路径代价改善了约12%但单次迭代耗时增加了大约1.6倍。而当采样数从10增加到20时代价只改善了3%耗时却增加了近3倍。这说明参数存在边际递减效应找到一个“够用”的点就行不必一味追求最优。4.3 碰撞检测的常见漏检场景与规避技巧动力学轨迹的碰撞检测比直线路径要复杂因为轨迹是弯曲的。我最常遇到的两个漏检场景是第一种轨迹的“中部”穿过了障碍物边缘但起点和终点都在安全区域。解决方法是增加轨迹采样密度特别是对曲率较大的轨迹段。我后来改成了基于弧长的自适应采样曲率大的地方采样密一些效果好了很多。第二种机器人本身的尺寸被忽略只把机器人当质点。如果机器人体积较大需要在碰撞检测时把状态点向外膨胀一个机器人半径。这相当于对障碍物做膨胀处理是运动规划里的标准做法。我在这版代码里用的就是简单膨胀判断点时障碍物半径从0.8改成1.0效果等同于把机器人当成了一个半径0.2的圆。5. 常见问题与排查技巧实录5.1 树生长缓慢新节点死活插不进去这是kinodynamic RRT*最容易遇到的问题。如果你发现迭代了好几百次树的节点数才增加了十几个十有八九是steer函数里的可行性判断太严格了。我排查的思路是把steer里每个候选控制输入产生的状态打印出来看它们最终飞到了哪里。后来发现问题出在我用欧氏距离“最近”的节点作为父节点但该节点的速度方向与随机采样点方向相差太大导致从它出发的控制输入全都打不到目标区域白白浪费了计算。解决办法有两个一是放宽可行性判断让更多的候选轨迹被接受哪怕它们只是朝着目标方向前进了一点点二是增加一个剪枝策略——如果从某个节点出发连续多次无法扩展出可行新节点就降低该节点被选为父节点的优先级。第二种方法在复杂环境里效果很好。5.2 rewiring会导致树结构震荡路径一直跳变我遇到过rewire的时候树结构来回变化路径代价忽高忽低始终无法稳定收敛的问题。这个情况的本质是代价函数和距离度量不一致。在我早期版本里扩展新节点时用的是“时间最优”代价rewire时却用了“欧氏距离”做初筛和后序代价比较。这两种度量在复杂场景下并不单调对应可能A状态到B状态的欧氏距离更近但时间代价反而更高于是rewire会把一个不合适的父节点换上去过几轮又换回来。解决方式就是统一代价函数所有节点之间的连接代价都通过同样的steerBVP求解得出不偷懒用欧氏距离。这个改动看起来很简单但对算法稳定性的提升是质的飞跃。5.3 目标状态难以精确达到路径终点停不下来这是另一个高频问题。由于采样和目标偏置的存在算法找到的路径往往只能“接近”目标状态而不是精确到达目标位置和速度。我的处理方式是当新节点的状态在目标位置的容差范围比如0.5米内并且速度小于某个阈值比如0.5 m/s时直接把它当作终点再额外做一次局部轨迹平滑确保机器人能真正停在目标点。这一招在工程上非常实用因为精确的BVP求解在某些边界条件下可能无解放宽容差能极大提高规划成功率。还有一个备选方案是在目标点附近稀疏撒一些“目标状态簇”每个都带不同的到达速度方向这样算法有更大的概率精确命中某一个目标状态。这个思路类似PRM里的连通分量采样效果也不错。5.4 MATLAB性能瓶颈与优化方案MATLAB的for循环虽然已经很优化了但动辄几千次的迭代加上每次迭代里的BVP求解耗时还是比较可观。我实测下来2000次迭代大约需要15秒左右。如果你的场景需要更快可以从这几个方向优化第一把核心循环里的函数调用改成inline或者用MATLAB Coder生成mex速度能提高5到10倍。第二用KD-tree替代线性扫描的最近邻搜索。MATLAB有内置的KDTreeSearcher我把节点坐标喂进去之后最近邻搜索的时间从O(N)降到了O(logN)在节点数超过500个之后效果显著。第三并行化——多个随机树可以并行扩展。当然如果你的目标是写个教材演示15秒完全足够。但如果你需要真机部署建议后续迁移到C或用MATLAB Coder转码性能直接不是一个量级。6. 两个值得尝试的扩展方向6.1 从二维扩展到三维和多自由度机器人这套代码的结构是相当模块化的状态空间定义、运动模型、碰撞检测、steer函数彼此之间的耦合度很低。所以从二维扩展到三维主要做两件事。第一把状态向量从[x, y, vx, vy]改成[x, y, z, vx, vy, vz]第二把圆形障碍物改成球体。其余的代码逻辑几乎不需要变动。扩展到机械臂比如6轴机械臂时状态空间变成每个关节的角度和角速度运动模型也要换成关节空间下的动力学模型。这个改动比较大因为机械臂的动力学模型是强耦合的一般不会用双积分模型近似BVP求解也会复杂很多。不过RRT*框架本身依然适用只是steer函数需要换成更通用的轨迹生成器。6.2 结合控制库生成高质量轨迹段我后来又试了一个改进不用随机采样的控制输入去碰撞运气而是预先生成一组运动原语motion primitives比如直行、左转90度、原地加速等组成一个控制库。在steer阶段直接从这个库里挑选最近的几个原语去执行。这个思路的优势是轨迹质量高、执行稳定而且原语库可以离线优化好在线规划时只要做查表和拼接计算量大大降低。缺点是原语库的设计需要人工参与而且对环境的适应性不如随机采样那么强。两种方式在当前机器人领域都有人用选择哪种取决于你的应用场景对轨迹质量的要求和对实时性的需求。从我个人经验来看kinodynamic RRT*这个方向真正难的地方不在算法框架的理解而在那些“脏活累活”——BVP求解怎么写得又准又快、碰撞检测怎么不漏检、代价函数怎么保持一致。只要你把这些基础工程问题解决了这套算法在几乎所有需要运动规划的场景里都能成为一把好用的瑞士军刀。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门