机器人篇做到第004号,来聊一个从建图走向任务时的真问题:SLAM 把环境扫成图了,自主导航也能跑了,可你对着机器人说一句“去找红色椅子”,它为什么还是听不懂?这不是段子,是很多人在跑通 SLAM 建图和导航之后都会撞上的墙。SLAM 给你的是几何答案——哪里能走、哪里有墙、我在哪里;但“红色椅子”是人类语义,需要把视觉识别、语义理解、空间定位和任务执行串成一条完整链路。这篇文章就围绕这条链路展开,讲讲我从原理到真机实现踩过的坑,以及一个可落地的小系统该怎么搭。
先说一个关键判断:如果你继续优化 SLAM 的精度、换更好的雷达、调更顺滑的建图参数,这个问题大概率还是解决不了。因为问题不在“地图够不够精确”,而在“地图里没有物体概念”。想知道“去找红色椅子”能不能变成机器人的行动,我们得先把 SLAM 建出来的地图到底缺什么这件事聊透。
1. 先重新审视:SLAM 建出来的图,到底只是一张“几何图”
1.1 栅格地图、点云地图和“物体认知”是两码事
最常见的 2D SLAM,比如 Gmapping、Cartographer,建出来的是占据栅格地图(Occupancy Grid Map)。你可以把这张图理解成一张“格子纸”,每个格子存一个概率:0 表示确定空闲,100 表示确定有障碍,-1 表示还不知道。导航规划器拿到这张图,只知道哪里可以走、哪里不能走,至于这个障碍是一面墙、一个柜子还是一把椅子,它完全不关心。
3D 点云地图会好一些吗?从视觉表现上,点云地图确实能看出椅子的轮廓,但对机器人来说,那就是一堆带三维坐标的点,没有类别标签、没有边界框、更没有任何颜色信息。激光雷达即使带反射强度,也无法告诉你“这是红色的”;普通 RGB 相机有颜色信息,但得到的是像素矩阵,不是“椅子”这个对象。
我见过不少初学者把 Cartographer 或 Fast-LIO 跑得很漂亮,办公室里一圈云图在 Rviz 里转起来非常有成就感,然后开始给机器人下自然语言指令,等待它“理解环境”。实际上,机器人的存储系统里只有坐标和几何体,没有一个叫“红色椅子”的实体。这就好比给了你一张城市的街道图,但图上只画了马路和建筑轮廓,没有标“哪个地方有咖啡店”,然后让你去“买杯美式”——能走出门,但到了目的地也不知道该进哪个门。
1.2 “红色椅子”在传感器里到底意味着什么
要回答“机器人怎么知道红色椅子”,先得拆解人类说出这个词时包含了哪些信息。“红色椅子”至少包含三层内容:类别信息(它是一把椅子,不是桌子或纸箱)、属性信息(它的颜色是红色,可能还隐含可移动、可坐)、位置信息(在空间坐标系里有一个具体的 pose)。这三层信息,分别对应不同的感知能力。
类别信息需要目标检测或分类模型,比如基于深度学习的 YOLO、Detectron2、Grounding DINO;颜色属性需要视觉颜色分析,通常是 HSV 空间或 Lab 空间的阈值与判断;位置信息则要能从图像坐标反推世界坐标,这离不开相机标定、深度图像和 TF 坐标变换。而这些,没有一样是传统 SLAM 建图环节会帮你做的。
SLAM 的定位与建图,本质是在估计传感器自身的运动轨迹和环境的空间结构。它给出的“椅子位置”,最多是扫描到某个障碍物簇时的几何中心,而不是语义对象。这也解释了为什么“SLAM 会用,却执行不了语义指令”——因为从建图模型到对象认知之间,存在一条明显的断层。
1.3 再提升建图精度,也补不上语义鸿沟
有人觉得,是不是我的地图分辨率太低,导致椅子没被扫清楚?不是。即使你把 2D 栅格地图分辨率做到 1 厘米,椅腿之间的空隙可能都能体现出来,但地图存储的数据结构里依然没有“Chair”字段。激光扫描到的是物体表面的反射点,颜色是光学属性,类别是推理产物。
所以,正确的思路不是改造 SLAM 前端去识别物体,而是在建图之上叠加一个“语义层”。几何地图负责提供空间骨架和可通行性,语义层负责把检测到的目标对象、属性和位姿记录下来。两者可以分开维护,也可以组合使用。我后来选择的工程架构,就是“底图跑 Nav2,语义层跑对象数据库,中间通过坐标变换和状态机衔接”。
这个分层思路是理解整篇文章的基础。后面要说的所有方法,几乎都是为了在不破坏 SLAM 稳定性的前提下,给机器人补齐“理解红色椅子”的能力。
2. 给机器人认识“红色椅子”,我试过的三种路线
2.1 路线 A:离线人工标定,先录一把“目标入库”
最朴素的方法,是我先带机器人找到那椅子,人工告诉它“这个位置叫 red_chair_001”。具体操作就是,把机器人开到椅子附近,通过遥控器微调位姿,然后记录当前机器人在 map 坐标系下的位置,再输入一个标签,保存到数据库里。
这个方案简单到什么程度?它甚至不需要深度学习,也不需要相机,只要机器人本身定位准确,能在之后导航到记录点就行。仓库、工厂和很多固定场景的 AGV 到现在还是采用类似逻辑,把“托盘”“充电桩”“工位”作为预置坐标写进系统,本质上就是人工做了一次语义标注。
优点是非常稳定、可预期;缺点也非常明显:如果椅子被移动了,机器人不知道;如果现场出现一把新的红色椅子,它也不会自动认识;如果用户换个说法“去找那把会客椅”,你依然要重新标定。所以这条路适合“点位少、环境固定”的场景,不适合做通用语义理解。但它给我提供了一个很清晰的启示:所有复杂系统,都至少要有一个“对象登记表”,否则后续识别结果没地方放。
2.2 路线 B:在线目标检测,把“当前看到的椅子”流式写入地图
这是我在家用机器人项目上真正采用的做法。机器人运行时,RGB-D 相机的每一帧图像都会经过目标检测模型,识别出常见物体类别,比如椅子、桌子、沙发、人。检测到椅子后,再对检测框内的像素做颜色分析,判断是不是红色。
如果检测到“红色椅子”,就把检测框对应的深度像素值读出来,结合相机内参反投影成相机坐标系下的三维点,再通过相机到机器人 base 再到 map 坐标系的变换,得到这个物体在地图坐标系下的位置。这条信息会写进对象的临时数据库,时间戳、置信度、位置一起存下来。
在线检测最大的好处,是机器人可以在自主探索或日常巡航过程中增量式地构建“语义物体列表”。哪怕没有专门去看椅子,只要路过时扫到了,它就会自己记录下来。之后“去找红色椅子”,直接查对象数据库就能选出一个坐标,不用重新扫描整个房间。
当然,在线检测也有不少坑。检测模型的推理速度、目标框抖动、深度图中的黑洞、相机外参误差,都会直接影响语义坐标的准确性。后面我专门用几个小节讲这些坑。
2.3 路线 C:开放词汇模型,让机器人认识“没见过的椅子”
传统检测模型的问题是只能识别训练集里的类别。如果我的数据集里只有 “chair”,用户说“去找红色椅子”,模型能检测出椅子,再靠颜色过滤解决;但用户如果说“去找那个电竞椅”,传统模型大概率无能为力,除非你给数据集新增 “gaming_chair” 类别并重新训练。
开放词汇检测模型则把这件事变成了“文本和图像的匹配问题”。你可以传入任意文本短语,比如 “red chair”“gaming chair”“office chair with armrests”,模型在当前图像中找对应区域。Detic、Grounding DINO、CLIP 这类模型的思路各不相同,但都能让机器人对“新词”有一定泛化能力。在服务机器人的原型阶段,这是很诱人的方案。
代价也很明显:模型体积大、对推理硬件要求高、延迟不稳定。我在 Jetson Orin Nano 上跑轻量级检测模型还能实时,跑开放词汇模型就明显吃力。如果你也在做类似项目,建议不要一上来就追求“什么都会认”的大模型,先在固定类别加颜色属性这条路上跑通。跑通之后,你把检测模块替换成开放词汇模型的难度并不高,接口是相同的。
为了帮助你快速决策,我把三条路线放在一起对比:
| 路线 | 识别新类别难度 | 地图维护成本 | 硬件要求 | 适用环境 |
|---|---|---|---|---|
| A 离线人工标定 | 重新标定 | 低,固定点位 | 极低 | 固定工位、仓储场景 |
| B 在线固定类别检测 | 需扩展训练数据 | 中,需处理动态目标 | 中,普通 GPU/嵌入式即可 | 家庭、办公室等常见物体 |
| C 开放词汇模型 | 低,文本即类别 | 高 | 高,需要独立推理设备 | 科研原型、高动态变化场景 |
3. 一句“去找红色椅子”,在系统里经历了怎样的拆解
3.1 第一步:语言拆解,知道“想让我干什么”
自然语言指令“去找红色椅子”,表面看是一句话,但系统内部要先把它转成一个结构化表达。我的做法是先做意图识别和槽位抽取。
意图是“去某地找某物”,槽位包括:目标类别 = 椅子,颜色属性 = 红色,动作方式 = 移动到目标附近。如果用户说“去厨房找红色椅子”,位置槽就多了一个“厨房”区域;如果用户说“把这把红椅子推过来”,动作就从“导航至附近”变成了“导航 + 操作”。
在原型阶段,不一定要上大语言模型。简单关键词模板匹配就能应付“去找红色椅子”这类句式:先匹配动词“找”或“去”,再匹配类别词,再过滤颜色词。问题是这种匹配在真实对话中很脆弱,“帮我看看客厅有没有红色椅子”“刚才那把椅子在哪”都不好处理。
我现在的做法比较务实:基础语义由轻量级 NLU 规则承担,意图不清晰时交给大语言模型做一次上下文补全,例如把“那把椅子”解析为“之前提到过的红色椅子”。对于固定命令式任务,规则系统完全足够;如果做聊天机器人或复杂人机交互,再引入 LLM 也不迟。
3.2 第二步:目标绑定,把“红色椅子”对应到地图里的一个对象
语言层给出“类别=椅子,颜色=红色”之后,下一步是查询语义对象数据库。这个数据库里可能已经有三条记录:客厅红色单人椅、书房的黑色转椅、阳台蓝色塑料椅。查询逻辑很直接:筛选类别为椅子、颜色包含红色的记录。
如果只有一条记录,系统就直接选中;如果有两条以上红色椅子,就要用到“指代消解”和场景优先级。机器人当前在客厅,客厅这条红椅置信度又高,就优先选它;如果机器人站在门口,两条红椅都在不同方向,那就得触发澄清,用语音合成反问用户:“你要找客厅那把,还是卧室那把?”
这一步经常被忽略。很多人以为,自然语言处理完了就直接导航,实际上“找哪个目标”本身就是一个不确定性问题。我在最初实现时遇到过最尴尬的情况:地图里早就记录了办公桌左侧有一把红色折叠椅,但我后来把它挪到了阳台,机器人执行旧指令时会固执地跑到办公桌旁边对着空气说“找到了”。所以语义对象不能只有名字和坐标,还要有时间戳和“最近观测状态”。
3.3 第三步:行为编排,导航只是任务里的一个动作
“去找红色椅子”这个任务,如果按传统导航框架理解,就是给 move_base 一个目标点。但实际完整行为至少包括:查询候选目标、确认是否能直接到达、发布导航目标、跟踪导航状态、到达后二次确认、返回任务结果。
如果机器人在行进过程中迷路或被障碍挡住,那么系统应该在超时后重新规划,或者调整目标点;如果到了目标点附近却没有在相机视野里发现红色椅子,系统不应直接任务成功,而是应该原地旋转一圈,尝试再次检测。若始终找不到,再向用户报告“目标可能不在预设位置”。
我在代码实现里把这套逻辑写成了状态机。状态包括 IDLE、QUERYING、PLANNING、NAVIGATING、ARRIVED、CONFIRMING、REPORTING、ABORTED。每个状态都有进入条件和退出条件。别看这个状态机简单,它把自然语言到最终行动的每个节点都变成了可测试、可追踪的动作。这也让我意识到,“机器人知道你说什么”从来不只是算法问题,还是任务编排问题。
4. 一个可落地的例子:在 ROS2 机器人上实现“去找红色椅子”
4.1 我用到的硬件和软件栈
先交代一下我跑通的这套配置,方便你做参考。机器人底盘是差速轮式小车,搭载了一块 Jetson Orin Nano 作为主控。建图用的传感器是 2D 激光雷达,型号是 RPLIDAR A2,负责运行 SLAM 构建 2D 栅格地图。识别红色椅子用的是 Intel RealSense D435i,它是 RGB-D 相机,能同时输出彩色图和深度图。ROS2 环境是 Humble,导航栈用 Nav2。
为什么没有直接上一套 3D 激光雷达做视觉语义?原因很简单,2D SLAM + Nav2 在家庭场景已经足够稳定,门槛低、材料多、调试成本小。视觉语义层完全由 RGB-D 相机补足,不一定要和高精点云建图绑在一起。如果你已经有 Fast-LIO 或 LIO-SAM 建的点云地图,可以把导航代价层换成 3D 的,但针对“语义目标定位”的原理是一样的。
4.2 视觉结果变成地图坐标的三个关键步骤
要发布一个 Nav2 能接受的目标位姿,我必须完成从“图像像素”到“map 坐标”的转换。整个过程分成三步。
第一步,在彩色图中检测椅子并判断颜色。检测模型输出目标框,矩形框内像素做 HSV 颜色统计,如果红色占比超过阈值,就认为这个检测结果是“红色椅子”。这一步的输出是二维图像坐标,比如目标框中心点 (u, v)。
第二步,从深度图读取目标中心位置的深度值。RGB-D 相机如果做了对齐,彩色图和深度图的像素是一一对应的。深度值为 Z,单位是米。结合相机内参,可以用针孔模型反投影出相机坐标系下的三维坐标: X = (u - cx) / fx * Z Y = (v - cy) / fy * Z Z = Z 这里的 cx、cy、fx、fy 是相机内参,通常在相机标定或厂家出厂参数里能拿到。
第三步,把相机坐标系下的点变换到机器人 base 坐标系,再变换到 map 坐标系。这一步依赖 TF 树,我写了类似 transformPoint 的调用,输入相机坐标系中的一个点,查询 camera -> base_link -> map 的变换关系,输出地图坐标点。重点提醒:查询时一定要判断 transform 是否可用,不要盲目缓存旧变换,尤其在机器人刚启动、TF 树还没稳定的时候,这一步特别容易出错。
4.3 观察点要比目标点本身更重要
工程上还有一个经验:“去找红色椅子”的目标点,最好选择“能清楚看到椅子的观察位置”,而不是直接指向椅子的几何中心。为什么?对于导航机器人来说,最终目的是“找到并确认红色椅子”,机器人需要站在一个合适的距离和角度,让相机能看到它。
我处理的方法是对目标物体坐标做一次“可视偏移”:沿着物体朝向的反方向推出 1.2 米,作为导航目标点。比如椅子面向客厅中央,机器人就从椅子前方 1.2 米位置看过去。如果椅子紧贴墙角,观察点就位于它的斜前方。用这个观察点导航,到达后相机基本能正对目标,二次检测的成功率高很多。
如果没有这个观察点设计,单纯导航到椅子坐标上,机器人很可能会贴着椅子,相机里只有红色椅背的局部纹理,检测框极不稳定,二次确认经常失败。后来我把观察点计算也放进对象数据库,每次更新坐标时顺手存一个 suggested_view_pose,问题就解决了。
4.4 目标数据持久化,不能让机器人每次“失忆”
语义识别出的坐标如果只放在内存变量里,机器人重启之后就全忘了。我在 map 目录下放了一个 JSON 数据库,每条对象记录至少包含这些字段:object_id、class_name、color、x、y、yaw、confidence、last_seen、view_x、view_y、view_yaw。启动时加载,运行时更新,退出时保存。
值得强调的是,对象数据库和 SLAM 的栅格地图是分开保存的。栅格地图负责静态环境的通行性,对象数据库负责动态物体的语义位置。这样即便你优化了 SLAM 地图、重写了代价地图参数,对象数据也可以保留。反过来,如果目标物体移动了,你只需要更新数据库中的坐标,不需要重新建图。
5. 真机调试最容易翻车的 5 个地方
5.1 颜色识别在光照变化下非常脆弱
我第一次真机测试时选了下午四点的客厅,阳光从窗户斜射进来,红色椅子的识别效果还行。到晚上开暖黄色吸顶灯后,同一把红椅子在相机里变成了偏暗的橙红色,HSV 的红色阈值直接失效。后来我把颜色判断从 RGB 改成了 HSV 空间,并且把 Hue 范围放宽到 0~10 和 170~180 两个区间,Sat 和 Value 做了自适应下限,才算稳定一些。
如果你觉得“红色”很好识别,说明你还没在复杂光照下试过。经验是,不要只依赖单个像素的颜色,要看目标框内的颜色直方图,甚至结合检测模型输出的类别置信度做加权。户外场景更麻烦,自动白平衡会让同一种红色在不同时段产生完全不同的 RGB 值。加上相机白平衡锁定的思路可能比调阈值更实用。
5.2 多把红色椅子在场时的选择策略
客厅和餐厅各放一把红色椅子,机器人站在过道中间,查询结果返回两个候选。如果系统默默选择第一条记录,用户看到机器人走向了客厅,可能根本不是他想的那把。这里需要“澄清”或“主动猜测”。
我的策略是:如果候选有多个,先看各候选是否在最近的语义地图中可见。可见且距离近的候选优先;如果两个都在视野中,我会让机器人播放语音“附近有两把红色椅子,你要找哪一把?”然后等待用户通过触摸屏选择或语音回复“左边那把”。语音交互一旦介入,“左右”又要和机器人当前朝向绑定,这又是一个新的语义对齐问题。所以更实用的做法,是给每个语义目标记录一个语义别名:“客厅红色单人椅”“阳台红色折叠椅”,在澄清时直接列出,用户很容易确认。
5.3 相机外参没标定,投影点会偏得离谱
视觉检测结果转地图坐标,误差来源里最容易被忽视的是相机到机器人底盘的外参。机器人底盘里,相机的安装位置通常和激光雷达中心不重合,如果使用默认值或者粗略量测,你会发现检测出的物体坐标和实际位置差出二三十厘米很正常。
我最初直接用尺子量相机安装位置填入 URDF,误差在 5 厘米内,导航到椅子附近没问题,但当椅子靠近墙壁时,偏差可能导致机器人尝试“穿墙”到墙的另一侧。后来我用标定板做了一次相机到 base_link 的外参标定,把旋转和平移都精确到毫米级,物体坐标的误差才降到可接受范围。你如果不想做太复杂的标定,至少要在运行时通过 TF 工具确认相机坐标系和 base_link 的变换不是凭空拍脑袋写的。
5.4 不要把检测到的物体直接画进代价地图
很多人做完目标检测后,会顺手把物体中心点标成代价地图里的障碍物,因为这样机器人会绕开它。这个做法在静态物体上还行,但椅子是会移动的。一旦你今天把它当作永久障碍写进 costmap,明天椅子挪走了,地图里却还留着“鬼影”,导致机器人规划路径时莫名其妙绕一个圈子。
我建议不要一上来就修改全局成本图。临时检测到的物体可以作为“动态物体列表”单独管理,只在当前会话中有效。如果想让机器人避障,可以在 local costmap 中传递一个瞬时障碍层;如果目标是最终要去的位置,那更不能把它设置成永久障碍,否则导航规划会认为目标不可达。
5.5 目标位置应该在导航结束后被再次确认和更新
机器人到达观察点后,应该再做一次完整的“确认循环”:检测红色椅子、投影到地图坐标、和目标数据库中的历史坐标做比较。如果两次检测结果的距离误差大于阈值,就以后者为准更新对象位置,同时更新 last_seen 时间戳。
这个机制在椅子被挪动过的情况下特别重要。旧坐标不仅会导致导航去错地方,还会让用户觉得机器人“很蠢”。加上二次确认后,机器人至少有机会发现位置变化,然后进行修正。当然,如果二次确认也找不到目标,我就会触发重新搜索流程:在原地旋转 360 度扫描常见物体并更新数据库,仍然是找不到,就上报用户处理。
6. 从“跑通Demo”到“像点样子”:语义层设计里的几条体会
6.1 把地图和对象数据库分开,是最重要的架构决策
我在前面的实现中反复提到“对象数据库”,这确实是总结下来最值得坚持的做法。几何地图是低动态层,代表环境的固定结构;语义对象库是高动态层,记录物体标签、时间戳和置信度。二者通过坐标系统一,但不直接耦合。这样做的好处是:重定位、重新建图和语义对象更新互不拖累,系统每个部分都能独立调试。
如果你去看很多科研中的“语义 SLAM”论文,会发现它们尝试把目标识别和地图估计融合到一个概率模型里,理论上很优雅,实践时却需要大量调参。对于普通机器人项目,我更推荐先跑通“分离式语义地图”,把对象数据库和 SLAM 图松耦合,等系统稳定后再考虑深度融合。
6.2 语义对象要带“生命周期”,不能一直有效
红色椅子今天在客厅,明天可能在卧室,后天可能被搬到阳台。如果对象数据库里每一条记录都是永久的,时间一长,机器人查询时会被大量过期信息干扰。
我给对象记录增加了置信度和时间戳,并设置一个有效周期。如果一件低频移动的物体超过三天没有被重新观测到,它的查询优先级就会降低;如果在线识别到同一物体出现在新位置,就更新而不是新增。对于临时出现的目标,比如用户带来的折叠椅,则标记为 ephemeral,任务结束后自动清理。这套机制虽然简单,却让机器人对不同物体的“记忆稳定性”有了区分。
6.3 想继续做深,下一个重点不是更多模型,而是目标重识别
很多人觉得让机器人认识更多物体是下一步重点,我反而觉得更有价值的是“目标重识别和长期更新”。机器人今天看到的那把红色椅子,明天还能认出它是同一把吗?如果两把完全相同的红色椅子出现过,怎么保证不会把它们的身份搞混?
这些小问题会直接影响用户体验。“去找红色椅子”不只是找到任意一把红椅,而是要找到用户心里想的那一把。解决目标重识别,需要视觉特征嵌入、对象跟踪和时间一致性约束,模型和工程复杂度都会上升。如果你也想做系列机器人项目,我建议在一次导航任务跑得很顺之后,优先花时间打磨这一步,它会让你对“机器人到底知不知道自己在找什么”这件事有完全不同的理解。