ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

火星车路径规划:从A*全局搜索到DWA局部避障的工程实战

火星车路径规划:从A*全局搜索到DWA局部避障的工程实战

简介:这是一份关于火星探测车路径规划方法的外文学术论文高清PDF,原题为《Path Planning for Mars Rovers Using the Fast Marching Method》,面向机器人、人工智能及深空探测领域的研究者。论文围绕基于快速行进法(FMM)的路径规划展开,重点讨论如何融合地形坡度、粗糙度、障碍物分布及沙地滑移等多维因素,构造加权速度矩阵W,并借助外部矢量场模拟侧向滑动,从而改进出FMVF算法,为行星表面未知环境下的自主导航和安全行驶提供可行方案。资源共1个PDF文件,包体大小约4.56MB;正文包含方法推导、实验验证和项目资助说明,结构清晰,适合作为算法对比、课程论文写作或火星探测任务预研的参考资料。该论文由西班牙马德里卡洛斯三世大学团队完成,并获得RoboCity2030-II-CM项目资助。当前已有66人学习下载,适合具备一定路径规划或移动机器人基础的中高级研究者阅读。

1. 火星探测车路径规划:双闭环里最容易被低估的一环

很多人提到火星探测车路径规划,第一反应是车辆靠摄像头在火星表面实时找路。实际并非如此:火星与地面的通信时延接近二十分钟,遥控周期很难覆盖每一米的行驶,车必须靠星载传感器和机载计算,把全局规划、局部避障、停车判断在一段自主运行窗口内做完。这篇文章讲的就是这套闭环如何搭建——从栅格地图构建,到全局路径搜索,再到局部速度控制,以及我踩过的几个影响最大的坑。适合正在做移动机器人路径规划、又需要把算法真正跑到车体上的工程师参考。

2. 总体架构与地图准备:把火星表面变成规划器能用的栅格模型

2.1 全局规划、局部规划与轨迹跟踪:三个层各管一摊

火星车的路径规划不是一个算法的事,而是由全局规划、局部规划、轨迹跟踪三个模块叠起来的分层结构。全局规划负责在开机前或每次大状态更新时,用数字高程模型生成一段足够长的参考路径;局部规划负责在执行过程中以 10 到 20 赫兹的频率接收当前位姿和传感器点云,实时纠正偏差并绕开新出现的障碍;轨迹跟踪则在底盘层面把这些速度指令平滑成电机转速。

为什么必须分层而不是用一个端到端模型把图像直接映射成转向角?一个原因是可解释性:火星车任务对安全性要求极高,地面团队需要能随时回答“为什么走这条路”,分层结构里每一层都能单独回放和审计。另一个原因是算力与功耗约束,机载计算机不可能每帧都对整张地图做一次搜索,全局规划通常只在地图更新或偏离路径超过阈值时触发,局部规划才需要实时跑。这个思想也直接决定了后续对算法选型和参数设计的取舍。

从数据流看,感知模块输出障碍物列表和地表纹理信息,定位模块输出车在世界坐标系下的位姿,地图模块把多源数据融合成代价地图,规划模块在代价地图上做搜索。这个过程中最容易犯的错误是过早开始调搜索算法,而忽略了地图本身的精度,后面第五章会集中展开。

2.2 坐标系与感知链路:火星车如何知道自己在地图上哪个点

火星上没有可以随时使用的导航卫星,这个前提直接改变了定位链路的写法。常见做法是组合轮式里程计、惯性测量单元(IMU)和视觉测程(VO),用扩展卡尔曼滤波或因子图做融合。轮式里程计在松软沙地上会快速漂移,视觉测程在纹理稀少区域容易丢失,所以还需要星敏或太阳敏感器做低频绝对姿态校正。

规划器关心的不是经纬度,而是局部平面坐标。实际工程里一般以着陆点或者某个已知地物为原点,建立一个局部切平面坐标系,把所有栅格位置、车辆位姿、目标点都投影到这个平面里。坐标变换链路是:全局坐标系 → 车体坐标系 → 传感器坐标系,每一步都需要精确的外参标定。我见过不少项目在仿真里跑得通,一上真实车就偏,最后查到是标定时把车体系的原点放错了位置,而不是规划算法本身的问题。

