搭建路径高级几个周末

做一台能看见标记并开过去的 ROS 2 机器人

一台机器人:能在房间另一头发现一个标记,算出它相对于自己的位置,然后正对着它开过去——整体按真正的 ROS 2 节点图来组织。

做一台能看见标记并开过去的 ROS 2 机器人 技术示意图POSE → STANDOFF → DOCK

做的是什么

把机器人放在房间里的某个位置,它会一直转向,直到在墙上找到一个打印出来的标记,然后朝它开过去——在半米外停下,车身正对着标记所在的平面,而不只是靠得近。

本站其他所有项目都是在单片机上跑一个程序。这一个是一台装了轮子的计算机,这一点改变了项目的性质。难点不再是时序和接线,而是弄清楚各个部件彼此之间的相对位置,以及在几段软件同时运行时让它们互相对得上账。

为什么这台车需要计算机

Arduino 做不到这件事,原因值得说清楚。问题不在于原始算力——而在于一帧图像有四分之一兆,ArUco 检测需要浮点线性代数,而且这一切还得在车轮仍以 50 Hz 接受指令的同时完成。这类工作需要一个拥有真实内存和真正调度机制的操作系统。

作为交换,这台 Linux 机器换来的是钻进正在运行的机器人内部看一眼的能力。任意两部分之间的每一条消息都是一个话题,你可以在笔记本电脑上订阅它。当机器人从标记旁边开过去而不是开到标记上时,你不必再加打印语句、重新烧录——只要用 ros2 topic echo 把位姿打出来,实时看着它出错。

节点图的形状

五项职责,每个对应一个节点:

节点 发布内容 职责
camera /image_raw、/camera_info 从 CSI 总线上取帧
aruco_detector TF camera_link → marker_7 找到标记,解算它的位姿
docking_controller /cmd_vel 决定往哪里开
diff_drive 通过串口连 Arduino 把 Twist 转成轮速
odometry TF odom → base_link 对编码器计数做积分

正是这些接口让这个结构值得好好设计。控制器从不碰相机,也不碰电机——它消费一个坐标变换,输出一个 Twist。这意味着你可以用键盘遥操作同一台机器人,可以在完全没有硬件的情况下用一个假造的变换来测试控制器,日后还能换上一套发布同样话题的导航栈。

在动手画自己那张表之前,先读节点、话题、服务与动作。这个阶段最常见的错误,是把对接控制器做成一个服务——它是一个必须能够取消的长时间运行的目标,而这正是动作存在的意义。

物料清单

这就是那台双脑机器人,物料清单也体现了这一点:一台负责感知的计算机,加上一块微控制器负责那些计算机无法可靠完成的事情。

零件 数量 大致价格 备注
树莓派 4 4 GB 1 $55 4 GB 是实际下限,ROS 2 加视觉只能这么多。1 GB 会用上交换内存,然后卡死
树莓派相机模块 3 1 $25 广角版本更适合导航——视角 102°,而不是 66°
A2 级 microSD 卡,32 GB 1 $8 或者从 USB SSD 启动,确实更快
散热片或风扇 1 $5 树莓派 4 到 80 °C 会降频,降频的树莓派会掉帧
Arduino Uno 或 Nano 1 $5 编码器与电机控制——实时的那一半
N20 编码器电机 2 $16
TB6612FNG 1 $3
两轮底盘 1 $12 放得下树莓派、相机支架和电池
电池包 1 $15 3S,配一个额定 3 A 的开关型 5 V 稳压器
打印好的 ArUco 标记 — $0 打印之后量一下实际尺寸,并使用量出来的数值

**合计大约 $145–175。**这是全站最贵的项目,领先幅度很大,其中大部分开销就是树莓派。

**电源是这类机器人最容易出问题的地方,而一旦出问题代价不菲。**在写 SD 卡时电压跌落会损坏整个系统镜像。开关稳压器的额定电流要远高于树莓派的功耗,绝不要和电机共用同一条电源轨,并且要检查 vcgencmd get_throttled——只要返回值不是零,就说明供电不足,接下来很可能就有一张卡被写坏。

**打印出来的标记要量尺寸。**ArUco 位姿解算的结果与你声明的标记尺寸成正比。打印机用“适合页面”功能把标记缩到 95% 再打印,各个距离上的位姿都会偏差 5%,而整条流水线里不会有什么东西提醒你。

制作顺序

**1——让 ROS 2 跑起来,并证明网络可用。**按在树莓派上安装 ROS 2的步骤做。在笔记本上执行 ros2 topic echo 能看到机器人上发布的话题之前,不要往下走。Wi-Fi 上的节点发现是最可能吃掉你一个晚上的东西,最好现在就把它吃掉,而不是在调试视觉的时候再吃。

**2——让轮子听 Twist 的。**把 Arduino、编码器和 TB6612FNG接好线,并搭出 cmd_vel 桥。里程碑:用笔记本上的键盘遥操作就能开动真实机器人。轮距在这一步标定——后面所有环节都会继承这个数值。

