rospy订阅/odom并用matplotlib绘制机器人轨迹的完整指南

发布时间:2026/10/15 20:29:05
rospy订阅/odom并用matplotlib绘制机器人轨迹的完整指南 做机器人导航调试的时候看终端里一行行滚动的坐标数值远不如在屏幕上看到一条清晰的运动轨迹来得直观。我自己第一次在轮式机器人底盘上做里程标定时就是在反复对比/odom的数据和实际行走路线硬是靠着打印出来的 x、y 坐标一点一点抠参数效率低到让人崩溃。后来把 rospy 订阅的里程计数据接到 matplotlib 里直接画出机器人在二维平面上的轨迹图整个调试过程像是开了加速器很多问题一眼就能看出来。这篇文章就把这套思路完整拆开讲。内容围绕“rospy 订阅/odom话题再用 matplotlib 绘制轨迹图”这个核心任务展开覆盖消息结构解析、订阅回调实现、动态与静态绘图、常见坑位排查以及后续扩展方向。适合正在做机器人底盘测试、导航算法调参、里程计标定或者课程实验的同学参考不管你是刚接触 ROS 的新手还是已经写过几个节点的老手都能从中找到可以直接抄作业的代码和思路。1. 整体思路与方案选型1.1 为什么直接订阅 /odom 而不是自己维护里程计很多刚开始接触机器人定位的同学会有一个疑惑明明底盘驱动里已经计算了里程/odom话题里的数据就是现成的为什么还要有人自己写一个节点去维护里程这里得说清楚ROS 的/odom话题是由robot_state_publisher或者底盘驱动节点发布的默认使用nav_msgs/Odometry消息类型里面包含机器人的位置、姿态、线速度、角速度以及协方差矩阵。你在自己的节点里订阅它本质上是做一个“数据消费者”不需要关心底层传感器融合或者轮式编码器的具体算法这大大降低了轨迹绘制的门槛。直接订阅/odom还有一个好处数据是经过 ROS 时间戳同步的并且各个传感器模块发布的里程信息已经在驱动层做过初步滤波。你拿到手的位置信息相对干净尤其在室内机器人平台上短时间内的轨迹误差不会太夸张。如果自己从底层编码器读数推断位移还得考虑轮径误差、打滑、安装偏移等一系列标定问题这在最初学画轨迹图阶段完全没必要。当然如果机器人平台本身没有发布/odom那你确实需要自己从轮式编码器或者惯性测量单元去推算位置再发布一个/odom话题。这种情况下画的轨迹就不仅仅是可视化了它同时还承担了验证里程计算法是否正确的任务。我在一个自制底盘上就干过类似的事先写驱动节点计算位移再订阅回来画轨迹比对实际行走距离算法里的轮径系数就是这样一点一点修正的。1.2 技术栈与替代方案对比实现“订阅/odom 画轨迹”这个需求技术选型非常明确用rospy写订阅节点用matplotlib做绘图。这两个库一个负责拿到机器人的位姿信息一个负责把位姿信息变成可视化图形配合起来非常自然。Python 生态里做数据可视化matplotlib目前依然是最靠谱的选择API 简单、文档丰富、实时绘制也有成熟的接口支持。相比 C 环境下用rviz可视化rospy matplotlib更加轻量不需要启动整套可视化工具适合做离线分析和快速脚本验证。不过我也见过有人用pyqtgraph或者plotly来做实时绘图它们在某些场景下确实性能更好。pyqtgraph的 OpenGL 加速渲染在数据点数上万时依然流畅而matplotlib在数据点特别多的时候会逐渐卡顿。但对于绝大多数测试场景比如在一块几平方米的室内场地里跑一两分钟matplotlib的实时刷新完全没有压力。还有一个容易忽略的方案是直接用rosbag记录下来后面离线回放时再画图。这里有个关键区别在线绘图能边跑边看适合现场调试离线复盘则数据完整可以反复调整图形样式。我的建议是两条腿走路调试阶段在线看趋势出问题时再用 rosbag 录一段离线做精细分析。2. /odom 话题的数据结构解析2.1 Odometry 消息的层级结构动手写代码之前必须先搞清楚nav_msgs/Odometry里面装的到底是什么。很多人第一次看到这个消息类型会被它的嵌套结构绕晕其实拆开来看非常有规律。消息体主要包含header、child_frame_id、pose和twist四个部分其中画轨迹图真正用到的只有header和pose。引用pose.pose.position里的x和y就是机器人在地图坐标系下的平面坐标单位是米。pose.pose.orientation是一个四元数x、y、z、w它表示机器人的姿态朝向。如果只在二维平面里画轨迹x和y已经足够如果要带箭头表示朝向就需要把四元数转换成偏航角这部分后面我会展开讲。from nav_msgs.msg import Odometry def odom_callback(msg): x msg.pose.pose.position.x y msg.pose.pose.position.y # msg.pose.pose.orientation 是四元数 qx msg.pose.pose.orientation.x qy msg.pose.pose.orientation.y qz msg.pose.pose.orientation.z qw msg.pose.pose.orientation.w很多新手在写回调函数时会把x和y直接打印在终端里然后人工记录。这个方法在只有一个点的时候可行但机器一旦运动起来数据哗哗地刷根本记不过来。正确的做法是把数据追加到一个列表或者numpy数组里后续再整体绘图。2.2 从四元数换算偏航角四元数本身是一个四维向量无法直接在二维坐标图上表示。如果想让轨迹带上机器人的朝向信息就需要把它转换成欧拉角。ROS 的tf.transformations模块提供了封装好的转换函数直接用euler_from_quaternion就能拿到翻滚角、俯仰角和偏航角。对于地面机器人通常只关心偏航角也就是绕 Z 轴旋转的角度范围是 [-π, π]。from tf.transformations import euler_from_quaternion def odom_callback(msg): q ( msg.pose.pose.orientation.x, msg.pose.pose.orientation.y, msg.pose.pose.orientation.z, msg.pose.pose.orientation.w ) roll, pitch, yaw euler_from_quaternion(q)我建议把x、y、yaw打包存放这样画轨迹时可以随时决定画点、画线还是画箭头。有些机器人平台发的四元数不规范可能不在单位长度上euler_from_quaternion内部会做归一化处理但为了保险也可以手动做一次单位化。2.3 坐标话题的取舍为什么不用 /tf除了/odom经常出现在大家视野里的还有/tf话题里面也有机器人的位置和姿态信息。那画轨迹为什么不直接监听/tf这个问题我一开始也踩过坑。监听/tf本身没问题但它提供的是坐标变换树需要你去查odom - base_link或者map - odom的变换关系再从中取出平移量和旋转量代码比订阅/odom复杂一些。更关键的是/tf的发布频率和结构在不同平台上差异很大有些机器人只在特定时刻发布变换你如果监听时机不对拿到的数据可能断断续续。/odom的发布频率稳定数据就是直接可用的位姿拿来画轨迹图足够。除非你确实需要验证坐标变换关系本身否则没必要绕道/tf。3. 用 rospy 订阅数据并缓存轨迹3.1 订阅回调函数怎么写写订阅节点的核心就是初始化节点、创建订阅者、在回调函数里处理数据这三步。我的习惯是先写一个轨迹缓存类把坐标数据放到类成员变量里避免每次回调都去操作全局变量结构更清晰。#!/usr/bin/env python3 import rospy from nav_msgs.msg import Odometry from tf.transformations import euler_from_quaternion class TrajectoryLogger: def __init__(self): self.xs [] self.ys [] self.yaws [] self.lock rospy.RLock() def odom_callback(self, msg): x msg.pose.pose.position.x y msg.pose.pose.position.y q ( msg.pose.pose.orientation.x, msg.pose.pose.orientation.y, msg.pose.pose.orientation.z, msg.pose.pose.orientation.w ) _, _, yaw euler_from_quaternion(q) with self.lock: self.xs.append(x) self.ys.append(y) self.yaws.append(yaw) def main(): rospy.init_node(trajectory_logger) logger TrajectoryLogger() rospy.Subscriber(/odom, Odometry, logger.odom_callback) rospy.spin()这段代码是整套方案的地基。注意我用了RLock可重入锁在回调里加锁是为了避免主线程读取列表绘制时另一线程还在写列表导致数据不一致。Python 的列表在单线程下没问题但 ROS 回调线程和主绘图线程之间是并发访问不加锁很容易出现异常。3.2 轨迹数据的组织与去重如果/odom的发布频率是 50Hz那跑一分钟就有 3000 个点这些数据全部存下来在大多数情况下没有问题。但实际操作中会遇到另一种情况机器人原地不动时只要没有漂移回调也会反复收到相同或者非常接近的坐标点。如果把这些点全部画出来轨迹线会变得非常粗甚至看不出运动方向。我的做法是设置一个最小距离阈值只有机器人的坐标相对于上一点移动超过一定距离比如 0.01 米才记录该点。这样既能减少冗余数据又能保证轨迹形状完整。需要注意的是这个去重逻辑要放在角速度变化明显的场景下格外小心因为原地旋转时平移坐标基本不变如果只按距离过滤会丢失朝向变化的信息。因此记录时最好也判断偏航角变化量。MIN_TRANSLATION_THRESHOLD 0.02 MIN_ROTATION_THRESHOLD 0.05 # 弧度 def odom_callback(self, msg): x msg.pose.pose.position.x y msg.pose.pose.position.y _, _, yaw euler_from_quaternion(q) with self.lock: if self.xs: dx x - self.xs[-1] dy y - self.ys[-1] dyaw abs(yaw - self.yaws[-1]) delta_distance (dx * dx dy * dy) ** 0.5 if delta_distance MIN_TRANSLATION_THRESHOLD and dyaw MIN_ROTATION_THRESHOLD: return self.xs.append(x) self.ys.append(y) self.yaws.append(yaw)3.3 参数约定与示例工程结构写脚本时我建议至少预留几个可配置参数/odom话题名称、缓存队列长度、去重距离阈值、绘图保存路径。这些参数通过命令行传进来的方式有很多种最简单的是直接在文件顶部定义常量但后续调整就会麻烦一些。更规范的做法是用rospy.get_param读取参数服务器里的配置。工程结构方面如果只是画一个简单的轨迹图单个脚本文件完全够用。但如果后续要加实时显示、插值处理、多话题对比我建议拆分成三个模块数据采集模块、数据处理模块、绘图模块。这样各自的改动互不干扰。我自己写过一个稍微完整一点的版本结构大概是trajectory_logger.py、trajectory_plot.py、run_trajectory_demo.launch三个文件跑起来非常顺手。4. matplotlib 动态绘制与静态成图4.1 实时绘图的实现方式实时绘制轨迹有两种常见做法一种是开启matplotlib的交互模式在 ROS 回调或者主循环里不断更新曲线另一种是使用matplotlib.animation.FuncAnimation定时刷新。两种方式各有适应场景我通常建议数据量不大时用plt.ion()配合plt.pause()代码简单直观。import matplotlib.pyplot as plt plt.ion() fig, ax plt.subplots() line, ax.plot([], [], b-) while not rospy.is_shutdown(): with logger.lock: xs list(logger.xs) ys list(logger.ys) line.set_data(xs, ys) ax.relim() ax.autoscale_view() fig.canvas.draw() plt.pause(0.01)这里最关键的是ax.relim()和ax.autoscale_view()它们的作用是重新计算坐标轴范围让轨迹始终完整显示在窗口内。如果不加这两行机器人一旦走出初始坐标轴范围画面上就看不到轨迹了这是新手经常踩的坑。plt.pause(0.01)则是给图形界面一个刷新时间否则窗口会一直无响应。4.2 静态轨迹图的后处理技巧在线实时绘图适合调试时看趋势但如果要写进实验报告或者用于定位误差分析还是离线生成一张静态高清图更合适。静态绘图的优势在于可以精细控制线宽、颜色、字体、坐标轴标签还能叠加起点和终点标记。fig, ax plt.subplots(figsize(8, 8)) ax.plot(xs, ys, b-, linewidth2, labelodom trajectory) ax.scatter(xs[0], ys[0], cgreen, markero, s80, labelstart) ax.scatter(xs[-1], ys[-1], cred, markerx, s80, labelend) ax.set_xlabel(X / m) ax.set_ylabel(Y / m) ax.grid(True, linestyle--, alpha0.6) ax.set_aspect(equal) ax.legend() plt.savefig(trajectory.png, dpi150, bbox_inchestight)set_aspect(equal)这行特别重要。默认情况下matplotlib的横纵坐标轴比例并不是 1:1如果机器人实际走的路线是正方形画出来却可能是长方形这会扭曲对轨迹形状的判断。加上等比例约束后图片上的距离关系才真实。4.3 绘制带方向箭头的轨迹纯线条轨迹只能看出机器人去了哪里但看不出机器人当时面朝哪个方向。如果机器人做的是圆周运动线轨迹就是一个圆根本区分不了正转反转。把朝向信息用箭头画出来就能直观看到旋转方向。具体的做法是在轨迹上每隔一段采样一个点用ax.arrow或者ax.quiver绘制方向箭头。quiver的底层绘制更快但arrow的箭头形状更好看我一般就用arrow每个采样点画一个短的箭头。arrow_interval 20 # 每隔20个点画一个箭头 skip max(1, len(xs) // 100) # 总箭头数量控制在100个以内 for i in range(0, len(xs), skip): dx 0.3 * math.cos(yaws[i]) dy 0.3 * math.sin(yaws[i]) ax.arrow(xs[i], ys[i], dx, dy, width0.01, colorred, alpha0.6)箭头长度建议跟机器人的实际尺寸成比例比如轮式机器人底盘长 0.5 米箭头画 0.3 米就比较合适。画得太短看不清方向画得太长又会覆盖掉轨迹线。这个参数我一般会放到配置项里方便不同尺寸的机器人平台使用。5. 实操过程中踩过的坑与排查方法5.1 回调阻塞导致坐标漂移我在第一次写这个脚本的时候直接在回调函数里调用了plt.show()结果机器人的轨迹画出来飘得离谱跑直线都歪歪扭扭。排查了半天才意识到问题不在里程计数据而是plt.show()在回调线程里执行阻塞了回调函数的返回导致 ROS 订阅队列里的消息堆积后面的坐标数据被延迟处理视觉上看起来就像轨迹跳跃。正确做法是回调函数只做数据缓存绘图逻辑放到主循环或者FuncAnimation里。无论何时都不要在 ROS 回调函数里执行任何可能阻塞的 UI 操作这是写 rospy 节点的一个通用原则。5.2 多坐标系混用另一个非常折腾人的问题是坐标系混用。有的机器人平台同时发布了/odom和/map两个坐标系下的位姿信息如果你错误地把/map系下的数据当成/odom系数据来画初始位置就可能对不上轨迹也会整体平移甚至旋转。这通常不会在轨迹形状上暴露问题但如果你同时画多条轨迹对比就会发现它们之间存在固定的偏移。要避免这个问题最直接的办法是在回调里打印消息的header.frame_id字段确认订阅到的话题确实是在预期的坐标系下发布。某些平台的驱动存在参数配置项可以切换里程计输出的坐标系这时需要检查启动参数是否与实验场景一致。5.3 话题频率与缓存长度不匹配ROS 订阅话题时有一个队列长度参数它表示最多缓存多少条消息。设置过小会导致消息被丢弃画出来的轨迹会缺一段设置过大在某些处理器上会占用较多内存。对于轨迹绘图场景一般设置 10 到 100 个队列长度足够反正我们只是采样点不需要保证每条消息都不丢丢几条对轨迹形状影响微乎其微。但如果机器人的运动速度很快而你对轨迹精度要求很高就需要同时考虑话题发布频率和队列长度的匹配。我的经验是队列长度不小于 100回调函数本身处理得要快保证消息不会在回调里积压。5.4 matplotlib 在无图形环境下的替代方案如果说你用的是远程服务器或者没有图形界面的微型机器人主控直接跑plt.show()会报错因为matplotlib默认使用的后端需要图形显示环境。此时可以强行使用非交互后端比如agg它不依赖图形界面能把图片保存为文件。import matplotlib matplotlib.use(Agg)这行代码必须在导入matplotlib.pyplot之前设置否则不生效。设置了Agg后端之后plt.show()不会弹窗代码里的plt.savefig()依然可以正常保存图片。我经常在机器人板卡上跑一个脚本跑完直接用scp把图片拷贝回本地看比在现场守着屏幕省事很多。6. 后续扩展思路6.1 叠加地图与导航可视化轨迹图最基本的功能是显示机器人走过哪条路但我们可以在此基础上叠加更多信息。比如把激光雷达构建的地图栅格图显示在背景上轨迹叠加在地图上就能很直观地看出机器人是否跑到障碍物区域里。如果导航规划模块发布了目标点和局部路径也可以把move_base的目标坐标点画到图上对比实际轨迹和规划路径之间的偏差这对调导航参数帮助非常大。叠加地图的方式很简单用matplotlib的imshow把地图图片显示在坐标轴上再设置好图片与真实坐标的映射关系之后继续画轨迹线即可。需要注意地图图片可能很大画之前可以先做一次缩放否则缩放图形时会很卡。6.2 与 rosbag 离线回放结合在线绘制适合调试但有些问题只在特定情况下复现反复跑机器人寻找复现条件很浪费时间。更好的方法是用rosbag record /odom把话题记录下来离线回放时再用这段代码绘图。离线模式下不需要担心回调和绘图互相阻塞因为可以从 bag 文件中一帧一帧读取数据绘图速度不受真实时间约束。读取 rosbag 的代码也很简洁用rosbag.Bag遍历指定话题的消息把坐标数据提取到列表再调用绘图逻辑。import rosbag xs [] ys [] bag rosbag.Bag(odom.bag) for topic, msg, t in bag.read_messages(topics[/odom]): xs.append(msg.pose.pose.position.x) ys.append(msg.pose.pose.position.y) bag.close()这样处理后轨迹图的数据来源就从在线订阅切换到了离线文件代码复用率很高我强烈建议把绘图逻辑封装成函数在线和离线分别调用同一个入口。最后再分享一个小细节无论在线还是离线画完轨迹之后养成打印一下数据点总数和总位移的习惯。数据点总数能反映订阅话题是否稳定总位移则能帮你一眼判断里程计参数是否大致合理。我经常在实验结束前看一眼总位移如果命令机器人走两米计算结果却是 1.7 米那就说明轮径标定或者坐标换算还有问题根本不需要再去仔细看曲线。这一步虽小但能省下不少排查时间。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询