想象一下,你和朋友在不同的城市,各自拿着一张没有经纬度、只有相对位置的地图,你们如何描述“我在你东边500米”?这就是多机器人坐标系对齐要解决的核心问题:让每个机器人用同一把“尺子”和“方向”来描述自己的位置。
生活类比:就像在大型商场里,每个店铺都有自己的门牌号(局部坐标系),但所有店铺都统一参照商场入口和楼层导览图(全局坐标系)来定位。多机器人坐标系对齐,就是为所有机器人建立这样一个共用的“商场导览图”。
技术定义:多机器人坐标系对齐,是指通过某种方法,将多个机器人各自的局部坐标系(如robot1/base_link、robot2/base_link)统一到一个公共的全局参考系(如world)下的过程,使得不同机器人感知到的位置和姿态信息可以相互理解和换算。
单台机器人的坐标变换链是线性的,从传感器到本体再到世界。但在多机系统中,这个链条变成了一个网络,每个机器人都是网络中的一个节点。对齐的本质,就是确定网络中任意两个节点之间相对位姿的变换关系。
上图清晰地展示了核心矛盾:每个机器人可能都定义了自己的世界坐标系(/world_A, /world_B),但它们之间没有已知的变换关系。对齐就是要解决这个“?”。这带来了三个主要挑战:
根据对齐的时机和依赖的信息,主要有三种方法。选择哪种,取决于你的硬件条件和任务需求。
| 方法 | 核心思想 | 所需条件 | 优点 | 缺点 | 典型场景 |
|---|---|---|---|---|---|
| 共享全局地标 | 所有机器人观测同一组预先定义或已知的全局特征点(如二维码、UWB基站)。 | 环境中部署可观测的全局标志物。 | 原理简单,对齐精度高,初始化快。 | 依赖外部基础设施,环境部署成本高。 | 仓库分拣、固定工站协作。 |
| 相对观测对齐 | 机器人之间互相观测(通过视觉、激光、UWB),直接估计彼此相对位姿。 | 机器人具备相互探测的传感器。 | 无需外部设施,自主性强。 | 依赖视距,初始未“见面”时无法对齐。 | 机器人编队、探索集群。 |
| 地图融合对齐 | 各机器人先独立建图,然后通过匹配地图的公共部分(如重叠区域)来求解相对变换。 | 机器人具备SLAM能力,环境有可匹配的特征。 | 完全自主,无需预先部署或直接观测。 | 算法复杂,计算量大,需要足够的重叠区域。 | 多机协同探索未知环境。 |
在实际项目中,我参与过一个AGV集群项目,初期尝试了相对观测对齐,但AGV货架太高经常遮挡视线,导致对齐不稳定。后来我们改为在仓库立柱上贴二维码(共享全局地标),虽然部署麻烦了点,但系统鲁棒性大幅提升。大部分室内多机系统,如果环境允许,我推荐采用“共享全局地标”作为基础方案。
/world)应该作为所有机器人坐标系的共同“根”。如果每个机器人都发布自己到/world的变换,但它们的/world含义不同,就会导致混乱。我们以最典型的“共享全局地标”方法为例,拆解其实现步骤。假设我们在环境中固定位置放置了多个已知全局坐标的二维码(ArUco标记)。
这个过程的关键在于,每个二维码的全局坐标(在/world系下)是预先测量并存储在系统中的。当机器人摄像头看到二维码时,它能计算出“二维码相对于摄像头”的位姿T_camera_marker。由于T_world_marker已知,我们就可以解算出机器人在世界坐标系中的位姿:T_world_camera = T_world_marker * (T_camera_marker)^-1。
关键点:至少需要观测到一个地标。理论上,观测到一个已知尺寸和全局位姿的地标,就可以解算机器人6自由度的位姿(位置和姿态)。为了提高精度和鲁棒性,通常会观测多个地标,并使用最小二乘法等优化方法来求解。
下面是一个简化的代码片段,演示了在ROS2中,一个机器人节点如何根据观测到的二维码计算并发布自身到世界坐标系的变换。这里假设已经有了从图像中识别并解算出的T_camera_marker(类型为geometry_msgs.msg.TransformStamped)。
// 假设已知:
// 1. marker_id: 观测到的二维码ID
// 2. T_camera_marker: 二维码在相机坐标系下的变换
// 3. world_marker_pose_map: 一个字典,存储每个二维码ID在世界坐标系下的位姿 (geometry_msgs.msg.Pose)
#include <geometry_msgs/msg/transform_stamped.hpp>
#include <tf2_geometry_msgs/tf2_geometry_msgs.hpp>
#include <tf2/LinearMath/Transform.h>
void publishWorldTransform(int marker_id,
const geometry_msgs::msg::TransformStamped& T_camera_marker) {
// 1. 获取该二维码在世界系下的位姿
auto it = world_marker_pose_map.find(marker_id);
if (it == world_marker_pose_map.end()) {
// 错误处理:未知的二维码ID
return;
}
geometry_msgs::msg::Pose world_marker_pose = it->second;
// 2. 将世界系下的二维码位姿转换为tf2::Transform格式
tf2::Transform T_world_marker;
tf2::fromMsg(world_marker_pose, T_world_marker);
// 3. 将相机系下的变换消息转换为tf2::Transform格式
tf2::Transform T_camera_marker_tf;
tf2::fromMsg(T_camera_marker.transform, T_camera_marker_tf);
// 4. 核心计算:T_world_camera = T_world_marker * inverse(T_camera_marker)
tf2::Transform T_world_camera = T_world_marker * T_camera_marker_tf.inverse();
// 5. 假设已知相机到机器人底盘的固定变换 T_base_camera (例如从URDF加载)
tf2::Transform T_base_camera = ...; // 从URDF或标定获得
// 6. 计算机器人底盘在世界系下的位姿
tf2::Transform T_world_base = T_world_camera * T_base_camera;
// 7. 创建并发布TF消息
geometry_msgs::msg::TransformStamped T_world_base_msg;
T_world_base_msg.header.stamp = this->now();
T_world_base_msg.header.frame_id = "world"; // 父坐标系
T_world_base_msg.child_frame_id = "robot1/base_link"; // 子坐标系
T_world_base_msg.transform = tf2::toMsg(T_world_base);
tf_broadcaster_->sendTransform(T_world_base_msg);
}
这段代码的核心是第4步的矩阵运算。它体现了坐标系对齐的基本数学原理:通过一个已知的“桥梁”(二维码),将局部观测与全局知识连接起来。
编者提示:在实际部署中,直接使用单次观测结果发布变换可能会因为检测抖动导致位姿跳变。一个更稳健的做法是使用一个滤波器(如卡尔曼滤波器或简单的低通滤波器),对多次观测计算出的位姿进行滤波,再用滤波后的结果发布TF。这能有效平滑运动,提升其他模块(如路径规划)的稳定性。
当多个机器人都完成对齐后,整个系统的TF树应该如何组织?一个好的结构能简化数据的使用。我推荐采用“星型”拓扑。
/world坐标系作为根。/base_link都直接作为/world的子节点。/base_link为根。/world,计算路径唯一且简单。/world -> /robot1/base_link -> /robot2/base_link。在ROS2的TF2系统中,无论采用哪种拓扑,你都可以使用tf2_ros::Buffer::lookupTransform函数,直接查询任意两个坐标系之间的变换,库会自动计算最短路径。但星型拓扑在概念理解和调试上更具优势。
你可以在仿真环境中体验多机对齐。使用Gazebo或类似仿真器,加载两个差分驱动机器人模型。在仿真世界中的固定位置放置一个视觉标记(如一个红色方块)。
/base_link在/world系下的位姿,并发布相应的TF变换。rviz2观察两个机器人的/base_link坐标系是否都正确地出现在/world坐标系下,并且它们的相对位置与仿真世界中的实际情况一致。/base_link坐标系的子节点。/world为根的星型拓扑,结构清晰且容错性更好。