适合 ROS2 新手学习,承接前面的话题、服务、参数知识点;本文包含坐标变换原理、可直接编译的 C++ 代码、API 逐行解析、可视化与录包工具实操。环境:ROS2 Humble
前言
我们前面学习了 ROS2 的三大通信方式:话题(高频数据流)、服务(一问一答指令)、参数(配置项)。光有通信代码还不够,机器人开发中还有几个刚需问题:
- 机器人有底盘、雷达、相机,每个传感器都有自己的坐标系,怎么把雷达检测到的点换算到地图坐标系?→ TF2 坐标变换
- 看不到激光、机器人位姿、坐标系,怎么调试?→ RViz2 可视化
- 想看节点连接关系、日志、实时调参?→ rqt 工具集
- 想保存激光、里程计数据,离线复现问题?→ ros2 bag
这一套工具链是 ROS2 开发的标配,几乎所有机器人项目都会用到。
一、TF2 坐标变换工具(核心重点)
1. TF2 是用来干嘛的?
TF(Transform)= 坐标变换。机器人上每一个硬件都有独立坐标系:
map:全局地图坐标系(固定不动)odom:里程计坐标系base_link:机器人底盘本体中心laser_link:激光雷达坐标系(雷达安装在底盘上方,有固定偏移)camera_link:相机坐标系
TF 的作用:维护一套坐标系之间的平移 + 旋转关系,随时查询:A 坐标系下的一个点,在 B 坐标系下坐标是多少。
TF2 分两类:
- 静态变换 Static TF:两个坐标系之间位置永远不变。比如雷达装在底盘上,安装位置固定,一次发布即可。
- 动态变换 Dynamic TF:坐标系之间持续变化。比如机器人底盘在地图里移动,
odom → base_link的变换每帧更新。
ROS2 推荐使用
tf2_ros包,底层库tf2,消息依赖geometry_msgs。
2. 示例 1:动态 TF 发布器(发布 odom → base_link)
功能:定时发布底盘相对于里程计的位姿变换(模拟里程计输出 TF)
package.xml 依赖
xml
<buildtool_depend>ament_cmake</buildtool_depend> <depend>rclcpp</depend> <depend>tf2_ros</depend> <depend>geometry_msgs</depend>CMakeLists.txt
cmake
find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(tf2_ros REQUIRED) find_package(geometry_msgs REQUIRED) add_executable(tf_broadcaster src/tf_broadcaster.cpp) ament_target_dependencies(tf_broadcaster rclcpp tf2_ros geometry_msgs) install(TARGETS tf_broadcaster DESTINATION lib/${PROJECT_NAME} )src/tf_broadcaster.cpp 完整代码
cpp
#include "rclcpp/rclcpp.hpp" #include "tf2_ros/transform_broadcaster.h" #include "geometry_msgs/msg/transform_stamped.hpp" #include "tf2/LinearMath/Quaternion.h" class TFBroadcasterNode : public rclcpp::Node { public: TFBroadcasterNode() : Node("tf_broadcaster_node") { // 创建TF广播器 tf_broadcaster_ = std::make_shared<tf2_ros::TransformBroadcaster>(this); // 定时器:100ms发布一次TF(10Hz) timer_ = this->create_wall_timer( std::chrono::milliseconds(100), std::bind(&TFBroadcasterNode::publish_tf, this) ); } private: void publish_tf() { // 构造TransformStamped消息 geometry_msgs::msg::TransformStamped transform; // 时间戳:取当前节点时间 transform.header.stamp = this->get_clock()->now(); // 父坐标系:odom transform.header.frame_id = "odom"; // 子坐标系:底盘base_link transform.child_frame_id = "base_link"; // 平移:x,y,z 模拟机器人向前缓慢移动 transform.transform.translation.x = 0.1 * count_; transform.transform.translation.y = 0.0; transform.transform.translation.z = 0.0; // 旋转:欧拉角转四元数(绕z轴旋转,yaw) tf2::Quaternion q; q.setRPY(0, 0, 0.0); // roll,pitch,yaw,这里不旋转 transform.transform.rotation.x = q.x(); transform.transform.rotation.y = q.y(); transform.transform.rotation.z = q.z(); transform.transform.rotation.w = q.w(); // 广播TF变换 tf_broadcaster_->sendTransform(transform); RCLCPP_INFO(this->get_logger(), "发布TF odom->base_link, x=%.2f", transform.transform.translation.x); count_ ++; } std::shared_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_; rclcpp::TimerBase::SharedPtr timer_; int count_ = 0; }; int main(int argc, char ** argv) { rclcpp::init(argc, argv); auto node = std::make_shared<TFBroadcasterNode>(); rclcpp::spin(node); rclcpp::shutdown(); return 0; }✅ 代码 API 逐行讲解
tf2_ros::TransformBroadcasterTF 广播器,用来向外发布坐标变换,内部会自动订阅/tf和/tf_static话题。sendTransform(transform):核心 API,把 TransformStamped 消息发出去。
geometry_msgs::msg::TransformStampedTF 消息结构体,每一条 TF 必须包含:header.stamp:时间戳(非常关键,TF 是带时间的,不同时刻变换不一样)header.frame_id:父坐标系child_frame_id:子坐标系transform:平移 + 旋转
tf2::Quaterniontf2 库的四元数工具类,setRPY(roll,pitch,yaw)把欧拉角(翻滚、俯仰、偏航)转为四元数。ROS 里旋转统一用四元数,避免万向锁问题。
create_wall_timer定时器定时回调,周期性发布 TF,动态 TF 必须持续发送,不能只发一次。std::bind:绑定定时器回调,和之前服务、话题回调原理一致。
💡 静态 TF 一般不用 C++ 写,launch 里直接调用
static_transform_publisher节点更简单:
python
# launch里静态TF示例(base_link -> laser_link,雷达在底盘上方0.2m) from launch_ros.actions import Node Node( package='tf2_ros', executable='static_transform_publisher', arguments=['0','0','0.2','0','0','0','base_link','laser_link','100'] )3. 示例 2:TF 监听器(TFListener,查询 & 坐标点变换)
功能:监听 TF 树,查询laser_link下的一个点,转换到base_link坐标系。
cpp
#include "rclcpp/rclcpp.hpp" #include "tf2_ros/transform_listener.h" #include "tf2_ros/buffer.h" #include "geometry_msgs/msg/point_stamped.hpp" #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp" class TFListenerNode : public rclcpp::Node { public: TFListenerNode() : Node("tf_listener_node") { // 1. 创建TF缓存,保存一段时间内的TF数据 tf_buffer_ = std::make_shared<tf2_ros::Buffer>(this->get_clock()); // 2. 创建TF监听器,绑定buffer,自动监听/tf话题,存入buffer tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_); timer_ = this->create_wall_timer( std::chrono::milliseconds(500), std::bind(&TFListenerNode::lookup_tf, this) ); } private: void lookup_tf() { // 构造一个laser_link坐标系下的点:雷达前方1m处 geometry_msgs::msg::PointStamped point_laser; point_laser.header.frame_id = "laser_link"; point_laser.header.stamp = this->get_clock()->now(); point_laser.point.x = 1.0; point_laser.point.y = 0.0; point_laser.point.z = 0.0; try { // 目标坐标系:base_link geometry_msgs::msg::PointStamped point_base; // transformPoint:把点从源坐标系转到目标坐标系 tf_buffer_->transform(point_laser, point_base, "base_link"); RCLCPP_INFO(this->get_logger(), "雷达坐标系(%.2f,%.2f,%.2f) -> base_link(%.2f,%.2f,%.2f)", point_laser.point.x, point_laser.point.y, point_laser.point.z, point_base.point.x, point_base.point.y, point_base.point.z); } catch (tf2::TransformException &ex) { RCLCPP_WARN(this->get_logger(), "TF查询失败:%s", ex.what()); } } std::shared_ptr<tf2_ros::Buffer> tf_buffer_; std::shared_ptr<tf2_ros::TransformListener> tf_listener_; rclcpp::TimerBase::SharedPtr timer_; }; int main(int argc, char ** argv) { rclcpp::init(argc, argv); auto node = std::make_shared<TFListenerNode>(); rclcpp::spin(node); rclcpp::shutdown(); return 0; }✅ API 详解
tf2_ros::BufferTF 缓冲区:缓存一段时间内的所有 TF 变换,支持按时间查询变换。TF 不是实时立刻可用,需要缓存。tf2_ros::TransformListener监听器,后台自动订阅/tf,收到的变换自动存入 Buffer。tf_buffer_->transform(输入点, 输出点, target_frame)最常用的坐标转换 API:把带坐标系的 PointStamped 点,变换到目标坐标系。除了 Point,还支持 Pose、PoseStamped 等。- 异常捕获
tf2::TransformException高频坑:TF 还没收到、时间戳过期、两个坐标系之间没有连通的 TF 树,都会抛出异常,必须 try-catch,否则节点崩溃。 - 补充 API:
lookupTransform(target, source, time, timeout)直接拿到两个坐标系之间的 TransformStamped 变换:cpp
auto transform = tf_buffer_->lookupTransform("base_link", "laser_link", tf2::TimePointZero);
核心一句话:Broadcaster 发布变换,Listener+Buffer 查询、使用变换。
二、RViz2 可视化工具
RViz2 是 ROS2 最核心可视化工具,本身不做任何计算,只是订阅话题、渲染数据。启动命令:
bash
rviz2常用插件(Add → 找到对应插件)
- TF:绘制坐标系箭头,直观看到所有 frame 的位置关系,调试 TF 首选
- LaserScan:订阅
/scan,显示激光雷达点云 - Map:订阅
/map,显示栅格地图 - Odometry:里程计轨迹、机器人位姿箭头
- PointCloud2:3D 点云(深度相机、激光雷达)
- PoseArray / Marker:自定义标记、路径点
使用小技巧
Fixed Frame(固定坐标系):一般填map或者odom,是 RViz 的参考基准;填错会看不到数据- 配置保存:保存
.rviz文件,launch 可以直接加载配置,不用每次手动添加插件 - 配合 TF:如果 RViz 里 TF 报错
No transform,就是 TF 树断开、frame 名字写错,优先用rqt_tf_tree排查
区分:RViz = 可视化;TF 是数据,RViz 只是展示 TF。
三、rqt 系列调试工具(rqt 是 Qt 写的 GUI 工具集)
rqt 是一堆轻量化调试工具,启动方式:
bash
rqt也可以单独启动:
- rqt_graph
bash
作用:查看节点、话题、服务之间的连接图。排查:节点有没有成功订阅话题、消息通路是否正常。rqt_graph - rqt_tf_tree(TF 树)
bash
直接图形化展示整个 TF 树,父子关系,非常适合排查 TF 丢失、frame 名字错误。rqt_tf_tree - rqt_console
bash
日志面板,过滤 INFO/WARN/ERROR,比终端看日志友好,可以筛选、保存日志。rqt_console - rqt_reconfigure
bash
动态参数调参!对应我们前面学习的 ROS2 参数,运行时拖动滑块修改参数,需要节点开启参数回调。rqt_reconfigure - rqt_plot:绘制话题数值曲线(比如速度、PID 输出)
对比:RViz 看几何数据;rqt 看连接、日志、参数、曲线。
四、ros2 bag 数据录制与回放工具
ros2 bag 用来录制话题数据,离线复现机器人场景,是定位 bug、算法测试神器。
⚠️ bag 只能录话题,不能录服务、不能录参数。
常用命令
- 录制(录制所有话题)
bash
ros2 bag record -o my_bag /scan /odom /tf-o指定包名- 后面跟上你想要录制的话题,不写话题默认全部录制
- 查看包信息
bash
ros2 bag info my_bag可以看到里面包含哪些话题、消息类型、时长、消息数量3. 回放数据(离线跑)
bash
ros2 bag play my_bag回放的时候,bag 会按照录制时间,把消息重新发出来,RViz、你的节点可以和在线一样接收数据。4. 压缩录制
bash
ros2 bag record --compression-mode file --compression-format zstd /scan典型使用场景
现场机器人跑起来,录 bag;回到电脑,不需要真实雷达、底盘,直接 play bag,在 RViz 里看激光、TF,调试算法。
五、工具组合标准开发流程(实战顺序)
- 编写 TF 发布节点 / 启动静态 TF,构建坐标系树
- 启动 RViz2,添加 TF、LaserScan,验证坐标系和传感器数据是否正常
- 打开 rqt_graph,确认节点话题连通;rqt_tf_tree 检查 TF 树
- 调试算法,使用 rqt_reconfigure 修改参数
- 效果满意后,ros2 bag 录制激光、里程计、TF 数据留底
- 离线回放 bag,复现问题,修改代码
六、总结
表格
| 工具 | 用途 | 适合场景 |
|---|---|---|
| TF2 | 坐标系之间的平移旋转转换 | 多传感器融合、导航、相机雷达标定 |
| RViz2 | 可视化激光、地图、TF、点云 | 直观观察机器人状态 |
| rqt_graph | 节点拓扑图 | 排查通信是否连通 |
| rqt_tf_tree | TF 树可视化 | 排查坐标变换错误 |
| rqt_console | 日志查看 | 看警告报错 |
| rqt_reconfigure | 动态调参 | 在线修改参数 |
| ros2 bag | 录制回放话题 | 离线调试、数据集保存 |
重点回顾:TF 的两个核心类
TransformBroadcaster(发)、Buffer+TransformListener(收 + 查询);坐标变换是导航、SLAM、多传感器融合的底层基础。