处理栅格地图投影时要小心:经纬度直接当平面坐标用,在纬度较高或者范围超过数十公里时会产生明显畸变。火星车任务覆盖范围通常只有几公里,所以大多数落地项目会用高斯-克吕格投影或者等距圆柱投影先转一次,再进入规划器。这一步看起来基础,却决定了下游所有代价栅格是否对得上。

2.3 从数字高程模型到代价地图:坡度与粗糙度怎么算

路径规划的输入不能是单纯的障碍二值图。火星表面有大量缓坡、碎石、沙堆,这些区域既不是完全不可通行,也不能像平坦路面一样直接高速通过。常见做法是用数字高程模型(DEM)生成坡度图,再结合点云高度方差得到粗糙度,把两者归一化后叠加成地形代价图。

下面这段代码把 DEM 转成带坡度和障碍信息的代价地图,是后续全局规划器的输入。它不复杂,但几个阈值参数直接决定规划质量。

import numpy as np def build_cost_map(dem_array, resolution=0.5, max_slope_deg=35.0): # dem_array: 二维标高数组,单位米 # resolution: 每个栅格的边长,单位米 gy, gx = np.gradient(dem_array, resolution, resolution) slope = np.degrees(np.arctan(np.sqrt(gx**2 + gy**2))) # 10 度以内视为舒适通行,10~35 度线性增加代价,超过 35 度视为不可通行 slope_cost = np.clip((slope - 10.0) / (max_slope_deg - 10.0), 0.0, 1.0) # 超过车体越障能力的垂直台阶直接标记为障碍 obstacle = np.where(dem_array > 10.0, 1.0, 0.0) cost_map = np.maximum(slope_cost, obstacle) return cost_map

逻辑说明:np.gradient在 x 和 y 方向分别求高度差分,合成坡度角;np.clip把 10 度以下的坡度压到 0,以免平坦区域被噪声干扰;35 度以上直接给满代价。这里的 10 度和 35 度不是拍脑袋定的,而是从车体最大爬坡能力和稳定性裕度反推出来的。具体车体设计不同,阈值就要跟着改:轮径大、重心低的车可以把上限放松到 40 度,窄体车建议保守一点。

参数说明:resolution=0.5表示每格 0.5 米,适合中等尺度勘探场景。如果岩石密集区域,建议提到 0.2 米,但地图尺寸会膨胀 6 倍以上,全局搜索时间也随之增加。对火星车这种算力受限的平台,我一般先在 0.5 米地图上规划,靠近目标前再用 0.2 米局部地图做精细避障,比单纯追求高分辨率更划算。

3. 全局路径搜索:用 A* 在代价地图上找出一条能走的路

3.1 为什么不是 RRT 也不是 Dijkstra:火星全局规划的第一选项

二维栅格地图上的全局规划,工程首选基本是 A* 或者 D* Lite,而不是 RRT 系列。RRT 的优势在高维空间和强运动学约束场景,火星车底盘近似可分解为位置加航向,三维空间里 RRT 的随机采样反而会带来路径抖动和不可复现性。Dijkstra 保证最优但搜索范围太大,在几十万栅格的地图上会浪费大量算力。A* 用启发函数引导搜索方向,在栅格图上的时间和最优性平衡最好。

这里有一个常见误解:A* 只能处理静态地图。真实火星任务中,地图在着陆后会持续更新,这时直接从零重跑 A* 并不是最优解。D* Lite 可以在障碍变化时增量修复已规划的路径,避免全图重新搜索。我一般这样取舍:如果一次任务中地图只更新两三次,A* 完全够用;如果车载传感器持续扫描并频繁更新地图,就要把全局规划器换成 D* Lite。两者共享代价地图和数据结构,切换成本没有想象中高。

