ROS2 集成方案
你的 Ubuntu 22.04 对应 ROS2 Humble——这是生态支持最好的组合, 也是车企量产主流。本节给出三条路线的取舍,并推荐一条能彻底绕开版本地狱的做法。
按 M1 总览的决策,ROS2 的实操价值在 M6 部署阶段, 现在做算法阶段用纯 Python 直连 CARLA 更高效。
所以本节推荐:现在通读一遍,理解方案设计和版本关系; 实际动手可以推迟到 M6。但如果你想早点把 rviz2 可视化用起来(调试确实方便), 现在做也完全可以——本节给的方案不会拖慢你。
先说结论:三条路线怎么选
| 方案 | 做法 | 优点 | 风险 |
|---|---|---|---|
| ① 官方 ros-bridge | 装 carla-ros-bridge |
功能全(TF、交通灯、地图全都有) | 主线针对 Foxy,Humble 要用社区分支,版本随 CARLA 升级易失效 |
| ② CARLA 原生 ROS2 | 启动加 --ros2 |
无需额外组件,官方维护 | 较新功能,话题覆盖不全(TF/地图等仍在完善) |
| ③ 自己写轻量桥接 | 用 rclpy 发你需要的话题 |
版本无关、完全可控、学到真东西 | 需要自己写(但只有 200 行) |
① 彻底绕开版本地狱。你自己写的桥只依赖 rclpy 和 CARLA Python API,
两边都是稳定接口。CARLA 升到 0.9.16、ROS2 升到 Jazzy,你的代码基本不用改。
② 你只需要少数几个话题。官方 bridge 发几十个话题, 而你的项目实际只用图像、点云、里程计、TF 这四类。为用不到的功能承担维护成本不划算。
③ 面试价值更高。"我用了官方 bridge" 和 "我自己实现了 CARLA 到 ROS2 的数据桥接,处理了时间同步与 TF 树构建"—— 后者才体现工程能力。而这正是你目标岗位要考察的。
第一步:安装 ROS2 Humble
Ubuntu 22.04 是 Humble 的官方目标平台,直接 apt 装即可,不需要 Docker。
# ---------- 1. 设置字符集 ----------
sudo apt update && sudo apt install -y locales
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8
# ---------- 2. 添加 ROS2 apt 源 ----------
sudo apt install -y software-properties-common curl
sudo add-apt-repository universe -y
sudo curl -sSL \
https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \
-o /usr/share/keyrings/ros-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" \
| sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
# ---------- 3. 安装 Humble ----------
sudo apt update
sudo apt install -y ros-humble-desktop # 含 rviz2,约 2GB
# 若只要核心不要 GUI: sudo apt install ros-humble-ros-base
# 开发工具
sudo apt install -y python3-colcon-common-extensions python3-rosdep
# ---------- 4. 验证 ----------
source /opt/ros/humble/setup.bash
ros2 --version
printenv ROS_DISTRO # 应输出 humble
# 跑个官方示例确认通信正常(开两个终端)
# 终端A: ros2 run demo_nodes_cpp talker
# 终端B: ros2 run demo_nodes_py listener
这是 22.04 上最常见的问题。ROS2 Humble 用系统 Python 3.10,你的 conda 环境也是 3.10——
同时 source 两边会导致 rclpy 找不到、或 numpy 版本打架。
本课程的处理原则:不要在同一个终端里同时激活两者。
# 不要把这两行都自动执行,改成手动切换的函数
# 用 ROS2 时执行这个
use_ros() {
conda deactivate 2>/dev/null
source /opt/ros/humble/setup.bash
echo "→ ROS2 Humble 环境(系统 Python)"
}
# 用训练环境时执行这个
use_ad() {
conda activate ad
echo "→ 训练环境 ad(conda Python)"
}
# 桥接脚本要同时用 carla 和 rclpy —— 在 ROS2 环境里单独装 carla
# use_ros
# pip install --user carla==0.9.15
为什么这样分:训练(PyTorch)和 ROS2 通信本来就是两个阶段的事, 物理隔离比费劲调和依赖更省心。M6 做部署时,模型会先导出成 TensorRT/ONNX, ROS2 节点只负责加载推理引擎,不需要 PyTorch。
第二步:理解桥接要做什么
① 时间戳:CARLA 的仿真时间与 ROS2 时钟不同步,需要统一(用 use_sim_time)。
② 坐标系:CARLA 是左手系(1.4 节讲过), ROS 用右手系且约定 x 前、y 左、z 上——注意 y 轴方向和 CARLA 相反。
③ TF 树:ROS2 靠 TF 描述坐标关系,需要正确发布
map → odom → base_link → sensor 这条链。
④ 点云格式:PointCloud2 是二进制打包格式,
字段定义要写对,否则 rviz2 里什么都看不到。
第三步:完整桥接节点(本节核心产出)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
CARLA → ROS2 Humble 轻量桥接节点
只发布项目实际需要的话题,避免官方 bridge 的版本依赖问题。
发布话题:
/carla/camera/image sensor_msgs/Image
/carla/camera/info sensor_msgs/CameraInfo
/carla/lidar/points sensor_msgs/PointCloud2
/carla/odom nav_msgs/Odometry
/tf 坐标变换树
运行前:
use_ros # 切到 ROS2 环境(见本节 bashrc 配置)
pip install --user carla==0.9.15
python3 carla_ros2_bridge.py
"""
import math
import queue
import numpy as np
import carla
import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy
from sensor_msgs.msg import Image, CameraInfo, PointCloud2, PointField
from nav_msgs.msg import Odometry
from geometry_msgs.msg import TransformStamped, Quaternion
from tf2_ros import TransformBroadcaster
from std_msgs.msg import Header
IMG_W, IMG_H, FOV = 1280, 720, 90.0
FPS = 20
def euler_to_quat(roll, pitch, yaw):
"""欧拉角(弧度)→ 四元数"""
cy, sy = math.cos(yaw * 0.5), math.sin(yaw * 0.5)
cp, sp = math.cos(pitch * 0.5), math.sin(pitch * 0.5)
cr, sr = math.cos(roll * 0.5), math.sin(roll * 0.5)
q = Quaternion()
q.w = cr * cp * cy + sr * sp * sy
q.x = sr * cp * cy - cr * sp * sy
q.y = cr * sp * cy + sr * cp * sy
q.z = cr * cp * sy - sr * sp * cy
return q
class CarlaBridge(Node):
def __init__(self):
super().__init__("carla_ros2_bridge")
# 传感器数据用 BEST_EFFORT,避免网络抖动时阻塞
qos = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
history=HistoryPolicy.KEEP_LAST,
depth=5,
)
self.pub_img = self.create_publisher(Image, "/carla/camera/image", qos)
self.pub_info = self.create_publisher(CameraInfo, "/carla/camera/info", qos)
self.pub_pcd = self.create_publisher(PointCloud2, "/carla/lidar/points", qos)
self.pub_odom = self.create_publisher(Odometry, "/carla/odom", 10)
self.tf_broadcaster = TransformBroadcaster(self)
self.get_logger().info("连接 CARLA...")
self._setup_carla()
self.get_logger().info("桥接就绪,开始发布话题")
# 主循环定时器
self.timer = self.create_timer(1.0 / FPS, self.tick)
# ---------------- CARLA 侧 ----------------
def _setup_carla(self):
self.client = carla.Client("localhost", 2000)
self.client.set_timeout(15.0)
self.world = self.client.get_world()
settings = self.world.get_settings()
settings.synchronous_mode = True
settings.fixed_delta_seconds = 1.0 / FPS
self.world.apply_settings(settings)
self.tm = self.client.get_trafficmanager(8000)
self.tm.set_synchronous_mode(True)
bp_lib = self.world.get_blueprint_library()
# 主车
v_bp = bp_lib.filter("vehicle.tesla.model3")[0]
spawns = self.world.get_map().get_spawn_points()
self.vehicle = None
for sp in spawns:
self.vehicle = self.world.try_spawn_actor(v_bp, sp)
if self.vehicle:
break
if self.vehicle is None:
raise RuntimeError("无法生成主车")
self.vehicle.set_autopilot(True, self.tm.get_port())
# 相机
c_bp = bp_lib.find("sensor.camera.rgb")
c_bp.set_attribute("image_size_x", str(IMG_W))
c_bp.set_attribute("image_size_y", str(IMG_H))
c_bp.set_attribute("fov", str(FOV))
c_bp.set_attribute("sensor_tick", "0.0")
self.camera = self.world.spawn_actor(
c_bp, carla.Transform(carla.Location(x=1.5, z=2.0)),
attach_to=self.vehicle)
self.q_img = queue.Queue()
self.camera.listen(self.q_img.put)
# 激光雷达
l_bp = bp_lib.find("sensor.lidar.ray_cast")
l_bp.set_attribute("channels", "64")
l_bp.set_attribute("range", "80")
l_bp.set_attribute("points_per_second", "600000")
l_bp.set_attribute("rotation_frequency", str(FPS))
l_bp.set_attribute("sensor_tick", "0.0")
self.lidar = self.world.spawn_actor(
l_bp, carla.Transform(carla.Location(x=0.0, z=2.4)),
attach_to=self.vehicle)
self.q_pcd = queue.Queue()
self.lidar.listen(self.q_pcd.put)
# 预计算相机内参(1.4 节的公式)
f = IMG_W / (2.0 * math.tan(math.radians(FOV) / 2.0))
self.K = [f, 0.0, IMG_W / 2.0,
0.0, f, IMG_H / 2.0,
0.0, 0.0, 1.0]
# ---------------- 主循环 ----------------
def tick(self):
frame = self.world.tick()
stamp = self.get_clock().now().to_msg()
# 取本帧数据(帧号校验,见 1.2 节)
try:
img = self._take(self.q_img, frame)
pcd = self._take(self.q_pcd, frame)
except queue.Empty:
self.get_logger().warn("传感器数据超时,跳过本帧")
return
self._pub_tf(stamp)
self._pub_odom(stamp)
self._pub_image(img, stamp)
self._pub_pointcloud(pcd, stamp)
def _take(self, q, frame_id, timeout=2.0):
while True:
data = q.get(timeout=timeout)
if data.frame == frame_id:
return data
if data.frame > frame_id:
raise queue.Empty
# ---------------- 各话题发布 ----------------
def _pub_tf(self, stamp):
"""
发布 TF 树: map → base_link → {camera_link, lidar_link}
坐标系转换要点(1.4 节讲过 CARLA 是左手系):
CARLA: x前 y右 z上
ROS : x前 y左 z上 ← y 轴方向相反!
所以 y 和 yaw 都要取负。
"""
tf = self.vehicle.get_transform()
t = TransformStamped()
t.header.stamp = stamp
t.header.frame_id = "map"
t.child_frame_id = "base_link"
t.transform.translation.x = tf.location.x
t.transform.translation.y = -tf.location.y # 左手→右手
t.transform.translation.z = tf.location.z
t.transform.rotation = euler_to_quat(
math.radians(tf.rotation.roll),
-math.radians(tf.rotation.pitch),
-math.radians(tf.rotation.yaw)) # 同样取负
self.tf_broadcaster.sendTransform(t)
# 传感器相对车身的静态变换
for child, loc in [("camera_link", (1.5, 0.0, 2.0)),
("lidar_link", (0.0, 0.0, 2.4))]:
s = TransformStamped()
s.header.stamp = stamp
s.header.frame_id = "base_link"
s.child_frame_id = child
s.transform.translation.x = loc[0]
s.transform.translation.y = loc[1]
s.transform.translation.z = loc[2]
s.transform.rotation.w = 1.0
self.tf_broadcaster.sendTransform(s)
def _pub_odom(self, stamp):
tf = self.vehicle.get_transform()
vel = self.vehicle.get_velocity()
odom = Odometry()
odom.header.stamp = stamp
odom.header.frame_id = "map"
odom.child_frame_id = "base_link"
odom.pose.pose.position.x = tf.location.x
odom.pose.pose.position.y = -tf.location.y
odom.pose.pose.position.z = tf.location.z
odom.pose.pose.orientation = euler_to_quat(
math.radians(tf.rotation.roll),
-math.radians(tf.rotation.pitch),
-math.radians(tf.rotation.yaw))
odom.twist.twist.linear.x = vel.x
odom.twist.twist.linear.y = -vel.y
odom.twist.twist.linear.z = vel.z
self.pub_odom.publish(odom)
def _pub_image(self, carla_img, stamp):
# CARLA 给的是 BGRA
arr = np.frombuffer(carla_img.raw_data, dtype=np.uint8)
arr = arr.reshape((IMG_H, IMG_W, 4))[:, :, :3] # 取 BGR
msg = Image()
msg.header.stamp = stamp
msg.header.frame_id = "camera_link"
msg.height = IMG_H
msg.width = IMG_W
msg.encoding = "bgr8"
msg.is_bigendian = 0
msg.step = IMG_W * 3
msg.data = arr.tobytes()
self.pub_img.publish(msg)
# 相机内参(下游做投影要用)
info = CameraInfo()
info.header = msg.header
info.height = IMG_H
info.width = IMG_W
info.distortion_model = "plumb_bob"
info.d = [0.0] * 5 # CARLA 无畸变
info.k = self.K
info.p = [self.K[0], 0.0, self.K[2], 0.0,
0.0, self.K[4], self.K[5], 0.0,
0.0, 0.0, 1.0, 0.0]
self.pub_info.publish(info)
def _pub_pointcloud(self, carla_pcd, stamp):
"""
转 PointCloud2 —— 字段定义必须精确,否则 rviz2 显示不出来
"""
pts = np.frombuffer(carla_pcd.raw_data, dtype=np.float32)
pts = pts.reshape(-1, 4).copy() # x, y, z, intensity
# CARLA 左手系 → ROS 右手系:y 取负
pts[:, 1] = -pts[:, 1]
msg = PointCloud2()
msg.header.stamp = stamp
msg.header.frame_id = "lidar_link"
msg.height = 1 # 无序点云
msg.width = pts.shape[0]
msg.is_dense = True
msg.is_bigendian = False
msg.fields = [
PointField(name="x", offset=0,
datatype=PointField.FLOAT32, count=1),
PointField(name="y", offset=4,
datatype=PointField.FLOAT32, count=1),
PointField(name="z", offset=8,
datatype=PointField.FLOAT32, count=1),
PointField(name="intensity", offset=12,
datatype=PointField.FLOAT32, count=1),
]
msg.point_step = 16 # 4 个 float32
msg.row_step = msg.point_step * msg.width
msg.data = pts.tobytes()
self.pub_pcd.publish(msg)
# ---------------- 清理 ----------------
def destroy_node(self):
self.get_logger().info("清理 CARLA actor...")
for s in [self.camera, self.lidar]:
try: s.stop(); s.destroy()
except Exception: pass
try: self.vehicle.destroy()
except Exception: pass
# 恢复异步,避免影响别人
try:
st = self.world.get_settings()
st.synchronous_mode = False
st.fixed_delta_seconds = None
self.world.apply_settings(st)
self.tm.set_synchronous_mode(False)
except Exception: pass
super().destroy_node()
def main():
rclpy.init()
node = CarlaBridge()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
第四步:运行与可视化
# ---------- 终端 1: CARLA 服务端 ----------
cd ~/carla && QUALITY=Low ./start_carla.sh
# ---------- 终端 2: 桥接节点 ----------
use_ros # 切 ROS2 环境(不要激活 conda)
pip install --user carla==0.9.15 # 首次需要
python3 carla_ros2_bridge.py
# ---------- 终端 3: 验证话题 ----------
use_ros
# 列出所有话题
ros2 topic list
# 看发布频率(应接近 20Hz)
ros2 topic hz /carla/camera/image
ros2 topic hz /carla/lidar/points
# 看一条消息的结构
ros2 topic echo /carla/odom --once
# 检查 TF 树是否完整
ros2 run tf2_tools view_frames
# 会生成 frames.pdf,应看到 map → base_link → camera_link/lidar_link
# ---------- 终端 4: rviz2 可视化 ----------
use_ros
rviz2
# rviz2 里手动添加显示项:
# 1. Fixed Frame 设为 map
# 2. Add → Image → Topic 选 /carla/camera/image
# 3. Add → PointCloud2 → Topic 选 /carla/lidar/points
# 4. Add → Odometry → Topic 选 /carla/odom
# 5. Add → TF
# 注意 QoS: 传感器话题要把 Reliability 设成 Best Effort
L20 是远程服务器,rviz2 是 GUI 程序。两种方案:
① X11 转发(简单,但卡):ssh -X l20 然后直接跑 rviz2
② 本地跑 rviz2(推荐):本地机器也装 ROS2 Humble,
设相同的 ROS_DOMAIN_ID,两台机器在同一局域网就能自动发现——
这才是 ROS2 分布式通信的正确用法。
# 两台机器都设置相同 domain id
export ROS_DOMAIN_ID=42
# L20 上跑桥接,本地跑 rviz2,话题自动互通
常见报错与排查
| 现象 | 原因 | 解决 |
|---|---|---|
ModuleNotFoundError: rclpy |
没 source ROS2,或在 conda 环境里 | conda deactivate 后 source /opt/ros/humble/setup.bash |
ModuleNotFoundError: carla |
carla 装在 conda 环境,ROS2 用的是系统 Python | 在 ROS2 环境里 pip install --user carla==0.9.15 |
| numpy 版本冲突报错 | conda 的 numpy 污染了系统 Python 路径 | 检查 PYTHONPATH,确保没混入 conda 路径 |
| rviz2 里点云不显示 | QoS 不匹配(发布是 BEST_EFFORT) | rviz2 中把该显示项的 Reliability 改成 Best Effort |
| rviz2 报 "No TF data" | Fixed Frame 设错,或 TF 没发出来 | Fixed Frame 设为 map;用 ros2 run tf2_tools view_frames 检查 |
| 点云左右镜像 | 忘了 CARLA 左手系 → ROS 右手系的 y 取负 | 检查 pts[:,1] = -pts[:,1] 是否执行 |
| 两台机器话题不互通 | ROS_DOMAIN_ID 不一致或防火墙 |
两边设相同 ID;检查 UDP 端口是否放行 |
| 话题频率远低于 20Hz | GPU 被占满,CARLA tick 变慢 | QUALITY=Low;降低点云 points_per_second |
验收清单
- 理解三条集成路线的取舍,能说清为什么本课程推荐自写桥接
- ROS2 Humble 安装完成,
ros2 --version正常 - 配好
use_ros/use_ad切换函数,理解为什么要隔离 conda 与 ROS2 - 桥接节点跑通,
ros2 topic list能看到全部 5 个话题 ros2 topic hz显示频率接近 20Hz- TF 树完整:
map → base_link → camera_link / lidar_link - rviz2 里能同时看到图像、点云、里程计轨迹,且点云方向正确(不镜像)
本节小结
- Ubuntu 22.04 + ROS2 Humble 是最佳组合,可原生 apt 安装,无需 Docker
- 自写轻量桥接优于官方 bridge:版本无关、只发需要的话题、面试价值更高
- conda 与 ROS2 必须隔离——不要在同一终端同时激活,用切换函数管理
- CARLA 左手系 → ROS 右手系:y 和 yaw 都要取负(1.4 节坐标系知识的延续)
PointCloud2的字段定义要精确,point_step=16对应 4 个 float32- 远程可视化推荐本地跑 rviz2 + 相同 ROS_DOMAIN_ID,这才是 ROS2 分布式的正确用法
你现在拥有:可训练的 Python 环境、能采数据的 CARLA、真实数据集 nuScenes、 标定验证工具、以及 ROS2 通路。地基打完了。
下一模块 M2 开始进入算法主线——采集 BEV 训练数据、训练 BEVFormer。 那时你会发现 M1 这些准备每一样都用得上。