
自动驾驶 ROS2 项目最常见的返工,不是控制算法收敛不了,也不是感知模型精度不够,而是传感器数据在节点之间静默丢失或错配。激光雷达在发布,相机在发布,融合节点在运行,日志没有一行错误,但融合输出里一半的帧是空的,或者点云和图像在时间上差了 80 毫秒。这类问题不抛异常,只让下游算法在一个不完整的数据流上“正常工作”。下面从环境、节点、融合三个位置,给出可运行的配置与代码。
ROS2 Humble 的默认中间件是 Fast DDS,但在多传感器、大点云的自动驾驶场景下,Cyclone DDS 的共享内存传输通常更稳。关键不是换实现,而是显式打开共享内存,否则同一台机器上的点云发布/订阅仍走回环网络,拷贝开销直接吃掉融合节点的处理预算。
~/.ros/cyclonedds.xml:
<CycloneDDS>
<Domain Id="0">
<SharedMemory>
<Enable>true</Enable>
<LogLevel>warn</LogLevel>
</SharedMemory>
</Domain>
</CycloneDDS>export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
export CYCLONEDDS_URI=file://$HOME/.ros/cyclonedds.xml同时把内核的 UDP 接收缓冲调大,否则高频率点云会在内核层丢包,而 DDS 层只会表现为“偶尔丢帧”:
sudo sysctl -w net.core.rmem_max=2147483647ROS2 的 QoS 有一个反直觉的规则:发布端和订阅端的 Reliability 不兼容时,连接根本不会建立,但节点不会报错,只是回调永远不触发。传感器驱动通常用 BEST_EFFORT 发布,而订阅端如果用了默认的 RELIABLE,结果是订阅者静默收不到任何数据。
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy
from sensor_msgs.msg import PointCloud2, Image
SENSOR_QOS = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
history=HistoryPolicy.KEEP_LAST,
depth=5, # 只保留最新 5 帧,防止积压
)
self.create_subscription(PointCloud2, '/lidar/points',
self.on_cloud, SENSOR_QOS)
self.create_subscription(Image, '/camera/image',
self.on_image, SENSOR_QOS)排查时用 ros2 topic info /lidar/points --verbose 看两端的 QoS 是否兼容,比在代码里加打印快得多。depth=5 也不是随手写的:融合节点如果处理慢于发布频率,深度过大会让队列里堆积过期帧,融合出来的结果对应的是几百毫秒前的世界。
激光雷达 10Hz、相机 30Hz,两个回调各自触发再融合,时间戳天然对不齐。直接用 message_filters 的近似同步是标准做法,但 slop 和 queue_size 设错会导致两种相反的失效:slop 太小,大量帧被丢弃;slop 太大,把相隔 100ms 的点云和图像配成一对,运动补偿完全错误。
import message_filters
import rclpy
from rclpy.node import Node
from rclpy.time import Time
from rclpy.duration import Duration
from tf2_ros import Buffer, TransformListener
class FusionNode(Node):
def __init__(self):
super().__init__('fusion_node')
self.tf_buffer = Buffer()
self.tf_listener = TransformListener(self.tf_buffer, self)
lidar_sub = message_filters.Subscriber(
self, PointCloud2, '/lidar/points', qos_profile=SENSOR_QOS)
cam_sub = message_filters.Subscriber(
self, Image, '/camera/image', qos_profile=SENSOR_QOS)
self.sync = message_filters.ApproximateTimeSynchronizer(
[lidar_sub, cam_sub], queue_size=10, slop=0.03)
self.sync.registerCallback(self.fuse)
def fuse(self, cloud, image):
stamp = cloud.header.stamp # 用消息时间,不用 now()
try:
tf = self.tf_buffer.lookup_transform(
'base_link', cloud.header.frame_id,
Time.from_msg(stamp),
timeout=Duration(seconds=0.05))
except Exception as e:
self.get_logger().warn(f'tf lookup failed: {e}')
return
# tf.transform 用于把点云投到 base_link,再与相机外参对齐
self.project_and_fuse(cloud, image, tf)这里最容易被写错的是 lookup_transform 的第三个参数。很多示例传 Time()(即“最新可用”),在离线回放或传感器有固定延迟时,拿到的外参对应的是“当前”而不是“这一帧采集时”的位姿,车辆在运动时外参就错了。必须用 cloud.header.stamp 构造查询时间,TF 缓冲区才能插值出该时刻的真实变换。timeout 也不能省,TF 树在启动阶段尚未完整时,不设超时的查询会直接抛异常,融合节点在第一帧就崩掉。
slop=0.03 是经验值:它允许点云和图像相差 30ms 以内配对,超过就丢弃。对于 10Hz 雷达、30Hz 相机,这个窗口既能保证大部分帧配上,又不会把运动中的两帧硬凑成一对。代价是融合频率会降到接近雷达的 10Hz——这个代价是应该接受的,因为错配的融合比少一帧融合危害大得多。
ROS2 自动驾驶开发的工程纪律,集中在三个“数据进入算法之前”的位置:DDS 层要显式开共享内存并调大内核缓冲,否则大点云在回环上白白拷贝;QoS 要按传感器特性配置而不是用默认值,否则订阅端静默收不到数据;时间同步和 TF 查询必须以消息时间戳为准,否则融合结果对应的是一个错位的世界。这三件事都不改变算法本身,但它们决定了算法是在真实的数据流上运行,还是在一个看起来正常、实际上缺帧错位的流上运行。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。