简介:本资源面向自动驾驶与ROS初学者及算法验证人员,提供基于Carla模拟器的纯追踪(Pure Pursuit)控制实践方案,帮助读者在仿真环境中快速搭建车辆循迹与速度控制流程。包内共10个文件,以launch启动文件、Python脚本、pickle数据文件为主,另含xml、json、txt与md说明文档,压缩包约90KB,结构紧凑便于直接导入工作空间运行。已有505人学习下载,适合作为ROS与Carla联合调试的入门参考。资源核心围绕pure_pursuit.py展开,采用林肯MKZ 2017车型模型,速度控制使用PID算法,目标速度设定为12m/s,转向控制以纯追踪算法实现路径跟踪;同时通过解析/carla/ego_vehicle/odometry消息获取车辆位姿,路径分辨率以0.2m为步长可调,并附带运行录屏与参考路径记录,便于读者对照复现、理解参数调节与循迹效果验证。
1. 当仿真器遇上ROS:为什么这套组合值得你花一个周末跑通
如果你正在做自动驾驶感知或控制的算法验证,大概率遇到过两个头疼的问题:一是实车测试成本高、风险大,一个误刹车就可能让整个项目组写检讨;二是纯软件仿真又太“干净”,传感器噪声、通信延迟、动态障碍物这些真实世界的脏东西一个都没有。Carla仿真器加上ROS这套组合,恰好卡在中间——它提供了一个带物理引擎、可编程天气、可配置传感器套件的虚拟城市,同时通过ROS的通信机制,让你用几乎和实车一致的代码结构去驱动这个虚拟世界。我第一次接触这套方案时,最直观的感受是:原来调PID参数可以不用提心吊胆,原来换传感器布局只需要改几行配置。这篇文章面向的是已经了解ROS基础概念、想找一个可复现的自动驾驶仿真验证环境的工程师,我会从环境搭建讲到传感器配置、控制接口对接,再到那些让我熬夜排查的坑。不吹这套方案能替代实车,但作为算法迭代的第一道筛子,它足够扎实。
2. 把Carla和ROS接起来:从零搭建可运行的最小系统
2.1 为什么选Carla而不是其他仿真器
做自动驾驶仿真,选型时绕不开几个候选:CARLA、AirSim、LGSVL。AirSim强在无人机和视觉逼真度,但车辆动力学模型偏简化;LGSVL曾经生态不错,但维护节奏放缓。Carla的优势在于它是从零为自动驾驶设计的——UE4渲染管线提供了接近真实的视觉输入,内置的车辆动力学基于真实车辆参数调校,而且它原生支持Python API和ROS Bridge。我一般会跟团队里新来的同学说:如果你要做的是感知算法在闭环中的表现验证,Carla的传感器模型和场景编排能力是目前开源方案里最省心的。ROS这边,无论你用ROS1还是ROS2,Carla都有对应的bridge包,通信层不用自己造轮子。
选型时还要看一个硬指标:你的算法栈是不是已经跑在ROS上了。如果是,Carla的ROS Bridge能让你把仿真器当成一个“传感器+执行器”的黑匣子,topic名字和消息类型对齐后,算法节点几乎不用改。这个迁移成本低到让人感动。
2.2 环境搭建:版本对齐比什么都重要
Carla和ROS的版本兼容性是个玄学。我踩过的最大坑是:Carla 0.9.13配ROS2 Foxy,bridge编译时提示fastdds版本冲突,折腾了一下午才发现是Carla自带的fastdds和ROS2安装的版本不一致。所以第一步不是急着装,而是先确定版本矩阵。
常见做法是:Carla 0.9.14 + ROS2 Foxy + Ubuntu 20.04,或者Carla 0.9.13 + ROS1 Noetic + Ubuntu 20.04。如果你用ROS2 Humble,建议Carla 0.9.15以上。下面以Carla 0.9.14 + ROS2 Foxy为例,给出最小可运行步骤。
# 1. 安装Carla 0.9.14(从官方release下载预编译包) # 假设解压到 ~/carla cd ~/carla ./CarlaUE4.sh -quality-level=Low -prefernvidia # 2. 安装ROS2 Foxy(如果还没装) # 按ROS2官方文档添加源后: sudo apt install ros-foxy-desktop ros-foxy-ros-base # 3. 安装carla-ros-bridge mkdir -p ~/carla_ws/src cd ~/carla_ws/src git clone -b foxy https://github.com/carla-simulator/ros-bridge.git cd ~/carla_ws rosdep install --from-paths src --ignore-src -r colcon build --symlink-install逻辑说明:Carla服务端先跑起来,它负责渲染和物理计算;ros-bridge作为中间层,把Carla的传感器数据转成ROS topic,同时把ROS的control指令转成Carla的车辆控制信号。-quality-level=Low在调试阶段够用,能省显存。--symlink-install方便你改bridge源码后不用重新编译。
参数说明:-prefernvidia强制用独显,如果你是多显卡机器,不加这个可能跑到集显上,帧率会掉到个位数。rosdep install那步如果报错,多半是某些依赖包在Foxy源里名字变了,可以手动装对应的ros-foxy-xxx。
2.3 跑通第一个闭环:让车动起来
环境装好后,别急着上算法。先验证最基础的通信链路:Carla发传感器数据,ROS收,ROS发控制指令,Carla执行。
# 终端1:启动Carla服务端 cd ~/carla && ./CarlaUE4.sh -quality-level=Low # 终端2:启动ros-bridge的carla_ros_bridge source ~/carla_ws/install/setup.bash ros2 launch carla_ros_bridge carla_ros_bridge.launch.py # 终端3:生成一辆车和一个RGB相机 ros2 run carla_ros_bridge carla_spawn_objects --ros-args -p objects_definition_file:=/path/to/spawn_objects.json # 终端4:手动发一个控制指令 ros2 topic pub /carla/ego_vehicle/vehicle_control_cmd carla_msgs/CarlaEgoVehicleControl \ "{steer: 0.0, throttle: 0.3, brake: 0.0, hand_brake: false, reverse: false, gear: 1, manual_gear_shift: false}"逻辑说明:carla_spawn_objects读取一个JSON文件,里面定义了要生成的车辆和传感器。这个JSON的格式很关键,写错一个字段就静默失败。控制指令通过/carla/ego_vehicle/vehicle_control_cmd这个topic下发,消息类型是CarlaEgoVehicleControl。
参数说明:throttle: 0.3表示30%油门,steer范围是-1到1,对应左满舵到右满舵。gear: 1是前进挡。如果你发现车不动,先检查/carla/ego_vehicle/vehicle_status这个topic有没有数据,没有的话说明车辆没生成成功。
提示:spawn_objects.json里传感器的
type字段必须和Carla版本支持的传感器列表完全一致,大小写敏感。我见过有人写RgbCamera而不是rgb_camera,排查了半小时。
3. 传感器配置与数据对齐:让仿真数据能直接喂给算法
3.1 相机与激光雷达的联合标定参数
仿真环境里做感知,最怕的是传感器之间的外参对不上。Carla允许你在spawn JSON里直接指定每个传感器相对于车辆中心的位姿,这比实车标定省事,但参数写错一样翻车。
{ "type": "sensor.camera.rgb", "id": "front_camera", "x": 1.5, "y": 0.0, "z": 1.4, "roll": 0.0, "pitch": 0.0, "yaw": 0.0, "image_size_x": 800, "image_size_y": 600, "fov": 90.0, "sensor_tick": 0.05 }, { "type": "sensor.lidar.ray_cast", "id": "top_lidar", "x": 0.0, "y": 0.0, "z": 2.0, "roll": 0.0, "pitch": 0.0, "yaw": 0.0, "range": 50.0, "channels": 32, "points_per_second": 56000, "rotation_frequency": 10.0, "sensor_tick": 0.1 }逻辑说明:x是前向,y是右向,z是上向,单位是米。sensor_tick是采样间隔,0.05对应20Hz。激光雷达的rotation_frequency是旋转频率,10Hz意味着每0.1秒转一圈,和sensor_tick配合决定点云密度。
参数说明:fov是视场角,90度适合前视感知。range是最大探测距离,50米在城市工况够用,但如果你要测高速场景,得加到100以上。channels是线数,32线在仿真里已经能提供不错的点云质量,再高对显存压力大。
注意:相机和激光雷达的
sensor_tick如果不一致,做时间同步时会有麻烦。我一般让所有传感器用同一个tick,或者成整数倍关系,比如相机20Hz、雷达10Hz,这样对齐时不用插值。
3.2 用ROS topic拿到数据并验证时间戳
传感器生成后,数据会以ROS topic的形式发布。验证数据是否正常,不能只看有没有消息,还要看时间戳是否单调递增、帧率是否稳定。
import rclpy from rclpy.node import Node from sensor_msgs.msg import Image, PointCloud2 from cv_bridge import CvBridge import cv2 class SensorChecker(Node): def __init__(self): super().__init__('sensor_checker') self.bridge = CvBridge() self.last_cam_stamp = None self.last_lidar_stamp = None self.create_subscription(Image, '/carla/ego_vehicle/front_camera/image', self.cam_cb, 10) self.create_subscription(PointCloud2, '/carla/ego_vehicle/top_lidar/point_cloud', self.lidar_cb, 10) def cam_cb(self, msg): if self.last_cam_stamp is not None: dt = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9 - self.last_cam_stamp self.get_logger().info(f'Camera dt: {dt:.4f}s') self.last_cam_stamp = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9 # 转成OpenCV格式看一眼 cv_img = self.bridge.imgmsg_to_cv2(msg, 'bgr8') cv2.imshow('front_camera', cv_img) cv2.waitKey(1) def lidar_cb(self, msg): if self.last_lidar_stamp is not None: dt = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9 - self.last_lidar_stamp self.get_logger().info(f'Lidar dt: {dt:.4f}s') self.last_lidar_stamp = msg.header.stamp.sec + msg.header.stamp.nanosec * 1e-9 def main(): rclpy.init() node = SensorChecker() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()逻辑说明:这个节点订阅相机和激光雷达的topic,计算相邻两帧的时间差,同时把相机图像显示出来。如果dt波动超过预期值的20%,说明仿真器帧率不稳,可能是场景太复杂或者显存不够。
参数说明:imgmsg_to_cv2的第二个参数bgr8要和Carla发布的图像编码一致,Carla默认是bgra8,这里转成bgr8方便OpenCV显示。如果报编码错误,改成bgra8再试。
3.3 点云与图像的坐标系对齐
做前融合时,点云投影到图像上是个常见需求。Carla的坐标系是左手系,ROS是右手系,bridge会做转换,但外参矩阵还是要自己算。
import numpy as np # 相机内参(从Carla的fov和图像尺寸推算) fov = 90.0 width, height = 800, 600 focal = width / (2 * np.tan(np.radians(fov / 2))) K = np.array([[focal, 0, width / 2], [0, focal, height / 2], [0, 0, 1]]) # 激光雷达到相机的外参(根据spawn JSON里的位姿计算) # 相机在(1.5, 0, 1.4),雷达在(0, 0, 2.0) # 平移向量:雷达到相机的平移 t = np.array([1.5, 0, -0.6]) # 旋转:这里假设都朝前,无旋转 R = np.eye(3) T = np.column_stack((R, t)) def project_lidar_to_image(points, K, T): # points: Nx3 激光雷达坐标系下的点 points_homo = np.hstack((points, np.ones((points.shape[0], 1)))) cam_points = (T @ points_homo.T).T # 只保留相机前方的点 mask = cam_points[:, 2] > 0 cam_points = cam_points[mask] img_points = (K @ cam_points[:, :3].T).T img_points = img_points[:, :2] / img_points[:, 2:3] return img_points, mask逻辑说明:focal由fov和图像宽度算出,这是针孔相机模型。外参矩阵T把激光雷达坐标系下的点转到相机坐标系。投影后只保留z>0的点,即相机前方的点。
参数说明:t的计算是相机位置减雷达位置,因为要把雷达点转到相机坐标系。如果相机有俯仰角,R矩阵要用对应的旋转矩阵,不能简单用单位阵。这个投影结果可以用来做点云着色或者前融合的输入。
4. 控制接口对接:从ROS指令到车辆动力学
4.1 理解Carla的控制消息类型
Carla的ROS Bridge提供了几种控制接口,最直接的是CarlaEgoVehicleControl,它对应油门、刹车、方向盘、挡位。另一种是CarlaEgoVehicleAckermannControl,提供更底层的阿克曼模型控制,适合做轨迹跟踪。
from carla_msgs.msg import CarlaEgoVehicleControl, CarlaEgoVehicleAckermannControl # 方式一:直接控制 control = CarlaEgoVehicleControl() control.throttle = 0.4 control.brake = 0.0 control.steer = -0.1 # 左转 control.gear = 1 control.hand_brake = False control.reverse = False control.manual_gear_shift = False # 方式二:阿克曼控制 ackermann = CarlaEgoVehicleAckermannControl() ackermann.steer = -0.1 ackermann.steer_speed = 0.5 # 转向速率限制 ackermann.speed = 8.0 # 目标速度 m/s ackermann.acceleration = 1.0 # 加速度限制 ackermann.jerk = 0.5 # 加加速度限制逻辑说明:CarlaEgoVehicleControl是直接映射到Carla的车辆控制接口,适合做端到端的控制输出。CarlaEgoVehicleAckermannControl则把速度、加速度、jerk作为输入,Carla内部用PID去跟踪,适合做规划控制的分层验证。
参数说明:steer的范围是-1到1,但实际对应多少度取决于车辆模型。steer_speed限制转向的变化率,防止方向盘打死。jerk是加加速度,限制这个值能让控制更平滑,但太大又会导致响应慢。
4.2 用PID做速度跟踪的完整节点
速度跟踪是控制里最基础也最容易翻车的环节。下面是一个完整的PID速度控制节点,订阅车辆状态,发布控制指令。
import rclpy from rclpy.node import Node from carla_msgs.msg import CarlaEgoVehicleControl, CarlaEgoVehicleStatus import time class SpeedController(Node): def __init__(self): super().__init__('speed_controller') self.target_speed = 10.0 # m/s self.kp = 0.5 self.ki = 0.1 self.kd = 0.05 self.integral = 0.0 self.prev_error = 0.0 self.prev_time = time.time() self.pub = self.create_publisher(CarlaEgoVehicleControl, '/carla/ego_vehicle/vehicle_control_cmd', 10) self.sub = self.create_subscription(CarlaEgoVehicleStatus, '/carla/ego_vehicle/vehicle_status', self.status_cb, 10) def status_cb(self, msg): current_speed = msg.velocity # m/s error = self.target_speed - current_speed now = time.time() dt = now - self.prev_time if dt <= 0: return self.integral += error * dt derivative = (error - self.prev_error) / dt output = self.kp * error + self.ki * self.integral + self.kd * derivative self.prev_error = error self.prev_time = now control = CarlaEgoVehicleControl() if output >= 0: control.throttle = min(output, 1.0) control.brake = 0.0 else: control.throttle = 0.0 control.brake = min(-output, 1.0) control.steer = 0.0 control.gear = 1 control.hand_brake = False control.reverse = False control.manual_gear_shift = False self.pub.publish(control) def main(): rclpy.init() node = SpeedController() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()逻辑说明:订阅vehicle_status拿到当前速度,计算误差,PID输出正数给油门,负数给刹车。积分项累加误差,微分项抑制超调。dt用实际时间差,不能用固定值,否则仿真帧率波动时PID会震荡。
参数说明:kp=0.5是比例增益,太大容易超调,太小响应慢。ki=0.1消除稳态误差,但太大会积分饱和。kd=0.05抑制震荡。这三个参数没有万能值,得根据车辆质量和仿真步长调。我一般先用纯比例,调到临界震荡,再加微分,最后加积分。
提示:Carla的
vehicle_status里的velocity是标量速度,不是矢量。如果你需要纵向和横向速度,得从twist里取。
4.3 转向控制与路径跟踪的简化实现
速度控制稳了之后,转向控制是下一个坎。纯追踪(Pure Pursuit)是最容易上手的路径跟踪算法,适合在仿真里快速验证。
import numpy as np class PurePursuit: def __init__(self, wheelbase=2.8, lookahead=5.0): self.wheelbase = wheelbase self.lookahead = lookahead def compute_steer(self, vehicle_pos, vehicle_yaw, path): # path: Nx2 的路径点 # 找到最近的路径点 dists = np.linalg.norm(path - vehicle_pos, axis=1) nearest_idx = np.argmin(dists) # 从最近点往前找,直到距离大于lookahead for i in range(nearest_idx, len(path)): if np.linalg.norm(path[i] - vehicle_pos) >= self.lookahead: target = path[i] break else: target = path[-1] # 转到车辆坐标系 dx = target[0] - vehicle_pos[0] dy = target[1] - vehicle_pos[1] local_x = dx * np.cos(-vehicle_yaw) - dy * np.sin(-vehicle_yaw) local_y = dx * np.sin(-vehicle_yaw) + dy * np.cos(-vehicle_yaw) # 计算曲率 alpha = np.arctan2(local_y, local_x) steer = np.arctan2(2 * self.wheelbase * np.sin(alpha), self.lookahead) # 归一化到[-1, 1] steer = np.clip(steer / np.radians(30), -1.0, 1.0) return steer逻辑说明:先找路径上离车辆最近的点,然后从该点往前找,直到找到一个距离大于前视距离的点作为目标点。把目标点转到车辆坐标系,用几何关系算出前轮转角。
参数说明:wheelbase是轴距,Carla默认车辆大约2.8米。lookahead是前视距离,太小会导致震荡,太大则切弯。一般设为速度的1.5到2倍,比如10m/s时用15到20米。np.radians(30)是最大前轮转角,Carla的车辆大概在30度左右,超过这个值steer就饱和了。
5. 避坑与排查:那些让我熬夜的仿真问题
5.1 车辆生成失败但没有任何报错
现象:运行carla_spawn_objects后,终端没有错误输出,但/carla/ego_vehicle/vehicle_status一直没有消息。
原因:spawn JSON里的某个字段名拼写错误,或者传感器类型不被当前Carla版本支持。Carla的Python API在解析JSON时对未知字段是静默忽略的,不会抛异常。
解决:先用最简单的JSON只生成一辆车,不加任何传感器,确认车辆能出现。然后逐个添加传感器,每加一个就验证对应的topic有没有数据。另外,检查Carla服务端的日志,有时候错误只在那里打印。
5.2 相机图像延迟越来越大
现象:刚开始跑时图像实时,跑了几分钟后图像延迟累积到好几秒,topic的队列长度暴涨。
原因:ROS2的默认QoS是reliable,如果订阅端的处理速度跟不上发布端,消息会堆积。Carla的相机以固定频率发布,但你的可视化或算法节点如果每帧处理时间超过采样间隔,就会积压。
解决:把订阅端的QoS改成sensor_data,即best_effort,丢帧但不延迟。在Python里创建订阅时指定QoS配置:
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy qos = QoSProfile( reliability=ReliabilityPolicy.BEST_EFFORT, history=HistoryPolicy.KEEP_LAST, depth=1 ) self.create_subscription(Image, '/carla/ego_vehicle/front_camera/image', self.cam_cb, qos)5.3 控制指令发了但车不动
现象:/carla/ego_vehicle/vehicle_control_cmd有消息发布,但车辆速度始终为零。
原因:最常见的是hand_brake没有设为False,或者gear设成了0。另一个可能是车辆生成时没有指定role_name为ego_vehicle,导致控制topic的名字不对。
解决:先ros2 topic echo /carla/ego_vehicle/vehicle_status看hand_brake和gear的状态。如果hand_brake是True,检查你的控制消息里有没有显式设为False。如果topic名字不对,在spawn JSON里给车辆加上"role_name": "ego_vehicle"。
5.4 激光雷达点云稀疏或缺失
现象:点云topic有数据,但点云非常稀疏,或者只有零星几个点。
原因:points_per_second设得太低,或者range太小导致大部分点被截断。另一个可能是rotation_frequency和sensor_tick不匹配,导致每帧只采到部分扫描。
解决:points_per_second建议至少30000,32线雷达用56000比较合适。range根据场景设,城市道路50米够用。确保rotation_frequency乘以sensor_tick约等于1,比如10Hz旋转、0.1秒tick,正好一圈。
5.5 仿真帧率骤降导致控制震荡
现象:PID控制本来调好了,但跑一段时间后车辆开始画龙,速度波动大。
原因:Carla的渲染帧率受场景复杂度影响,当帧率从20Hz掉到5Hz时,PID的dt变大,积分项和微分项的计算都失真。
解决:在PID节点里加一个保护,如果dt超过阈值(比如0.2秒),就跳过这次计算,或者把积分项清零。另外,可以在Carla启动时加-benchmark -fps=20强制固定帧率,但这样物理精度会下降。我一般用-quality-level=Low加限制场景里的车辆数量来保帧率。
6. 进阶技巧:用场景编排做回归测试
6.1 用Carla Scenario Runner批量跑用例
单次手动测试只能验证功能,要做回归必须自动化。Carla Scenario Runner允许你用XML定义场景,然后批量执行并收集结果。
<scenario name="cut_in_test" type="CutIn"> <trigger name="start" type="Distance"> <distance>50</distance> </trigger> <actors> <actor name="ego" type="vehicle" model="vehicle.tesla.model3" /> <actor name="npc" type="vehicle" model="vehicle.audi.a2" /> </actors> <maneuvers> <maneuver name="npc_cut_in" actor="npc"> <lane_change lane="-1" speed="8" /> </maneuver> </maneuvers> </scenario>逻辑说明:这个XML定义了一个切入场景:当ego车距离npc车50米时触发,npc车以8m/s的速度向左变道。Scenario Runner会解析这个文件,在Carla里生成对应的车辆并执行动作。
参数说明:lane="-1"表示向左变道,speed是变道时的速度。Distance触发器的distance是触发距离。实际使用时,ego车由你的算法控制,npc车由Scenario Runner控制。
6.2 用rosbag2录制仿真数据做离线回放
仿真跑一遍不容易,把关键topic录下来,后面调算法时直接回放,能省大量时间。
# 录制 ros2 bag record -o sim_run_01 \ /carla/ego_vehicle/front_camera/image \ /carla/ego_vehicle/top_lidar/point_cloud \ /carla/ego_vehicle/vehicle_status \ /carla/ego_vehicle/vehicle_control_cmd # 回放 ros2 bag play sim_run_01 --loop逻辑说明:-o指定输出目录,后面跟要录的topic列表。回放时--loop让数据循环播放,方便反复调试。注意回放时Carla服务端可以关掉,因为数据已经录好了。
参数说明:如果录制的数据量太大,可以在record时加--max-cache-size限制缓存,或者用--compression-mode压缩。但压缩会消耗CPU,仿真机上本来CPU就紧张,我一般用外接硬盘存。
6.3 一个让我后悔没早做的习惯:给每个仿真用例打标签
早期我跑仿真就是随手跑,结果出了问题想复现时,忘了当时用的什么参数、什么场景、什么代码版本。后来我强制自己每次跑用例前,在输出目录里放一个meta.json,记录Carla版本、bridge commit、算法参数、场景文件哈希。
import json import hashlib import subprocess def save_meta(output_dir, params): meta = { "carla_version": "0.9.14", "bridge_commit": subprocess.check_output(["git", "-C", "~/carla_ws/src/ros-bridge", "rev-parse", "HEAD"]).decode().strip(), "params": params, "scenario_hash": hashlib.md5(open("scenario.xml", "rb").read()).hexdigest() } with open(f"{output_dir}/meta.json", "w") as f: json.dump(meta, f, indent=2)这个习惯看起来不起眼,但当你三个月后想复现一个边缘case时,没有这个文件就只能凭记忆猜。仿真环境里变量太多,人的记忆靠不住。希望帮到你。
本文还有配套的精品资源,点击获取