3. 传感器配置
说实话,传感器配置这块,是仿真里最「实在」的环节。你算法写得再花哨,传感器数据不对,全是白搭。我在项目里见过太多人,花了一周调模型,最后发现是相机内参设错了——那种感觉,嗯,挺崩溃的。
今天咱们就把 CARLA 里常用的几种传感器捋一遍。RGB 相机、深度相机、语义分割相机、激光雷达、GNSS、IMU,一个一个来。我会把配置参数、数据格式、代码示例都给你讲透。
核心要点:传感器配置的本质,就是告诉仿真引擎「你要采集什么、怎么采、采完存成什么样」。参数设对了,后面的事就顺了。
3.1 传感器蓝图与通用配置逻辑
在 CARLA 里,每个传感器都是一个 Actor。你通过蓝图(Blueprint)来创建它。我习惯先把蓝图库拉出来看一眼,确认传感器名字对不对。
# 获取所有传感器蓝图
blueprint_library = world.get_blueprint_library()
sensors = blueprint_library.filter('sensor.*')
for bp in sensors:
print(bp.id)
输出会有一长串,像 sensor.camera.rgb、sensor.lidar.ray_cast 这些。每个蓝图都带一堆属性,比如图像分辨率、激光雷达的线数、GNSS 的噪声参数。我个人建议,拿到蓝图后先 print(bp.get_attributes()) 看一眼,心里有个底。
小技巧:传感器蓝图的名字是固定的,别自己瞎编。比如深度相机就是 sensor.camera.depth,语义分割是 sensor.camera.semantic_segmentation。记不住?那就用 filter 搜一下。
3.2 相机配置:RGB、深度、语义分割
相机是仿真里最常用的传感器。三种相机本质上共享同一套配置逻辑,只是输出的数据类型不同。
3.2.1 RGB 相机
RGB 相机输出的是彩色图像,格式是 carla.Image。你需要设置分辨率、视场角(FOV)、以及传感器在世界中的位置。
# 创建 RGB 相机蓝图
bp = blueprint_library.find('sensor.camera.rgb')
bp.set_attribute('image_size_x', '800')
bp.set_attribute('image_size_y', '600')
bp.set_attribute('fov', '90')
# 挂载到车辆上
camera_transform = carla.Transform(carla.Location(x=1.5, z=2.4))
rgb_camera = world.spawn_actor(bp, camera_transform, attach_to=vehicle)
# 监听数据
rgb_camera.listen(lambda image: image.save_to_disk('output/rgb/%.6d.png' % image.frame))
这里有个坑——图像保存的路径必须提前建好。我曾经因为忘了建文件夹,跑了一整晚的仿真,第二天发现一张图都没存下来。嗯,从那以后我都在代码开头加一句 os.makedirs()。
3.2.2 深度相机
深度相机输出的是深度图,每个像素的值代表该点到相机的距离。注意,CARLA 的深度数据是编码在 RGB 三个通道里的,你需要解码才能用。
# 创建深度相机
bp = blueprint_library.find('sensor.camera.depth')
bp.set_attribute('image_size_x', '800')
bp.set_attribute('image_size_y', '600')
bp.set_attribute('fov', '90')
depth_camera = world.spawn_actor(bp, camera_transform, attach_to=vehicle)
# 监听并解码
def process_depth(image):
# 将深度图转为数组
image.convert(carla.ColorConverter.Depth)
array = np.frombuffer(image.raw_data, dtype=np.uint8)
array = np.reshape(array, (image.height, image.width, 4))
# 提取深度值(单位:米)
depth = array[:, :, 0] + array[:, :, 1] * 256 + array[:, :, 2] * 256 * 256
depth = depth / (256**3 - 1) * 1000 # 归一化到 0-1000 米
return depth
depth_camera.listen(lambda image: process_depth(image))
注意:深度相机的 FOV 和 RGB 相机保持一致,这样你才能做像素级的对齐。我见过有人 RGB 用 90 度,深度用 60 度,结果融合的时候怎么都对不上。
3.2.3 语义分割相机
语义分割相机输出的是标签图,每个像素的值代表物体类别。比如 0 代表未标记,1 代表建筑,2 代表行人,等等。完整的标签列表可以在 CARLA 文档里找到。
# 创建语义分割相机
bp = blueprint_library.find('sensor.camera.semantic_segmentation')
bp.set_attribute('image_size_x', '800')
bp.set_attribute('image_size_y', '600')
bp.set_attribute('fov', '90')
seg_camera = world.spawn_actor(bp, camera_transform, attach_to=vehicle)
# 保存时自动着色
seg_camera.listen(lambda image: image.save_to_disk(
'output/seg/%.6d.png' % image.frame,
carla.ColorConverter.CityScapesPalette
))
你想想看,语义分割图其实特别有用。训练感知模型时,你不需要手动标注——仿真直接给你 ground truth。我在做车道线检测项目时,就是用语义分割图来提取车道线像素,省了至少两周的标注时间。
3.3 激光雷达配置
激光雷达(LiDAR)是自动驾驶的「眼睛」。CARLA 默认提供的是射线投射模型,模拟 360 度旋转扫描。
# 创建激光雷达
bp = blueprint_library.find('sensor.lidar.ray_cast')
bp.set_attribute('channels', '64') # 线数
bp.set_attribute('range', '100') # 最大探测距离(米)
bp.set_attribute('points_per_second', '1000000') # 每秒点数
bp.set_attribute('rotation_frequency', '10') # 旋转频率(Hz)
bp.set_attribute('upper_fov', '10') # 上视角(度)
bp.set_attribute('lower_fov', '-30') # 下视角(度)
lidar_transform = carla.Transform(carla.Location(x=0.0, z=2.0))
lidar = world.spawn_actor(bp, lidar_transform, attach_to=vehicle)
# 处理点云数据
def process_lidar(data):
# 点云数据格式:x, y, z, intensity
points = np.frombuffer(data.raw_data, dtype=np.dtype('f4'))
points = np.reshape(points, (len(points) // 4, 4))
return points
lidar.listen(lambda data: process_lidar(data))
这里有几个参数我特别想强调一下:
- channels:线数。64 线算高端了,32 线也够用。我一般先用 16 线调试,跑通了再换 64 线。
- points_per_second:每秒点数。这个值越大,点云越密,但计算压力也越大。100 万点是个比较平衡的值。
- rotation_frequency:旋转频率。10 Hz 意味着每秒转 10 圈,和真实 Velodyne 一致。
避坑指南:激光雷达的 upper_fov 和 lower_fov 决定了垂直视野范围。如果你发现点云里地面点太多或者天空点太多,八成是这两个值没调好。我曾经把 upper_fov 设成 30 度,结果点云里全是树叶,根本看不到路。
3.4 GNSS 与 IMU 配置
GNSS 和 IMU 是定位系统的核心。GNSS 提供经纬度,IMU 提供加速度和角速度。两者融合,才能得到稳定的位姿估计。
3.4.1 GNSS
# 创建 GNSS
bp = blueprint_library.find('sensor.other.gnss')
bp.set_attribute('noise_alt_stddev', '0.0005') # 高度噪声标准差
bp.set_attribute('noise_lat_stddev', '0.0001') # 纬度噪声标准差
bp.set_attribute('noise_lon_stddev', '0.0001') # 经度噪声标准差
bp.set_attribute('noise_alt_bias', '0.0') # 高度偏差
bp.set_attribute('noise_lat_bias', '0.0') # 纬度偏差
bp.set_attribute('noise_lon_bias', '0.0') # 经度偏差
gnss_transform = carla.Transform(carla.Location(x=0.0, z=0.0))
gnss = world.spawn_actor(bp, gnss_transform, attach_to=vehicle)
# 获取数据
def process_gnss(data):
print(f"Latitude: {data.latitude}, Longitude: {data.longitude}, Altitude: {data.altitude}")
gnss.listen(lambda data: process_gnss(data))
GNSS 的噪声参数很关键。真实环境下,GPS 信号会有漂移。如果你把噪声全设成 0,那仿真出来的定位数据就太理想了,算法拿到真实场景里会直接崩掉。我一般会加一点高斯噪声,模拟城市峡谷里的多径效应。
3.4.2 IMU
# 创建 IMU
bp = blueprint_library.find('sensor.other.imu')
bp.set_attribute('noise_accel_stddev_x', '0.01')
bp.set_attribute('noise_accel_stddev_y', '0.01')
bp.set_attribute('noise_accel_stddev_z', '0.01')
bp.set_attribute('noise_gyro_stddev_x', '0.001')
bp.set_attribute('noise_gyro_stddev_y', '0.001')
bp.set_attribute('noise_gyro_stddev_z', '0.001')
imu_transform = carla.Transform(carla.Location(x=0.0, z=0.0))
imu = world.spawn_actor(bp, imu_transform, attach_to=vehicle)
# 获取数据
def process_imu(data):
print(f"Accel: {data.accelerometer}, Gyro: {data.gyroscope}, Compass: {data.compass}")
imu.listen(lambda data: process_imu(data))
IMU 的数据频率通常很高(100 Hz 以上),而 GNSS 只有 10 Hz。所以做定位融合时,一般用 IMU 做状态预测,GNSS 做观测更新。说白了,IMU 负责「猜」,GNSS 负责「纠偏」。
3.5 传感器数据同步与可视化
多传感器协同工作时,最头疼的问题就是时间同步。CARLA 里每个传感器都有自己的回调函数,它们可能在不同时间触发。我常用的做法是:
- 给每个传感器打上时间戳(
data.timestamp) - 用一个队列收集所有数据
- 在固定时间窗口内做对齐
from queue import Queue
from collections import defaultdict
data_queue = defaultdict(Queue)
def sensor_callback(sensor_type, data):
data_queue[sensor_type].put(data)
# 注册回调时带上类型
rgb_camera.listen(lambda data: sensor_callback('rgb', data))
lidar.listen(lambda data: sensor_callback('lidar', data))
下面这张图展示了传感器数据流的整体结构:
你看,整个流程就是:传感器采集 → 数据入队 → 时间同步 → 处理输出。这个架构我在多个项目里都用过,稳定可靠。
3.6 实战:批量保存传感器数据
最后,给你一个完整的示例。这个脚本会同时启动 RGB、深度、语义分割、LiDAR、GNSS、IMU 六个传感器,并把数据保存到磁盘。
import carla
import os
import numpy as np
def main():
client = carla.Client('localhost', 2000)
world = client.get_world()
blueprint_library = world.get_blueprint_library()
# 生成车辆
vehicle_bp = blueprint_library.filter('vehicle.*')[0]
spawn_point = world.get_map().get_spawn_points()[0]
vehicle = world.spawn_actor(vehicle_bp, spawn_point)
# 创建输出目录
os.makedirs('output/rgb', exist_ok=True)
os.makedirs('output/depth', exist_ok=True)
os.makedirs('output/seg', exist_ok=True)
os.makedirs('output/lidar', exist_ok=True)
# 配置传感器
sensors = []
# RGB
rgb_bp = blueprint_library.find('sensor.camera.rgb')
rgb_bp.set_attribute('image_size_x', '800')
rgb_bp.set_attribute('image_size_y', '600')
rgb_bp.set_attribute('fov', '90')
rgb_cam = world.spawn_actor(rgb_bp, carla.Transform(carla.Location(x=1.5, z=2.4)), attach_to=vehicle)
rgb_cam.listen(lambda img: img.save_to_disk('output/rgb/%.6d.png' % img.frame))
sensors.append(rgb_cam)
# 深度
depth_bp = blueprint_library.find('sensor.camera.depth')
depth_bp.set_attribute('image_size_x', '800')
depth_bp.set_attribute('image_size_y', '600')
depth_bp.set_attribute('fov', '90')
depth_cam = world.spawn_actor(depth_bp, carla.Transform(carla.Location(x=1.5, z=2.4)), attach_to=vehicle)
depth_cam.listen(lambda img: img.save_to_disk('output/depth/%.6d.png' % img.frame, carla.ColorConverter.Depth))
sensors.append(depth_cam)
# 语义分割
seg_bp = blueprint_library.find('sensor.camera.semantic_segmentation')
seg_bp.set_attribute('image_size_x', '800')
seg_bp.set_attribute('image_size_y', '600')
seg_bp.set_attribute('fov', '90')
seg_cam = world.spawn_actor(seg_bp, carla.Transform(carla.Location(x=1.5, z=2.4)), attach_to=vehicle)
seg_cam.listen(lambda img: img.save_to_disk('output/seg/%.6d.png' % img.frame, carla.ColorConverter.CityScapesPalette))
sensors.append(seg_cam)
# LiDAR
lidar_bp = blueprint_library.find('sensor.lidar.ray_cast')
lidar_bp.set_attribute('channels', '32')
lidar_bp.set_attribute('range', '100')
lidar_bp.set_attribute('points_per_second', '500000')
lidar_bp.set_attribute('rotation_frequency', '10')
lidar = world.spawn_actor(lidar_bp, carla.Transform(carla.Location(z=2.0)), attach_to=vehicle)
lidar.listen(lambda data: data.save_to_disk('output/lidar/%.6d.ply' % data.frame))
sensors.append(lidar)
# GNSS
gnss_bp = blueprint_library.find('sensor.other.gnss')
gnss = world.spawn_actor(gnss_bp, carla.Transform(), attach_to=vehicle)
gnss.listen(lambda data: print(f"GNSS: {data.latitude}, {data.longitude}"))
sensors.append(gnss)
# IMU
imu_bp = blueprint_library.find('sensor.other.imu')
imu = world.spawn_actor(imu_bp, carla.Transform(), attach_to=vehicle)
imu.listen(lambda data: print(f"IMU: Accel={data.accelerometer}, Gyro={data.gyroscope}"))
sensors.append(imu)
# 让车辆动起来
vehicle.set_autopilot(True)
# 跑 100 帧
for _ in range(100):
world.tick()
# 清理
for sensor in sensors:
sensor.destroy()
vehicle.destroy()
if __name__ == '__main__':
main()
最后说一句:传感器配置这事,看着简单,但细节特别多。我建议你先把 RGB 和 LiDAR 跑通,再慢慢加其他传感器。一次配太多,出了问题都不知道是哪个环节的锅。
好了,这一章的内容就到这。传感器配置是仿真实验的「地基」,地基打牢了,后面的自动化批量实验才能跑得稳。