另外一个容易被忽略的点是启发函数。火星车规划中最稳妥的是欧氏距离启发,因为它在八邻域栅格图上永远不会高估实际代价,保证 A* 能找到最优解。如果随手上曼哈顿距离,在对角线移动代价为 √2 的地图上会削弱启发作用,搜索的栅格数明显变多。这个细节经常被熟手踩到,调搜索速度时先检查这里,比改权重有效得多。

3.2 A* 在带地形代价的栅格图上的最小实现

这里给出一个可以直接跑通的最小实现。与教科书版本的区别是可以消费上一章生成的代价地图,而不是只接受二值障碍标记。

import heapq import numpy as np def heuristic(a, b): return np.hypot(a[0] - b[0], a[1] - b[1]) def neighbors(node, w, h, allow_diagonal=True): x, y = node for dx, dy in [(1,0),(-1,0),(0,1),(0,-1)]: if 0 <= x+dx < w and 0 <= y+dy < h: yield (x+dx, y+dy) if allow_diagonal: for dx, dy in [(1,1),(1,-1),(-1,1),(-1,-1)]: if 0 <= x+dx < w and 0 <= y+dy < h: yield (x+dx, y+dy) def reconstruct_path(came_from, current): path = [current] while current in came_from: current = came_from[current] path.append(current) return path[::-1] def astar_plan(start, goal, cost_map, w_terrain=0.3, allow_diagonal=True): h, w = cost_map.shape open_heap = [] heapq.heappush(open_heap, (0.0, start)) came_from = {} g_score = {start: 0.0} f_score = {start: heuristic(start, goal)} while open_heap: _, current = heapq.heappop(open_heap) if current == goal: return reconstruct_path(came_from, current) for nxt in neighbors(current, w, h, allow_diagonal): step = 1.0 if (nxt[0] == current[0] or nxt[1] == current[1]) else 1.414 terrain_cost = cost_map[nxt[1], nxt[0]] tentative_g = g_score[current] + step + w_terrain * terrain_cost if tentative_g < g_score.get(nxt, float('inf')): came_from[nxt] = current g_score[nxt] = tentative_g f_score[nxt] = tentative_g + heuristic(nxt, goal) heapq.heappush(open_heap, (f_score[nxt], nxt)) return []

逻辑说明:g_score记录从起点到当前格子的累计代价,terrain_cost是从代价地图中取出的地形风险值,w_terrain决定地形代价在总代价里的占比。每次从堆顶弹出f_score最小的节点,直到到达目标或堆空。allow_diagonal=False时强制四邻域搜索,路径会变成直角折线,但更安全;开启对角线后路径更短,代价是可能贴着障碍物切角,需要配合膨胀层使用。

参数说明:w_terrain=0.3是火星地形场景下的推荐起始值。设得太大,路径会绕很远只为躲开一个缓坡;设得太小,地形代价的存在就失去意义。我从 0.3 起步,如果发现路径频繁跨越高坡度区域就上调到 0.5,如果搜索时间明显变长就下调到 0.15。f_score的排序完全交给heapq,不要为了“优化”自己实现优先队列,Python 内置堆的性能和稳定性都更好。

3.3 参数怎么设:栅格分辨率、搜索步长与边界控制

栅格分辨率直接影响搜索空间大小。0.5 米分辨率的 1000×1000 地图有一百万个栅格,A* 在最坏情况下可能扫描其中很大一部分。我常用的策略是“全局粗、局部细”:全局地图用 1.0 米甚至 2.0 米分辨率,只负责找到一条走廊级的路径;真正执行的路径由局部规划器在 0.2 米分辨率的局部代价地图上细化。这样既保证全局方向正确,又避免机载算力被搜索算法占满。

allow_diagonal这个开关也要按场景处理。在开阔平地上开启它能显著缩短路径长度;在岩石密集区域,对角线移动容易把路径压进窄缝,导致局部规划器找不到可行的速度指令。我的习惯是在全局层开启对角线,但把代价地图里所有障碍先做一轮膨胀,用膨胀层吸收切角风险。