**3——标定相机。**要正儿八经地用真实标定板标定,按照相机标定与 ArUco 位姿的流程做。未标定的相机算出的位姿看着都挺像样,但在画面边缘会偏差几十个百分点,这是你能遇到的最糟糕的失效方式。

**4——把标记作为坐标变换发布出去。**不要用一条只装着 x 和 y 的自定义消息。把 camera_link → marker_7 发布到 TF2里,意味着控制器可以问“在 base_link 坐标系中,标记在哪里?”,而答案会自动算上相机的具体安装方式——包括它确实是倾斜安装这一点。

**5——朝标记开过去。**一个基于方位角的比例控制器:转向它、向前开、在预设的停止距离处停下。这能跑通,但它到达时是斜着贴上去的,这正是下一步要解决的问题。

6——沿标记法线方向接近。不要直接朝标记转向,而是先算出一个沿标记所在平面的法线方向、停在标记面前的那个航点,开到航点,然后再直线驶入。这一步就是“开到对接点”和“真正对接好”之间的差别。在这里,标记的朝向——也就是位姿里你此前一直用不到的那部分——开始变得重要,你也会在这里遇上位姿歧义。

7——用状态机包起来。搜索、接近、对准、最后进场、完成,再加上每个状态在标记丢失时如何转移出去。面向机器人行为的有限状态机讲的是整体结构;这台机器人特别需要的,是一个标记丢失超时机制:超时就回到搜索状态,而不是继续按最后一次已知的位姿往前开。

值得注意的硬件细节

**相机要刚性安装,并且要量清楚。**机器人算出的每一个位姿都是相对于相机的,TF2 再用你声明的 base_link 到 camera_link 变换,把它换算成相对于车体的位姿。装在柔性支架上的相机会让这个声明的变换变成一个会随加速度变化的谎言。

**把相机向下倾斜,并把这个倾角声明出来。**对着地平线的相机,恰恰会在机器人靠近时丢失标记。向下倾斜十到十五度,能让标记在最后进场阶段始终留在画面里——而且这个倾角要写进静态变换,而不是变成控制器里的一个补偿系数。

**用游标卡尺量标记尺寸。**位姿的尺度与你告诉解算器的标记尺寸成正比。因为打印机缩放以适应页面而按 96% 打印,会让每个距离都偏差 4%,而且悄无声息、系统性地偏。

**树莓派要和电机分开供电。**电机堵转会把电源轨拉低,Arduino 遇到这种情况只是重启,没有实质损失,而树莓派则会损坏文件系统。用独立稳压器或者独立电池包都可以——这里不值得省一个元件。

**让相机排线远离电机线。**这是一条没有屏蔽的高速链路。耦合进去的电机噪声表现为画面损坏,再表现为毫无明显原因的检测闪烁。

做到什么程度算好

测量项 树莓派 4 上的典型值
640×480 下的相机采集 30 fps
640×480 下的 ArUco 检测 25–30 fps,占用单核 40–60%
1920×1080 下的 ArUco 检测 5–8 fps——而且每一帧都是过期的
标记检测距离(100 mm 标记,640×480) 1.5–2.5 m
1 m 处的位姿精度 位置 ±2–5 cm,朝向 ±3–5°
从图像到决策的总延迟 60–120 ms
Arduino 上的电机控制循环 100–200 Hz
树莓派经串口到 Arduino 的 ROS 2 话题延迟 5–15 ms

其中两行承载着整个项目最核心的教训。

**分辨率不是免费的,而且更高的分辨率往往更糟。**从 640×480 换到 1080p,处理量要涨 4–6 倍,换来的检测距离大约只有 1.7 倍。除非你确实需要看得更远的标记,否则更小的画面能给你更快、更新鲜的结果——而在控制循环里,新鲜度比精度更重要。

**延迟那一行正是双脑分工存在的原因。**在 0.3 m/s 的速度下,100 ms 的视觉延迟就意味着 3 cm 的位移;以 90°/s 转动时则意味着 9° 的转角。视觉测到的每一个量,描述的都是机器人刚才在哪儿。直接拿它去闭合一个快速回路,回路就会振荡。可行的结构正是这台机器人采用的结构:Arduino 用编码器跑快速回路,视觉慢慢地修正它——这与航点车上把 GPS 与里程计融合的思路完全同形,理由也完全一样。

如果机器人边走边看:卷帘快门

标准树莓派相机模块采用卷帘快门:各行依次曝光,因此画面顶部比底部早几毫秒拍到。机器人静止时,这一点看不出来。以 90°/s 转动时,ArUco 标记的四个角是在四种不同的机器人朝向下拍到的——由它们解算出的位姿,其偏差是标定无法修正的。

