简介:这份资源面向机器人自主导航方向的初学者与工程实践者,聚焦地图环境已知条件下的全局路径规划与运动控制问题。包内以Matlab 2018实现RRT与双向RRT(RRT-Connect)算法,完成从起点到目标点的路径搜索,并将规划点列导出为txt文件;随后通过VS2013读取点列数据,采用较为粗糙的控制方法驱动机器人沿路径移动,形成规划与执行相衔接的完整流程。资源共89个文件,涵盖m脚本、cpp与h源码、exe可执行程序、txt点列与地图数据、mat地图文件及工程配置与日志文件,压缩包约76.6MB,目录按Matlab规划与VS客户端两部分组织。目前已有770人学习下载。读者可据此理解RRT与双向RRT的实现差异、路径点导出与跨平台读取方式,并借助必看运行文档快速复现实验,为后续改进控制精度与算法效率提供参考。
1. 地图已知的机器人自主导航程序:为什么“有地图”反而更容易翻车
很多人第一次接触机器人自主导航,是从“地图已知”这个设定开始的。听起来很美好:环境地图已经建好了,机器人只要知道自己在哪、要去哪,规划一条路径走过去就行。但真正上手写程序的人会发现,地图已知的自主导航反而比未知环境更容易踩坑——因为你会不自觉地假设地图是完美的、定位是准确的、路径是可行的,而现实往往在这三个假设上同时打脸。
这个标题讲的核心,是一套完整的导航程序:给定一张先验地图(通常是栅格占据地图),机器人通过定位模块确定自身位姿,再用全局规划器算出一条从当前位置到目标点的路径,最后由局部规划器控制底盘沿着路径移动,同时处理动态障碍和定位漂移。它适合已经跑通建图、手里有一张还算靠谱的地图、想把机器人真正“用起来”的开发者——不管是做巡检、送物、还是实验室里的移动平台验证。
我见过太多项目卡在“地图有了,但机器人就是走不对”这一步。问题往往不在算法本身,而在程序架构和参数配置的细节里。接下来我会按实际落地顺序,把定位、全局规划、局部规划、避坑和调参这几块拆开讲清楚。
2. 定位与地图加载:让机器人知道自己在地图的哪个像素上
2.1 先验地图的格式与加载方式
地图已知的导航程序,第一步永远是加载地图并建立“地图坐标系 → 世界坐标系”的映射。最常见的做法是用栅格地图,每个像素代表一个固定物理尺寸,比如 0.05 米/像素。地图文件通常包含两部分:图像数据(占据/空闲/未知)和元数据(分辨率、原点坐标、阈值)。
下面是一个典型的加载与校验代码,用 Python 写,逻辑清晰,方便你直接改成自己项目的入口模块:
import numpy as np from PIL import Image class OccupancyMap: def __init__(self, image_path, resolution=0.05, origin=(0.0, 0.0)): # resolution: 每个像素代表的实际距离(米) # origin: 地图左下角在世界坐标系中的位置 self.resolution = resolution self.origin = origin img = Image.open(image_path).convert('L') self.data = np.array(img) # 常见约定:像素值 > 250 为空闲,< 50 为占据,其余为未知 self.free_thresh = 250 self.occupied_thresh = 50 def is_free(self, row, col): if row < 0 or col < 0 or row >= self.data.shape[0] or col >= self.data.shape[1]: return False val = self.data[row, col] return val > self.free_thresh def world_to_map(self, wx, wy): # 世界坐标转地图行列 col = int((wx - self.origin[0]) / self.resolution) row = int((wy - self.origin[1]) / self.resolution) # 注意:图像行号通常从上往下,而世界坐标Y轴向上,需要翻转 row = self.data.shape[0] - 1 - row return row, col def map_to_world(self, row, col): wx = col * self.resolution + self.origin[0] wy = (self.data.shape[0] - 1 - row) * self.resolution + self.origin[1] return wx, wy这段代码里最容易被忽略的是行号翻转。图像坐标系原点在左上角,行号向下增长;而机器人世界坐标系通常原点在左下角,Y 轴向上。如果不做翻转,机器人会以为自己在镜像世界里,路径规划结果完全相反。另一个参数是resolution,它必须和建图时使用的分辨率一致,否则所有距离计算都会按比例出错。origin也要和建图时的起点对齐,差一个像素就是几厘米的定位偏差。
2.2 蒙特卡洛定位的粒子数与重采样策略
地图加载好之后,机器人需要知道自己在地图上的位姿。最常见的是蒙特卡洛定位(粒子滤波)。它的核心思想是用一堆带权重的粒子表示位姿分布,通过运动模型预测、观测模型更新、重采样三步迭代。
实际写程序时,粒子数不是越多越好。粒子数太多,计算量爆炸,在低算力平台上直接卡死;粒子数太少,定位容易发散,机器人会“跳变”。我一般会按地图面积和激光雷达角度分辨率来估算:室内 100 平米以内,粒子数 500 到 1000 足够;如果环境特征稀疏(长走廊、空旷大厅),需要加到 2000 以上。
重采样策略也有讲究。标准做法是“低方差重采样”,但如果你发现机器人定位在某个区域反复丢失,可以改成“自适应重采样”:只有当有效粒子数低于阈值时才重采样,避免粒子多样性过快丧失。下面是一个简化的重采样判断逻辑:
def effective_particle_count(weights): # 权重已归一化 return 1.0 / np.sum(np.square(weights)) def should_resample(weights, ratio_threshold=0.5): n_eff = effective_particle_count(weights) return n_eff < len(weights) * ratio_thresholdratio_threshold这个参数我一般设 0.5,意思是当有效粒子数不到总粒子数一半时才重采样。设得太高,粒子退化快;设得太低,计算浪费。这个值需要根据你的运动模型噪声和观测噪声来微调,没有万能值。
2.3 定位丢失时的恢复行为
定位丢失是导航程序最常见的故障之一。表现是机器人在地图上“瞬移”,或者路径规划突然指向完全错误的方向。程序里必须有一个恢复机制:当定位置信度低于阈值时,不要继续执行导航任务,而是进入恢复模式——原地旋转、重新初始化粒子、或者请求人工介入。
我通常会在定位模块里维护一个“连续低置信度帧数”计数器,超过 10 帧就触发恢复。恢复时把粒子重新均匀撒在地图空闲区域,而不是只撒在当前位置附近。这样虽然慢一点,但能避免机器人“自信地走错路”。
3. 全局规划与局部规划:从 A 到 B 的路径怎么算、怎么走
3.1 全局规划器的选型与代价地图配置
全局规划负责在地图上找一条从当前位置到目标点的可行路径。常见算法有 A*、Dijkstra、RRT 等。对于栅格地图,A* 是最常用的,因为它在有启发式函数的情况下效率高,而且容易加入代价项。
但直接用原始 A* 有个问题:它只考虑“能不能走”,不考虑“好不好走”。机器人贴着墙走、穿过狭窄门缝、在障碍物边缘反复蹭,都是因为代价地图没有配置好。代价地图通常有多层:静态层(来自先验地图)、障碍层(来自实时传感器)、膨胀层(把障碍物按机器人半径膨胀)。
下面是一个 A* 规划的核心代码片段,重点看代价函数的写法:
import heapq def astar(grid_map, start, goal, cost_weight=1.0): # grid_map: 二维数组,0 表示空闲,1 表示占据 # start/goal: (row, col) rows, cols = grid_map.shape open_set = [] heapq.heappush(open_set, (0, start)) came_from = {} g_score = {start: 0} f_score = {start: heuristic(start, goal)} while open_set: _, current = heapq.heappop(open_set) if current == goal: return reconstruct_path(came_from, current) for dr, dc in [(-1,0),(1,0),(0,-1),(0,1),(-1,-1),(-1,1),(1,-1),(1,1)]: neighbor = (current[0]+dr, current[1]+dc) if not is_valid(neighbor, rows, cols) or grid_map[neighbor] == 1: continue # 对角线移动代价更高,避免机器人走“锯齿”路径 move_cost = 1.414 if dr != 0 and dc != 0 else 1.0 tentative_g = g_score[current] + move_cost * cost_weight if neighbor not in g_score or tentative_g < g_score[neighbor]: came_from[neighbor] = current g_score[neighbor] = tentative_g f_score[neighbor] = tentative_g + heuristic(neighbor, goal) heapq.heappush(open_set, (f_score[neighbor], neighbor)) return None def heuristic(a, b): # 欧几里得距离,比曼哈顿距离更平滑 return ((a[0]-b[0])**2 + (a[1]-b[1])**2) ** 0.5cost_weight这个参数用来调节路径的“保守程度”。设大了,机器人会绕远路避开所有障碍;设小了,机器人会贴着障碍走。我一般从 1.0 开始调,如果发现机器人经常蹭墙,就加到 1.5 或 2.0。对角线移动代价用 1.414 而不是 1.0,是为了让路径更直,减少不必要的转向。
3.2 局部规划器的控制频率与速度采样
全局规划给出的是“大方向”,局部规划负责“每一步怎么走”。最常见的局部规划器是 DWA(动态窗口法),它在速度空间里采样,模拟一段时间内的轨迹,选一条最接近全局路径且不撞障碍的。
DWA 的关键参数有三个:控制频率、速度采样范围、评价函数权重。控制频率一般设 10 到 20 Hz,太低会导致机器人反应迟钝,太高会浪费算力。速度采样范围要根据底盘性能来,不能超过最大速度和最大角速度。
下面是一个简化的 DWA 速度采样逻辑:
def sample_velocities(max_v, max_w, v_res, w_res): velocities = [] v = -max_v while v <= max_v: w = -max_w while w <= max_w: velocities.append((v, w)) w += w_res v += v_res return velocities def evaluate_trajectory(traj, global_path, obstacles): # 三个评价项:路径跟随、障碍距离、速度 path_score = calc_path_following(traj, global_path) obstacle_score = calc_obstacle_distance(traj, obstacles) velocity_score = traj[-1][2] # 末端速度 return 0.5 * path_score + 0.3 * obstacle_score + 0.2 * velocity_score评价函数的权重需要根据场景调。如果机器人经常撞障碍,就加大obstacle_score的权重;如果机器人走得太慢,就加大velocity_score。我一般把路径跟随权重设 0.5 左右,障碍权重 0.3,速度权重 0.2,然后根据实际表现微调。
3.3 全局与局部的衔接:路径更新与重规划触发
全局规划和局部规划不是各跑各的,它们之间需要衔接。全局路径不能一直不变,否则动态障碍出现时机器人会卡死。常见的做法是:局部规划器检测到当前路径被阻塞超过一定时间(比如 2 秒),就触发全局重规划。
重规划时要注意,不要每次都从当前位置重新算整条路径,那样计算量大且路径会跳变。更好的做法是只重规划从当前位置到目标点的一段,或者用增量式规划算法。另外,重规划频率不能太高,否则机器人会“犹豫不决”,走走停停。
我一般会设两个阈值:局部路径阻塞距离阈值(比如 0.5 米)和阻塞持续时间阈值(比如 1.5 秒)。只有两个都超过时才触发重规划,避免因为传感器噪声误触发。
4. 避坑与常见问题:导航程序跑不起来先查这几条
4.1 机器人原地转圈或走“S”形
现象:机器人启动后不往目标走,而是原地打转,或者走几步就偏一下,轨迹像蛇一样。
原因:最常见的是局部规划器的角速度评价权重过大,或者全局路径的朝向和机器人当前朝向差异太大,局部规划器一直在“纠偏”。另一个可能是定位的朝向估计有偏差,机器人以为自己朝向不对,反复调整。
解决:先检查定位模块输出的朝向是否稳定。如果朝向跳变,降低激光雷达匹配的搜索角度范围,或者增加粒子数。如果朝向稳定但机器人还是画龙,把局部规划器的角速度权重降低,同时增大路径跟随的横向容差。我一般会把角速度采样范围缩小到最大角速度的 60%,避免机器人过度转向。
4.2 路径规划成功但机器人不动
现象:全局规划器返回了路径,但底盘没有速度指令,机器人静止。
原因:可能是局部规划器没有收到全局路径,或者速度指令被安全模块拦截。常见的是代价地图膨胀层把机器人当前位置也标记为“占据”,导致局部规划器认为机器人已经撞了,拒绝输出速度。
解决:检查代价地图的膨胀半径是否大于机器人半径。如果膨胀半径设得太大,机器人所在位置会被膨胀层覆盖。把膨胀半径设为机器人内切圆半径加上少量余量,通常 0.1 到 0.2 米就够。另外检查安全模块的急停条件,确保没有误触发。
4.3 定位突然跳变导致路径错乱
现象:机器人走着走着,地图上的位置突然跳到几米外,路径重新规划后指向完全错误的方向。
原因:激光雷达观测和地图匹配失败,粒子滤波发散。常见于长走廊、玻璃墙面、或者动态障碍物多的场景。
解决:增加定位恢复机制,检测到连续低置信度时重新初始化粒子。另外可以在定位模块里加入里程计约束,限制相邻两帧之间的位姿变化范围。如果环境里玻璃多,考虑加超声波或红外传感器辅助。我还会在程序里加一个“定位跳变检测”:如果当前位姿和上一帧位姿差异超过物理可能的最大移动距离,就判定为跳变,丢弃该帧观测。
4.4 机器人贴着障碍物走甚至蹭墙
现象:路径明明离墙有距离,但机器人实际走的时候总是往墙边靠,甚至蹭上去。
原因:代价地图的膨胀层没有正确配置,或者局部规划器的障碍评价函数权重太低。另一个可能是机器人的 footprint 设置比实际小,导致规划器认为可以通过更窄的缝隙。
解决:先确认 footprint 参数是否准确,包括长宽和中心偏移。然后把膨胀半径设为 footprint 外接圆半径,确保规划器不会让机器人进入任何可能碰撞的区域。如果还是蹭墙,加大局部规划器的障碍权重,或者降低速度上限,给局部规划器更多反应时间。
4.5 导航过程中频繁重规划导致机器人抖动
现象:机器人走走停停,路径频繁更新,速度指令断断续续。
原因:重规划触发条件太敏感,或者全局规划器每次算出的路径差异太大。
解决:增加重规划的时间间隔阈值,比如至少 2 秒才允许一次重规划。另外可以在全局规划器里加入路径平滑,让相邻两次规划的路径更接近。如果机器人还是抖动,检查局部规划器的控制频率是否稳定,避免因为算力不足导致控制周期忽长忽短。
5. 调参进阶:用日志和可视化把导航程序变成可观测系统
导航程序最怕的是“黑匣子”——机器人走不对,但你不知道是哪一步出了问题。我的习惯是,在程序里埋足够的日志和可视化输出,把定位、规划、控制三个模块的中间结果都记录下来。
具体做法是:每帧记录当前位姿、粒子分布、全局路径、局部轨迹、速度指令、代价地图关键区域。用轻量级的日志库写到本地文件,同时通过一个简单的 WebSocket 推送到浏览器里可视化。这样机器人跑一次,你就能回放整个过程,看到底是定位跳了、路径歪了、还是控制没跟上。
下面是一个日志记录的结构示例:
import json import time class NavLogger: def __init__(self, log_path): self.log_path = log_path self.buffer = [] def log_frame(self, pose, particles, global_path, local_traj, cmd_vel): frame = { "timestamp": time.time(), "pose": {"x": pose[0], "y": pose[1], "theta": pose[2]}, "particle_count": len(particles), "global_path_len": len(global_path) if global_path else 0, "local_traj_len": len(local_traj) if local_traj else 0, "cmd_vel": {"v": cmd_vel[0], "w": cmd_vel[1]} } self.buffer.append(frame) if len(self.buffer) >= 100: self.flush() def flush(self): with open(self.log_path, 'a') as f: for frame in self.buffer: f.write(json.dumps(frame) + '\n') self.buffer = []这个日志结构不复杂,但足够你事后分析。比如机器人走歪了,你可以看pose的theta是不是在跳;如果particle_count突然掉到很低,说明定位可能发散;如果global_path_len频繁变化,说明重规划太频繁。
另一个实用技巧是录制 rosbag 或类似的数据包,把传感器原始数据、定位输出、规划结果全部录下来。这样你可以在离线环境下反复回放,调整参数,而不用每次都让机器人实际跑。我一般会在调试阶段录 5 到 10 分钟的数据,然后离线跑不同的参数组合,找到最优解后再上实车。
最后说一个我自己的教训:不要在地图已知的导航程序里过度依赖全局路径。全局路径只是参考,局部规划器才是真正控制机器人的。如果局部规划器被全局路径“绑死”,机器人遇到动态障碍时就会反应迟钝。我现在的习惯是,全局路径只给局部规划器一个大致方向,局部规划器有足够的自由度去避障和调整。这样机器人走起来更自然,也不容易卡死。
希望帮到你。
本文还有配套的精品资源,点击获取