还有一个边界控制参数容易被忽略:搜索范围上限。火星车的地图可能覆盖整个着陆区,但单次全局规划只需要到达下一个航点。我会给 A* 设置最大搜索步数,比如 20 万栅格,超过就放弃并报错,而不是任由算法在地图上跑几秒钟。错误信息要带上触发时的坐标和地图更新时间,这个信息在排障时价值极高。

3.4 从栅格路径到车体能执行的航点序列

A* 返回的是像素坐标序列,直接丢给底盘执行是不行的。常见做法是先按固定间隔抽样,比如每 0.5 米取一个航点,再把栅格坐标乘以分辨率并加上地图原点坐标,恢复成平面坐标。抽样时要注意保留路径的转向特征,直线段上的密集点可以删掉,弯道处的点必须保留。

抽样后还要检查航点之间的曲率。如果相邻三点构成的角度太小,说明这里有个急转弯,要么用样条曲线平滑,要么标记为低速通过区。火星车的转向机构不像普通汽车那样灵活,原地转向更是要尽量避免,因为松软地表下原地转向常常导致轮子下陷。这部分只做静态检查,真正的动态约束留给下一章的局部规划器去处理。

4. 局部避障与运动控制:动态窗口法在火星场景里的工程参数

4.1 为什么局部层用动态窗口法而不是纯追踪

纯追踪算法在空旷场地非常稳定,但它的本质是“追踪前方一个目标点”,不会主动考虑障碍物是否在轨迹上。火星表面碎石遍布,局部路径上随时可能出现新障碍,纯追踪需要外部避障模块频繁干预,控制量容易抖动。动态窗口法(DWA)直接在速度空间采样,天然把车体的运动学约束和障碍物距离都放进评价函数里,更适合这类场景。

混合 A* 也能在局部层使用,它在有强曲率约束的地下车库场景表现优异,但计算量明显高于 DWA。火星车速度低、加速度不大,速度空间很小,DWA 的采样数量级完全覆盖得了;混合 A* 带来的平滑性收益在这种低速平台上并不明显。还有一个实际考虑:DWA 的参数调整非常直观,评价函数的三项权重可以用场地实验快速标定,对工程排障友好得多。

如果车体带差速转向能力,DWA 甚至可以简化成线速度加角速度的二维采样,采样密度从 12×12 降到 9×9 也能满足实时性。关键是不要忘记动态窗口的含义:当前速度决定了可达速度区间,加速度约束进一步缩小了这个区间。下面代码实现了这个逻辑。

4.2 DWA 采样与评价函数实现

import numpy as np def simulate_trajectory(state, v, omega, dt, predict_time): x, y, theta = state[0], state[1], state[2] traj = [(x, y)] steps = int(predict_time / dt) for _ in range(steps): x += v * np.cos(theta) * dt y += v * np.sin(theta) * dt theta += omega * dt traj.append((x, y)) return traj def evaluate_trajectory(traj, goal, cost_map, v, omega, alpha=0.3, beta=0.4, gamma=0.3): end = traj[-1] dist_to_goal = np.hypot(end[0] - goal[0], end[1] - goal[1]) heading_score = 1.0 / (1.0 + dist_to_goal) # 取轨迹上所有点里最小障碍距离作为安全评分 min_clearance = min([cost_at_point(cost_map, pt) for pt in traj[::2]]) clearance_score = min_clearance velocity_score = np.hypot(v, omega) / (1.0 + np.hypot(v, omega)) return alpha * heading_score + beta * clearance_score + gamma * velocity_score def dwa_control(state, goal, cost_map, robot_params): v_min, v_max = robot_params['v_range'] w_min, w_max = robot_params['w_range'] a_max, alpha_max = robot_params['accel_range'] dt = robot_params['dt'] predict_time = robot_params['predict_time'] cur_v, cur_w = state[3], state[4] # 动态窗口:围绕当前速度加减加速度约束 v_min = max(v_min, cur_v - a_max * dt) v_max = min(v_max, cur_v + a_max * dt) w_min = max(w_min, cur_w - alpha_max * dt) w_max = min(w_max, cur_w + alpha_max * dt) best_score = -float('inf') best_vel = (0.0, 0.0) for dv in np.linspace(v_min, v_max, 12): for dw in np.linspace(w_min, w_max, 12): traj = simulate_trajectory(state, dv, dw, dt, predict_time) score = evaluate_trajectory(traj, goal, cost_map, dv, dw) if score > best_score: best_score = score best_vel = (dv, dw) return best_vel