如果机器人必须在运动中做视觉,那就是全局快门模块的用武之地。如果它能负担得起“停下来看一眼再走”,便宜得多的模块完全够用——而且停下来再看是一个正当的设计选择,不是权宜之计。

出问题时看这张表

现象 通常原因
笔记本看不到任何话题 无线接入点拦截了组播,或者两端的 ROS_DOMAIN_ID 不一致
检测到标记,车却不动 发出去的是 Twist,订阅端却是 TwistStamped
距离一直差几个百分点 标记尺寸填错,或者打印机缩放了页面
远处位姿剧烈抖动 标记在画面中太小——靠近一些,或者印大一点
标记法线来回翻转 位姿歧义——偏轴接近,或者改用 ChArUco 标定板
车到了标记附近,却停得歪斜 是在朝标记转向,而不是沿标记法线走
最后进场时冲过头 用的是 50–100 ms 前的旧位姿来转向——最后一段要减速
标记移出画面后车还在往前开 状态机里没有标记丢失超时

开始动手之前不妨先注意这张表的分布:其中几乎没有一条是计算机视觉问题。它们是网络、坐标系、单位和控制——这才是做一台 ROS 2 机器人真正学到的东西,也是ROS 2 基础路线把时间花在通信模式和坐标系上、而不是 OpenCV 上的原因。

如果这一步对你来说跨得太大,微型迷宫鼠在完全不涉及 Linux 的单片机上讲建图与规划,手机遥控车则用少得多的零件讲清“指令通过网络到达”这一半问题。

项目路线图

搭建路径

沿着技术树,从零件一路走到一台能正常工作的机器人。每个节点在其前置条件完成后解锁,你的进度会保存在这台设备上。

已完成 0 / 19

100%
搭建

用笔记本电脑驾驶

制作

搭建

沿法线方向接近

制作

目标

可重复地对接到标记上

目标

元器件

本路径中的教程

常见问题解答

为什么要用 ROS 2,而不是写一个 Python 脚本?

如果只做这台机器人,一个脚本就能胜任,而且代码会更短。它扛不住的是第二个功能。一旦相机循环、驱动循环和决策逻辑必须以不同的频率运行,并且其中一个卡住时其他的还要继续运行,单个脚本就变成了一个你得自己解决的调度问题。ROS 2 给你进程隔离、各部分之间的通信格式,以及——你立刻就能感受到的那一点——在机器人运行时从笔记本电脑上查看任意话题的能力。这个项目值得做,是因为它在一个仍然小到可以理解的问题上,教会你这种结构。

有了树莓派,还需要 Arduino 吗?

严格来说不需要,但有了它,机器人会好得多。树莓派运行的是 Linux,这意味着你的电机控制循环和其他所有东西一起被调度,偶尔会晚上几毫秒。这对视觉来说无关紧要,对车轮 PID 来说却是致命的。把机器人拆开——树莓派负责感知和决策,单片机负责那些不能抖动的控制循环——之所以是标准架构,是有原因的,而这也正是 ROS 2 本来就提倡的划分。

为什么用 ArUco 标记,而不是检测真实物体?

因为标记只用一帧图像就能给出完整的六自由度位姿,不需要训练数据,也不会对你找到的是哪个物体产生歧义。检测一把椅子只能告诉你椅子大概在哪里;检测一个 ArUco 标记则能告诉你它以米为单位的位置和朝向,而这正是控制器真正需要的。它也是一个实事求是的起点——真实的充电桩、仓储机器人和无人机降落台,都是出于同样的原因使用基准标记。

为什么标记的朝向会来回翻转?

这是位姿歧义,它是几何本身的性质,而不是你代码里的缺陷。一个几乎正对着观察的扁平正方形,有两种三维朝向会投影到几乎相同的像素上,所以一点点噪声就会让解算器在两者之间来回翻转。距离保持稳定,而估计出的法线却在跳。可以使用实际尺寸更大的标记,从稍微偏轴的方向接近(这时两个解会分开),或者在对接站上改用 ChArUco 标定板或多个标记——这些做法都能给解算器提供更多几何信息,让它能在自己的两个解之间作出裁决。

对接能有多精确?

在相机经过标定、标记尺寸量到毫米的情况下,在半米的停止距离处达到一两厘米的精度是现实的。起主导作用的误差并不是人们预想的那些:标记尺寸填错,会让每一个距离估计都按比例缩放;而相机流水线的延迟意味着,你用来转向的位姿描述的是 50 到 100 毫秒之前标记所在的位置。在最后 30 cm 放慢速度,就能免费消除第二个问题的大部分。

可以用树莓派 Zero 来做吗?

对这个项目来说不行——至少不会顺手。Zero 2 W 能运行 ROS 2,也能解码相机视频流,但在此之上再以有用的帧率做 ArUco 检测,就什么余量都不剩了,而且在它上面构建工作空间非常痛苦。请用 4 GB 的树莓派 4 或树莓派 5。对于树莓派只负责转发指令的机器人,Zero 是块好板子。