想象你是一名网约车司机:手机里装着一款“会呼吸”的城市地图。每条街道实时变色——绿色说明畅通,深红代表施工或事故,灰色是没探过的区域;危险点周围还套着一圈圈淡紫色的安全缓冲带。接到订单后,导航App给你划出一条从A到B的大路线,而你握着方向盘,眼睛盯着前方三五米的路况,随时准备为突然变道的外卖小哥轻打一把方向。 这套“活地图+大路线+脚下功夫”的组合,正是机器人导航的核心架构:代价地图把环境编码成一张充满数字的栅格图(哪里能走、哪里危险);全局规划负责在整张图上搜出一条大路线;局部规划负责行驶中实时避障和把线跟稳。本文就借着这趟“城市送人”之旅,把 Nav2 导航栈中“决策与导航”的完整链路拆给你看。
掌握代价地图的编码逻辑与全局/局部规划的配合方式,是从事机器人导航、无人配送和仓储调度开发的看家本领——它直接决定了机器人在真实世界里“敢不敢走、会不会撞、走不走得到”。
可视化演示
展示栅格代价地图分层结构、A*全局路径搜索过程、以及DWA局部规划器实时避障轨迹,直观呈现Nav2导航栈核心机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
栅格代价地图 障碍物(红,代价254)周围膨胀区(黄,代价128)形成安全缓冲 绿色为自由空间(代价0),S为起点G为目标点 cost=inflate(obstacle,radius) 膨胀半径决定机器人与障碍物的最小安全距离
代价地图基础
网约车司机最怕的不是堵车,而是导航给了一条“地图上不存在”的路。你手机里那张活地图,是市政部门把城市切成无数小方块,一格一格标注出来的:绿色格子可以走,红色格子是施工围挡,灰色格子是数据盲区。机器人世界里的“活地图”就是代价地图(costmap),它把环境切成一格一格的栅格(grid cell),每个格子只存一个数字,代表“能不能走、有多大风险”。
机器人怎么给格子涂色?靠传感器。激光雷达像扫街车一样转一圈,扫到障碍物就把对应格子涂红;深度相机像你的眼睛,把看到的东西投影到地图上;里程计则告诉机器人“你现在站在地图的哪个位置”。这些数据源源不断地写进代价地图,地图也就跟着“呼吸”起来——每次传感器刷新,颜色就变一次。
代价地图的数值不是随便定的,它遵循一套约定俗成的编码:自由空间记 0,代表放心走;占据空间记 254,代表绝对不可通行;未知空间记 255,代表“没探过、按最坏情况处理”。为什么未知比占据还高?因为对机器人来说,看不见的危险比看得见的危险更可怕。
这张会呼吸的活地图,就是司机判断“哪里敢去”的唯一依据。导航 App 敢不敢让你走一条街,先看地图上那串数字答不答应。
- 自由栅格记 0传感器明确扫过、确认没有障碍物的格子,代价最低,规划器优先选它。
- 占据栅格记 254障碍物坐实的格子,规划器视为墙,路径绝对不允许穿过。
- 未知栅格记 255从未被观测过的区域,代价最高,机器人宁可绕远路也不愿冒险闯入。
分层代价地图
如果所有信息都画在同一张纸上,问题就来了:道路底图半年才更新一次,实时路况每秒都在变,施工点的安全缓冲还要单独描一圈。底图一覆盖,实时信息就没了;实时信息一更新,底图又看不清。聪明的做法是把它们画在不同的透明胶片上,叠在一起看。
代价地图也采用了同样的“胶片叠加”思路,这就是分层代价地图。Nav2 里最常见的三层各司其职:静态图层(static layer)对应那张半年更新一次的底图,来自 SLAM 建好的地图,刷新频率最低;障碍图层(obstacle layer)对应实时路况,激光雷达扫到什么立刻写进去,刷新频率最高;膨胀图层(inflation layer)则在障碍物基础上向外“晕染”一圈代价,对应施工点外的安全缓冲。
这三层最终要合成一张主图(master costmap),规划器只认主图。合并规则很简单粗暴——同一个格子,哪一层给出的代价值最高,就采用哪个,因为安全问题上永远要按最坏情况打算。
分层的好处,就像把底图、路况、缓冲圈分开放:哪块变了就换哪张胶片,既不会互相覆盖,也方便单独调试——这也正是 Nav2 插件化图层设计的初衷。
- 主图层合并结果机器人导航最终读取的合并代价图,规划器只见它不见各层。
- 取最大值同一位置三个图层各报一个数,取最大者保证不低估风险。
- 三个输入层静态层给底图,障碍层给实时阻挡,膨胀层给安全余量,三者缺一不可。
膨胀与代价设置
老司机开车有个习惯:路过施工围挡,绝不贴着警示锥走,而是主动留出半条车道。围挡本身只占一小块地,但危险区域是一圈“看不见的力场”——越靠近,越心虚。代价地图里的膨胀层,就是这个力场。
膨胀层要做的事,是把每个障碍物往外“吹”出一圈代价渐变带。这里面有两个关键半径:内切半径(inscribed radius)和膨胀半径(inflation radius)。内切半径对应机器人的体型——如果机器人中心离障碍物比这还近,那就铁定撞上了,代价值直接拉满到 254;膨胀半径则是安全缓冲的外边界,超过这个距离,代价归零。中间的过渡带,代价按指数衰减,离障碍越近越红,越远越淡。
实际调试时还有个“缩放因子”(cost scaling factor),它决定衰减曲线是陡峭还是平缓:调大了,安全圈薄而锐利,机器人贴着障碍走;调小了,安全圈厚而绵长,机器人绕得远远的。膨胀半径设多大,要看机器人尺寸和通道宽度——圈画太大,狭窄通道会被堵死;圈画太小,转弯刮蹭的风险又回来了。老司机的那把“尺度感”,就藏在膨胀参数里。
- 内切半径内判死距离小于内切半径时机器人必撞,代价直接设为致命值 254。
- 指数衰减过渡缓冲区内代价从 253 开始指数下降,衰减快慢由两个半径之差控制。
- 圈外代价归零超出膨胀半径后不再设防,代价值回到 0。
全局路径规划
出发前,导航 App 会先干一件事:在整张城市地图上算出一条从乘客上车点到目的地的大路线。它考虑的是宏观问题——走哪几条主干道、在哪里左转、要不要上高架,完全不理会眼前那辆加塞的车。机器人的全局规划(global planning)干的正是这件事。
全局规划器在全局代价地图上进行搜索,目标是从起点到终点找一条总代价最低的路径。最经典的算法是 A*:它像一位精明的导航员,每到一个路口都知道“已经走了多远”和“离终点还差多远”,把两者加起来作为评判标准,优先探索最有希望的方向,而不是无头苍蝇一样乱转。
全局规划有一个重要前提:它假设环境是静态的。也就是说,这条路线不会每秒钟都重新算,而是规划一次、走一阵子。手机导航也是这个思路——它规划大路线,但路上突发的新状况,得靠司机现场发挥。
- 总代价 f(n)对任意格子 n,用“已走 + 预估”判断这条路值不值得继续走。
- 已走代价 g(n)从起点到格子 n 实际累计的代价,包括穿过膨胀区域付出的“风险税”。
- 启发预估 h(n)从 n 到终点直线距离,永远乐观估计,保证 A* 找到最优解。
局部轨迹规划
真正握着方向盘的是司机本人。你不可能盯着整张城市地图开车,你只看眼前三五米:前车刹车灯亮了,你松油门;右边有人探头,你往左偏一点。这种“短视但高频”的决策,对应机器人的局部规划(local planning)。
Nav2 里最常用的局部规划器之一是 DWA(Dynamic Window Approach,动态窗口法)。它的工作方式像司机在脑子里快速过一遍备选动作:每 100 毫秒左右,在“油门和方向盘”的速度空间里采样出一批候选组合,每个组合往前模拟一小段轨迹,然后给这些轨迹打分,选出最合适的一条执行。打分标准有三项:朝向——这条轨迹是不是指向目标方向;安全——轨迹离障碍物够不够远;速度——能不能保持较高效率。
局部规划只盯着局部代价地图——机器人周围一个小窗口,高频刷新。这就是为什么它能应对“突然加塞的外卖小哥”:障碍物一进窗口,DWA 下一帧就能算出一条绕行轨迹。司机的“手感”,就来自这套三个目标的加权评分。
- 候选速度对局部规划采样出的线性速度 v 和角速度 ω,对应油门和方向盘。
- 朝向项模拟轨迹的终点朝向目标越正,分越高,引导机器人不要跑偏。
- 安全项轨迹与最近障碍物的距离越远分越高,逼着机器人绕开危险区。
- 速度项速度越快分越高,避免机器人在空旷地带磨磨蹭蹭。
全局与局部协调
导航 App 和司机从来不是各干各的。App 负责大方向,司机负责脚下;但一旦 App 的路线过时了——比如前方修路封死了——司机不会傻等,而是提醒 App“此路不通,重新算!”机器人的两个规划器,也是这样一对搭档。
配合流程是这样的:全局规划器先在全局代价地图上算出一条路径,由一串路径点组成;局部规划器拿到这些路径点作为“参考线”,每个控制周期做两件事:一是沿参考线往前跟,二是根据局部代价地图实时避障。两者一高一低、一慢一快,像接力赛一样把机器人送到终点。
那什么时候需要重规划?当机器人因为避障偏离全局路径太远,或者局部代价地图显示前方的路径点已经被新障碍堵死时,继续死跟只会越走越偏。此时局部规划器向全局规划器发出“重来”信号,全局规划器以当前位置为起点重新搜索一条新路径。这个过程的数学判据,就是跟踪偏差超过阈值。
这套配合还离不开坐标系变换(TF)这个“翻译官”:激光雷达的数据在传感器坐标系里,机器人的位置在底盘坐标系里,规划器要用的路线在地图坐标系里,全靠 TF 把数据统一起来,两个规划器才能在同一张图上对话。好搭档的标准不过三条:大方向对、脚下稳、该重算时绝不犹豫——代价地图和两级规划器,正是机器人的这三位一体。
- 跟踪偏差机器人当前位姿到全局路径的最近距离,衡量“偏航了多少”。
- 全局路径点集全局规划输出的路径点集合,是局部规划的参考路标。
- 取最小距离遍历路径点找最近的一个,算出机器人离线的垂直距离。
- 阈值触发重规划偏差超过阈值 δ 就重新调用全局规划,而不是死守旧路线。
AI 应用场景
仓储 AGV 调度系统
阶段:规划与控制
解决的问题:传统固定轨道或磁条导引的 AGV 改造费用高、路线僵化,临时堆放的货物或倒下的货箱就会让整条线瘫痪。基于代价地图与两级规划,AGV 用激光雷达实时刷新局部代价地图,全局规划负责跨巷道的大路线,局部规划处理实时避障,动态库位也能从容应对。
具体案例:某电商仓库部署几十台潜伏式 AGV,调度中心下发任务后,每台车在全局代价地图上规划巷道间的主路线;行驶中遇到拣货员或临时托盘,DWA 局部规划器在 100 毫秒内改出绕行轨迹,偏离过远时再触发全局重规划,整体搬运效率提升约 30%。
校园无人配送车
阶段:决策与导航
解决的问题:室外环境变化快,市政施工、违停车辆、突然横穿的行人都会让预先规划的路线瞬间失效。全局规划结合高精地图给出大方向,局部规划用激光雷达与相机的高频观测实时修正轨迹,从而在没有安全员的情况下稳定行驶。
具体案例:某大学校园的无人快递车在送件途中遇到一辆临时停放的面包车挡住去路,局部代价地图将该区域标为致命代价,DWA 先控制车辆绕行,跟踪偏差超阈值后触发全局重规划,从相邻道路生成新路线,最终按时送达。
餐厅送餐机器人
阶段:感知与规划协同
解决的问题:餐厅里人腿密集、桌椅频繁移动,静态地图几分钟就过时。若只依赖全局规划,机器人会被“刚刚还不存在”的椅子拦住。高频更新的局部代价地图加上 DWA 的轨迹评分,让机器人在人群缝隙里走出平滑的 S 形路线。
具体案例:某连锁餐厅的送餐机器人高峰时段在餐桌间穿行,顾客突然拉出椅子时,障碍图层立刻把对应栅格涂成占据,DWA 在下一控制周期就给出绕行轨迹,同时朝向项保证机器人绕完还能回到主路线继续送餐。
Python 演示
# -*- coding: utf-8 -*-import mathimport heapq class Costmap: """栅格代价地图: 0 自由, 254 障碍, 中间为膨胀代价""" def __init__(self, w, h, inflation=3): self.w = w self.h = h self.grid = [[0] * w for _ in range(h)] self.inflation = inflation def add_obstacle(self, x, y): self.grid[y][x] = 254 def inflate(self): # 对每个障碍栅格, 给周围栅格赋指数衰减代价 for y in range(self.h): for x in range(self.w): if self.grid[y][x] >= 254: for dy in range(-self.inflation, self.inflation + 1): for dx in range(-self.inflation, self.inflation + 1): nx, ny = x + dx, y + dy if 0 <= nx < self.w and 0 <= ny < self.h: d = math.hypot(dx, dy) if d <= self.inflation: cost = int(253 * math.exp(-d)) if self.grid[ny][nx] < cost: self.grid[ny][nx] = cost def astar(costmap, start, goal): """A* 全局规划: f(n)=g(n)+h(n)""" w, h = costmap.w, costmap.h open_heap = [(0, start)] came = {start: None} g = {start: 0} while open_heap: _, cur = heapq.heappop(open_heap) if cur == goal: path = [] while cur is not None: path.append(cur) cur = came[cur] return path[::-1] for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)): nxt = (cur[0] + dx, cur[1] + dy) if 0 <= nxt[0] < w and 0 <= nxt[1] < h and costmap.grid[nxt[1]][nxt[0]] < 100: ng = g[cur] + 1 + costmap.grid[nxt[1]][nxt[0]] / 100.0 if nxt not in g or ng < g[nxt]: g[nxt] = ng h_score = math.hypot(goal[0] - nxt[0], goal[1] - nxt[1]) heapq.heappush(open_heap, (ng + h_score, nxt)) came[nxt] = cur return [] def local_check(costmap, path, idx, lookahead=3): """局部检测: 往前看几步, 发现致命代价就要求重规划""" for k in range(1, lookahead + 1): p = path[min(idx + k, len(path) - 1)] if costmap.grid[p[1]][p[0]] >= 200: return False return True def main(): cm = Costmap(30, 20, inflation=3) for x in range(8, 12): cm.add_obstacle(x, 5) for x in range(18, 22): cm.add_obstacle(x, 14) cm.inflate() start, goal = (1, 10), (28, 10) path = astar(cm, start, goal) assert len(path) > 5, "全局规划失败" print("全局规划成功, 路径格数:", len(path)) # 运行中突然出现新障碍, 模拟真实世界的动态变化 cm.add_obstacle(14, 10) cm.inflate() idx = 0 replan_count = 0 while idx < len(path) - 1: if not local_check(cm, path, idx): replan_count += 1 print("局部检测到前方障碍, 执行重规划 %d 次" % replan_count) path = astar(cm, path[idx], goal) if not path: print("无法到达目标") return idx = 0 continue idx += 1 print("到达目标, 全程重规划次数:", replan_count) if __name__ == "__main__": main()
self.grid = [[0] * w for _ in range(h)]初始化一张全为 0(自由)的栅格图,0 代表畅通。def inflate(self):模拟膨胀层:让障碍物“长胖”,周围栅格获得由近到远的衰减代价。cost = int(253 * math.exp(-d))用指数衰减计算膨胀代价,越靠近障碍数值越高。open_heap = [(0, start)]优先队列存放待探索格子,A* 每次都先取当前总代价最小的格子扩展。ng = g[cur] + 1 + costmap.grid[nxt[1]][nxt[0]] / 100.0扩展邻居时代价 = 步数 + 膨胀代价,让路径主动躲开危险带。h_score = math.hypot(goal[0] - nxt[0], goal[1] - nxt[1])启发式用欧氏距离估计“到终点还有多远”。def local_check(...)局部规划“往前看三步”,发现前方被新障碍堵死就报告。path = astar(cm, path[idx], goal)从当前位置重新跑全局搜索,对应 Nav2 的全局重规划机制。
常见误区
误区刚接触时以为代价地图是一张类似照片的画面,障碍物画得越像实物越真实,传感器数据直接“贴”上去就行。
正解代价地图是一张抽象的二维栅格表,每个格子只存一个 0~255 的数值,代表自由、代价或致命,不含颜色、纹理和形状。它更像地图 App 上的色块标注,而不是实景照片——规划器要的是“能不能走”的数字,不是“长什么样”的画面。
为何常见:可视化工具常把代价渲染成深浅色块,看起来像图像,容易让人误以为它存储的是视觉信息。
误区机器人金贵,怕撞,那就把膨胀半径调到很大,离所有障碍物都远远的,总该安全了吧?
正解膨胀半径过大会把狭窄通道完全堵死。膨胀区域内并非全部不可通行——致命的只有内切半径以内,缓冲区只是“代价高”而非“墙”。若膨胀半径超过通道半宽,全局规划会直接判定无路可走,机器人反而哪儿都去不了。正确做法是让膨胀半径略大于机器人内切半径,再留少量余量。
为何常见:“离障碍越远越安全”的直觉忽略了通行性约束,没意识到安全余量是用可达性换来的。
误区既然全局规划能找最短路径,那行人走过来、车辆插进来,它应该也能自动避开吧?
正解全局规划在全局代价地图上搜索,前提是环境近似静态,路径不会逐帧重算。真正负责躲人、躲车、躲突发障碍的是高频局部规划器(如 DWA)。全局出大纲、局部做细节,两者分工明确——让全局规划逐帧响应动态障碍既算不过来,也反应不过来。
为何常见:把“全局”误解为“万能”,忽略了计算开销和局部感知窗口的必要性。
误区局部规划器反应快,天天在跑,那干脆让它直接一口气导航到终点,还要全局规划干什么?
正解局部代价地图只有机器人周围一小圈视野(通常 3~5 米),局部规划只能做“近视决策”。没有全局路径作参考,遇到 U 形障碍或长走廊它会陷入局部极小值——原地兜圈出不来。全局路径提供方向性引导,局部规划才知道每个弯该往哪边打。
为何常见:直观上觉得“看得近也能走远路”,忽略了局部视野有限带来的方向性迷失问题。
误区静态层、障碍层、膨胀层各画各的,最后就是把三张图叠在一起,谁都不会影响谁。
正解分层代价地图有严格的更新与合成顺序:静态层低频提供底图,障碍层高频写入传感器观测,膨胀层在障碍基础上做距离衰减,主图层按“取各层最大代价”合并。层与层之间有依赖关系——膨胀层依赖障碍层,主图依赖所有层,改一层会影响整张主图。
为何常见:看到不同图层被独立可视化,误以为它们是平行、互不相关的,忽略了 Nav2 中图层的插件化叠加机制。
面试题
Q1. 在代价地图中,栅格代价值 254 通常表示什么?
- A.完全自由的可通过区域
- B.未知的未探索区域
- C.被障碍物占据的致命区域← 正确答案
- D.膨胀后的警告区域
答案:C。代价地图约定 0 为自由、254 为占据、255 为未知。254 意味着该栅格已被障碍物占死,规划器视为不可通行;膨胀后的警告区是介于 0 和 254 之间的渐变代价,不是致命值。
Q2. 全局规划器通常在哪种地图上进行路径搜索?
- A.局部代价地图
- B.传感器原始点云
- C.全局代价地图← 正确答案
- D.视觉纹理图像
答案:C。全局规划需要整张环境地图来搜索宏观路线,用的是全局代价地图;局部代价地图只有机器人周围小窗口,供局部规划高频刷新使用;原始点云和纹理图像都不是规划器直接搜索的代价图。
Q3. 代价地图中一个栅格的三种基本状态是?
- A.红黄蓝三原色
- B.自由、占据、未知← 正确答案
- C.近中远三种距离
- D.大中小三类尺寸
答案:B。栅格三态是自由(0)、占据(254)、未知(255)。红黄蓝是可视化配色,距离和尺寸不是状态分类。未知比占据代价更高,体现机器人对未知区域的保守态度。
Q4. DWA 局部规划器通过哪种方式选择最优轨迹?
- A.沿全局路径逐点插值
- B.枚举所有历史轨迹
- C.随机游走蒙特卡洛
- D.对采样速度组合进行评分← 正确答案
答案:D。DWA 在速度空间采样一批“线速度+角速度”组合,每个组合前向模拟出轨迹,再用朝向、安全、速度三项加权评分选最优。逐点插值属于路径跟踪,不是 DWA 的选轨迹机制。
Q5. 机器人内切半径为 0.3 米,走廊宽 1.2 米,下列膨胀半径设置最合理的是?
- A.0.5 米,略大于内切半径留余量← 正确答案
- B.2.0 米,越远越安全
- C.0.1 米,越小越灵活
- D.无需设置膨胀层
答案:A。膨胀半径约等于内切半径加上少量安全余量(0.5 米左右),致命区只到 0.3 米,走廊仍可通行。2.0 米会把 1.2 米走廊完全堵死;0.1 米小于内切半径,缓冲区失去意义;不设膨胀则机器人贴墙行驶,风险过高。
Q6. 关于全局规划与局部规划的关系,下列说法正确的是?
- A.局部规划结果会覆盖全局规划输出
- B.两者完全独立无需通信
- C.局部规划以全局路径为参考进行跟线← 正确答案
- D.全局规划每帧重算处理动态障碍
答案:C。全局规划输出路径点作为参考线,局部规划沿参考线跟线并实时避障,二者接力配合。局部规划不会覆盖全局输出;完全独立则局部会迷失方向;全局规划不是每帧重算,动态障碍交给局部处理。
Q7. 局部代价地图的更新频率通常远高于全局代价地图,主要原因是?
- A.近距离障碍信息必须实时响应← 正确答案
- B.远距离信息比近处更重要
- C.高频更新可以节省算力
- D.全局规划不需要准确数据
答案:A。局部代价地图覆盖机器人周围小窗口,直接服务于高频避障决策,近距离障碍晚一秒更新就可能撞上。远距离信息由全局地图低频维护即可;高频更新消耗更多算力而非节省,全局规划同样需要准确底图。
Q8. 下列哪些属于 Nav2 中常见的代价地图图层?
- A.静态地图图层← 正确答案
- B.传感器障碍图层← 正确答案
- C.膨胀图层← 正确答案
- D.天气渲染图层
答案:ABC。Nav2 典型分层包含静态层(底图)、障碍层(传感器实时数据)和膨胀层(安全缓冲),三者合成主代价地图。天气渲染是导航无关的前端效果,不属于代价地图图层体系。
Q9. 全局路径上出现新障碍时,系统的处理方式可以包括?
- A.局部规划器临时绕行避障← 正确答案
- B.触发重规划更新全局路径← 正确答案
- C.无视障碍继续直线行驶
- D.修改代价地图并重新搜索← 正确答案
答案:ABD。新障碍先被传感器写入局部代价地图,局部规划器临时绕行;若偏离全局路径太远或路径被堵死,则触发全局重规划,基于更新后的代价地图重新搜索。无视障碍继续行驶会导致碰撞,绝不可取。
Q10. 关于代价地图膨胀层参数,下列理解正确的有?
- A.膨胀半径越大,狭窄通道越可能被堵死← 正确答案
- B.代价值随距离指数衰减← 正确答案
- C.内切半径内代价值为致命← 正确答案
- D.膨胀半径必须大于地图尺寸
答案:ABC。膨胀半径过大确实会封死窄通道;缓冲区代价值按指数衰减,越近越高;内切半径以内代表必撞区域,代价值设为致命 254。膨胀半径只与机器人尺寸和场景有关,与地图尺寸无关,D 明显错误。