想象你是一名刚上岗的外卖骑手,被派到一个大雾弥漫的陌生城市。没有地图、没有导航,只有一栋栋大楼和禁止驶入的小区。你怎么把餐送到顾客手里?你不会先画完整张城市地图,而是随手往几个路口扔一把“探针”:能走的路记下来,不能走的绕开。跑得越久,路网越密,最终找到一条可行的送餐路线。这就是基于采样的运动规划——不追求把地图背下来,而是用随机采样一点一点“摸”出可行路径。 机器人也有自己的“陌生城市”:机械臂的关节角、无人车的坐标、避障姿态组合成的高维空间。PRM、RRT、RRT* 是三种“摸路”的典型打法:PRM 先撒点建路网,RRT 像牵着绳子乱跑探路,RRT* 在探路时不断改绳子,把路线越改越短。这篇文章就用这个“外卖骑手摸路”的故事,带你系统理解这三种算法。
采样规划是机器人、自动驾驶和工业机械臂在复杂高维空间里找路的“必修课”。学会 PRM/RRT/RRT*,你就掌握了从算法原型到工程调试的关键一步,是运动规划相关岗位的核心竞争力。
可视化演示
展示PRM路线图构建、RRT快速探索随机树生长与RRT*渐近最优重连过程,可视化三种采样规划算法在高维空间中的路径搜索策略。
本节配有交互动效(进入视口后自动加载,约 3KB)。
RRT快速探索随机树 从起点出发,随机采样并朝最近节点延伸,快速覆盖空间 x_new←x_rand 概率完备但非渐近最优
采样思想起源
小李的第一课是不求满分地图。雾城太大,如果把每条巷子都点亮再送餐,等天亮了也送不完。于是他在城市里随机撒一把“探针”,这些探针是一组随机坐标;落在楼里的答案作废,落在路上的保留。这一小步就是采样。机器人也一样,它关心的不是所有可能姿势,而是“哪个姿势不撞墙、哪个动作能走通”。
在机器人学里,这个“城市”叫配置空间(C-space),机器人的每一个可能状态都对应空间里的一个点。障碍物占据的集合叫障碍空间,剩下的叫自由空间。随机采样的精髓,就是不去显式画出自由空间的边界,而是用一个个随机点去试探。每抛出一个点,就做一次碰撞检测;能落脚的,就认为是自由空间的一部分。这样避开了“把整个高维地图都建模出来”的难题,这也是采样思想在高维空间里能突围的原因。
为什么这一招管用?因为基于采样的规划具有概率完备性:只要采样点足够多,找到一条可行路径的概率会趋于 1。
回到小李:摸几个路口不能保证每次送成,但摸得越多,送成的把握越大。采样规划也一样,它用“更多的脚印”换“更稳的路径”。
- 极限 \(n\to\infty\)采样点的数量 \(n\) 无限增加,表示“摸路摸得越来越密”。
- 概率 \(P\)表示随机采样过程能找到一条可行路径的机会。
- 等式右边等于 1概率收敛到 1,说明只要肯撒足够多的点,几乎必然能找到路。
概率路图PRM
PRM 很像小李白天先做“路书”:随机撒几百个路标,把相邻且能直线到达的路标连起来,形成一张城市路网。这份路书一旦做好,晚上接单时就不用再满城乱跑,只要查一下路网就能快速给出候选路线。PRM 的核心思路,就是把“找路”拆成两个阶段:先建网,再查网。
建图阶段会做大量工作:在自由空间里随机采样,把距离较近且连线不碰障碍的采样点连成边。查询阶段则是给定起点和终点,把起点终点接入已有路网,再在图上来一次最短路径搜索。因为路网可以反复使用,所以 PRM 特别适合静态场景里“同一张地图要回答多个起点终点问题”的情况,比如仓储机器人的固定货架区。
PRM 的连边条件可以写成:
对小李来说,PRM 就是白天把路书做扎实,晚上送餐时“按图索骥”。路书做得越密,后续查路就越省事;但如果路书覆盖不到顾客小区,那晚上照样会送迷路。
- 两点距离 \(\|q_i - q_j\|\)计算两个采样点在配置空间里的欧氏距离,用来判断它们是否“足够近”。
- 邻域半径 \(r\)只有距离小于 \(r\) 的点才考虑连边,避免所有点两两相连造成边爆炸。
- 线段与障碍空间交集 \(L \cap \mathcal{C}_{\text{obs}}\)检查从 \(q_i\) 到 \(q_j\) 的整条线段是否都落在自由空间里。
- 两者同时满足距离近且无碰撞,这条路才能加入路网。
快速扩展RRT
如果说 PRM 是先修路后开车,那 RRT 就是“边开车边修路”。小李从站点牵出一根弹性绳索,随机向某个方向跑一小段,绳索留在地上形成树根;下一次又从某个新位置再随机跑一段,慢慢长成一棵探索树。RRT 不需要预先知道全局地图,它只需要知道“哪个方向撞墙、哪个方向能走”,就能不断把树往未知区域推进。
实现时,每次迭代从自由空间里采一个随机目标点,在现有树上找最近节点,然后沿着最近节点指向随机目标点的方向,走一个固定步长。如果走到的位置没有碰撞,就把这个新点挂到树上。这样反复生长,树会越来越接近目标点。RRT 往往会加一点“目标偏置”:以较小概率直接把目标点当作随机点,让树别只顾着乱逛。
新节点生成公式:
对小李来说,RRT 就像“不认路的骑手,绳子一头拴在站点,另一头到处试探”。它最大的优点是快速、内存友好,特别适合单次查询、动态环境;缺点是找到的路往往比较绕,未必是最优。
- 最近节点 \(q_{\text{near}}\)在现有树上找一个离随机点最近的节点,作为本次生长的起点。
- 步长上限 \(\delta\)每次生长最多走这么远,防止树一步跨过障碍或飞得太远。
- 方向向量 \(\frac{q_{\text{rand}} - q_{\text{near}}}{\|q_{\text{rand}} - q_{\text{near}}\|}\)表示从最近节点指向随机点的单位方向。
- 最小值 \(\min(\delta,\ \text{距离})\)如果随机点很近就直接走到那里;如果很远就只走一个步长。
渐近最优RRT*
小李跑了几单后发现:绳子虽然能到顾客家,但路线经常绕远路。于是他想了个改进办法:每次走到一个新路口,别急着把绳子钉死,先看看附近还有没有更近的路口;如果有,就把绳子的挂点从原来的地方“改挂”到更近的地方。这样随着探索不断继续,整棵探索树的路径会逐步变短。这就是 RRT* 的核心直觉。
RRT* 在 RRT 的基础上增加两步:一是“邻域选父”,新节点生成后,看它周围有哪些老节点,选择一个能让“从起点到新节点总代价最小”的老节点作为父节点;二是“重布线”,新节点加入后,如果老节点通过新节点走到起点的代价更小,就把老节点的父节点改成新节点,让整棵树不断被优化。
选父节点的目标是最小化累积代价:
RRT* 之所以叫“渐近最优”,就是因为它不承诺一次找到最短路径,但会随着迭代增加不断向最优路径逼近。小李也因此越跑越精:先求送到,再求送得近。
- 累积代价 \(c(q_p)\)从起点按当前树走到节点 \(q_p\) 的总路程。
- 邻域集合 \(\mathcal{X}_{\text{near}}\)新节点周围半径范围内的所有老节点,它们都有机会当新父节点。
- 距离增量 \(d(q_p, q_{\text{new}})\)从候选父节点到新节点的连线长度。
- 取最小值 \(\min\)在所有候选里挑一个总代价最小的作为父节点,让新节点从“最划算”的地方接入树。
参数调优经验
参数调优就像小李调整自己的跑腿习惯:撒多少点、迈多大步、看多远的邻居、算多远的距离,都会直接影响找到路的速度和质量。没有一组万能参数,真正工作里要靠场景反复调试。
首先是采样规模。采样点太少,自由空间覆盖不完整,很可能漏掉可行通道;采样点太多,建图、碰撞检测、连边的计算量会急剧上升。其次是步长 \(\delta\)。步长太小,树前进缓慢,需要更多迭代;步长太大,容易跳过窄通道或直接穿过障碍物边界。再次是邻域半径 \(r\)。半径过小,候选父节点太少,难找到好路径;半径过大,连边和重布线开销会爆炸。最后是距离度量。在机械臂关节空间里,欧氏距离不一定能反映真实代价,有时要给不同关节分配不同权重。
RRT* 的理论邻域半径常按下面公式取:
小李的经验是:先跑一次默认参数看树长什么样,再观察失败原因是“覆盖不够”还是“计算太慢”,对症下药调参数。盲目堆采样量,常常只是让程序更慢,而不是让路径更好。
- 采样数量 \(n\)当前树上已经生成的节点数,采样越多,邻域半径理应越小。
- 对数项 \(\log n\)让半径不会随节点数线性增长,避免高密度时连边过多。
- 维度 \(d\)配置空间的自由度数;维度越高,几何覆盖越稀疏,半径调整越敏感。
- 系数 \(\gamma\)与地图尺度、碰撞检测代价相关的经验常数。
高维应用边界
当雾城不断升级:不仅有狭窄小巷,还有多层立交、限行时段、电动车电量约束,地图维度陡然上升。小李很快发现,随机撒点开始失灵了——并非方法错了,而是“城市太大,随机脚印太稀疏”。
窄通道是采样规划最典型的痛点。如果自由空间里只有一条细细的通道,随机采样点绝大多数都会落在通道两侧的障碍区,只有极少数能准确落进通道。维度越高,这种“碰运气”的难度越大。高维空间看似每个维度都有自由度,但自由空间往往只占很小一块区域,随机点像沙粒撒进大海,很难精准命中可通行区域。这就是“维数灾难”。
更复杂的是非完整约束,比如汽车不能原地掉头、机械臂关节有速度上限。这些约束让随机采样点之间不能随便直线连接,新节点必须满足运动学可行性。采样规划不能只回答“能不能到”,还要回答“怎么用符合物理规律的方式到”。
生成随机点落入窄通道的概率可以近似理解为:
对小李来说,解决方式不是不撒点,而是“聪明地撒点”:在窄道附近增加密度、使用桥采样、结合局部规划器处理动力学。理解采样规划的适用边界,比记住算法代码更能决定工程天花板。
- 窄道宽度 \(\epsilon\)可通行区域的典型宽度,越窄越难命中。
- 地图尺度 \(L\)整个配置空间的特征边长,通常远大于 \(\epsilon\)。
- 比率 \(\frac{\epsilon}{L}\)表示窄道在单一维度上所占的比例。
- 维度指数 \(d\)自由度的个数,相乘时这个比例不断被“稀释”,维度越高衰减越剧烈。
工程实践落地
采样规划早已不只是论文里的算法,它已经像外卖平台调度系统一样嵌进现实世界的机器人产品中。小李的故事,正在无数仓库、工厂和街道上真实发生。
在无人仓储场景中,AGV(自动导引车)要在货架间快速穿行。PRM 适合提前把静态货架区的路网建好,工作过程中反复查路;一旦遇到临时障碍,RRT* 再局部重规划一段新路,兼顾效率和实时性。在工业机械臂场景中,六轴机械臂要在狭小空间里做抓取,关节空间往往有 6 个甚至更多维度。工程师通常用 RRT* 在关节空间搜索,并配合逆运动学把关节路径映射成末端轨迹。在自动驾驶场景中,车辆换道、绕障需要毫秒级响应。RRT 系列算法能利用车辆运动学模型生成符合转向约束的局部路径,再由后端优化器做平滑。
这些场景的共同点都是:地图不能被完整预知、维度高、障碍复杂。基于采样的规划用一个非常朴素的哲学解决问题——不追求全知,只追求在有限时间内找到一条“足够好”的路。这恰恰是机器人从实验室走进真实世界所需要的工程智慧。
AI 应用场景
无人仓储AMR路径规划
阶段:运动规划与决策控制层
解决的问题:传统导航依赖预先建立的精确栅格地图,货架一旦变动就要重新建图,维护成本高。基于采样的规划只需随机采样探测可行空间,结合 RRT* 的重布线机制,能在动态变化的仓库中快速生成绕障路径。
具体案例:某电商仓库里,AGV 需要在密集货架之间搬运包裹。工程师先用 PRM 离线建好静态路网,运行中遇到临时堆放的纸箱,则触发 RRT* 局部重规划,让 AGV 在 100 毫秒内生成新路径,绕开障碍后重新汇入主干道。
工业机械臂高维避障
阶段:运动规划与轨迹生成层
解决的问题:六轴机械臂的关节空间维度高,直接画三维栅格地图会引发“维数灾难”。采样规划在关节空间随机采样,能天然避开末端执行器的几何复杂度,只需要做关节角度的碰撞检测。
具体案例:汽车工厂里,机械臂要在车体内侧完成焊接。工程师使用 RRT* 在 6 维关节空间搜索无碰撞路径,再用 B 样条平滑,最终让机械臂从车门缝隙伸入并完成焊接,整个规划耗时不到 0.5 秒。
自动驾驶局部避障
阶段:感知融合后的局部规划层
解决的问题:自动驾驶车辆需要满足转弯半径、加速度等运动学约束,普通最短路径算法生成的折线根本开不出来。RRT 系列采样时结合车辆运动学模型,随机产出的每段路径都符合“车能开”约束。
具体案例:城市道路中遇到施工围挡,自动驾驶系统在感知模块输出障碍物边界后,使用带转向约束的 RRT* 快速生成一条局部绕行轨迹,再由二次规划平滑成连续可执行的期望路径。
Python 演示
import mathimport random class Node: def __init__(self, x, y): self.x = x self.y = y self.parent = None self.cost = 0.0 class RRTStar: def __init__(self, start, goal, obstacles, area, extend_len=0.2, neighbor_radius=0.3, max_iter=600): self.start = start self.goal = goal self.obstacles = obstacles self.area = area self.extend_len = extend_len self.neighbor_radius = neighbor_radius self.max_iter = max_iter self.node_list = [Node(*start)] self.goal_node = Node(*goal) def sample_point(self): if random.random() < 0.1: return self.goal x = random.uniform(self.area[0], self.area[2]) y = random.uniform(self.area[1], self.area[3]) return (x, y) def nearest_index(self, x, y): best = 0 best_d = float('inf') for i, node in enumerate(self.node_list): d = (node.x - x) ** 2 + (node.y - y) ** 2 if d < best_d: best_d = d best = i return best def steer(self, x_near, y_near, x_rand, y_rand): dx = x_rand - x_near dy = y_rand - y_near dist = math.hypot(dx, dy) if dist < self.extend_len: return x_rand, y_rand ratio = self.extend_len / dist return x_near + dx * ratio, y_near + dy * ratio def collision_free(self, x1, y1, x2, y2): steps = 12 for i in range(1, steps): t = i / steps px = x1 + (x2 - x1) * t py = y1 + (y2 - y1) * t for ox, oy, ow, oh in self.obstacles: if ox <= px <= ox + ow and oy <= py <= oy + oh: return False return True def near_indices(self, x, y, r): res = [] for i, node in enumerate(self.node_list): if (node.x - x) ** 2 + (node.y - y) ** 2 <= r * r: res.append(i) return res def plan(self): for _ in range(self.max_iter): rx, ry = self.sample_point() ni = self.nearest_index(rx, ry) near = self.node_list[ni] nx, ny = self.steer(near.x, near.y, rx, ry) if not self.collision_free(near.x, near.y, nx, ny): continue new_node = Node(nx, ny) new_node.parent = near new_node.cost = near.cost + math.hypot(nx - near.x, ny - near.y) indices = self.near_indices(nx, ny, self.neighbor_radius) for idx in indices: cand = self.node_list[idx] d = math.hypot(nx - cand.x, ny - cand.y) if self.collision_free(cand.x, cand.y, nx, ny) and \ cand.cost + d < new_node.cost: new_node.parent = cand new_node.cost = cand.cost + d self.node_list.append(new_node) for idx in indices: other = self.node_list[idx] d = math.hypot(other.x - nx, other.y - ny) if self.collision_free(nx, ny, other.x, other.y) and \ new_node.cost + d < other.cost: other.parent = new_node other.cost = new_node.cost + d if math.hypot(nx - self.goal[0], ny - self.goal[1]) < 0.15: if self.collision_free(nx, ny, self.goal[0], self.goal[1]): self.goal_node.parent = new_node self.goal_node.cost = new_node.cost + \ math.hypot(self.goal[0] - nx, self.goal[1] - ny) break if self.goal_node.parent is None: return [] path = [] node = self.goal_node while node is not None: path.append((node.x, node.y)) node = node.parent path.reverse() return path def main(): start = (0.2, 0.2) goal = (1.2, 1.0) obstacles = [(0.3, 0.0, 0.18, 0.55), (0.65, 0.45, 0.2, 0.55), (0.5, 0.25, 0.25, 0.15)] planner = RRTStar(start, goal, obstacles, [0, 0, 1.5, 1.2], extend_len=0.15, neighbor_radius=0.25, max_iter=800) path = planner.plan() print("path nodes:", len(path)) print("path start:", path[:1]) print("path end:", path[-1:]) if path: total = sum(math.hypot(path[i + 1][0] - path[i][0], path[i + 1][1] - path[i][1]) for i in range(len(path) - 1)) print("total length:", round(total, 4)) if __name__ == "__main__": main()
self.node_list = [Node(*start)]初始化探索树,把起点当作树根。def sample_point(self):随机在自由空间采样,偶尔直接返回目标点,模拟“目标偏置”。new_node.cost = near.cost + math.hypot(nx - near.x, ny - near.y)先按 RRT 方式接上父节点,计算出起点到新节点的累积代价。if self.collision_free(cand.x, cand.y, nx, ny) and \检查候选父节点到新节点是否无碰撞,这是 RRT* 邻域选父的前提。other.parent = new_node重布线:如果老节点通过新节点更划算,就“改挂”到新节点上。path.reverse()从目标沿父指针回溯到起点,再反转成从起点到目标的完整路径。
常见误区
误区既然采样规划靠随机点找路,那就疯狂增加采样数量,采样越多、覆盖越密,结果肯定越好。
正解采样数量增加确实会提高找到路径的概率,但也会让边数、碰撞检测和重布线开销快速增长。工程上更看重“有效覆盖”,而不是“盲目堆点”。就像小李不可能把全城每块砖都踩一遍才去送餐,而是在关键路口多踩几脚。参数调优的目标是用尽量少的点覆盖尽量关键的自由区域。
为何常见:人容易把“数量”和“质量”划等号,而且采样量增加带来的概率完备性直觉非常强,却忽略了计算代价和边界效应。
误区算法有概率完备性,所以只要跑得够久,最后一定既能找到路径,又能找到最短路径。
正解概率完备只保证“存在可行路径时,采样无限多能找到一条可行路径的概率趋于 1”,并不保证路径最优。RRT 只能找到可行路径,RRT* 才有渐近最优性。就像小李随手扔飞镖,扔得够多总能扎中靶子,但不保证每支飞镖都是十环。
为何常见:很多人把“完备性”和“最优性”混为一谈,因为日常语言里“搞定”常被理解成“搞定得最好”。
误区只要两个采样点本身不在障碍物里,它们之间的连线就是安全的。
正解两点连线可能穿过障碍物。正确做法是在线段中间均匀采样多个中间状态做碰撞检测,确认整条线段都落在自由空间里。小李也不会因为两个朋友站在楼两侧招手,就认为他们之间有一条直路。
为何常见:人脑处理二维直线时容易默认“两点之间直线最短且畅通”,但在复杂配置空间里,障碍可能横在两点中间。
误区距离度量就用普通欧氏距离,反正代码里也是这么写的。
正解机械臂关节空间、车辆运动学约束等场景下,欧氏距离并不能真实反映“走起来有多费劲”。关节角具有周期性,方向变化需要特殊处理;不同关节的最大速度不同,要给距离加权重。小李也不会用地图上的直线距离代替实际骑行距离,因为还要考虑红绿灯和单行道。
为何常见:欧氏距离直观、容易实现,初学者容易忽略配置空间的多维几何结构,尤其是非欧氏和带约束的情形。
面试题
Q1. 下列哪一项最符合基于采样规划的核心思想?
- A.对配置空间做像素级规划
- B.随机采样探测可行空间← 正确答案
- C.用解析方法穷举所有路径
- D.对地图做颜色聚类分析
答案:B。基于采样规划通过随机撒点、碰撞检测、逐步连接来探索配置空间,而不是对整个空间做像素级或解析式建模。B 最准确地描述了这个核心思想。
Q2. PRM 算法通常分为哪两个阶段?
- A.建图阶段与查询阶段← 正确答案
- B.训练阶段与推理阶段
- C.编码阶段与解码阶段
- D.采样阶段与死循环阶段
答案:A。PRM 先离线建路图,再在线查询路径。训练/推理是机器学习术语,编码/解码与采样规划无关。
Q3. 关于 RRT 中新节点 \(q_{\text{new}}\) 的描述,正确的是?
- A.新节点一定落在随机采样点上
- B.新节点离最近节点不超过步长← 正确答案
- C.新节点一定穿过障碍物的中心
- D.新节点只受目标点姿态影响
答案:B。RRT 从最近节点向随机点方向走一个步长 \(\delta\),如果随机点距离小于步长就直接到达随机点,否则不会越过步长。因此新节点不会无限远离最近节点,也不会一定落在随机采样点上。
Q4. RRT* 相对于 RRT 最关键的改进是什么?
- A.使用更长的步长并合并路径
- B.扩展后对邻域节点重新布线← 正确答案
- C.每次采样后删除全部旧节点
- D.用深度优先搜索替代随机采样
答案:B。RRT* 新增邻域选父和重布线,在扩展后不断优化树上已有节点的父节点关系。这正是它能从“可行解”走向“渐近最优解”的原因。
Q5. “概率完备”的含义是?
- A.样本无限时找到可行解概率趋一← 正确答案
- B.机器人必然在有限步内最优收敛
- C.规划器能处理任意高维动态障碍
- D.输出路径与随机种子完全无关
答案:A。概率完备指当采样点数量趋于无穷时,找到一条可行路径的概率趋于 1。它不保证有限步内最优,也不保证对任意动态障碍都有效。
Q6. 对六自由度机械臂做运动规划时,更合适的距离度量是?
- A.末端笛卡尔空间直线距离
- B.关节角加权后的欧式距离← 正确答案
- C.彩色图像像素灰度差之和
- D.机械臂表面涂料的颜色差
答案:B。机械臂规划通常发生在关节空间,关节角之差比末端直线距离更能反映机械臂真实运动代价。加权欧式距离还能表达不同关节速度或惯量的差异。
Q7. 在窄通道场景中,下列哪些说法正确?
- A.窄通道内随机采样命中率低← 正确答案
- B.窄通道是采样规划主要难点← 正确答案
- C.增加采样密度可缓解该问题← 正确答案
- D.窄通道会阻塞所有可行路径
答案:ABC。窄通道体积占比小,随机点很难命中;它确实是采样规划在复杂场景中的典型瓶颈;增加采样密度可以提高命中概率。但窄通道不会阻塞所有可行路径,只是让路径搜索更难。
Q8. 下列哪些是采样规划中常见的可调参数?
- A.采样点的总数量← 正确答案
- B.扩展步长的尺度← 正确答案
- C.邻域半径的大小← 正确答案
- D.摄像头曝光的时间
答案:ABC。采样数量、步长、邻域半径分别控制覆盖密度、探索粒度和连边范围,是 PRM/RRT/RRT* 的核心调试项。摄像头曝光时间属于感知模块,跟运动规划无关。
Q9. 关于 PRM 与 RRT 的场景选择,正确的是?
- A.PRM适合静态场景多查询← 正确答案
- B.RRT* 一定比PRM更慢且无法使用
- C.PRM只能处理二维平面地图
- D.RRT完全不需要碰撞检测
答案:A。PRM 强调先建路网再查询,适合地图固定、需要多次回答路径问题的场景。RRT 更适合单次动态规划;PRM 不限于二维;RRT 仍然需要碰撞检测。
Q10. 关于 RRT* 的邻域优化策略,正确的是?
- A.扩展后选择代价最小的近邻作父节点← 正确答案
- B.对邻域节点进行重新布线← 正确答案
- C.状态转移方程必须使用五次多项式
- D.每次采样前必须清空整棵树
答案:AB。RRT* 通过“邻域选父”让新节点接入总代价最小的路径,再通过“重布线”优化老节点路径。它不强制使用五次多项式,也不会清空整棵树,树是持续累积探索结果的。