逻辑说明:simulate_trajectory用单车模型做匀速推演,predict_time=3.0意味着预测未来三秒内的轨迹。evaluate_trajectory里三项评分分别对应“靠近目标”“远离障碍”“保持移动”。其中cost_at_point把连续坐标映射到栅格坐标并返回该处代价,我用它作为障碍距离的近似。注意轨迹点只取偶数下标参与障碍距离计算,这是为了省算力,代价是可能漏掉极短的尖角障碍。

参数说明:alpha=0.3, beta=0.4, gamma=0.3是火星场景的推荐起点,beta 最高是为了优先保证安全间距。如果车在障碍密集区频繁刹停,就把 gamma 下调到 0.2;如果车总是固执地绕远路,就上调 alpha。predict_time我几乎不会超过 4 秒,因为在松软地形上推演越长,速度误差造成的轨迹偏差越大,预测轨迹本身也不可信了。

4.3 火星场景的必调参数与调节顺序

DWA 最容易翻车的地方是权重调节顺序。直观感觉是“先让车跑起来”,于是把 gamma 提到最高,结果车辆在障碍物前频繁急转,轨迹出现大量锯齿。我的经验是反过来:先保证 beta,让车在任何速度下都与障碍物保持足够距离;再调 alpha,确认车在安全的前提下愿意朝目标走;最后才是 gamma,小心翼翼地把平均速度提上来。这个顺序能让你从参数空间里更快收敛。

采样数量也别盲目加大。12×12 的采样已经覆盖了绝大多数低速底盘需求,提高到 20×20 带来的路径质量提升微乎其微,却可能让控制频率从 20 赫兹掉到 10 赫兹以下。如果控制频率不足,DWA 的动态窗口失去了意义,因为车辆在一个控制周期内可能跑出远超采样间隔的距离。遇到这种情况,优先降预测时间而不是降采样密度。

5. 避坑:火星车路径规划最容易翻车的 5 个工程细节

5.1 坑一:路径贴着障碍物走——膨胀半径没有生效

现象:A* 规划出来的路径在代价地图上紧贴障碍物边界,车体真实宽度一出,必然剐蹭。 原因:直接把障碍二值图作为规划输入,规划器把车当成一个点,忽略车体宽度和安全余量。 解决:在进入 A* 之前先做膨胀处理。使用scipy.ndimage.binary_dilation对障碍栅格向外扩展,迭代次数等于膨胀半径除以栅格分辨率。

from scipy.ndimage import binary_dilation radius_cells = int(0.6 / 0.5) # 车体半径 0.6m,栅格 0.5m inflated = binary_dilation(obstacle_map, iterations=radius_cells) cost_map[inflated] = 1.0

膨胀半径取车体外接圆半径加 0.3 米安全余量,这是我在多个模拟项目里反复验证过的保守值。如果地图分辨率是 0.2 米,迭代次数要相应增加到 5 次左右。

5.2 坑二:地图上看着平,实际场地里翻车——平面代价忽略了坡度

现象:规划路径横穿一片视觉上很平的缓坡,实际车体侧倾角超出安全范围,导致任务中断。 原因:代价地图只编码了障碍物,没编码地形坡度信息,A* 自然对坡面熟视无睹。 解决:用第二章的build_cost_map把坡度代价叠加到基础地图上。其中阈值不要照抄,要按车体实际侧倾极限重新标定。我见过某团队的模拟项目把 35 度设为上限,结果车体在 28 度坡上就出现轮子离地,这是典型的阈值与物理底盘不匹配。

5.3 坑三:局部规划器在原地打转——评价函数权重出了问题

现象:DWA 输出的指令让车在原地反复转向,前进距离很少,看起来像在犹豫。 原因:alpha(朝向目标)权重过高而 gamma(速度)权重接近零,车辆认为原地调整航向比低速前进更“划算”。 解决:按 beta → alpha → gamma 的顺序重新调整权重。同时在速度采样里增加一个最小前进速度门槛,禁止输出接近零的低速加高转速组合,除非前方确实被完全堵死。

5.4 坑四:里程计漂移让地图“张冠李戴”——定位比算法更影响路径质量

现象:规划器一切正常,但车在某段路上频繁触发避障,传感器明明显示前方没有东西。 原因:轮式里程计在松软沙地上打滑严重,车认为自己在一个位置,实际已经在另一个位置。火星上没有导航卫星信号,这个问题比地球上严重得多。 解决:把视觉测程和 IMU 接入融合定位,并在规划器中读取位姿的协方差。当位置不确定性超过半个车体宽度时,强制停车并重新做视觉特征匹配校准。这个触发条件要写在规划器外面,而不是等路径走歪了再补救。

5.5 坑五:每次重规划路径都换一条——全局路径跳变

现象:一次任务中连续触发多次全局重规划,路径方向完全改变,局部规划器来回切换参考线。 原因:全局重规划的触发条件太宽松,任何小的地图更新都导致 A* 从头搜索,新旧路径差异被无限放大。 解决:设定重规划触发阈值,例如偏离参考路径超过 2 米或前方 8 米内出现新障碍才重规划。新路径生成后,先用一个过渡航点把车引回原路径附近,而不是立刻整体切换。这个技巧能显著减少车体的晃动感,同时也降低计算功耗。

6. 验证与进阶:仿真闭环、等效场测试与路径平滑的工程习惯

6.1 三级验证阶梯:单算法测试、整舱级仿真、等效场测试

我习惯把路径规划验证分成三级,顺序不能颠倒。第一级是单算法测试:把真实 DEM 数据喂给 A* 和 DWA,只验证逻辑正确性,通过标准是路径无碰撞且长度不超过参考路径的 1.3 倍。第二级是整舱级仿真:把感知、定位、规划、底盘全部接入,让车在地图里自主跑完一段数百米的航程,全程不允许人工干预。第三级才是等效场测试,在铺了碎石和沙坡的场地上做有限里程验证。

整舱级仿真最容易暴露接口问题,比如规划器输出的航向角没有做角度归一化,导致车轮在过零度时反转一整圈。这类问题单算法测试时根本不会出现,但它们在实际任务中的破坏力比算法本身大得多。

6.2 让路径更好走的进阶技巧:B 样条平滑与曲率约束

A* 输出的折线路径在转折处常常有尖角,直接跟踪会导致底盘频繁加减速。常见做法是用 B 样条做平滑,然后用曲率检查约束最终路径。

from scipy.interpolate import splprep, splev def smooth_path(path, smoothing=1.0): pts = np.array(path) k = min(3, len(pts) - 1) tck, u = splprep([pts[:, 0], pts[:, 1]], s=smoothing, k=k) new_pts = np.array(splev(np.linspace(0, 1, len(pts) * 3), tck)).T return new_pts

逻辑说明:splprep对折线路径做参数化样条拟合,smoothing控制拟合的松弛程度。平滑度太高会偏离原始路径裁弯取直,太低则保留几乎所有折角。我一般从 1.0 起步,如果平滑后路径与障碍物的最小距离低于膨胀半径,就降低平滑度重新拟合。

曲率检查的做法是遍历平滑后的路径点,用相邻三点计算局部曲率,超过车体最大转向能力的点标记出来,适当降低局部平滑强度或在该处分段。这个技巧配合 DWA 使用后,车的行驶姿态会明显稳定,功耗也更低。

我最早做火星车导航时,把大量时间花在比较 A* 和 RRT 的搜索效率上,后来在实际测试里栽的跟头几乎都来自地图阈值标定、定位漂移和权重调节顺序,而不是搜索算法本身。现在我的习惯永远是先看代价地图和位姿协方差,再回头看规划代码。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进